├── .github
└── workflows
│ └── npm-publish.yml
├── .gitignore
├── .project
├── LICENSE
├── README.md
├── docker-build
├── Dockerfile
├── README.md
├── docker-init.sh
├── docker-run.sh
├── flows.json
├── local-build.sh
└── settings.js
├── docker-compose.yml
├── docker-env-start.sh
├── images
├── test-scen.png
└── test-spec.png
├── package.json
├── selenium-webdriver.html
└── selenium-webdriver.js
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3 |
4 | name: Node.js Package
5 |
6 | on:
7 | release:
8 | types: [created]
9 |
10 | jobs:
11 | publish-npm:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 | - uses: actions/setup-node@v1
16 | with:
17 | node-version: 12
18 | registry-url: https://registry.npmjs.org/
19 | - run: npm install
20 | - run: npm publish
21 | env:
22 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # node-waf configuration
21 | .lock-wscript
22 |
23 | # Compiled binary addons (http://nodejs.org/api/addons.html)
24 | build/Release
25 |
26 | # Dependency directory
27 | node_modules
28 |
29 | # Optional npm cache directory
30 | .npm
31 |
32 | # Optional REPL history
33 | .node_repl_history
34 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | node-red-contrib-selenium-webdriver
4 |
5 |
6 |
7 |
8 |
9 |
10 | com.aptana.projects.webnature
11 |
12 |
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2016 - DUONG Dinh Cuong
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # node-red-contrib-selenium-webdriver
2 | The automation framework which allows to create UI test flow by visual IDE tool for web application
3 |
4 | [](https://badge.fury.io/js/node-red-contrib-selenium-webdriver)
5 | []()
6 | []()
7 |
8 | Local Development Environment
9 | =============================
10 |
11 | **Install Selenium Server**
12 |
13 | npm install -g webdriver-manager
14 |
15 | **Setting up a Selenium Server**
16 |
17 | Prior to starting the selenium server, download the selenium server jar and driver binaries. By default it will download the selenium server jar and chromedriver binary.
18 |
19 | webdriver-manager update
20 |
21 | **Starting the Selenium Server**
22 |
23 | By default, the selenium server will run on http://localhost:4444/wd/hub.
24 |
25 | webdriver-manager start
26 |
27 | **Install Node-RED and nodes**
28 |
29 | npm install -g --unsafe-perm node-red
30 |
31 | cd ~/.node-red && npm install node-red-contrib-selenium-webdriver
32 |
33 | Automated UI Test /w Docker Environment
34 | ==================================
35 |
36 | 1. Install Docker Tool Box from https://www.docker.com/products/docker-toolbox
37 |
38 | 2. Create **docker-compose.xml** to your local folder
39 |
40 | ```
41 | version: '2'
42 | services:
43 | node-red:
44 | image: cuongquay/node-red-ui-automation
45 | ports:
46 | - 1880:1880
47 | selenium-hub:
48 | image: selenium/hub
49 | ports:
50 | - 4444:4444
51 | node-chrome:
52 | image: selenium/node-chrome
53 | depends_on:
54 | - selenium-hub
55 | environment:
56 | - HUB_PORT_4444_TCP_ADDR=selenium-hub
57 | ```
58 |
59 | 3. Run with **docker-compose up** command
60 |
61 | ```
62 | eval "$(docker-machine env default)"
63 | docker-compose up -d --force-recreate
64 | ```
65 |
66 | 4. Launch Kitematic, choose **node-red-ui-automation** container and look for the Access URL from HOME tab.
67 |
68 | 5. Browsing the application by http://ACCESS_URL:1880/ (usually it's http://192.168.99.100:1880/)
69 |
70 |
71 | **Example flow**
72 |
73 | ```javascript
74 | [{"id":"fb07c885.3874c8","type":"function","z":"a3fe32e2.a1b96","name":"ErrorHandle","func":"if (msg.error) {\n msg.statusCode = 400;\n msg.payload = msg.error;\n}\nreturn msg;","outputs":1,"noerr":0,"x":830,"y":140,"wires":[["2f3d9e67.eb9d52"]]},{"id":"e1d1fc61.92093","type":"inject","z":"a3fe32e2.a1b96","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":100,"y":60,"wires":[["b334ff56.fb323"]]},{"id":"2f3d9e67.eb9d52","type":"debug","z":"a3fe32e2.a1b96","name":"","active":true,"console":"false","complete":"false","x":990,"y":140,"wires":[]},{"id":"b334ff56.fb323","type":"open-web","z":"a3fe32e2.a1b96","name":"","weburl":"https://www.google.com/","width":"480","height":"640","webtitle":"Google","timeout":"3000","maximized":false,"server":"d404327f.b8517","x":270,"y":60,"wires":[["aa60f00c.b5221"]]},{"id":"473291bb.ff669","type":"close-web","z":"a3fe32e2.a1b96","name":"","waitfor":"1500","x":670,"y":140,"wires":[["fb07c885.3874c8"]]},{"id":"ae932825.695ad8","type":"delay","z":"a3fe32e2.a1b96","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":520,"y":140,"wires":[["473291bb.ff669"]]},{"id":"aa60f00c.b5221","type":"find-object","z":"a3fe32e2.a1b96","name":"","selector":"name","target":"btnK","timeout":"1000","waitfor":"1500","x":460,"y":60,"wires":[["bc933182.599ed"]]},{"id":"bc933182.599ed","type":"get-value","z":"a3fe32e2.a1b96","name":"CheckButton","expected":"Tìm với Google","selector":"name","target":"btnK","timeout":"1000","waitfor":"1500","savetofile":false,"x":650,"y":60,"wires":[["62aa9f69.b07a8"]]},{"id":"62aa9f69.b07a8","type":"send-keys","z":"a3fe32e2.a1b96","name":"","text":"cuongdd1","selector":"xpath","target":"//*[@id=\"lst-ib\"]","timeout":"1000","waitfor":"1500","clearval":false,"x":830,"y":60,"wires":[["b72c1331.d769d"]]},{"id":"1fb4e2ff.3a2a2d","type":"click-on","z":"a3fe32e2.a1b96","name":"","selector":"name","target":"btnG","timeout":"10000","waitfor":"1500","clickon":false,"x":180,"y":140,"wires":[["1d0e4aca.6e3825"]]},{"id":"1d0e4aca.6e3825","type":"run-script","z":"a3fe32e2.a1b96","name":"","func":"\nreturn arguments[0].innerHTML;","selector":"name","target":"","timeout":"10000","waitfor":"1500","x":350,"y":140,"wires":[["ae932825.695ad8"]]},{"id":"7689f307.deb30c","type":"link in","z":"a3fe32e2.a1b96","name":"","links":["b72c1331.d769d"],"x":35,"y":140,"wires":[["1fb4e2ff.3a2a2d"]]},{"id":"b72c1331.d769d","type":"link out","z":"a3fe32e2.a1b96","name":"","links":["7689f307.deb30c"],"x":955,"y":60,"wires":[]},{"id":"d404327f.b8517","type":"selenium-server","z":"a3fe32e2.a1b96","remoteurl":"http://localhost:4444/wd/hub","browser":"chrome"}]
75 | ```
76 | **Demo Screenshot**
77 |
78 | 
79 | 
80 |
81 |
82 |
--------------------------------------------------------------------------------
/docker-build/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mhart/alpine-node
2 | MAINTAINER DUONG Dinh Cuong
3 |
4 | RUN apt-get update -y
5 | RUN apt-get install -y wget imagemagick
6 | RUN npm install -g --unsafe-perm node-red
7 | RUN cd /usr/local/lib/node_modules/node-red/ && npm install node-red-contrib-selenium-webdriver
8 |
9 | ADD settings.js /root/.node-red/settings.js
10 | ADD flows.json /root/.node-red/flows.json
11 |
12 | # expose port
13 | EXPOSE 1880
14 |
15 | # Container's entry point, executing supervisord in the foreground
16 | CMD ["/usr/bin/node-red"]
17 |
--------------------------------------------------------------------------------
/docker-build/README.md:
--------------------------------------------------------------------------------
1 | # node-red-ui-automation
2 | The automation framework which allows to create UI test flow by visual IDE tool for web application
3 |
4 | ```javascript
5 | $ docker run -d -P --name selenium-hub selenium/hub
6 | ```
7 |
8 | ```javascript
9 | $ docker run -d --name node-chrome --link selenium-hub:hub selenium/node-chrome
10 | ```
11 |
12 | ```javascript
13 | $ docker run -d --name node-firefox --link selenium-hub:hub selenium/node-firefox
14 | ```
15 |
16 | ```javascript
17 | $ docker run -d -P --name node-red --link selenium-hub:hub cuongquay/node-red-ui-automation
18 | ```
--------------------------------------------------------------------------------
/docker-build/docker-init.sh:
--------------------------------------------------------------------------------
1 | eval "$(docker-machine env default)"
--------------------------------------------------------------------------------
/docker-build/docker-run.sh:
--------------------------------------------------------------------------------
1 | sh docker run -d -P --name selenium-hub selenium/hub
2 | sh docker run -d --name node-chrome --link selenium-hub:hub selenium/node-chrome
3 | sh docker run -d --name node-firefox --link selenium-hub:hub selenium/node-firefox
4 | sh docker run -d -P --name node-red --link selenium-hub:hub cuongquay/node-red-ui-automation
--------------------------------------------------------------------------------
/docker-build/flows.json:
--------------------------------------------------------------------------------
1 | [{"id":"1d673d65.23ce53","type":"tab","label":"Flow 1"},{"id":"39589244.06e2ee","type":"mqtt-broker","z":"1d673d65.23ce53","broker":"localhost","port":"1883","tls":null,"clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"willTopic":"","willQos":"0","willRetain":null,"willPayload":"","birthTopic":"","birthQos":"0","birthRetain":null,"birthPayload":""},{"id":"4a853a4e.1addc4","type":"selenium-server","z":"1d673d65.23ce53","remoteurl":"http://localhost:4444/wd/hub"},{"id":"e43f97a7.73f328","type":"selenium-server","z":"1d673d65.23ce53","remoteurl":"http://selenium-hub:4444/wd/hub"},{"id":"f6bdc90.e021a38","type":"function","z":"1d673d65.23ce53","name":"ErrorHandle","func":"if (msg.error) {\n msg.statusCode = 400;\n msg.payload = msg.error;\n}\nreturn msg;","outputs":1,"noerr":0,"x":815,"y":182,"wires":[["8f0a2f32.a03c4"]]},{"id":"c8531886.050b98","type":"inject","z":"1d673d65.23ce53","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":85,"y":102,"wires":[["f9b2f359.9a038"]]},{"id":"8f0a2f32.a03c4","type":"debug","z":"1d673d65.23ce53","name":"","active":true,"console":"false","complete":"false","x":975,"y":182,"wires":[]},{"id":"f9b2f359.9a038","type":"open-web","z":"1d673d65.23ce53","name":"","browser":"chrome","weburl":"https://www.google.com/","width":"480","height":"640","webtitle":"Google","timeout":"3000","maximized":false,"server":"e43f97a7.73f328","x":261,"y":103,"wires":[["69a9589f.dbefc8"]]},{"id":"e05e251e.991b58","type":"close-web","z":"1d673d65.23ce53","name":"","waitfor":"1500","x":655,"y":182,"wires":[["f6bdc90.e021a38"]]},{"id":"db67f805.930688","type":"delay","z":"1d673d65.23ce53","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":505,"y":182,"wires":[["e05e251e.991b58"]]},{"id":"69a9589f.dbefc8","type":"find-object","z":"1d673d65.23ce53","name":"","selector":"name","target":"btnK","timeout":"1000","waitfor":"1500","x":445,"y":102,"wires":[["7aeee1d4.d5113"]]},{"id":"7aeee1d4.d5113","type":"get-value","z":"1d673d65.23ce53","name":"CheckButton","expected":"Tìm với Google","selector":"name","target":"btnK","timeout":"1000","waitfor":"1500","savetofile":false,"x":635,"y":102,"wires":[["d33bf27a.7b162"]]},{"id":"d33bf27a.7b162","type":"send-keys","z":"1d673d65.23ce53","name":"","text":"cuongdd1","selector":"xpath","target":"//*[@id=\"lst-ib\"]","timeout":"1000","waitfor":"1500","clearval":false,"x":815,"y":102,"wires":[["46ec834c.be079c"]]},{"id":"732d0e45.99d89","type":"click-on","z":"1d673d65.23ce53","name":"","selector":"name","target":"btnG","timeout":"10000","waitfor":"1500","clickon":false,"x":165,"y":182,"wires":[["df4feb78.84a108"]]},{"id":"df4feb78.84a108","type":"run-script","z":"1d673d65.23ce53","name":"","func":"\nreturn arguments[0].innerHTML;","selector":"name","target":"","timeout":"10000","waitfor":"1500","x":335,"y":182,"wires":[["db67f805.930688"]]},{"id":"cbf33fbc.14736","type":"link in","z":"1d673d65.23ce53","name":"","links":["46ec834c.be079c"],"x":20,"y":182,"wires":[["732d0e45.99d89"]]},{"id":"46ec834c.be079c","type":"link out","z":"1d673d65.23ce53","name":"","links":["cbf33fbc.14736"],"x":940,"y":102,"wires":[]}]
--------------------------------------------------------------------------------
/docker-build/local-build.sh:
--------------------------------------------------------------------------------
1 | eval "$(docker-machine env default)"
2 | docker stop $(docker ps -a -q)
3 | docker rm $(docker ps -a -q)
4 | docker rmi $(docker ps -a -q)
5 | docker build -t local-build-test .
6 | docker run -dp 1880:1880 --restart=always local-build-test
7 |
8 |
--------------------------------------------------------------------------------
/docker-build/settings.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2013, 2015 IBM Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 |
17 | // The `https` setting requires the `fs` module. Uncomment the following
18 | // to make it available:
19 | //var fs = require("fs");
20 |
21 | module.exports = {
22 | // the tcp port that the Node-RED web server is listening on
23 | uiPort: 1880,
24 |
25 | // By default, the Node-RED UI accepts connections on all IPv4 interfaces.
26 | // The following property can be used to listen on a specific interface. For
27 | // example, the following would only allow connections from the local machine.
28 | //uiHost: "127.0.0.1",
29 |
30 | // Retry time in milliseconds for MQTT connections
31 | mqttReconnectTime: 15000,
32 |
33 | // Retry time in milliseconds for Serial port connections
34 | serialReconnectTime: 15000,
35 |
36 | // Retry time in milliseconds for TCP socket connections
37 | //socketReconnectTime: 10000,
38 |
39 | // Timeout in milliseconds for TCP server socket connections
40 | // defaults to no timeout
41 | //socketTimeout: 120000,
42 |
43 | // Timeout in milliseconds for HTTP request connections
44 | // defaults to 120 seconds
45 | //httpRequestTimeout: 120000,
46 |
47 | // The maximum length, in characters, of any message sent to the debug sidebar tab
48 | debugMaxLength: 1000,
49 |
50 | // The file containing the flows. If not set, it defaults to flows_.json
51 | flowFile: 'flows.json',
52 |
53 | // To enabled pretty-printing of the flow within the flow file, set the following
54 | // property to true:
55 | //flowFilePretty: true,
56 |
57 | // By default, all user data is stored in the Node-RED install directory. To
58 | // use a different location, the following property can be used
59 | //userDir: '/home/nol/.node-red/',
60 |
61 | // Node-RED scans the `nodes` directory in the install directory to find nodes.
62 | // The following property can be used to specify an additional directory to scan.
63 | //nodesDir: '/home/nol/.node-red/nodes',
64 |
65 | // By default, the Node-RED UI is available at http://localhost:1880/
66 | // The following property can be used to specifiy a different root path.
67 | // If set to false, this is disabled.
68 | //httpAdminRoot: '/admin',
69 |
70 | // Some nodes, such as HTTP In, can be used to listen for incoming http requests.
71 | // By default, these are served relative to '/'. The following property
72 | // can be used to specifiy a different root path. If set to false, this is
73 | // disabled.
74 | //httpNodeRoot: '/red-nodes',
75 |
76 | // The following property can be used in place of 'httpAdminRoot' and 'httpNodeRoot',
77 | // to apply the same root to both parts.
78 | //httpRoot: '/red',
79 |
80 | // When httpAdminRoot is used to move the UI to a different root path, the
81 | // following property can be used to identify a directory of static content
82 | // that should be served at http://localhost:1880/.
83 | //httpStatic: '/home/nol/node-red-dashboard/',
84 |
85 | // Securing Node-RED
86 | // -----------------
87 | // To password protect the Node-RED editor and admin API, the following
88 | // property can be used. See http://nodered.org/docs/security.html for details.
89 | //adminAuth: {
90 | // type: "credentials",
91 | // users: [{
92 | // username: "admin",
93 | // password: "{pass-hash}",
94 | // permissions: "*"
95 | // },{
96 | // username: "developer",
97 | // password: "{pass-hash}",
98 | // permissions: "read"
99 | // }]
100 | //},
101 |
102 | // To password protect the node-defined HTTP endpoints (httpNodeRoot), or
103 | // the static content (httpStatic), the following properties can be used.
104 | // The pass field is a bcrypt hash of the password.
105 | // See http://nodered.org/docs/security.html#generating-the-password-hash
106 | //httpNodeAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."},
107 | //httpStaticAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."},
108 |
109 | // The following property can be used to enable HTTPS
110 | // See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener
111 | // for details on its contents.
112 | // See the comment at the top of this file on how to load the `fs` module used by
113 | // this setting.
114 | //
115 | //https: {
116 | // key: fs.readFileSync('privatekey.pem'),
117 | // cert: fs.readFileSync('certificate.pem')
118 | //},
119 |
120 | // The following property can be used to disable the editor. The admin API
121 | // is not affected by this option. To disable both the editor and the admin
122 | // API, use either the httpRoot or httpAdminRoot properties
123 | //disableEditor: false,
124 |
125 | // The following property can be used to configure cross-origin resource sharing
126 | // in the HTTP nodes.
127 | // See https://github.com/troygoode/node-cors#configuration-options for
128 | // details on its contents. The following is a basic permissive set of options:
129 | httpNodeCors: {
130 | origin: "*",
131 | methods: "GET,PUT,POST,DELETE"
132 | },
133 |
134 | // If you need to set an http proxy please set an environment variable
135 | // called http_proxy (or HTTP_PROXY) outside of Node-RED in the operating system.
136 | // For example - http_proxy=http://myproxy.com:8080
137 | // (Setting it here will have no effect)
138 | // You may also specify no_proxy (or NO_PROXY) to supply a comma separated
139 | // list of domains to not proxy, eg - no_proxy=.acme.co,.acme.co.uk
140 |
141 | // The following property can be used to add a custom middleware function
142 | // in front of all http in nodes. This allows custom authentication to be
143 | // applied to all http in nodes, or any other sort of common request processing.
144 | //httpNodeMiddleware: function(req,res,next) {
145 | // // Handle/reject the request, or pass it on to the http in node
146 | // // by calling next();
147 | // next();
148 | //},
149 |
150 | // Anything in this hash is globally available to all functions.
151 | // It is accessed as context.global.
152 | // eg:
153 | // functionGlobalContext: { os:require('os') }
154 | // can be accessed in a function block as:
155 | // context.global.os
156 |
157 | functionGlobalContext: {
158 | // os:require('os'),
159 | // octalbonescript:require('octalbonescript'),
160 | // jfive:require("johnny-five"),
161 | // j5board:require("johnny-five").Board({repl:false})
162 | },
163 |
164 | // The following property can be used to order the categories in the editor
165 | // palette. If a node's category is not in the list, the category will get
166 | // added to the end of the palette.
167 | // If not set, the following default order is used:
168 | //paletteCategories: ['subflows', 'input', 'output', 'function', 'social', 'mobile', 'storage', 'analysis', 'advanced'],
169 |
170 | // Configure the logging output
171 | logging: {
172 | // Only console logging is currently supported
173 | console: {
174 | // Level of logging to be recorded. Options are:
175 | // fatal - only those errors which make the application unusable should be recorded
176 | // error - record errors which are deemed fatal for a particular request + fatal errors
177 | // warn - record problems which are non fatal + errors + fatal errors
178 | // info - record information about the general running of the application + warn + error + fatal errors
179 | // debug - record information which is more verbose than info + info + warn + error + fatal errors
180 | // trace - record very detailed logging + debug + info + warn + error + fatal errors
181 | level: "info",
182 |
183 | // Whether or not to include metric events in the log output
184 | metrics: false,
185 | // Whether or not to include audit events in the log output
186 | audit: false
187 | }
188 | }
189 | };
190 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 | node-red:
4 | image: cuongquay/node-red-ui-automation
5 | ports:
6 | - 1880:1880
7 | selenium-hub:
8 | image: selenium/hub
9 | ports:
10 | - 4444:4444
11 | node-chrome:
12 | image: selenium/node-chrome
13 | depends_on:
14 | - selenium-hub
15 | environment:
16 | - HUB_PORT_4444_TCP_ADDR=selenium-hub
17 |
--------------------------------------------------------------------------------
/docker-env-start.sh:
--------------------------------------------------------------------------------
1 | eval "$(docker-machine env default)"
2 | docker-compose up -d --force-recreate
3 |
--------------------------------------------------------------------------------
/images/test-scen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cuongquay/node-red-contrib-selenium-webdriver/57dae4d154c1728c20a9ac89bb78a053783f0f08/images/test-scen.png
--------------------------------------------------------------------------------
/images/test-spec.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cuongquay/node-red-contrib-selenium-webdriver/57dae4d154c1728c20a9ac89bb78a053783f0f08/images/test-spec.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-red-contrib-selenium-webdriver",
3 | "version": "0.2.14",
4 | "description": "A selenium-webdriver nodes for Node-RED",
5 | "dependencies": {
6 | "is-utf8": ">=0.2.1",
7 | "fs-extra": ">=0.30.0",
8 | "easyimage": ">=2.1.0",
9 | "selenium-webdriver": ">=2.53.2",
10 | "is-reachable": ">=2.0.0",
11 | "q": ">=1.4.1"
12 | },
13 | "license": "Apache-2.0",
14 | "keywords": [
15 | "node-red",
16 | "selenium-webdriver"
17 | ],
18 | "node-red": {
19 | "nodes": {
20 | "selenium-webdriver": "selenium-webdriver.js"
21 | }
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "https://github.com/cuongquay/node-red-contrib-selenium-webdriver"
26 | },
27 | "engines": {
28 | "node": ">= 6.9.0"
29 | },
30 | "author": {
31 | "name": "DUONG Dinh Cuong",
32 | "email": "cuong3ihut@gmail.com"
33 | },
34 | "scripts": {
35 | "prepare": "mversion patch -mn 'Patch new version and publish'",
36 | "postpublish": "git push && git push --tags"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/selenium-webdriver.html:
--------------------------------------------------------------------------------
1 |
45 |
46 |
62 |
63 |
115 |
116 |
126 |
127 |
130 |
131 |
155 |
156 |
187 |
188 |
201 |
202 |
239 |
240 |
279 |
280 |
294 |
295 |
335 |
336 |
371 |
372 |
385 |
386 |
446 |
447 |
482 |
483 |
497 |
498 |
535 |
536 |
550 |
551 |
561 |
562 |
589 |
590 |
629 |
630 |
646 |
647 |
687 |
688 |
731 |
732 |
749 |
750 |
793 |
794 |
833 |
834 |
850 |
851 |
891 |
892 |
931 |
932 |
963 |
964 |
1051 |
1052 |
1087 |
1088 |
1101 |
1102 |
1139 |
1140 |
1154 |
1155 |
1163 |
1164 |
1192 |
1193 |
1203 |
1204 |
1211 |
1212 |
1236 |
1237 |
1247 |
1248 |
1255 |
1256 |
1280 |
1281 |
1291 |
1292 |
1299 |
1300 |
1324 |
1325 |
1338 |
1339 |
1369 |
--------------------------------------------------------------------------------
/selenium-webdriver.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Author: DUONG Dinh Cuong, cuong3ihut@gmail.com.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 | //$("head").append("");var css = $("head").children(":last");css.attr({rel: "stylesheet",type: "text/css",href: "https://cdn.rawgit.com/kamranahmedse/jquery-toast-plugin/master/dist/jquery.toast.min.css"});$.getScript("https://cdn.rawgit.com/kamranahmedse/jquery-toast-plugin/master/dist/jquery.toast.min.js")
17 |
18 | module.exports = function(RED) {
19 | "use strict";
20 | var q = require('q');
21 | var util = require("util");
22 | var fs = require("fs-extra");
23 | var easyimg = require("easyimage");
24 | var isReachable = require('is-reachable');
25 | var webdriver = require("selenium-webdriver"),
26 | By = webdriver.By,
27 | until = webdriver.until;
28 | var isUtf8 = require('is-utf8');
29 | var ___msgs = {};
30 |
31 | function saveToFile(node, msg) {
32 | node.filename = msg.filename || node.filename;
33 | var data = msg.payload;
34 | if (( typeof data === "object") && (!Buffer.isBuffer(data))) {
35 | data = JSON.stringify(data);
36 | }
37 | if ( typeof data === "boolean") {
38 | data = data.toString();
39 | }
40 | if ( typeof data === "number") {
41 | data = data.toString();
42 | }
43 | fs.writeFile(node.filename, data, "utf8", function(err) {
44 | if (err) {
45 | if ((err.code === "ENOENT") && node.createDir) {
46 | fs.ensureFile(node.filename, function(err) {
47 | if (err) {
48 | node.error(RED._("file.errors.createfail", {
49 | error : err.toString()
50 | }), msg);
51 | } else {
52 | fs.writeFile(node.filename, data, "utf8", function(err) {
53 | if (err) {
54 | node.error(RED._("file.errors.writefail", {
55 | error : err.toString()
56 | }), msg);
57 | }
58 | });
59 | }
60 | node.send(msg);
61 | });
62 | } else {
63 | node.error(RED._("file.errors.writefail", {
64 | error : err.toString()
65 | }), msg);
66 | node.send(msg);
67 | }
68 | } else {
69 | node.send(msg);
70 | }
71 | });
72 | }
73 |
74 | function waitUntilElementLocated(node, msg, callback) {
75 | node.selector = (node.selector && node.selector != "") ? node.selector : msg.selector;
76 | node.target = (node.target && node.target != "") ? node.target : msg.target;
77 | node.timeout = (node.timeout && node.timeout != "") ? node.timeout : msg.timeout;
78 | node.waitfor = msg.waitfor || node.waitfor;
79 | node.status({});
80 | if (msg.refresh) {
81 | node.status({});
82 | node.send(msg);
83 | } else if (msg.error) {
84 | node.send(msg);
85 | } else if (node.target && node.target != "") {
86 | try {
87 | node.status({
88 | fill : "blue",
89 | shape : "dot",
90 | text : "locating"
91 | });
92 | setTimeout(function() {
93 | if (msg.driver) {
94 | msg.driver.wait(until.elementLocated(By[node.selector](node.target)), parseInt(node.timeout)).catch(function(errorback) {
95 | msg.error = {
96 | name : node.name,
97 | selector : node.selector,
98 | target : node.target,
99 | value : "catch timeout after " + node.timeout + " seconds"
100 | };
101 | node.status({
102 | fill : "red",
103 | shape : "ring",
104 | text : "error"
105 | });
106 | }).then(function() {
107 | if (msg.error) {
108 | node.send(msg);
109 | } else {
110 | msg.element = msg.driver.findElement(By[node.selector](node.target));
111 | if ( typeof (callback) !== "undefined") {
112 | node.status({});
113 | callback(msg.element);
114 | }
115 | }
116 | }, function(err) {
117 | node.status({
118 | fill : "red",
119 | shape : "ring",
120 | text : "error"
121 | });
122 | node.send(msg);
123 | });
124 | } else {
125 | if ( typeof (callback) !== "undefined") {
126 | node.status({});
127 | callback(msg.element);
128 | }
129 | }
130 | }, node.waitfor);
131 | } catch (ex) {
132 | node.status({
133 | fill : "red",
134 | shape : "ring",
135 | text : "exception"
136 | });
137 | node.send(msg);
138 | }
139 | } else {
140 | if ( typeof (callback) !== "undefined") {
141 | node.status({
142 | fill : "blue",
143 | shape : "dot",
144 | text : "delay " + (node.waitfor / 1000).toFixed(1) + " s"
145 | });
146 | setTimeout(function() {
147 | node.status({});
148 | callback(msg.element);
149 | }, node.waitfor);
150 | }
151 | }
152 | }
153 |
154 | function sendErrorMsg(node, msg, text, type) {
155 | msg.error = {
156 | name : node.name,
157 | selector : node.selector,
158 | target : node.target,
159 | expected : (node.expected && node.expected != "") ? node.expected : msg.expected,
160 | value : text
161 | };
162 | node.status({
163 | fill : "red",
164 | shape : "ring",
165 | text : type || "unknown"
166 | });
167 | node.send(msg);
168 | };
169 |
170 | function getValueNode(node, msg) {
171 | try {
172 | msg.element.getAttribute("value").then(function(text) {
173 | msg.payload = text;
174 | var expected = (node.expected && node.expected != "") ? node.expected : msg.expected;
175 | if (expected && expected != "" && expected != text) {
176 | sendErrorMsg(node, msg, text, "unexpected");
177 | } else if (!msg.error) {
178 | node.status({
179 | fill : "green",
180 | shape : "ring",
181 | text : "passed"
182 | });
183 | delete msg.error;
184 | if (msg.filename && node.savetofile) {
185 | saveToFile(node, msg);
186 | } else {
187 | node.send(msg);
188 | }
189 | }
190 | }).catch(function(errorback) {
191 | sendErrorMsg(node, msg, errorback.message, "error");
192 | });
193 | } catch (ex) {
194 | node.send(msg);
195 | }
196 | };
197 |
198 | function getAttributeNode(node, msg) {
199 | try {
200 | msg.element.getAttribute(node.attribute).then(function(text) {
201 | msg.payload = text;
202 | var expected = (node.expected && node.expected != "") ? node.expected : msg.expected;
203 | if (expected && expected != "" && expected != text) {
204 | sendErrorMsg(node, msg, text, "unexpected");
205 | } else if (!msg.error) {
206 | node.status({
207 | fill : "green",
208 | shape : "ring",
209 | text : "passed"
210 | });
211 | delete msg.error;
212 | if (msg.filename && node.savetofile) {
213 | saveToFile(node, msg);
214 | } else {
215 | node.send(msg);
216 | }
217 | }
218 | }).catch(function(errorback) {
219 | sendErrorMsg(node, msg, errorback.message, "error");
220 | });
221 | } catch (ex) {
222 | node.send(msg);
223 | }
224 | };
225 |
226 | function getTextNode(node, msg) {
227 | try {
228 | msg.element.getText().then(function(text) {
229 | msg.payload = text;
230 | var expected = (node.expected && node.expected != "") ? node.expected : msg.expected;
231 | if (expected && expected != "" && expected != text) {
232 | sendErrorMsg(node, msg, text, "unexpected");
233 | } else if (!msg.error) {
234 | node.status({
235 | fill : "green",
236 | shape : "ring",
237 | text : "passed"
238 | });
239 | delete msg.error;
240 | if (msg.filename && node.savetofile) {
241 | saveToFile(node, msg);
242 | } else {
243 | node.send(msg);
244 | }
245 | }
246 | }).catch(function(errorback) {
247 | sendErrorMsg(node, msg, errorback.message, "error");
248 | });
249 | } catch (ex) {
250 | node.send(msg);
251 | }
252 | };
253 |
254 | function setValueNode(node, msg, callback) {
255 | try {
256 | var value = (node.value && node.value != "") ? node.value : msg.value;
257 | msg.driver.executeScript("arguments[0].setAttribute('value', '" + value + "')", msg.element).then(function() {
258 | if (!msg.error) {
259 | node.status({
260 | fill : "green",
261 | shape : "ring",
262 | text : "done"
263 | });
264 | delete msg.error;
265 | node.send(msg);
266 | }
267 |
268 | }).catch(function(errorback) {
269 | sendErrorMsg(node, msg, errorback.message, "error");
270 | });
271 | } catch (ex) {
272 | node.send(msg);
273 | }
274 | };
275 |
276 | function clickOnNode(node, msg) {
277 | try {
278 | msg.element.click().then(function() {
279 | if (!msg.error) {
280 | node.status({
281 | fill : "green",
282 | shape : "ring",
283 | text : "done"
284 | });
285 | delete msg.error;
286 | node.send(msg);
287 | }
288 | }).catch(function(errorback) {
289 | sendErrorMsg(node, msg, errorback.message, "error");
290 | });
291 | } catch (ex) {
292 | node.send(msg);
293 | }
294 | }
295 |
296 | function sendKeysNode(node, msg) {
297 | try {
298 | var value = (node.value && node.value != "") ? node.value : msg.value;
299 | if (node.clearval) {
300 | msg.element.clear().then(function() {
301 | msg.element.sendKeys(value).then(function() {
302 | if (!msg.error) {
303 | node.status({
304 | fill : "green",
305 | shape : "ring",
306 | text : "done"
307 | });
308 | delete msg.error;
309 | node.send(msg);
310 | }
311 | }).catch(function(errorback) {
312 | sendErrorMsg(node, msg, errorback.message, "error");
313 | });
314 | }).catch(function(errorback) {
315 | sendErrorMsg(node, msg, errorback.message, "error");
316 | });
317 | } else {
318 | msg.element.sendKeys(value).then(function() {
319 | if (!msg.error) {
320 | node.status({
321 | fill : "green",
322 | shape : "ring",
323 | text : "done"
324 | });
325 | delete msg.error;
326 | node.send(msg);
327 | }
328 | }).catch(function(errorback) {
329 | sendErrorMsg(node, msg, errorback.message, "error");
330 | });
331 | }
332 | } catch (ex) {
333 | node.send(msg);
334 | }
335 | };
336 |
337 | function runScriptNode(node, msg) {
338 | try {
339 | msg.driver.executeScript(node.func, msg.element).then(function(results) {
340 | if (!msg.error) {
341 | node.status({
342 | fill : "green",
343 | shape : "ring",
344 | text : "done"
345 | });
346 | delete msg.error;
347 | msg.payload = results;
348 | node.send(msg);
349 | }
350 | }).catch(function(errorback) {
351 | sendErrorMsg(node, msg, errorback.message, "error");
352 | });
353 | } catch (ex) {
354 | node.send(msg);
355 | }
356 | }
357 |
358 | function takeScreenShotNode(node, msg) {
359 | node.filename = msg.filename || node.filename;
360 | var cropInFile = function(size, location, srcFile) {
361 | if ( typeof (easyimg) !== "undefined") {
362 | easyimg.crop({
363 | src : srcFile,
364 | dst : srcFile,
365 | cropwidth : size.width,
366 | cropheight : size.height,
367 | x : location.x,
368 | y : location.y,
369 | gravity : 'North-West'
370 | }, function(err, stdout, stderr) {
371 | if (err) {
372 | throw err;
373 | }
374 | });
375 | }
376 | };
377 | try {
378 | msg.element.getSize().then(function(size) {
379 | msg.element.getLocation().then(function(location) {
380 | msg.driver.takeScreenshot().then(function(base64PNG) {
381 | if (node.filename.length == 0) {
382 | msg.payload = base64PNG;
383 | node.status({
384 | fill : "green",
385 | shape : "ring",
386 | text : "done"
387 | });
388 | delete msg.error;
389 | node.send(msg);
390 | } else {
391 | var base64Data = base64PNG.replace(/^data:image\/png;base64,/, "");
392 | fs.writeFile(node.filename, base64Data, 'base64', function(err) {
393 | if (err) {
394 | sendErrorMsg(node, msg, err.message, "error");
395 | } else {
396 | cropInFile(size, location, node.filename);
397 | }
398 | if (!msg.error) {
399 | node.status({
400 | fill : "green",
401 | shape : "ring",
402 | text : "done"
403 | });
404 | delete msg.error;
405 | node.send(msg);
406 | }
407 | });
408 | }
409 | }).catch(function(errorback) {
410 | sendErrorMsg(node, msg, errorback.message, "error");
411 | });
412 | }).catch(function(errorback) {
413 | sendErrorMsg(node, msg, errorback.message, "error");
414 | });
415 | }).catch(function(errorback) {
416 | sendErrorMsg(node, msg, errorback.message, "error");
417 | });
418 | } catch (ex) {
419 | node.send(msg);
420 | }
421 | };
422 |
423 | function getAbsoluteXPath(driver, element) {
424 | return driver.executeScript("function absoluteXPath(element) {" + "var comp, comps = [];" + "var parent = null;" + "var xpath = '';" + "var getPos = function(element) {" + "var position = 1, curNode;" + "if (element.nodeType == Node.ATTRIBUTE_NODE) {" + "return null;" + "}" + "for (curNode = element.previousSibling; curNode; curNode = curNode.previousSibling){" + "if (curNode.nodeName == element.nodeName) {" + "++position;" + "}" + "}" + "return position;" + "};" + "if (element instanceof Document) {" + "return '/';" + "}" + "for (; element && !(element instanceof Document); element = element.nodeType == Node.ATTRIBUTE_NODE ? element.ownerElement : element.parentNode) {" + "comp = comps[comps.length] = {};" + "switch (element.nodeType) {" + "case Node.TEXT_NODE:" + "comp.name = 'text()';" + "break;" + "case Node.ATTRIBUTE_NODE:" + "comp.name = '@' + element.nodeName;" + "break;" + "case Node.PROCESSING_INSTRUCTION_NODE:" + "comp.name = 'processing-instruction()';" + "break;" + "case Node.COMMENT_NODE:" + "comp.name = 'comment()';" + "break;" + "case Node.ELEMENT_NODE:" + "comp.name = element.nodeName;" + "break;" + "}" + "comp.position = getPos(element);" + "}" + "for (var i = comps.length - 1; i >= 0; i--) {" + "comp = comps[i];" + "xpath += '/' + comp.name.toLowerCase();" + "if (comp.position !== null) {" + "xpath += '[' + comp.position + ']';" + "}" + "}" + "return xpath;" + "} return absoluteXPath(arguments[0]);", element);
425 | }
426 |
427 | function SeleniumServerSetup(n) {
428 | RED.nodes.createNode(this, n);
429 |
430 | this.connected = false;
431 | this.connecting = false;
432 | this.usecount = 0;
433 | // Config node state
434 | this.remoteurl = n.remoteurl;
435 |
436 | var node = this;
437 | this.register = function() {
438 | node.usecount += 1;
439 | };
440 |
441 | this.deregister = function() {
442 | node.usecount -= 1;
443 | if (node.usecount == 0) {
444 | }
445 | };
446 |
447 | this.connect = function(browser) {
448 | if (!node.connected && !node.connecting) {
449 | node.connecting = true;
450 | var url = require('url').parse(node.remoteurl);
451 | return isReachable(url.host)
452 | .then(reachable => {
453 | if (reachable) {
454 | node.driver = new webdriver.Builder().forBrowser(browser).usingServer(node.remoteurl);
455 | node.log(RED._("connected", {
456 | server: ( browser ? browser + "@" : "") + node.remoteurl
457 | }));
458 | node.connected = true;
459 | node.emit('connected');
460 | return node.driver;
461 | } else {
462 | throw "Cannot connect to selenium";
463 | }
464 | })
465 | .catch(error => {
466 | node.connecting = false;
467 | node.connected = false;
468 | throw {
469 | Error: "Invalid configuration: " + error
470 | };
471 | });
472 | } else {
473 | if (node.driver) {
474 | return q(node.driver);
475 | } else {
476 | return q.reject({ Error : "No driver available" });
477 | }
478 | }
479 | };
480 |
481 | this.on('close', function(closecomplete) {
482 | if (this.connected) {
483 | this.on('disconnected', function() {
484 | closecomplete();
485 | });
486 | node.driver.quit();
487 | } else {
488 | closecomplete();
489 | }
490 | });
491 | }
492 |
493 |
494 | RED.nodes.registerType("selenium-server", SeleniumServerSetup);
495 |
496 | function SeleniumOpenURLNode(n) {
497 | RED.nodes.createNode(this, n);
498 | this.name = n.name;
499 | this.server = n.server;
500 | this.browser = n.browser;
501 | this.weburl = n.weburl;
502 | this.width = n.width;
503 | this.height = n.height;
504 | this.webtitle = n.webtitle;
505 | this.timeout = n.timeout;
506 | this.maximized = n.maximized;
507 | this.serverObj = RED.nodes.getNode(this.server);
508 | var node = this;
509 | if (node.serverObj) {
510 | node.serverObj.register();
511 | node.serverObj.connect(node.browser).then(function(webdriver) {
512 | node.status({
513 | fill : "green",
514 | shape : "ring",
515 | text : "connected"
516 | });
517 | }, function(error) {
518 | node.status({
519 | fill : "red",
520 | shape : "ring",
521 | text : "disconnected"
522 | });
523 | });
524 | } else {
525 | node.error("!configuration");
526 | }
527 | this.on("input", function(msg) {
528 | if (msg.topic == "RESET") {
529 | msg.refresh = true;
530 | node.status({});
531 | node.send(msg);
532 | } else {
533 | node.serverObj.connect(node.browser).then(function(webdriver) {
534 | function setWindowSize(driver, title) {
535 | if (node.maximized) {
536 | driver.manage().window().maximize().then(function() {
537 | msg.driver = driver;
538 | msg.payload = title;
539 | node.send(msg);
540 | });
541 | } else {
542 | driver.manage().window().setSize(parseInt(node.width), parseInt(node.height)).then(function() {
543 | msg.driver = driver;
544 | msg.payload = title;
545 | node.send(msg);
546 | });
547 | }
548 | node.status({
549 | fill : "green",
550 | shape : "ring",
551 | text : "connected"
552 | });
553 | }
554 |
555 | var driver = webdriver.build();
556 | driver.get(node.weburl);
557 | if (node.webtitle) {
558 | driver.wait(until.titleIs(node.webtitle), parseInt(node.timeout)).catch(function(errorback) {
559 | node.status({
560 | fill : "yellow",
561 | shape : "ring",
562 | text : "unexpected"
563 | });
564 | }).then(function() {
565 | driver.getTitle().then(function(title) {
566 | setWindowSize(driver, title);
567 | });
568 | });
569 | } else {
570 | setWindowSize(driver);
571 | }
572 | }, function(error) {
573 | node.status({
574 | fill : "red",
575 | shape : "ring",
576 | text : "disconnected"
577 | });
578 | });
579 | }
580 | });
581 | this.on('close', function() {
582 | if (node.serverObj) {
583 | node.serverObj.deregister();
584 | }
585 | });
586 | }
587 |
588 |
589 | RED.nodes.registerType("open-web", SeleniumOpenURLNode);
590 |
591 | function SeleniumCloseBrowserNode(n) {
592 | RED.nodes.createNode(this, n);
593 | this.name = n.name;
594 | this.waitfor = n.waitfor || 0;
595 | var node = this;
596 | this.on("input", function(msg) {
597 | if (msg.refresh) {
598 | msg.refresh = false;
599 | node.status({});
600 | node.send(msg);
601 | } else {
602 | setTimeout(function() {
603 | msg.driver.quit();
604 | node.send(msg);
605 | node.status({
606 | fill : "green",
607 | shape : "ring",
608 | text : "closed"
609 | });
610 | }, node.waitfor);
611 | }
612 | });
613 | }
614 |
615 |
616 | RED.nodes.registerType("close-web", SeleniumCloseBrowserNode);
617 |
618 | function SeleniumFindElementNode(n) {
619 | RED.nodes.createNode(this, n);
620 | this.name = n.name;
621 | this.selector = n.selector;
622 | this.timeout = n.timeout;
623 | this.target = n.target;
624 | this.waitfor = n.waitfor;
625 | var node = this;
626 | this.on("input", function(msg) {
627 | waitUntilElementLocated(node, msg, function(element) {
628 | node.send(msg);
629 | });
630 | });
631 | }
632 |
633 |
634 | RED.nodes.registerType("find-object", SeleniumFindElementNode);
635 |
636 | function SeleniumSendKeysNode(n) {
637 | RED.nodes.createNode(this, n);
638 | this.name = n.name;
639 | this.value = n.text;
640 | this.selector = n.selector;
641 | this.timeout = n.timeout;
642 | this.target = n.target;
643 | this.waitfor = n.waitfor;
644 | this.clearval = n.clearval;
645 | var node = this;
646 | this.on("input", function(msg) {
647 | waitUntilElementLocated(node, msg, function(element) {
648 | sendKeysNode(node, msg);
649 | });
650 | });
651 | }
652 |
653 |
654 | RED.nodes.registerType("send-keys", SeleniumSendKeysNode);
655 |
656 | function SeleniumClickOnNode(n) {
657 | RED.nodes.createNode(this, n);
658 | this.name = n.name;
659 | this.selector = n.selector;
660 | this.timeout = n.timeout;
661 | this.target = n.target;
662 | this.waitfor = n.waitfor;
663 | this.clickon = n.clickon;
664 | var node = this;
665 | this.on("input", function(msg) {
666 | waitUntilElementLocated(node, msg, function(element) {
667 | if (node.clickon) {
668 | if ( typeof (msg.payload) !== "undefined") {
669 | node.___msgs = msg;
670 | node.status({
671 | fill : "blue",
672 | shape : "dot",
673 | text : "click on"
674 | });
675 | } else {
676 | msg = node.___msgs;
677 | if ( typeof (msg) !== "undefined") {
678 | clickOnNode(node, msg);
679 | delete node.___msgs;
680 | }
681 | }
682 | } else {
683 | clickOnNode(node, msg);
684 | }
685 | });
686 | });
687 | }
688 |
689 |
690 | RED.nodes.registerType("click-on", SeleniumClickOnNode);
691 |
692 | function SeleniumSetValueNode(n) {
693 | RED.nodes.createNode(this, n);
694 | this.name = n.name;
695 | this.value = n.text;
696 | this.selector = n.selector;
697 | this.timeout = n.timeout;
698 | this.target = n.target;
699 | this.waitfor = n.waitfor;
700 | var node = this;
701 | this.on("input", function(msg) {
702 | waitUntilElementLocated(node, msg, function(element) {
703 | setValueNode(node, msg);
704 | });
705 | });
706 | }
707 |
708 |
709 | RED.nodes.registerType("set-value", SeleniumSetValueNode);
710 |
711 | function SeleniumToFileNode(n) {
712 | RED.nodes.createNode(this, n);
713 | this.name = n.name;
714 | this.filename = n.filename;
715 | this.waitfor = n.waitfor;
716 | var node = this;
717 | this.on("input", function(msg) {
718 | if (msg.refresh) {
719 | node.status({});
720 | node.send(msg);
721 | } else {
722 | saveToFile(node, msg);
723 | }
724 | });
725 | }
726 |
727 |
728 | RED.nodes.registerType("to-file", SeleniumToFileNode);
729 |
730 | function SeleniumGetValueNode(n) {
731 | RED.nodes.createNode(this, n);
732 | this.name = n.name;
733 | this.expected = n.expected;
734 | this.selector = n.selector;
735 | this.timeout = n.timeout;
736 | this.target = n.target;
737 | this.waitfor = n.waitfor;
738 | this.savetofile = n.savetofile;
739 | var node = this;
740 |
741 | this.on("input", function(msg) {
742 | waitUntilElementLocated(node, msg, function(element) {
743 | getValueNode(node, msg);
744 | });
745 | });
746 | }
747 |
748 |
749 | RED.nodes.registerType("get-value", SeleniumGetValueNode);
750 |
751 | function SeleniumGetAttributeNode(n) {
752 | RED.nodes.createNode(this, n);
753 | this.name = n.name;
754 | this.attribute = n.attribute;
755 | this.expected = n.expected;
756 | this.selector = n.selector;
757 | this.timeout = n.timeout;
758 | this.target = n.target;
759 | this.waitfor = n.waitfor;
760 | this.savetofile = n.savetofile;
761 | var node = this;
762 |
763 | this.on("input", function(msg) {
764 | waitUntilElementLocated(node, msg, function(element) {
765 | getAttributeNode(node, msg);
766 | });
767 | });
768 | }
769 |
770 |
771 | RED.nodes.registerType("get-attribute", SeleniumGetAttributeNode);
772 |
773 | function SeleniumGetTextNode(n) {
774 | RED.nodes.createNode(this, n);
775 | this.name = n.name;
776 | this.expected = n.expected;
777 | this.selector = n.selector;
778 | this.timeout = n.timeout;
779 | this.target = n.target;
780 | this.waitfor = n.waitfor;
781 | this.savetofile = n.savetofile;
782 | var node = this;
783 | this.on("input", function(msg) {
784 | waitUntilElementLocated(node, msg, function(element) {
785 | getTextNode(node, msg);
786 | });
787 | });
788 | }
789 |
790 |
791 | RED.nodes.registerType("get-text", SeleniumGetTextNode);
792 |
793 | function SeleniumRunScriptNode(n) {
794 | RED.nodes.createNode(this, n);
795 | this.name = n.name;
796 | this.func = n.func;
797 | this.waitfor = n.waitfor;
798 | var node = this;
799 | this.on("input", function(msg) {
800 | waitUntilElementLocated(node, msg, function(element) {
801 | runScriptNode(node, msg);
802 | });
803 | });
804 | }
805 |
806 |
807 | RED.nodes.registerType("run-script", SeleniumRunScriptNode);
808 |
809 | function SeleniumTakeScreenshotNode(n) {
810 | RED.nodes.createNode(this, n);
811 | this.name = n.name;
812 | this.selector = n.selector;
813 | this.timeout = n.timeout;
814 | this.target = n.target;
815 | this.waitfor = n.waitfor;
816 | this.filename = n.filename;
817 | var node = this;
818 | this.on("input", function(msg) {
819 | waitUntilElementLocated(node, msg, function(element) {
820 | takeScreenShotNode(node, msg);
821 | });
822 | });
823 | }
824 |
825 |
826 | RED.nodes.registerType("screenshot", SeleniumTakeScreenshotNode);
827 |
828 | function SeleniumNavToNode(n) {
829 | RED.nodes.createNode(this, n);
830 | this.name = n.name;
831 | this.url = n.url;
832 | this.waitfor = n.waitfor;
833 | var node = this;
834 | this.on("input", function(msg) {
835 | if (msg.refresh) {
836 | node.status({});
837 | node.send(msg);
838 | } else if (msg.driver) {
839 | setTimeout(function() {
840 | msg.driver.navigate().to(node.url).then(function() {
841 | node.send(msg);
842 | });
843 | }, node.waitfor);
844 | }
845 | });
846 | }
847 |
848 |
849 | RED.nodes.registerType("nav-to", SeleniumNavToNode);
850 |
851 | function SeleniumNavBackNode(n) {
852 | RED.nodes.createNode(this, n);
853 | this.name = n.name;
854 | this.waitfor = n.waitfor;
855 | var node = this;
856 | this.on("input", function(msg) {
857 | if (msg.refresh) {
858 | node.status({});
859 | node.send(msg);
860 | } else if (msg.driver) {
861 | setTimeout(function() {
862 | msg.driver.navigate().back().then(function() {
863 | node.send(msg);
864 | });
865 | }, node.waitfor);
866 | }
867 | });
868 | }
869 |
870 |
871 | RED.nodes.registerType("nav-back", SeleniumNavBackNode);
872 |
873 | function SeleniumNavForwardNode(n) {
874 | RED.nodes.createNode(this, n);
875 | this.name = n.name;
876 | this.waitfor = n.waitfor;
877 | var node = this;
878 | this.on("input", function(msg) {
879 | if (msg.refresh) {
880 | node.status({});
881 | node.send(msg);
882 | } else if (msg.driver) {
883 | setTimeout(function() {
884 | msg.driver.navigate().forward().then(function() {
885 | node.send(msg);
886 | });
887 | }, node.waitfor);
888 | }
889 | });
890 | }
891 |
892 |
893 | RED.nodes.registerType("nav-forward", SeleniumNavForwardNode);
894 |
895 | function SeleniumNavRefreshNode(n) {
896 | RED.nodes.createNode(this, n);
897 | this.name = n.name;
898 | this.waitfor = n.waitfor;
899 | var node = this;
900 | this.on("input", function(msg) {
901 | if (msg.refresh) {
902 | node.status({});
903 | node.send(msg);
904 | } else if (msg.driver) {
905 | setTimeout(function() {
906 | msg.driver.navigate().refresh().then(function() {
907 | node.send(msg);
908 | });
909 | }, node.waitfor);
910 | }
911 | });
912 | }
913 |
914 |
915 | RED.nodes.registerType("nav-refresh", SeleniumNavRefreshNode);
916 |
917 | RED.httpAdmin.post("/onclick/:id", RED.auth.needsPermission("inject.write"), function(req, res) {
918 | var node = RED.nodes.getNode(req.params.id);
919 | if (node != null) {
920 | try {
921 | node.receive({
922 | waitfor : 1
923 | });
924 | res.sendStatus(200);
925 | } catch(err) {
926 | res.sendStatus(500);
927 | node.error(RED._("inject.failed", {
928 | error : err.toString()
929 | }));
930 | }
931 | } else {
932 | res.sendStatus(404);
933 | }
934 | });
935 | };
936 |
--------------------------------------------------------------------------------