├── .github └── workflows │ └── main-ci.yml ├── .gitignore ├── .npmignore ├── CHANGELOG ├── LICENSE ├── README.md ├── doc ├── example │ └── flows.json └── img │ ├── node-help.png │ ├── wd2.png │ └── workflow.png ├── package.json ├── patch └── http.js ├── src ├── icons │ ├── chrome.svg │ ├── edge.svg │ ├── firefox.svg │ ├── ie.svg │ ├── opera.svg │ └── safari.svg ├── nodes │ ├── click-on.html │ ├── click-on.ts │ ├── close-web.html │ ├── close-web.ts │ ├── find-element.html │ ├── find-element.ts │ ├── get-attribute.html │ ├── get-attribute.ts │ ├── get-text.html │ ├── get-text.ts │ ├── get-title.html │ ├── get-title.ts │ ├── get-value.html │ ├── get-value.ts │ ├── navigate.html │ ├── navigate.ts │ ├── node-constructor.ts │ ├── node.ts │ ├── open-web.html │ ├── open-web.ts │ ├── run-script.html │ ├── run-script.ts │ ├── screenshot.html │ ├── screenshot.ts │ ├── send-keys.html │ ├── send-keys.ts │ ├── set-attribute.html │ ├── set-attribute.ts │ ├── set-value.html │ └── set-value.ts ├── selenium-wd2.ts ├── utils.ts └── wd2-manager.ts ├── tsconfig.json └── tslint.json /.github/workflows/main-ci.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v2 27 | 28 | - name: Setup Node.js environment 29 | uses: actions/setup-node@v2.1.4 30 | 31 | - name: Install chromium 32 | run: sudo apt install chromium-browser 33 | 34 | - name: Install chromium 35 | run: sudo apt install chromium-chromedriver 36 | 37 | # Runs a single command using the runners shell 38 | - name: Install npm dependencies 39 | run: npm install 40 | 41 | - name: Install node-red 42 | run: sudo npm install -g --unsafe-perm node-red 43 | 44 | - name: Install package into node-red 45 | run: sudo npm install -g . 46 | 47 | # Runs a single command using the runners shell 48 | - name: Run node-red 49 | run: node-red > node-red-log.txt 2>&1 & 50 | 51 | # Runs a single command using the runners shell 52 | - name: Run chromedriver 53 | run: chromedriver --verbose > chromedriver-log.txt 2>&1 & 54 | 55 | - name: Wait on 56 | uses: iFaxity/wait-on-action@v1 57 | with: 58 | http-get: http://localhost:1880 59 | 60 | - name: Echo log 61 | run: cat node-red-log.txt 62 | 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | build 4 | dist 5 | .vscode/ 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | .vscode/ 3 | test/ 4 | .gitignore 5 | package-lock.json 6 | tsconfig.json 7 | tslint.json 8 | *.map -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Version 0.1.6 : 2 | - Corrected issue during package installation on node-red 2.2 3 | Version 0.1.5 : 4 | - Corrected incorrect clearVal type thx to MyGg29 5 | Version 0.1.4 : 6 | - Added a system of postinstall to patch selenium-webdriver http client behavior (temporary) 7 | Version 0.1.2 : 8 | - Update of the package Json to correct a type 9 | Version 0.1.1 : 10 | - Update of README file 11 | Version 0.1.0 : 12 | - First official release of selenium-wd2 node-red plugin 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 [yyyy] [name of copyright owner] 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 | ### **This package is no longer maintened, you can switch to [node-red-contrib-simple-webdriver](https://github.com/simonradier/node-red-contrib-simple-webdriver) which provides the same set of feature but not based on selenium to avoid the dependency with Java.** 2 | 3 | 4 | # node-red-contrib-selenium-wd2 5 | Selenium-wd2 nodes for Node-Red allow web browser automation based on the [Selenium-Webdriver](https://www.selenium.dev/documentation/) API. Based on [node-red-constrib-selenium-webdriver](https://flows.nodered.org/node/node-red-contrib-selenium-webdriver) library, it was rewritten in Typescript to ease its maintenance, improve the overall stability and upgrade a little bit the set of features. 6 | 7 | ![wd2 workflow example](https://raw.githubusercontent.com/simonradier/node-red-contrib-selenium-wd2/master/doc/img/workflow.png "wd2 workflow example") 8 | 9 | ## Prerequisite 10 | In order to use node-red-contrib-selenium-wd2, you must fullfill the following prerequisite : 11 | * Install java 8 or later 12 | * Install a selenium server : `npm install -g webdriver-manager` 13 | * Install a node-red server : `npm install -g --unsafe-perm node-red` 14 | 15 | 16 | ## Installation 17 | * Install node-red-contrib-selenium-wd2 library : `npm install -g node-red-contrib-selenium-wd2` and that's all! 18 | 19 | ## Run 20 | Launch Node-red `node-red` and the selenium-wd2 will be loaded automatically. You should see the list of node under the wd2 section. 21 | 22 | ![wd2 section overview](https://raw.githubusercontent.com/simonradier/node-red-contrib-selenium-wd2/master/doc/img/wd2.png "wd2 section") 23 | 24 | 25 | ## Develop 26 | If you want to contribute, you can install clone the project and run the following command : 27 | * `npm run clean && npm run prepublishOnly` (linux only) 28 | 29 | To test it, you will have to : 30 | * Install a node-red locally (in another folder) `npm install -g node-red` 31 | * Launch, from the `node-red` folder, the following command to debug : 32 | 33 | `npm install [PATH_TO_SELENIUM_WD2] && node --inspect node_modules/node-red/red.js` 34 | 35 | ## Behavior 36 | You will always have to start with an 37 | Some nodes will provide two outputs a success and a failure one. 38 | * Success output is used if the node execution is successful and if the flow execution can continue (i.e. the driver is still ok) 39 | * Failure ouput is used in case of "soft" error (an element can't be found or an expected value is not correct). It aims to support dysfonctional use cases. (If something can't be clicked or found) 40 | * Error is launched in case of "critical" error (i.e. the driver can't be used anymore). It means you will have to handle yourselft the cleaning on the selenium-driver side in this case. 41 | 42 | ## Documentation 43 | All nodes provides their own documentation directly inside node-red. 44 | 45 | ![wd2 help overview](https://raw.githubusercontent.com/simonradier/node-red-contrib-selenium-wd2/master/doc/img/node-help.png "wd2 help") 46 | -------------------------------------------------------------------------------- /doc/example/flows.json: -------------------------------------------------------------------------------- 1 | [{"id":"6a5fd7c2.ab7598","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"bf4af36d.b83af","type":"open-web","z":"6a5fd7c2.ab7598","name":"","browser":"chrome","webURL":"https://duckduckgo.com/","width":1280,"height":1024,"timeout":3000,"maximized":true,"headless":false,"serverURL":"http://10.0.1.12:4444/wd/hub","x":370,"y":120,"wires":[["8206817b.71a7c"]]},{"id":"8035baaa.c1b388","type":"inject","z":"6a5fd7c2.ab7598","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":180,"y":120,"wires":[["bf4af36d.b83af"]]},{"id":"8206817b.71a7c","type":"find-element","z":"6a5fd7c2.ab7598","name":"Find Search input","selector":"id","target":"search_form_input_homepage","timeout":3000,"waitFor":"2000","x":390,"y":240,"wires":[["b9d2517e.a6d7c"],["2f745a90.666676"]]},{"id":"9583f32f.cc0f9","type":"close-web","z":"6a5fd7c2.ab7598","name":"","waitFor":"30000","x":1050,"y":320,"wires":[["c7c135a0.c0d6e8"]]},{"id":"c7c135a0.c0d6e8","type":"debug","z":"6a5fd7c2.ab7598","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1190,"y":320,"wires":[]},{"id":"c028eb25.13e088","type":"catch","z":"6a5fd7c2.ab7598","name":"","scope":null,"uncaught":false,"x":180,"y":40,"wires":[["5647d8a4.05c3e8"]]},{"id":"5647d8a4.05c3e8","type":"debug","z":"6a5fd7c2.ab7598","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":410,"y":40,"wires":[]},{"id":"d210dfa4.b7c89","type":"click-on","z":"6a5fd7c2.ab7598","name":"Click on search button","selector":"id","target":"search_button_homepage","timeout":1000,"waitFor":500,"clickOn":false,"x":400,"y":420,"wires":[["858ace24.4a56"],["2f745a90.666676"]]},{"id":"858ace24.4a56","type":"click-on","z":"6a5fd7c2.ab7598","name":"Click on first result","selector":"xpath","target":"//a[contains(@class,\"result__a\")][1]","timeout":"2000","waitFor":3000,"clickOn":true,"x":390,"y":520,"wires":[["a2206fd8.d9eb"],["2f745a90.666676"]]},{"id":"a2206fd8.d9eb","type":"get-title","z":"6a5fd7c2.ab7598","name":"Get web page title","expected":"","timeout":3000,"waitFor":500,"x":390,"y":620,"wires":[["2ba887ee.fc34d8","c2056332.91fa4"],["2f745a90.666676","c2056332.91fa4"]]},{"id":"2f745a90.666676","type":"screenshot","z":"6a5fd7c2.ab7598","name":"","filePath":"./test.png","waitFor":500,"x":870,"y":320,"wires":[["9583f32f.cc0f9"],["9583f32f.cc0f9"]]},{"id":"e99182df.60e55","type":"comment","z":"6a5fd7c2.ab7598","name":"Load Brower and look for node-red website","info":"","x":280,"y":80,"wires":[]},{"id":"a25ea2e3.ea1ec","type":"comment","z":"6a5fd7c2.ab7598","name":"Navigate on Node-Red Website","info":"","x":250,"y":480,"wires":[]},{"id":"2ba887ee.fc34d8","type":"get-text","z":"6a5fd7c2.ab7598","name":"Get H1 text","expected":"Node-RED","selector":"css","target":".title h1","timeout":1000,"waitFor":500,"savetofile":false,"x":370,"y":700,"wires":[["26a1b5e4.c3c18a","c2056332.91fa4"],["2f745a90.666676","c2056332.91fa4"]]},{"id":"26a1b5e4.c3c18a","type":"navigate","z":"6a5fd7c2.ab7598","name":"","url":"https://flows.nodered.org/","navType":"to","waitFor":500,"x":360,"y":780,"wires":[["14574237.855fbe"],["2f745a90.666676"]]},{"id":"e83ff6a8.152d08","type":"click-on","z":"6a5fd7c2.ab7598","name":"Click on first result","selector":"xpath","target":"//div[contains(@class,\"filter-results\")]//a[1]","timeout":1000,"waitFor":500,"clickOn":false,"x":390,"y":940,"wires":[["95e0c843.9bcf48"],["2f745a90.666676"]]},{"id":"14574237.855fbe","type":"send-keys","z":"6a5fd7c2.ab7598","name":"Write search text","keys":"selenium","selector":"id","target":"filter-term","timeout":1000,"waitFor":500,"clearval":false,"x":390,"y":860,"wires":[["e83ff6a8.152d08"],["2f745a90.666676"]]},{"id":"b9d2517e.a6d7c","type":"set-value","z":"6a5fd7c2.ab7598","name":"Set value to search","value":"node-red","selector":"","target":"filter-term","timeout":"2000","waitFor":"1500","x":390,"y":320,"wires":[["d210dfa4.b7c89"],["2f745a90.666676"]]},{"id":"95e0c843.9bcf48","type":"get-attribute","z":"6a5fd7c2.ab7598","name":"Check style from H1 flow-title","attribute":"style","expected":"margin-bottom: 10px;","selector":"className","target":"flow-title","timeout":1000,"waitFor":500,"savetofile":false,"x":400,"y":1020,"wires":[["2f745a90.666676","c2056332.91fa4"],["2f745a90.666676","c2056332.91fa4"]]},{"id":"c2056332.91fa4","type":"debug","z":"6a5fd7c2.ab7598","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":900,"y":760,"wires":[]}] -------------------------------------------------------------------------------- /doc/img/node-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonradier/node-red-contrib-selenium-wd2/a53c47004b53a43452278f35112e0c3d2e9c1af4/doc/img/node-help.png -------------------------------------------------------------------------------- /doc/img/wd2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonradier/node-red-contrib-selenium-wd2/a53c47004b53a43452278f35112e0c3d2e9c1af4/doc/img/wd2.png -------------------------------------------------------------------------------- /doc/img/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonradier/node-red-contrib-selenium-wd2/a53c47004b53a43452278f35112e0c3d2e9c1af4/doc/img/workflow.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-contrib-selenium-wd2", 3 | "version": "0.1.6", 4 | "description": "Selenium-webdriver nodes for Node-RED based on node-red-contrib-selenium-webdriver", 5 | "dependencies": { 6 | "rxjs": "~6.6.3", 7 | "selenium-webdriver": "4.0.0-alpha.7" 8 | }, 9 | "devDependencies": { 10 | "@types/node-red": "~1.1.0", 11 | "@types/selenium-webdriver": "~3.0.8", 12 | "@types/node-red-node-test-helper": "~0.2.1", 13 | "prettier": "~2.1.2", 14 | "tslint": "~6.1.3", 15 | "tslint-config-prettier": "~1.18.0", 16 | "typescript": "~4.0.3", 17 | "typescript-tslint-plugin": "~0.5.5" 18 | }, 19 | "license": "Apache-2.0", 20 | "keywords": [ 21 | "node-red", 22 | "selenium-wd2" 23 | ], 24 | "node-red": { 25 | "nodes": { 26 | "selenium-wd2": "dist/selenium-wd2.js" 27 | } 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/simonradier/node-red-contrib-selenium-wd2" 32 | }, 33 | "engines": { 34 | "node": ">= 14.0.0" 35 | }, 36 | "scripts": { 37 | "prepublishOnly": "mkdir dist && cp -r src/icons dist/icons && cat src/nodes/*.html > dist/selenium-wd2.html && tsc", 38 | "clean": "rm -rf dist", 39 | "postinstall": "cp ./patch/http.js ../selenium-webdriver/lib/" 40 | }, 41 | "author": { 42 | "name": "Simon Radier" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /patch/http.js: -------------------------------------------------------------------------------- 1 | // Licensed to the Software Freedom Conservancy (SFC) under one 2 | // or more contributor license agreements. See the NOTICE file 3 | // distributed with this work for additional information 4 | // regarding copyright ownership. The SFC licenses this file 5 | // to you under the Apache License, Version 2.0 (the 6 | // "License"); you may not use this file except in compliance 7 | // with the License. You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, 12 | // software distributed under the License is distributed on an 13 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | // KIND, either express or implied. See the License for the 15 | // specific language governing permissions and limitations 16 | // under the License. 17 | 18 | /** 19 | * @fileoverview Defines an environment agnostic {@linkplain cmd.Executor 20 | * command executor} that communicates with a remote end using JSON over HTTP. 21 | * 22 | * Clients should implement the {@link Client} interface, which is used by 23 | * the {@link Executor} to send commands to the remote end. 24 | */ 25 | 26 | 'use strict'; 27 | 28 | const cmd = require('./command'); 29 | const error = require('./error'); 30 | const logging = require('./logging'); 31 | const promise = require('./promise'); 32 | const {Session} = require('./session'); 33 | const {WebElement} = require('./webdriver'); 34 | 35 | const getAttribute = requireAtom( 36 | 'get-attribute.js', 37 | '//javascript/node/selenium-webdriver/lib/atoms:get-attribute.js'); 38 | const isDisplayed = requireAtom( 39 | 'is-displayed.js', 40 | '//javascript/node/selenium-webdriver/lib/atoms:is-displayed.js'); 41 | 42 | /** 43 | * @param {string} module 44 | * @param {string} bazelTarget 45 | * @return {!Function} 46 | */ 47 | function requireAtom(module, bazelTarget) { 48 | try { 49 | return require('./atoms/' + module); 50 | } catch (ex) { 51 | try { 52 | const file = bazelTarget.slice(2).replace(':', '/'); 53 | return require(`../../../../bazel-genfiles/${file}`); 54 | } catch (ex2) { 55 | console.log(ex2); 56 | throw Error( 57 | `Failed to import atoms module ${module}. If running in dev mode, you` 58 | + ` need to run \`bazel build ${bazelTarget}\` from the project` 59 | + `root: ${ex}`); 60 | } 61 | } 62 | } 63 | 64 | 65 | /** 66 | * Converts a headers map to a HTTP header block string. 67 | * @param {!Map} headers The map to convert. 68 | * @return {string} The headers as a string. 69 | */ 70 | function headersToString(headers) { 71 | let ret = []; 72 | headers.forEach(function(value, name) { 73 | ret.push(`${name.toLowerCase()}: ${value}`); 74 | }); 75 | return ret.join('\n'); 76 | } 77 | 78 | 79 | /** 80 | * Represents a HTTP request message. This class is a "partial" request and only 81 | * defines the path on the server to send a request to. It is each client's 82 | * responsibility to build the full URL for the final request. 83 | * @final 84 | */ 85 | class Request { 86 | /** 87 | * @param {string} method The HTTP method to use for the request. 88 | * @param {string} path The path on the server to send the request to. 89 | * @param {Object=} opt_data This request's non-serialized JSON payload data. 90 | */ 91 | constructor(method, path, opt_data) { 92 | this.method = /** string */method; 93 | this.path = /** string */path; 94 | this.data = /** Object */opt_data; 95 | this.headers = /** !Map */new Map( 96 | [['Accept', 'application/json; charset=utf-8']]); 97 | } 98 | 99 | /** @override */ 100 | toString() { 101 | let ret = `${this.method} ${this.path} HTTP/1.1\n`; 102 | ret += headersToString(this.headers) + '\n\n'; 103 | if (this.data) { 104 | ret += JSON.stringify(this.data); 105 | } 106 | return ret; 107 | } 108 | } 109 | 110 | 111 | /** 112 | * Represents a HTTP response message. 113 | * @final 114 | */ 115 | class Response { 116 | /** 117 | * @param {number} status The response code. 118 | * @param {!Object} headers The response headers. All header names 119 | * will be converted to lowercase strings for consistent lookups. 120 | * @param {string} body The response body. 121 | */ 122 | constructor(status, headers, body) { 123 | this.status = /** number */status; 124 | this.body = /** string */body; 125 | this.headers = /** !Map*/new Map; 126 | for (let header in headers) { 127 | this.headers.set(header.toLowerCase(), headers[header]); 128 | } 129 | } 130 | 131 | /** @override */ 132 | toString() { 133 | let ret = `HTTP/1.1 ${this.status}\n${headersToString(this.headers)}\n\n`; 134 | if (this.body) { 135 | ret += this.body; 136 | } 137 | return ret; 138 | } 139 | } 140 | 141 | 142 | const DEV_ROOT = '../../../../buck-out/gen/javascript/'; 143 | 144 | /** @enum {!Function} */ 145 | const Atom = { 146 | GET_ATTRIBUTE: getAttribute, 147 | IS_DISPLAYED: isDisplayed 148 | }; 149 | 150 | 151 | const LOG = logging.getLogger('webdriver.http'); 152 | 153 | 154 | function post(path) { return resource('POST', path); } 155 | function del(path) { return resource('DELETE', path); } 156 | function get(path) { return resource('GET', path); } 157 | function resource(method, path) { return {method: method, path: path}; } 158 | 159 | 160 | /** @typedef {{method: string, path: string}} */ 161 | var CommandSpec; 162 | 163 | 164 | /** @typedef {function(!cmd.Command): !cmd.Command} */ 165 | var CommandTransformer; 166 | 167 | 168 | class InternalTypeError extends TypeError {} 169 | 170 | 171 | /** 172 | * @param {!cmd.Command} command The initial command. 173 | * @param {Atom} atom The name of the atom to execute. 174 | * @return {!cmd.Command} The transformed command to execute. 175 | */ 176 | function toExecuteAtomCommand(command, atom, ...params) { 177 | if (typeof atom !== 'function') { 178 | throw new InternalTypeError('atom is not a function: ' + typeof atom); 179 | } 180 | 181 | return new cmd.Command(cmd.Name.EXECUTE_SCRIPT) 182 | .setParameter('sessionId', command.getParameter('sessionId')) 183 | .setParameter('script', `return (${atom}).apply(null, arguments)`) 184 | .setParameter('args', params.map(param => command.getParameter(param))); 185 | } 186 | 187 | 188 | 189 | /** @const {!Map} */ 190 | const COMMAND_MAP = new Map([ 191 | [cmd.Name.GET_SERVER_STATUS, get('/status')], 192 | [cmd.Name.NEW_SESSION, post('/session')], 193 | [cmd.Name.GET_SESSIONS, get('/sessions')], 194 | [cmd.Name.QUIT, del('/session/:sessionId')], 195 | [cmd.Name.CLOSE, del('/session/:sessionId/window')], 196 | [cmd.Name.GET_CURRENT_WINDOW_HANDLE, get('/session/:sessionId/window_handle')], 197 | [cmd.Name.GET_WINDOW_HANDLES, get('/session/:sessionId/window_handles')], 198 | [cmd.Name.GET_CURRENT_URL, get('/session/:sessionId/url')], 199 | [cmd.Name.GET, post('/session/:sessionId/url')], 200 | [cmd.Name.GO_BACK, post('/session/:sessionId/back')], 201 | [cmd.Name.GO_FORWARD, post('/session/:sessionId/forward')], 202 | [cmd.Name.REFRESH, post('/session/:sessionId/refresh')], 203 | [cmd.Name.ADD_COOKIE, post('/session/:sessionId/cookie')], 204 | [cmd.Name.GET_ALL_COOKIES, get('/session/:sessionId/cookie')], 205 | [cmd.Name.DELETE_ALL_COOKIES, del('/session/:sessionId/cookie')], 206 | [cmd.Name.DELETE_COOKIE, del('/session/:sessionId/cookie/:name')], 207 | [cmd.Name.FIND_ELEMENT, post('/session/:sessionId/element')], 208 | [cmd.Name.FIND_ELEMENTS, post('/session/:sessionId/elements')], 209 | [cmd.Name.GET_ACTIVE_ELEMENT, post('/session/:sessionId/element/active')], 210 | [cmd.Name.FIND_CHILD_ELEMENT, post('/session/:sessionId/element/:id/element')], 211 | [cmd.Name.FIND_CHILD_ELEMENTS, post('/session/:sessionId/element/:id/elements')], 212 | [cmd.Name.CLEAR_ELEMENT, post('/session/:sessionId/element/:id/clear')], 213 | [cmd.Name.CLICK_ELEMENT, post('/session/:sessionId/element/:id/click')], 214 | [cmd.Name.SEND_KEYS_TO_ELEMENT, post('/session/:sessionId/element/:id/value')], 215 | [cmd.Name.SUBMIT_ELEMENT, post('/session/:sessionId/element/:id/submit')], 216 | [cmd.Name.GET_ELEMENT_TEXT, get('/session/:sessionId/element/:id/text')], 217 | [cmd.Name.GET_ELEMENT_TAG_NAME, get('/session/:sessionId/element/:id/name')], 218 | [cmd.Name.IS_ELEMENT_SELECTED, get('/session/:sessionId/element/:id/selected')], 219 | [cmd.Name.IS_ELEMENT_ENABLED, get('/session/:sessionId/element/:id/enabled')], 220 | [cmd.Name.IS_ELEMENT_DISPLAYED, get('/session/:sessionId/element/:id/displayed')], 221 | [cmd.Name.GET_ELEMENT_LOCATION, get('/session/:sessionId/element/:id/location')], 222 | [cmd.Name.GET_ELEMENT_SIZE, get('/session/:sessionId/element/:id/size')], 223 | [cmd.Name.GET_ELEMENT_ATTRIBUTE, get('/session/:sessionId/element/:id/attribute/:name')], 224 | [cmd.Name.GET_ELEMENT_PROPERTY, get('/session/:sessionId/element/:id/property/:name')], 225 | [cmd.Name.GET_ELEMENT_VALUE_OF_CSS_PROPERTY, get('/session/:sessionId/element/:id/css/:propertyName')], 226 | [cmd.Name.TAKE_ELEMENT_SCREENSHOT, get('/session/:sessionId/element/:id/screenshot')], 227 | [cmd.Name.SWITCH_TO_WINDOW, post('/session/:sessionId/window')], 228 | [cmd.Name.MAXIMIZE_WINDOW, post('/session/:sessionId/window/current/maximize')], 229 | [cmd.Name.GET_WINDOW_POSITION, get('/session/:sessionId/window/current/position')], 230 | [cmd.Name.SET_WINDOW_POSITION, post('/session/:sessionId/window/current/position')], 231 | [cmd.Name.GET_WINDOW_SIZE, get('/session/:sessionId/window/current/size')], 232 | [cmd.Name.SET_WINDOW_SIZE, post('/session/:sessionId/window/current/size')], 233 | [cmd.Name.SWITCH_TO_FRAME, post('/session/:sessionId/frame')], 234 | [cmd.Name.SWITCH_TO_FRAME_PARENT, post('/session/:sessionId/frame/parent')], 235 | [cmd.Name.GET_PAGE_SOURCE, get('/session/:sessionId/source')], 236 | [cmd.Name.GET_TITLE, get('/session/:sessionId/title')], 237 | [cmd.Name.EXECUTE_SCRIPT, post('/session/:sessionId/execute')], 238 | [cmd.Name.EXECUTE_ASYNC_SCRIPT, post('/session/:sessionId/execute_async')], 239 | [cmd.Name.SCREENSHOT, get('/session/:sessionId/screenshot')], 240 | [cmd.Name.GET_TIMEOUT, get('/session/:sessionId/timeouts')], 241 | [cmd.Name.SET_TIMEOUT, post('/session/:sessionId/timeouts')], 242 | [cmd.Name.ACCEPT_ALERT, post('/session/:sessionId/accept_alert')], 243 | [cmd.Name.DISMISS_ALERT, post('/session/:sessionId/dismiss_alert')], 244 | [cmd.Name.GET_ALERT_TEXT, get('/session/:sessionId/alert_text')], 245 | [cmd.Name.SET_ALERT_TEXT, post('/session/:sessionId/alert_text')], 246 | [cmd.Name.GET_LOG, post('/session/:sessionId/log')], 247 | [cmd.Name.GET_AVAILABLE_LOG_TYPES, get('/session/:sessionId/log/types')], 248 | [cmd.Name.GET_SESSION_LOGS, post('/logs')], 249 | [cmd.Name.UPLOAD_FILE, post('/session/:sessionId/file')], 250 | [cmd.Name.LEGACY_ACTION_CLICK, post('/session/:sessionId/click')], 251 | [cmd.Name.LEGACY_ACTION_DOUBLE_CLICK, post('/session/:sessionId/doubleclick')], 252 | [cmd.Name.LEGACY_ACTION_MOUSE_DOWN, post('/session/:sessionId/buttondown')], 253 | [cmd.Name.LEGACY_ACTION_MOUSE_UP, post('/session/:sessionId/buttonup')], 254 | [cmd.Name.LEGACY_ACTION_MOUSE_MOVE, post('/session/:sessionId/moveto')], 255 | [cmd.Name.LEGACY_ACTION_SEND_KEYS, post('/session/:sessionId/keys')], 256 | [cmd.Name.LEGACY_ACTION_TOUCH_DOWN, post('/session/:sessionId/touch/down')], 257 | [cmd.Name.LEGACY_ACTION_TOUCH_UP, post('/session/:sessionId/touch/up')], 258 | [cmd.Name.LEGACY_ACTION_TOUCH_MOVE, post('/session/:sessionId/touch/move')], 259 | [cmd.Name.LEGACY_ACTION_TOUCH_SCROLL, post('/session/:sessionId/touch/scroll')], 260 | [cmd.Name.LEGACY_ACTION_TOUCH_LONG_PRESS, post('/session/:sessionId/touch/longclick')], 261 | [cmd.Name.LEGACY_ACTION_TOUCH_FLICK, post('/session/:sessionId/touch/flick')], 262 | [cmd.Name.LEGACY_ACTION_TOUCH_SINGLE_TAP, post('/session/:sessionId/touch/click')], 263 | [cmd.Name.LEGACY_ACTION_TOUCH_DOUBLE_TAP, post('/session/:sessionId/touch/doubleclick')], 264 | ]); 265 | 266 | 267 | /** @const {!Map} */ 268 | const W3C_COMMAND_MAP = new Map([ 269 | // Server status. 270 | [cmd.Name.GET_SERVER_STATUS, get('/status')], 271 | // Session management. 272 | [cmd.Name.NEW_SESSION, post('/session')], 273 | [cmd.Name.QUIT, del('/session/:sessionId')], 274 | [cmd.Name.GET_TIMEOUT, get('/session/:sessionId/timeouts')], 275 | [cmd.Name.SET_TIMEOUT, post('/session/:sessionId/timeouts')], 276 | // Navigation. 277 | [cmd.Name.GET_CURRENT_URL, get('/session/:sessionId/url')], 278 | [cmd.Name.GET, post('/session/:sessionId/url')], 279 | [cmd.Name.GO_BACK, post('/session/:sessionId/back')], 280 | [cmd.Name.GO_FORWARD, post('/session/:sessionId/forward')], 281 | [cmd.Name.REFRESH, post('/session/:sessionId/refresh')], 282 | // Page inspection. 283 | [cmd.Name.GET_PAGE_SOURCE, get('/session/:sessionId/source')], 284 | [cmd.Name.GET_TITLE, get('/session/:sessionId/title')], 285 | // Script execution. 286 | [cmd.Name.EXECUTE_SCRIPT, post('/session/:sessionId/execute/sync')], 287 | [cmd.Name.EXECUTE_ASYNC_SCRIPT, post('/session/:sessionId/execute/async')], 288 | // Frame selection. 289 | [cmd.Name.SWITCH_TO_FRAME, post('/session/:sessionId/frame')], 290 | [cmd.Name.SWITCH_TO_FRAME_PARENT, post('/session/:sessionId/frame/parent')], 291 | // Window management. 292 | [cmd.Name.GET_CURRENT_WINDOW_HANDLE, get('/session/:sessionId/window')], 293 | [cmd.Name.CLOSE, del('/session/:sessionId/window')], 294 | [cmd.Name.SWITCH_TO_WINDOW, post('/session/:sessionId/window')], 295 | [cmd.Name.SWITCH_TO_NEW_WINDOW, post('/session/:sessionId/window/new')], 296 | [cmd.Name.GET_WINDOW_HANDLES, get('/session/:sessionId/window/handles')], 297 | [cmd.Name.GET_WINDOW_RECT, get('/session/:sessionId/window/rect')], 298 | [cmd.Name.SET_WINDOW_RECT, post('/session/:sessionId/window/rect')], 299 | [cmd.Name.MAXIMIZE_WINDOW, post('/session/:sessionId/window/maximize')], 300 | [cmd.Name.MINIMIZE_WINDOW, post('/session/:sessionId/window/minimize')], 301 | [cmd.Name.FULLSCREEN_WINDOW, post('/session/:sessionId/window/fullscreen')], 302 | // Actions. 303 | [cmd.Name.ACTIONS, post('/session/:sessionId/actions')], 304 | [cmd.Name.CLEAR_ACTIONS, del('/session/:sessionId/actions')], 305 | // Locating elements. 306 | [cmd.Name.GET_ACTIVE_ELEMENT, get('/session/:sessionId/element/active')], 307 | [cmd.Name.FIND_ELEMENT, post('/session/:sessionId/element')], 308 | [cmd.Name.FIND_ELEMENTS, post('/session/:sessionId/elements')], 309 | [cmd.Name.FIND_CHILD_ELEMENT, post('/session/:sessionId/element/:id/element')], 310 | [cmd.Name.FIND_CHILD_ELEMENTS, post('/session/:sessionId/element/:id/elements')], 311 | // Element interaction. 312 | [cmd.Name.GET_ELEMENT_TAG_NAME, get('/session/:sessionId/element/:id/name')], 313 | [cmd.Name.GET_ELEMENT_PROPERTY, get('/session/:sessionId/element/:id/property/:name')], 314 | [cmd.Name.GET_ELEMENT_VALUE_OF_CSS_PROPERTY, get('/session/:sessionId/element/:id/css/:propertyName')], 315 | [cmd.Name.GET_ELEMENT_RECT, get('/session/:sessionId/element/:id/rect')], 316 | [cmd.Name.CLEAR_ELEMENT, post('/session/:sessionId/element/:id/clear')], 317 | [cmd.Name.CLICK_ELEMENT, post('/session/:sessionId/element/:id/click')], 318 | [cmd.Name.SEND_KEYS_TO_ELEMENT, post('/session/:sessionId/element/:id/value')], 319 | [cmd.Name.GET_ELEMENT_TEXT, get('/session/:sessionId/element/:id/text')], 320 | [cmd.Name.IS_ELEMENT_ENABLED, get('/session/:sessionId/element/:id/enabled')], 321 | [cmd.Name.GET_ELEMENT_ATTRIBUTE, (cmd) => { 322 | return toExecuteAtomCommand(cmd, Atom.GET_ATTRIBUTE, 'id', 'name'); 323 | }], 324 | [cmd.Name.IS_ELEMENT_DISPLAYED, (cmd) => { 325 | return toExecuteAtomCommand(cmd, Atom.IS_DISPLAYED, 'id'); 326 | }], 327 | // Cookie management. 328 | [cmd.Name.GET_ALL_COOKIES, get('/session/:sessionId/cookie')], 329 | [cmd.Name.ADD_COOKIE, post('/session/:sessionId/cookie')], 330 | [cmd.Name.DELETE_ALL_COOKIES, del('/session/:sessionId/cookie')], 331 | [cmd.Name.GET_COOKIE, get('/session/:sessionId/cookie/:name')], 332 | [cmd.Name.DELETE_COOKIE, del('/session/:sessionId/cookie/:name')], 333 | // Alert management. 334 | [cmd.Name.ACCEPT_ALERT, post('/session/:sessionId/alert/accept')], 335 | [cmd.Name.DISMISS_ALERT, post('/session/:sessionId/alert/dismiss')], 336 | [cmd.Name.GET_ALERT_TEXT, get('/session/:sessionId/alert/text')], 337 | [cmd.Name.SET_ALERT_TEXT, post('/session/:sessionId/alert/text')], 338 | // Screenshots. 339 | [cmd.Name.SCREENSHOT, get('/session/:sessionId/screenshot')], 340 | [cmd.Name.TAKE_ELEMENT_SCREENSHOT, get('/session/:sessionId/element/:id/screenshot')], 341 | // Log extensions. 342 | [cmd.Name.GET_LOG, post('/session/:sessionId/se/log')], 343 | [cmd.Name.GET_AVAILABLE_LOG_TYPES, get('/session/:sessionId/se/log/types')], 344 | ]); 345 | 346 | 347 | /** 348 | * Handles sending HTTP messages to a remote end. 349 | * 350 | * @interface 351 | */ 352 | class Client { 353 | 354 | /** 355 | * Sends a request to the server. The client will automatically follow any 356 | * redirects returned by the server, fulfilling the returned promise with the 357 | * final response. 358 | * 359 | * @param {!Request} httpRequest The request to send. 360 | * @return {!Promise} A promise that will be fulfilled with the 361 | * server's response. 362 | */ 363 | send(httpRequest) {} 364 | } 365 | 366 | 367 | 368 | /** 369 | * @param {Map} customCommands 370 | * A map of custom command definitions. 371 | * @param {boolean} w3c Whether to use W3C command mappings. 372 | * @param {!cmd.Command} command The command to resolve. 373 | * @return {!Request} A promise that will resolve with the 374 | * command to execute. 375 | */ 376 | function buildRequest(customCommands, w3c, command) { 377 | LOG.finest(() => `Translating command: ${command.getName()}`); 378 | let spec = customCommands && customCommands.get(command.getName()); 379 | if (spec) { 380 | return toHttpRequest(spec); 381 | } 382 | 383 | if (w3c) { 384 | spec = W3C_COMMAND_MAP.get(command.getName()); 385 | if (typeof spec === 'function') { 386 | LOG.finest(() => `Transforming command for W3C: ${command.getName()}`); 387 | let newCommand = spec(command); 388 | return buildRequest(customCommands, w3c, newCommand); 389 | } else if (spec) { 390 | return toHttpRequest(spec); 391 | } 392 | } 393 | 394 | spec = COMMAND_MAP.get(command.getName()); 395 | if (spec) { 396 | return toHttpRequest(spec); 397 | } 398 | throw new error.UnknownCommandError( 399 | 'Unrecognized command: ' + command.getName()); 400 | 401 | /** 402 | * @param {CommandSpec} resource 403 | * @return {!Request} 404 | */ 405 | function toHttpRequest(resource) { 406 | LOG.finest(() => `Building HTTP request: ${JSON.stringify(resource)}`); 407 | let parameters = command.getParameters(); 408 | let path = buildPath(resource.path, parameters); 409 | return new Request(resource.method, path, parameters); 410 | } 411 | } 412 | 413 | 414 | const CLIENTS = 415 | /** !WeakMap)> */new WeakMap; 416 | 417 | 418 | /** 419 | * A command executor that communicates with the server using JSON over HTTP. 420 | * 421 | * By default, each instance of this class will use the legacy wire protocol 422 | * from [Selenium project][json]. The executor will automatically switch to the 423 | * [W3C wire protocol][w3c] if the remote end returns a compliant response to 424 | * a new session command. 425 | * 426 | * [json]: https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol 427 | * [w3c]: https://w3c.github.io/webdriver/webdriver-spec.html 428 | * 429 | * @implements {cmd.Executor} 430 | */ 431 | class Executor { 432 | /** 433 | * @param {!(Client|IThenable)} client The client to use for sending 434 | * requests to the server, or a promise-like object that will resolve to 435 | * to the client. 436 | */ 437 | constructor(client) { 438 | CLIENTS.set(this, client); 439 | this.client = client; 440 | 441 | /** 442 | * Whether this executor should use the W3C wire protocol. The executor 443 | * will automatically switch if the remote end sends a compliant response 444 | * to a new session command, however, this property may be directly set to 445 | * `true` to force the executor into W3C mode. 446 | * @type {boolean} 447 | */ 448 | this.w3c = false; 449 | 450 | /** @private {Map} */ 451 | this.customCommands_ = null; 452 | 453 | /** @private {!logging.Logger} */ 454 | this.log_ = logging.getLogger('webdriver.http.Executor'); 455 | } 456 | 457 | /** 458 | * Defines a new command for use with this executor. When a command is sent, 459 | * the {@code path} will be preprocessed using the command's parameters; any 460 | * path segments prefixed with ":" will be replaced by the parameter of the 461 | * same name. For example, given "/person/:name" and the parameters 462 | * "{name: 'Bob'}", the final command path will be "/person/Bob". 463 | * 464 | * @param {string} name The command name. 465 | * @param {string} method The HTTP method to use when sending this command. 466 | * @param {string} path The path to send the command to, relative to 467 | * the WebDriver server's command root and of the form 468 | * "/path/:variable/segment". 469 | */ 470 | defineCommand(name, method, path) { 471 | if (!this.customCommands_) { 472 | this.customCommands_ = new Map; 473 | } 474 | this.customCommands_.set(name, {method, path}); 475 | } 476 | 477 | /** @override */ 478 | async execute(command) { 479 | let request = buildRequest(this.customCommands_, this.w3c, command); 480 | this.log_.finer(() => `>>> ${request.method} ${request.path}`); 481 | 482 | let client = CLIENTS.get(this) || this.client; 483 | if (promise.isPromise(client)) { 484 | client = await client; 485 | CLIENTS.set(this, client); 486 | } 487 | 488 | let response = await client.send(request); 489 | this.log_.finer(() => `>>>\n${request}\n<<<\n${response}`); 490 | 491 | let httpResponse = /** @type {!Response} */(response); 492 | let {isW3C, value} = parseHttpResponse(command, httpResponse); 493 | 494 | if (command.getName() === cmd.Name.NEW_SESSION) { 495 | if (!value || !value.sessionId) { 496 | throw new error.WebDriverError( 497 | `Unable to parse new session response: ${response.body}`); 498 | } 499 | 500 | // The remote end is a W3C compliant server if there is no `status` 501 | // field in the response. 502 | if (command.getName() === cmd.Name.NEW_SESSION) { 503 | this.w3c = this.w3c || isW3C; 504 | } 505 | 506 | // No implementations use the `capabilities` key yet... 507 | let capabilities = value.capabilities || value.value; 508 | return new Session( 509 | /** @type {{sessionId: string}} */(value).sessionId, capabilities); 510 | } 511 | 512 | return typeof value === 'undefined' ? null : value; 513 | } 514 | } 515 | 516 | 517 | /** 518 | * @param {string} str . 519 | * @return {?} . 520 | */ 521 | function tryParse(str) { 522 | try { 523 | return JSON.parse(str); 524 | } catch (ignored) { 525 | // Do nothing. 526 | } 527 | } 528 | 529 | 530 | /** 531 | * Callback used to parse {@link Response} objects from a 532 | * {@link HttpClient}. 533 | * 534 | * @param {!cmd.Command} command The command the response is for. 535 | * @param {!Response} httpResponse The HTTP response to parse. 536 | * @return {{isW3C: boolean, value: ?}} An object describing the parsed 537 | * response. This object will have two fields: `isW3C` indicates whether 538 | * the response looks like it came from a remote end that conforms with the 539 | * W3C WebDriver spec, and `value`, the actual response value. 540 | * @throws {WebDriverError} If the HTTP response is an error. 541 | */ 542 | function parseHttpResponse(command, httpResponse) { 543 | if (httpResponse.status < 200) { 544 | // This should never happen, but throw the raw response so users report it. 545 | throw new error.WebDriverError( 546 | `Unexpected HTTP response:\n${httpResponse}`); 547 | } 548 | 549 | let parsed = tryParse(httpResponse.body); 550 | if (parsed && typeof parsed === 'object') { 551 | let value = parsed.value; 552 | let isW3C = 553 | value !== null && typeof value === 'object' 554 | && typeof parsed.status === 'undefined'; 555 | 556 | if (!isW3C) { 557 | error.checkLegacyResponse(parsed); 558 | 559 | // Adjust legacy new session responses to look like W3C to simplify 560 | // later processing. 561 | if (command.getName() === cmd.Name.NEW_SESSION) { 562 | value = parsed; 563 | } 564 | 565 | } else if (httpResponse.status > 399) { 566 | error.throwDecodedError(value); 567 | } 568 | 569 | return {isW3C, value}; 570 | } 571 | 572 | if (parsed !== undefined) { 573 | return {isW3C: false, value: parsed}; 574 | } 575 | 576 | let value = httpResponse.body.replace(/\r\n/g, '\n'); 577 | 578 | // 404 represents an unknown command; anything else > 399 is a generic unknown 579 | // error. 580 | if (httpResponse.status == 404) { 581 | throw new error.UnsupportedOperationError(command.getName() + ': ' + value); 582 | } else if (httpResponse.status >= 400) { 583 | throw new error.WebDriverError(value); 584 | } 585 | 586 | return {isW3C: false, value: value || null}; 587 | } 588 | 589 | 590 | /** 591 | * Builds a fully qualified path using the given set of command parameters. Each 592 | * path segment prefixed with ':' will be replaced by the value of the 593 | * corresponding parameter. All parameters spliced into the path will be 594 | * removed from the parameter map. 595 | * @param {string} path The original resource path. 596 | * @param {!Object<*>} parameters The parameters object to splice into the path. 597 | * @return {string} The modified path. 598 | */ 599 | function buildPath(path, parameters) { 600 | let pathParameters = path.match(/\/:(\w+)\b/g); 601 | if (pathParameters) { 602 | for (let i = 0; i < pathParameters.length; ++i) { 603 | let key = pathParameters[i].substring(2); // Trim the /: 604 | if (key in parameters) { 605 | let value = parameters[key]; 606 | if (WebElement.isId(value)) { 607 | // When inserting a WebElement into the URL, only use its ID value, 608 | // not the full JSON. 609 | value = WebElement.extractId(value); 610 | } 611 | path = path.replace(pathParameters[i], '/' + value); 612 | delete parameters[key]; 613 | } else { 614 | throw new error.InvalidArgumentError( 615 | 'Missing required parameter: ' + key); 616 | } 617 | } 618 | } 619 | return path; 620 | } 621 | 622 | 623 | // PUBLIC API 624 | 625 | exports.Executor = Executor; 626 | exports.Client = Client; 627 | exports.Request = Request; 628 | exports.Response = Response; 629 | exports.buildPath = buildPath; // Exported for testing. 630 | -------------------------------------------------------------------------------- /src/icons/chrome.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/edge.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/firefox.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/ie.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/opera.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/safari.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/nodes/click-on.html: -------------------------------------------------------------------------------- 1 | 41 | 42 | 110 | 111 | -------------------------------------------------------------------------------- /src/nodes/click-on.ts: -------------------------------------------------------------------------------- 1 | import { WD2Manager } from "../wd2-manager"; 2 | import { SeleniumAction, SeleniumMsg, SeleniumNode, SeleniumNodeDef, waitForElement} from "./node"; 3 | import { GenericSeleniumConstructor } from "./node-constructor"; 4 | 5 | export interface NodeClickOnDef extends SeleniumNodeDef { 6 | clickOn? : boolean; 7 | } 8 | 9 | export interface NodeClickOn extends SeleniumNode { 10 | __msg : SeleniumMsg; 11 | } 12 | 13 | async function inputPreCondAction (node : NodeClickOn, conf : NodeClickOnDef, action : SeleniumAction) : Promise { 14 | return new Promise(async (resolve, reject) => { 15 | let msg = action.msg; 16 | if (msg.click && node.__msg) { 17 | msg = node.__msg; // msg restoration 18 | try { 19 | await msg.element.click() 20 | node.status({ fill : "green", shape : "dot", text : "success"}) 21 | if (msg.error) { delete msg.error; } 22 | action.send([msg, null]); 23 | action.done(); 24 | } catch(err) { 25 | if (WD2Manager.checkIfCritical(err)) { 26 | reject(err); 27 | } else { 28 | msg.error = { 29 | value : "Can't click on the the element : " + err.message 30 | }; 31 | node.status({ fill : "yellow", shape : "dot", text : "click error"}) 32 | action.send([null, msg]); 33 | action.done(); 34 | } 35 | } 36 | resolve(false); // We don't want to execute the full node 37 | } 38 | if (msg.click && !node.__msg) { 39 | node.status({ fill : "yellow", shape : "ring", text : "ignored"}); 40 | setTimeout(() => { 41 | node.status({}); 42 | }, 3000); 43 | resolve(false); 44 | } 45 | resolve(true); 46 | }); 47 | } 48 | 49 | async function inputAction (node : NodeClickOn, conf : NodeClickOnDef, action : SeleniumAction) : Promise { 50 | const msg = action.msg; 51 | return new Promise (async (resolve, reject) => { 52 | if (!conf.clickOn) { 53 | try { 54 | await msg.element.click() 55 | node.status({ fill : "green", shape : "dot", text : "success"}) 56 | if (msg.error) { delete msg.error; } 57 | action.send([msg, null]); 58 | action.done(); 59 | } catch(err) { 60 | if (WD2Manager.checkIfCritical(err)) { 61 | reject(err); 62 | } else { 63 | msg.error = { 64 | value : "Can't click on the the element : " + err.message 65 | }; 66 | node.status({ fill : "yellow", shape : "dot", text : "click error"}) 67 | action.send([null, msg]); 68 | action.done(); 69 | } 70 | } 71 | } else { // If we have to wait for the user click and we save the msg 72 | node.status({ fill : "blue", shape : "dot", text : "waiting for user click"}); 73 | node.__msg = msg; 74 | } 75 | resolve(); 76 | }) 77 | } 78 | 79 | const NodeClickOnConstructor = GenericSeleniumConstructor(inputPreCondAction, inputAction); 80 | 81 | export { NodeClickOnConstructor as NodeClickOnConstructor} 82 | 83 | export function NodeClickPrerequisite () { 84 | WD2Manager.RED.httpAdmin.post("/onclick/:id", WD2Manager.RED.auth.needsPermission("inject.write"), (req, res) => { 85 | const node = WD2Manager.RED.nodes.getNode(req.params.id); 86 | if (node != null) { 87 | try { 88 | // @ts-ignore 89 | node.receive({ click : true }); 90 | res.sendStatus(200); 91 | } catch(err) { 92 | res.sendStatus(500); 93 | node.error(WD2Manager.RED._("inject.failed", { 94 | error : err.toString() 95 | })); 96 | } 97 | } else { 98 | res.sendStatus(404); 99 | } 100 | }); 101 | } -------------------------------------------------------------------------------- /src/nodes/close-web.html: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 52 | 53 | -------------------------------------------------------------------------------- /src/nodes/close-web.ts: -------------------------------------------------------------------------------- 1 | import { WD2Manager } from "../wd2-manager"; 2 | import { SeleniumMsg, SeleniumNode, SeleniumNodeDef } from "./node"; 3 | 4 | // tslint:disable-next-line: no-empty-interface 5 | export interface NodeCloseWebDef extends SeleniumNodeDef { 6 | 7 | } 8 | 9 | // tslint:disable-next-line: no-empty-interface 10 | export interface NodeCloseWeb extends SeleniumNode { 11 | } 12 | 13 | 14 | export function NodeCloseWebConstructor (this : NodeCloseWeb, conf : NodeCloseWebDef) { 15 | WD2Manager.RED.nodes.createNode(this, conf); 16 | this.status({}); 17 | 18 | this.on("input", async (message : any, send, done) => { 19 | // Cheat to allow correct typing in typescript 20 | const msg : SeleniumMsg = message; 21 | 22 | if (null === msg.driver) { 23 | const error = new Error("Can't use this node without a working open-web node first"); 24 | this.status({ fill : "red", shape : "ring", text : "error"}); 25 | done(error); 26 | } else { 27 | const waitFor : number = parseInt(msg.waitFor ?? conf.waitFor,10); 28 | this.status({ fill : "blue", shape : "ring", text : "waiting for " + (waitFor / 1000).toFixed(1) + " s"}); 29 | setTimeout(async () => { 30 | try { 31 | this.status({ fill : "blue", shape : "ring", text : "closing"}); 32 | await msg.driver.quit(); 33 | msg.driver = null; 34 | this.status({ fill : "green", shape : "dot", text : "closed"}); 35 | send(msg); 36 | done(); 37 | } catch (e) { 38 | this.warn("Can't close the browser, check msg.error for more information"); 39 | msg.driver = null; 40 | msg.error = e; 41 | this.status({ fill : "red", shape : "dot", text : "critical error"}); 42 | done(e); 43 | } 44 | }, waitFor); 45 | } 46 | }); 47 | } -------------------------------------------------------------------------------- /src/nodes/find-element.html: -------------------------------------------------------------------------------- 1 | 34 | 35 | 98 | 99 | -------------------------------------------------------------------------------- /src/nodes/find-element.ts: -------------------------------------------------------------------------------- 1 | import { SeleniumAction, SeleniumNode, SeleniumNodeDef} from "./node"; 2 | import { GenericSeleniumConstructor } from "./node-constructor"; 3 | 4 | // tslint:disable-next-line: no-empty-interface 5 | export interface NodeFindElementDef extends SeleniumNodeDef { 6 | } 7 | 8 | // tslint:disable-next-line: no-empty-interface 9 | export interface NodeFindElement extends SeleniumNode { 10 | } 11 | 12 | async function inputAction (node : NodeFindElement, conf : NodeFindElementDef, action : SeleniumAction) : Promise { 13 | return new Promise ((resolve, reject) => { 14 | const msg = action.msg; 15 | node.status({ fill : "green", shape : "dot", text : "success"}); 16 | if (msg.error) { delete msg.error }; 17 | action.send([msg, null]); 18 | action.done(); 19 | resolve(); 20 | }); 21 | } 22 | 23 | const NodeFindElementConstructor = GenericSeleniumConstructor(null, inputAction); 24 | 25 | export { NodeFindElementConstructor as NodeFindElementConstructor} 26 | -------------------------------------------------------------------------------- /src/nodes/get-attribute.html: -------------------------------------------------------------------------------- 1 | 44 | 45 | 128 | 129 | -------------------------------------------------------------------------------- /src/nodes/get-attribute.ts: -------------------------------------------------------------------------------- 1 | import { WD2Manager } from "../wd2-manager"; 2 | import { SeleniumAction, SeleniumNode, SeleniumNodeDef } from "./node"; 3 | import { GenericSeleniumConstructor } from "./node-constructor"; 4 | 5 | // tslint:disable-next-line: no-empty-interface 6 | export interface NodeGetAttributeDef extends SeleniumNodeDef { 7 | expected : string; 8 | attribute : string; 9 | } 10 | 11 | // tslint:disable-next-line: no-empty-interface 12 | export interface NodeGetAttribute extends SeleniumNode { 13 | 14 | } 15 | 16 | async function inputAction (node : NodeGetAttribute, conf : NodeGetAttributeDef, action : SeleniumAction) : Promise { 17 | return new Promise (async (resolve, reject) => { 18 | const msg = action.msg; 19 | const expected = msg.expected ?? conf.expected; 20 | const attribute = msg.attribute ?? conf.attribute; 21 | const step = ""; 22 | try { 23 | msg.payload = await msg.element.getAttribute(attribute); 24 | if (expected && expected !== msg.payload) { 25 | msg.error = { 26 | message : "Expected attribute (" + attribute + ") value is not aligned, expected : " + expected + ", value : " + msg.payload 27 | }; 28 | node.status({ fill : "yellow", shape : "dot", text : step + "error"}) 29 | action.send([null, msg]); 30 | action.done(); 31 | } else { 32 | node.status({ fill : "green", shape : "dot", text : "success"}) 33 | if (msg.error) { delete msg.error; } 34 | action.send([msg, null]); 35 | action.done(); 36 | } 37 | } catch(err) { 38 | if (WD2Manager.checkIfCritical(err)) { 39 | reject(err); 40 | } else { 41 | msg.error = { 42 | message : "Can't send keys on the the element : " + err.message 43 | }; 44 | node.warn(msg.error.message); 45 | node.status({ fill : "yellow", shape : "dot", text : "expected value error"}) 46 | action.send([null, msg]); 47 | action.done(); 48 | } 49 | } 50 | resolve(); 51 | }); 52 | } 53 | 54 | const NodeGetAttributeConstructor = GenericSeleniumConstructor(null, inputAction); 55 | 56 | export { NodeGetAttributeConstructor as NodeGetAttributeConstructor} -------------------------------------------------------------------------------- /src/nodes/get-text.html: -------------------------------------------------------------------------------- 1 | 40 | 41 | 121 | 122 | -------------------------------------------------------------------------------- /src/nodes/get-text.ts: -------------------------------------------------------------------------------- 1 | import { WD2Manager } from "../wd2-manager"; 2 | import { SeleniumAction, SeleniumNode, SeleniumNodeDef } from "./node"; 3 | import { GenericSeleniumConstructor } from "./node-constructor"; 4 | 5 | // tslint:disable-next-line: no-empty-interface 6 | export interface NodeGetTextDef extends SeleniumNodeDef { 7 | expected : string; 8 | } 9 | 10 | // tslint:disable-next-line: no-empty-interface 11 | export interface NodeGetText extends SeleniumNode { 12 | 13 | } 14 | 15 | async function inputAction (node : NodeGetText, conf : NodeGetTextDef, action : SeleniumAction) : Promise { 16 | return new Promise (async (resolve, reject) => { 17 | const msg = action.msg; 18 | const expected = msg.expected ?? conf.expected; 19 | const step = ""; 20 | try { 21 | msg.payload = await msg.element.getText(); 22 | if (expected && expected !== msg.payload) { 23 | msg.error = { 24 | message : "Expected value is not aligned, expected : " + expected + ", value : " + msg.payload 25 | }; 26 | node.status({ fill : "yellow", shape : "dot", text : step + "error"}) 27 | action.send([null, msg]); 28 | action.done(); 29 | } else { 30 | node.status({ fill : "green", shape : "dot", text : "success"}) 31 | if (msg.error) { delete msg.error; } 32 | action.send([msg, null]); 33 | action.done(); 34 | } 35 | } catch(err) { 36 | if (WD2Manager.checkIfCritical(err)) { 37 | reject(err); 38 | } else { 39 | msg.error = { 40 | message : "Can't send keys on the the element : " + err.message 41 | }; 42 | node.warn(msg.error.message); 43 | node.status({ fill : "yellow", shape : "dot", text : "expected value error"}) 44 | action.send([null, msg]); 45 | action.done(); 46 | } 47 | } 48 | resolve(); 49 | }); 50 | } 51 | 52 | const NodeGetTextConstructor = GenericSeleniumConstructor(null, inputAction); 53 | 54 | export { NodeGetTextConstructor as NodeGetTextConstructor} -------------------------------------------------------------------------------- /src/nodes/get-title.html: -------------------------------------------------------------------------------- 1 | 20 | 21 | 87 | 88 | -------------------------------------------------------------------------------- /src/nodes/get-title.ts: -------------------------------------------------------------------------------- 1 | import { until } from "selenium-webdriver"; 2 | import { WD2Manager } from "../wd2-manager"; 3 | import { SeleniumMsg, SeleniumNode, SeleniumNodeDef } from "./node"; 4 | 5 | // tslint:disable-next-line: no-empty-interface 6 | export interface NodeGetTitleDef extends SeleniumNodeDef { 7 | expected : string; 8 | } 9 | 10 | // tslint:disable-next-line: no-empty-interface 11 | export interface NodeGetTitle extends SeleniumNode { 12 | } 13 | 14 | export function NodeGetTitleConstructor (this : NodeGetTitle, conf : NodeGetTitleDef) { 15 | WD2Manager.RED.nodes.createNode(this, conf); 16 | this.status({}); 17 | 18 | this.on("input", async (message : any, send, done) => { 19 | // Cheat to allow correct typing in typescript 20 | const msg : SeleniumMsg = message; 21 | const node = this; 22 | node.status({}); 23 | if (msg.driver == null) { 24 | const error = new Error("Open URL must be call before any other action. For node : " + conf.name); 25 | node.status({ fill : "red", shape : "ring", text : "error"}); 26 | done(error); 27 | } else { 28 | const expected = msg.expected ?? conf.expected; 29 | const waitFor : number = parseInt(msg.waitFor ?? conf.waitFor,10); 30 | const timeout : number = parseInt(msg.timeout ?? conf.timeout, 10); 31 | setTimeout (async () => { 32 | if (expected && expected !== "") { 33 | try { 34 | await msg.driver.wait(until.titleIs(expected), timeout); 35 | send([msg, null]); 36 | node.status({ fill : "green", shape : "dot", text : "success"}); 37 | done(); 38 | } catch (e) { 39 | if (WD2Manager.checkIfCritical(e)) { 40 | node.status({ fill : "red", shape : "dot", text : "critical error"}); 41 | done(e); 42 | } else { 43 | try { 44 | msg.payload = await msg.driver.getTitle(); 45 | } catch (sube) { 46 | msg.payload = "[Unknown]"; 47 | } 48 | const error = { message : "Browser windows title does not have the expected value", expected, found : msg.webTitle} 49 | node.warn(error.message); 50 | msg.error = error; 51 | node.status({ fill : "yellow", shape : "dot", text : "wrong title"}); 52 | send([null, msg]); 53 | done(); 54 | } 55 | } 56 | } else { 57 | try { 58 | msg.payload = await msg.driver.getTitle(); 59 | node.status({ fill : "green", shape : "dot", text : "success"}); 60 | if (msg.error) { delete msg.error; } 61 | send([msg, null]); 62 | done(); 63 | } catch (e) { 64 | node.status({ fill : "red", shape : "dot", text : "error"}); 65 | node.error("Can't get title of the browser window. Check msg.error for more information"); 66 | done (e); 67 | } 68 | } 69 | }, waitFor); 70 | } 71 | }); 72 | } -------------------------------------------------------------------------------- /src/nodes/get-value.html: -------------------------------------------------------------------------------- 1 | 40 | 41 | 122 | 123 | -------------------------------------------------------------------------------- /src/nodes/get-value.ts: -------------------------------------------------------------------------------- 1 | import { WD2Manager } from "../wd2-manager"; 2 | import { SeleniumAction, SeleniumNode, SeleniumNodeDef } from "./node"; 3 | import { GenericSeleniumConstructor } from "./node-constructor"; 4 | 5 | // tslint:disable-next-line: no-empty-interface 6 | export interface NodeGetValueDef extends SeleniumNodeDef { 7 | expected : string; 8 | } 9 | 10 | // tslint:disable-next-line: no-empty-interface 11 | export interface NodeGetValue extends SeleniumNode { 12 | 13 | } 14 | 15 | async function inputAction (node : NodeGetValue, conf : NodeGetValueDef, action : SeleniumAction) : Promise { 16 | return new Promise (async (resolve, reject) => { 17 | const msg = action.msg; 18 | const expected = msg.expected ?? conf.expected; 19 | const step = ""; 20 | try { 21 | msg.payload = await msg.element.getAttribute("value"); 22 | if (expected && expected !== msg.payload) { 23 | msg.error = { 24 | message : "Expected value is not aligned, expected : " + expected + ", value : " + msg.payload 25 | }; 26 | node.status({ fill : "yellow", shape : "dot", text : step + "error"}) 27 | action.send([null, msg]); 28 | action.done(); 29 | } else { 30 | node.status({ fill : "green", shape : "dot", text : "success"}) 31 | if (msg.error) { delete msg.error; } 32 | action.send([msg, null]); 33 | action.done(); 34 | } 35 | } catch(err) { 36 | if (WD2Manager.checkIfCritical(err)) { 37 | reject(err); 38 | } else { 39 | msg.error = { 40 | message : "Can't send keys on the the element : " + err.message 41 | }; 42 | node.warn(msg.error.message); 43 | node.status({ fill : "yellow", shape : "dot", text : "expected value error"}) 44 | action.send([null, msg]); 45 | action.done(); 46 | } 47 | } 48 | resolve(); 49 | }); 50 | } 51 | 52 | const NodeGetValueConstructor = GenericSeleniumConstructor(null, inputAction); 53 | 54 | export { NodeGetValueConstructor as NodeGetValueConstructor} -------------------------------------------------------------------------------- /src/nodes/navigate.html: -------------------------------------------------------------------------------- 1 | 25 | 26 | 76 | 77 | -------------------------------------------------------------------------------- /src/nodes/navigate.ts: -------------------------------------------------------------------------------- 1 | import { until } from "selenium-webdriver"; 2 | import { WD2Manager } from "../wd2-manager"; 3 | import { SeleniumMsg, SeleniumNode, SeleniumNodeDef } from "./node"; 4 | 5 | // tslint:disable-next-line: no-empty-interface 6 | export interface NodeNavigateDef extends SeleniumNodeDef { 7 | url : string; 8 | navType : string; 9 | } 10 | 11 | // tslint:disable-next-line: no-empty-interface 12 | export interface NodeNavigate extends SeleniumNode { 13 | } 14 | 15 | export function NodeNavigateConstructor (this : NodeNavigate, conf : NodeNavigateDef) { 16 | WD2Manager.RED.nodes.createNode(this, conf); 17 | this.status({}); 18 | 19 | this.on("input", async (message : any, send, done) => { 20 | // Cheat to allow correct typing in typescript 21 | const msg : SeleniumMsg = message; 22 | const node = this; 23 | node.status({}); 24 | if (msg.driver == null) { 25 | const error = new Error("Open URL must be call before any other action. For node : " + conf.name); 26 | node.status({ fill : "red", shape : "ring", text : "error"}); 27 | done(error); 28 | } else { 29 | const webTitle = msg.url ?? conf.url; 30 | const type = msg.navType ?? conf.navType; 31 | const url = msg.url ?? conf.url; 32 | const waitFor : number = parseInt(msg.waitFor ?? conf.waitFor,10); 33 | setTimeout (async () => { 34 | try { 35 | node.status({ fill : "blue", shape : "ring", text : "loading"}); 36 | switch (type) { 37 | case "forward" : 38 | await msg.driver.navigate().forward(); 39 | break; 40 | case "back": 41 | await msg.driver.navigate().back(); 42 | break; 43 | case "refresh": 44 | await msg.driver.navigate().refresh(); 45 | break; 46 | default: 47 | await msg.driver.navigate().to(url); 48 | } 49 | send([msg, null]); 50 | node.status({ fill : "green", shape : "dot", text : "success"}); 51 | done(); 52 | } catch (e) { 53 | if (WD2Manager.checkIfCritical(e)) { 54 | node.status({ fill : "red", shape : "dot", text : "critical error"}); 55 | done(e); 56 | } else { 57 | const error = { message : "Can't navigate " + type + (type === "to") ? " : " + url : ""} 58 | node.warn(error.message); 59 | msg.error = error; 60 | node.status({ fill : "yellow", shape : "dot", text : "navigate error"}); 61 | send([null, msg]); 62 | done(); 63 | } 64 | } 65 | }, waitFor); 66 | } 67 | }); 68 | } -------------------------------------------------------------------------------- /src/nodes/node-constructor.ts: -------------------------------------------------------------------------------- 1 | import { Node } from "node-red" 2 | import { WD2Manager } from "../wd2-manager"; 3 | import { SeleniumAction, SeleniumMsg, SeleniumNodeDef, waitForElement } from "./node"; 4 | 5 | export function GenericSeleniumConstructor, TNodeDef extends SeleniumNodeDef> ( 6 | inputPreCondAction : (node : TNode, conf : TNodeDef, action : SeleniumAction) => Promise, 7 | inputAction : (node : TNode, conf : TNodeDef, action : SeleniumAction) => Promise, 8 | nodeCreation : () => void = null) { 9 | return function (this : TNode, conf : TNodeDef) : void { 10 | WD2Manager.RED.nodes.createNode(this, conf); 11 | const node = this; 12 | node.status({}); 13 | this.on("input", async (message : any, send, done) => { 14 | // Cheat to allow correct typing in typescript 15 | const msg : SeleniumMsg = message; 16 | const action : SeleniumAction = { msg, send, done}; 17 | node.status({}); 18 | try { 19 | if (!inputPreCondAction || await inputPreCondAction(node, conf, action)) { 20 | if (msg.driver == null) { 21 | const error = new Error("Open URL must be call before any other action. For node : " + conf.name); 22 | node.status({ fill : "red", shape : "ring", text : "error"}); 23 | done(error); 24 | } else { 25 | // If InputPreCond return false, next steps will not be executed 26 | waitForElement(conf, msg).subscribe ({ 27 | next (val) { 28 | if (typeof val === "string") { 29 | node.status({ fill : "blue", shape : "dot", text : val}); 30 | } else { 31 | msg.element = val; 32 | } 33 | }, 34 | error(err) { 35 | if (WD2Manager.checkIfCritical(err)) { 36 | node.status({ fill : "red", shape : "dot", text : "critical error"}); 37 | node.error(err.toString()); 38 | done(err); 39 | } else { 40 | node.status({ fill : "yellow", shape : "dot", text : "location error"}); 41 | msg.error = err; 42 | send([null, msg]); 43 | done(); 44 | } 45 | }, 46 | async complete () { 47 | node.status({ fill : "blue", shape : "dot", text : "located"}); 48 | try { 49 | await inputAction(node, conf, action); 50 | } catch (e) { 51 | node.status({ fill : "red", shape : "dot", text : "critical error"}); 52 | node.error(e.toString()); 53 | delete msg.driver; 54 | done(e); 55 | } 56 | } 57 | }); 58 | } 59 | } 60 | } catch (e) { 61 | node.status({ fill : "red", shape : "dot", text : "critical error"}); 62 | node.error(e.toString()); 63 | delete msg.driver; 64 | done(e); 65 | } 66 | }); 67 | // Activity to do during Node Creation 68 | if (nodeCreation) 69 | nodeCreation(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/nodes/node.ts: -------------------------------------------------------------------------------- 1 | import { NodeMessageInFlow } from "node-red__registry"; 2 | import { Node, NodeDef, NodeMessage } from "node-red"; 3 | import { Observable } from "rxjs"; 4 | import { By, until, WebDriver, WebElement } from "selenium-webdriver"; 5 | 6 | 7 | export * from "./open-web"; 8 | export * from "./close-web"; 9 | export * from "./find-element"; 10 | export * from "./get-title"; 11 | export * from "./click-on"; 12 | export * from "./send-keys"; 13 | export * from "./get-value"; 14 | export * from "./set-value"; 15 | export * from "./get-attribute"; 16 | export * from "./get-text"; 17 | export * from "./run-script"; 18 | export * from "./screenshot"; 19 | export * from "./set-attribute"; 20 | 21 | export interface SeleniumNodeDef extends NodeDef { 22 | selector : string; 23 | target : string; 24 | // Node-red only push string from properties if modified by user 25 | timeout : string; 26 | waitFor : string; 27 | } 28 | 29 | export interface SeleniumNode extends Node { 30 | 31 | } 32 | 33 | export interface SeleniumAction { 34 | done : (err? : Error) => void; 35 | send : (msg: NodeMessage | NodeMessage[]) => void; 36 | msg : SeleniumMsg; 37 | } 38 | 39 | export interface SeleniumMsg extends NodeMessageInFlow { 40 | driver : WebDriver | null; 41 | selector? : string; 42 | // Node-red only push string from properties if modified by user 43 | target? : string; 44 | timeout? : string; 45 | waitFor? : string; 46 | error? : any; 47 | element? : WebElement; 48 | webTitle? : string; 49 | click? : boolean; 50 | clearVal? : boolean; 51 | keys? : string; 52 | value? : string; 53 | expected? : string; 54 | attribute? : string; 55 | script? : string; 56 | url? : string; 57 | navType? : string; 58 | filePath? : string; 59 | } 60 | 61 | /** 62 | * Wait for the location of an element based on a target & selector. 63 | * @param driver A valid WebDriver instance 64 | * @param conf A configuration of a node 65 | * @param msg A node message 66 | */ 67 | export function waitForElement(conf : SeleniumNodeDef, msg : SeleniumMsg) : Observable{ 68 | return new Observable ((subscriber) => { 69 | const waitFor : number = parseInt(msg.waitFor ?? conf.waitFor,10); 70 | const timeout : number = parseInt(msg.timeout ?? conf.timeout, 10); 71 | const target : string = msg.target ?? conf.target; 72 | const selector : string = msg.selector ?? conf.selector; 73 | let element : WebElement; 74 | subscriber.next("waiting for " + (waitFor / 1000).toFixed(1) + " s"); 75 | setTimeout (async () => { 76 | try { 77 | subscriber.next("locating"); 78 | if (selector !== "") { 79 | // @ts-ignore 80 | element = await msg.driver.wait(until.elementLocated(By[selector](target)), timeout); 81 | } else { 82 | if (msg.element) { 83 | element = msg.element; 84 | } 85 | } 86 | subscriber.next(element); 87 | subscriber.complete(); 88 | } catch (e) { 89 | let error : any; 90 | if (e.toString().includes("TimeoutError")) 91 | error = new Error("catch timeout after " + timeout + " milliseconds for selector type " + selector + " for " + target); 92 | else 93 | error = e; 94 | error.selector = selector; 95 | error.target = target; 96 | subscriber.error(error); 97 | } 98 | }, waitFor); 99 | }); 100 | } 101 | 102 | -------------------------------------------------------------------------------- /src/nodes/open-web.html: -------------------------------------------------------------------------------- 1 | 45 | 46 | 83 | 84 | -------------------------------------------------------------------------------- /src/nodes/open-web.ts: -------------------------------------------------------------------------------- 1 | import { WD2Manager } from "../wd2-manager"; 2 | import { SeleniumMsg, SeleniumNode, SeleniumNodeDef } from "./node"; 3 | 4 | // tslint:disable-next-line: no-empty-interface 5 | export interface NodeOpenWebDef extends SeleniumNodeDef { 6 | serverURL : string; 7 | name : string; 8 | browser : string; 9 | webURL : string; 10 | width : number; 11 | heigth : number; 12 | maximized : boolean; 13 | headless : boolean; 14 | } 15 | 16 | // tslint:disable-next-line: no-empty-interface 17 | export interface NodeOpenWeb extends SeleniumNode { 18 | 19 | } 20 | 21 | export function NodeOpenWebConstructor (this : NodeOpenWeb, conf : NodeOpenWebDef) { 22 | WD2Manager.RED.nodes.createNode(this, conf); 23 | 24 | if (!conf.serverURL) { 25 | this.log("Selenium server URL is undefined"); 26 | this.status({ fill : "red", shape : "ring", text : "no server defined"}); 27 | } else { 28 | WD2Manager.setServerConfig(conf.serverURL).then ((result) => { 29 | if (result) { 30 | this.log(conf.serverURL + " is reacheable by Node-red"); 31 | this.status({ fill : "green", shape : "ring", text : conf.serverURL + ": reachable"}); 32 | } else { 33 | this.log(conf.serverURL + " is not reachable by Node-red"); 34 | this.status({ fill : "red", shape : "ring", text : conf.serverURL + ": unreachable"}); 35 | } 36 | }).catch ((error) => { 37 | this.log(error); 38 | }); 39 | } 40 | this.on("input", async (message : any, send, done) => { 41 | // Cheat to allow correct typing in typescript 42 | const msg : SeleniumMsg = message; 43 | const node = this; 44 | let driverError = false; 45 | msg.driver = WD2Manager.getDriver(conf); 46 | this.status({ fill : "blue", shape : "ring", text : "opening browser"}); 47 | try { 48 | await msg.driver.get(conf.webURL); 49 | } catch (e) { 50 | msg.driver = null; 51 | node.error("Can't open an instance of " + conf.browser); 52 | node.status({ fill : "red", shape : "ring", text : "launch error"}); 53 | driverError = true; 54 | msg.driver = null; 55 | done(e); 56 | } 57 | try { 58 | if (msg.driver) { 59 | if (!driverError) 60 | if (!conf.headless) 61 | if (!conf.maximized) 62 | await msg.driver.manage().window().setSize(conf.width, conf.heigth); 63 | else 64 | await msg.driver.manage().window().maximize(); 65 | send(msg); 66 | this.status({ fill : "green", shape : "dot", text : "success"}); 67 | done(); 68 | } 69 | } catch (e) { 70 | node.error("Can't resize the instance of " + conf.browser); 71 | node.status({ fill : "red", shape : "ring", text : "resize error"}); 72 | driverError = true; 73 | done(e); 74 | } 75 | }); 76 | } -------------------------------------------------------------------------------- /src/nodes/run-script.html: -------------------------------------------------------------------------------- 1 | 44 | 45 | 114 | 115 | -------------------------------------------------------------------------------- /src/nodes/run-script.ts: -------------------------------------------------------------------------------- 1 | import { WD2Manager } from "../wd2-manager"; 2 | import { SeleniumAction, SeleniumNode, SeleniumNodeDef } from "./node"; 3 | import { GenericSeleniumConstructor } from "./node-constructor"; 4 | 5 | // tslint:disable-next-line: no-empty-interface 6 | export interface NodeRunScriptDef extends SeleniumNodeDef { 7 | script : string; 8 | } 9 | 10 | // tslint:disable-next-line: no-empty-interface 11 | export interface NodeRunScript extends SeleniumNode { 12 | } 13 | 14 | async function inputAction (node : NodeRunScript, conf : NodeRunScriptDef, action : SeleniumAction) : Promise { 15 | return new Promise (async (resolve, reject) => { 16 | const msg = action.msg; 17 | const script = msg.script ?? conf.script; 18 | try { 19 | msg.payload = await msg.driver.executeScript(script, msg.element); 20 | node.status({ fill : "green", shape : "dot", text : "success"}) 21 | if (msg.error) { delete msg.error; } 22 | action.send([msg, null]); 23 | action.done(); 24 | } catch(err) { 25 | if (WD2Manager.checkIfCritical(err)) { 26 | reject(err); 27 | } else { 28 | msg.error = { 29 | message : "Can't run script on the the element : " + err.message 30 | }; 31 | node.warn(msg.error.message); 32 | node.status({ fill : "yellow", shape : "dot", text : "run script error"}) 33 | action.send([null, msg]); 34 | action.done(); 35 | } 36 | } 37 | resolve(); 38 | }); 39 | } 40 | 41 | const NodeRunScriptConstructor = GenericSeleniumConstructor(null, inputAction); 42 | 43 | export { NodeRunScriptConstructor as NodeRunScriptConstructor} -------------------------------------------------------------------------------- /src/nodes/screenshot.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 62 | 63 | -------------------------------------------------------------------------------- /src/nodes/screenshot.ts: -------------------------------------------------------------------------------- 1 | import { until } from "selenium-webdriver"; 2 | import { WD2Manager } from "../wd2-manager"; 3 | import { SeleniumMsg, SeleniumNode, SeleniumNodeDef } from "./node"; 4 | import * as fs from "fs/promises"; 5 | 6 | 7 | // tslint:disable-next-line: no-empty-interface 8 | export interface NodeScreenshotDef extends SeleniumNodeDef { 9 | filePath : string; 10 | } 11 | 12 | // tslint:disable-next-line: no-empty-interface 13 | export interface NodeScreenshot extends SeleniumNode { 14 | } 15 | 16 | export function NodeScreenshotConstructor (this : NodeScreenshot, conf : NodeScreenshotDef) { 17 | WD2Manager.RED.nodes.createNode(this, conf); 18 | this.status({}); 19 | 20 | this.on("input", async (message : any, send, done) => { 21 | // Cheat to allow correct typing in typescript 22 | const msg : SeleniumMsg = message; 23 | const node = this; 24 | node.status({}); 25 | if (msg.driver == null) { 26 | const error = new Error("Open URL must be call before any other action. For node : " + conf.name); 27 | node.status({ fill : "red", shape : "ring", text : "error"}); 28 | done(error); 29 | } else { 30 | const waitFor : number = parseInt(msg.waitFor ?? conf.waitFor,10); 31 | const filePath : string = msg.filePath ?? conf.filePath; 32 | setTimeout (async () => { 33 | try { 34 | const sc = await msg.driver.takeScreenshot(); 35 | if (filePath) 36 | await fs.writeFile(filePath, sc, "base64"); 37 | msg.payload = sc; 38 | send([msg, null]); 39 | node.status({ fill : "green", shape : "dot", text : "success"}); 40 | done(); 41 | } catch (e) { 42 | if (WD2Manager.checkIfCritical(e)) { 43 | node.status({ fill : "red", shape : "dot", text : "critical error"}); 44 | done(e); 45 | } else { 46 | const error = { message : "Can't take a screenshot"} 47 | node.warn(error.message); 48 | msg.error = error; 49 | node.status({ fill : "yellow", shape : "dot", text : "screenshot error"}); 50 | send([null, msg]); 51 | done(); 52 | } 53 | } 54 | }, waitFor); 55 | } 56 | }); 57 | } -------------------------------------------------------------------------------- /src/nodes/send-keys.html: -------------------------------------------------------------------------------- 1 | 45 | 46 | 117 | 118 | -------------------------------------------------------------------------------- /src/nodes/send-keys.ts: -------------------------------------------------------------------------------- 1 | import { Node } from "node-red" 2 | import { WD2Manager } from "../wd2-manager"; 3 | import { SeleniumAction, SeleniumMsg, SeleniumNode, SeleniumNodeDef } from "./node"; 4 | import { GenericSeleniumConstructor } from "./node-constructor"; 5 | 6 | export interface NodeSendKeysDef extends SeleniumNodeDef { 7 | keys : string; 8 | clearVal : string; 9 | } 10 | 11 | export interface NodeSendKeys extends SeleniumNode { 12 | __msg : SeleniumMsg; 13 | } 14 | 15 | async function inputAction (node : NodeSendKeys, conf : NodeSendKeysDef, action : SeleniumAction) : Promise { 16 | return new Promise (async (resolve, reject) => { 17 | const msg = action.msg; 18 | const clearVal = msg.clearVal ?? conf.clearVal; 19 | const keys = msg.keys ?? conf.keys; 20 | let step = ""; 21 | try { 22 | if (clearVal){ 23 | step = "clear"; 24 | node.status({ fill : "blue", shape : "dot", text : "clearing"}); 25 | await msg.element.clear(); 26 | } 27 | step = "send keys"; 28 | await msg.element.sendKeys(keys); 29 | node.status({ fill : "green", shape : "dot", text : "success"}) 30 | if (msg.error) { delete msg.error; } 31 | action.send([msg, null]); 32 | action.done(); 33 | } catch(err) { 34 | if (WD2Manager.checkIfCritical(err)) { 35 | reject(err); 36 | } else { 37 | msg.error = { 38 | message : "Can't send keys on the the element : " + err.message 39 | }; 40 | node.status({ fill : "yellow", shape : "dot", text : step + "error"}) 41 | action.send([null, msg]); 42 | action.done(); 43 | } 44 | } 45 | resolve(); 46 | }); 47 | } 48 | 49 | const NodeSendKeysConstructor = GenericSeleniumConstructor(null, inputAction); 50 | 51 | export { NodeSendKeysConstructor as NodeSendKeysConstructor} -------------------------------------------------------------------------------- /src/nodes/set-attribute.html: -------------------------------------------------------------------------------- 1 | 44 | 45 | 127 | 128 | -------------------------------------------------------------------------------- /src/nodes/set-attribute.ts: -------------------------------------------------------------------------------- 1 | import { WD2Manager } from "../wd2-manager"; 2 | import { SeleniumAction, SeleniumNode, SeleniumNodeDef } from "./node"; 3 | import { GenericSeleniumConstructor } from "./node-constructor"; 4 | 5 | // tslint:disable-next-line: no-empty-interface 6 | export interface NodeSetAttributeDef extends SeleniumNodeDef { 7 | attribute : string; 8 | value : string; 9 | } 10 | 11 | // tslint:disable-next-line: no-empty-interface 12 | export interface NodeSetAttribute extends SeleniumNode { 13 | 14 | } 15 | 16 | async function inputAction (node : NodeSetAttribute, conf : NodeSetAttributeDef, action : SeleniumAction) : Promise { 17 | return new Promise (async (resolve, reject) => { 18 | const msg = action.msg; 19 | const attribute = msg.attribute ?? conf.attribute; 20 | const value = msg.value ?? conf.value; 21 | const step = ""; 22 | try { 23 | await msg.driver.executeScript("arguments[0].setAttribute(" + "'" + attribute + "', '" + value + "')", msg.element); 24 | node.status({ fill : "green", shape : "dot", text : "success"}) 25 | if (msg.error) { delete msg.error; } 26 | action.send([msg, null]); 27 | action.done(); 28 | } catch(err) { 29 | if (WD2Manager.checkIfCritical(err)) { 30 | reject(err); 31 | } else { 32 | msg.error = { 33 | message : "Can't send keys on the the element : " + err.message 34 | }; 35 | node.warn(msg.error.message); 36 | node.status({ fill : "yellow", shape : "dot", text : "expected value error"}) 37 | action.send([null, msg]); 38 | action.done(); 39 | } 40 | } 41 | resolve(); 42 | }); 43 | } 44 | 45 | const NodeSetAttributeConstructor = GenericSeleniumConstructor(null, inputAction); 46 | 47 | export { NodeSetAttributeConstructor as NodeSetAttributeConstructor} -------------------------------------------------------------------------------- /src/nodes/set-value.html: -------------------------------------------------------------------------------- 1 | 40 | 41 | 109 | 110 | 160 | -------------------------------------------------------------------------------- /src/nodes/set-value.ts: -------------------------------------------------------------------------------- 1 | import { WD2Manager } from "../wd2-manager"; 2 | import { SeleniumAction, SeleniumNode, SeleniumNodeDef } from "./node"; 3 | import { GenericSeleniumConstructor } from "./node-constructor"; 4 | 5 | // tslint:disable-next-line: no-empty-interface 6 | export interface NodeSetValueDef extends SeleniumNodeDef { 7 | value : string; 8 | } 9 | 10 | // tslint:disable-next-line: no-empty-interface 11 | export interface NodeSetValue extends SeleniumNode { 12 | 13 | } 14 | 15 | async function inputAction (node : NodeSetValue, conf : NodeSetValueDef, action : SeleniumAction) : Promise { 16 | return new Promise (async (resolve, reject) => { 17 | const msg = action.msg; 18 | const value = msg.value ?? conf.value; 19 | try { 20 | await msg.driver.executeScript("arguments[0].setAttribute('value', '" + value + "')", msg.element); 21 | node.status({ fill : "green", shape : "dot", text : "success"}) 22 | if (msg.error) { delete msg.error; } 23 | action.send([msg, null]); 24 | action.done(); 25 | } catch(err) { 26 | if (WD2Manager.checkIfCritical(err)) { 27 | reject(err); 28 | } else { 29 | msg.error = { 30 | message : "Can't set value on the the element : " + err.message 31 | }; 32 | node.warn(msg.error.message); 33 | node.status({ fill : "yellow", shape : "dot", text : "expected value error"}) 34 | action.send([null, msg]); 35 | action.done(); 36 | } 37 | } 38 | resolve(); 39 | }); 40 | } 41 | 42 | const NodeSetValueConstructor = GenericSeleniumConstructor(null, inputAction); 43 | 44 | export { NodeSetValueConstructor as NodeSetValueConstructor} -------------------------------------------------------------------------------- /src/selenium-wd2.ts: -------------------------------------------------------------------------------- 1 | import { NodeAPI, NodeAPISettingsWithData} from "node-red"; 2 | import { NodeNavigateConstructor } from "./nodes/navigate"; 3 | import { NodeClickOnConstructor, NodeClickPrerequisite, NodeCloseWebConstructor, NodeFindElementConstructor, NodeGetAttributeConstructor, NodeGetTextConstructor, NodeGetTitleConstructor, NodeGetValueConstructor, NodeOpenWebConstructor, NodeRunScriptConstructor, NodeScreenshotConstructor, NodeSendKeysConstructor, NodeSetAttributeConstructor, NodeSetValueConstructor } from "./nodes/node"; 4 | import { WD2Manager } from "./wd2-manager"; 5 | 6 | 7 | export = (RED : NodeAPI) => { 8 | WD2Manager.init(RED); 9 | RED.nodes.registerType("open-web", NodeOpenWebConstructor); 10 | RED.nodes.registerType("close-web", NodeCloseWebConstructor); 11 | NodeClickPrerequisite(); 12 | RED.nodes.registerType("find-element", NodeFindElementConstructor); 13 | RED.nodes.registerType("click-on", NodeClickOnConstructor); 14 | RED.nodes.registerType("get-title", NodeGetTitleConstructor); 15 | RED.nodes.registerType("send-keys", NodeSendKeysConstructor); 16 | RED.nodes.registerType("get-value", NodeGetValueConstructor); 17 | RED.nodes.registerType("set-value", NodeSetValueConstructor); 18 | RED.nodes.registerType("get-attribute", NodeGetAttributeConstructor); 19 | RED.nodes.registerType("set-attribute", NodeSetAttributeConstructor); 20 | RED.nodes.registerType("get-text", NodeGetTextConstructor); 21 | RED.nodes.registerType("run-script", NodeRunScriptConstructor); 22 | RED.nodes.registerType("navigate", NodeNavigateConstructor); 23 | RED.nodes.registerType("screenshot", NodeScreenshotConstructor); 24 | } 25 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from "net"; 2 | 3 | function getValueFromPropertyNameRec(obj : any, listProp : string[]) 4 | { 5 | let res = obj; 6 | for (const prop of listProp) { 7 | res = res[prop]; 8 | if (!res) 9 | break; 10 | } 11 | return res; 12 | } 13 | 14 | 15 | export function replaceVar (str :string, msg : any) { 16 | if (typeof str !== "string") 17 | return str; 18 | if (str.match(/^\{\{.*\}\}$/g)) { // if the string is in double brackets like {{ foo }} 19 | const s = str.substring(2, str.length - 2); 20 | const v = s.split("."); 21 | if (v.length < 2) 22 | return str; 23 | switch (v[0]) { 24 | case 'msg' : 25 | return getValueFromPropertyNameRec(msg, v.splice(1, v.length)); 26 | break; 27 | default: 28 | return str; 29 | break; 30 | } 31 | 32 | } else { 33 | return str; 34 | } 35 | } 36 | 37 | export async function portCheck(host : string, port : number) : Promise { 38 | return new Promise ((resolve, reject) => { 39 | const socket = new Socket(); 40 | let status : boolean = false; 41 | // Socket connection established, port is open 42 | socket.on('connect', () => { 43 | status = true; 44 | socket.end();}); 45 | socket.setTimeout(2000);// If no response, assume port is not listening 46 | socket.on('timeout', () => { 47 | socket.destroy(); 48 | resolve(status); 49 | }); 50 | socket.on('error', (exception) => {resolve(status)}); 51 | socket.on('close', (exception) => {resolve(status)}); 52 | socket.connect(port, host); 53 | }); 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/wd2-manager.ts: -------------------------------------------------------------------------------- 1 | import { NodeAPI, NodeAPISettingsWithData } from "node-red"; 2 | import { Builder, WebDriver } from "selenium-webdriver"; 3 | import * as chrome from "selenium-webdriver/chrome"; 4 | import * as firefox from "selenium-webdriver/firefox"; 5 | import { NodeOpenWebDef } from "./nodes/node"; 6 | import { portCheck } from "./utils"; 7 | 8 | export class WD2Manager { 9 | private static _RED : NodeAPI; 10 | private static _serverURL : string = ""; 11 | private static _driverList : WebDriver[] = new Array(); 12 | 13 | public static get RED () { 14 | return WD2Manager._RED; 15 | } 16 | 17 | public static init (RED : NodeAPI) : void { 18 | WD2Manager._RED = RED; 19 | } 20 | 21 | /** 22 | * Define the configuration of the Selenium Server and return a boolean if the server is reacheable 23 | * @param serverURL 24 | * @param browser 25 | */ 26 | public static async setServerConfig(serverURL : string) : Promise { 27 | WD2Manager._serverURL = serverURL; 28 | const server = serverURL.match(/\/\/([a-z0-9A-Z.:-]*)/)?.[1]; 29 | if (!server) 30 | return new Promise((resolve) => resolve(false)); 31 | const host = server.split(":")[0]; 32 | const port = server.split(":")[1] || "80"; 33 | return portCheck(host, parseInt(port, 10)); 34 | } 35 | 36 | public static getDriver(conf : NodeOpenWebDef) : WebDriver { 37 | let builder = new Builder().forBrowser(conf.browser).usingServer(conf.serverURL); 38 | if (conf.headless) { 39 | const width = conf.width; 40 | const height = conf.heigth; 41 | switch (conf.browser) { 42 | case 'firefox' : 43 | builder = builder.setFirefoxOptions( 44 | new firefox.Options().headless()); 45 | break; 46 | case 'chrome' : 47 | builder = builder.setChromeOptions( 48 | new chrome.Options().headless()); 49 | break; 50 | default : 51 | WD2Manager._RED.log.warn("unsupported headless configuration for" + conf.browser); 52 | break; 53 | } 54 | } 55 | const driver = builder.build(); 56 | WD2Manager._driverList.push(driver); 57 | 58 | return driver; 59 | } 60 | 61 | public static checkIfCritical(error : Error) : boolean { 62 | // Blocking error in case of "WebDriverError : Failed to decode response from marionett" 63 | if (error.toString().includes("decode response")) 64 | return true; 65 | // Blocking error in case of "NoSuchSessionError: Tried to run command without establishing a connection" 66 | if (error.name.includes("NoSuchSessionError")) 67 | return true; 68 | // Blocking error in case of "ReferenceError" like in case of msg.driver is modified 69 | if (error.name.includes("ReferenceError")) 70 | return true; 71 | // Blocking error in case of "TypeError" like in case of msg.driver is modified 72 | if (error.name.includes("TypeError")) 73 | return true; 74 | return false; 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ES5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | "lib": ["ES2017"], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | "strictNullChecks": false, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": { 7 | }, 8 | "rules": {}, 9 | "rulesDirectory": [] 10 | } --------------------------------------------------------------------------------