├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bower.json ├── package.json ├── src ├── Selenium.js ├── Selenium.purs └── Selenium │ ├── ActionSequence.js │ ├── ActionSequence.purs │ ├── Browser.js │ ├── Browser.purs │ ├── Builder.js │ ├── Builder.purs │ ├── Capabilities.js │ ├── Capabilities.purs │ ├── Combinators.purs │ ├── FFProfile.js │ ├── FFProfile.purs │ ├── Key.js │ ├── Key.purs │ ├── Monad.purs │ ├── MouseButton.js │ ├── MouseButton.purs │ ├── Remote.js │ ├── Remote.purs │ ├── ScrollBehaviour.js │ ├── ScrollBehaviour.purs │ ├── Types.purs │ └── XHR.purs └── test └── Main.purs /.gitignore: -------------------------------------------------------------------------------- 1 | /.* 2 | !/.gitignore 3 | !/.travis.yml 4 | /bower_components/ 5 | /node_modules/ 6 | /output/ 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: trusty 3 | sudo: required 4 | node_js: stable 5 | before_install: 6 | - export CHROME_BIN=/usr/bin/google-chrome 7 | - export DISPLAY=:99.0 8 | - sh -e /etc/init.d/xvfb start 9 | - sudo apt-get update 10 | - sudo apt-get install -y libappindicator1 fonts-liberation 11 | - wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb 12 | - sudo dpkg -i google-chrome*.deb 13 | install: 14 | - npm install -g bower 15 | - npm install 16 | script: 17 | - bower install --production 18 | - npm run -s build 19 | - bower install 20 | - npm -s test 21 | after_success: 22 | - >- 23 | test $TRAVIS_TAG && 24 | echo $GITHUB_TOKEN | pulp login && 25 | echo y | pulp publish --no-push 26 | -------------------------------------------------------------------------------- /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 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # purescript-webdriver 2 | 3 | [![Latest release](http://img.shields.io/github/release/slamdata/purescript-webdriver.svg)](https://github.com/slamdata/purescript-webdriver/releases) 4 | [![Build status](https://travis-ci.org/slamdata/purescript-webdriver.svg?branch=master)](https://travis-ci.org/slamdata/purescript-webdriver) 5 | 6 | A PureScript interface to Selenium's Node Webdriver. 7 | 8 | ## Installation 9 | 10 | ``` purescript 11 | bower install purescript-webdriver 12 | ``` 13 | 14 | ## Module documentation 15 | 16 | Module documentation is [published on Pursuit](http://pursuit.purescript.org/packages/purescript-webdriver). 17 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-webdriver", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/slamdata/purescript-webdriver" 6 | }, 7 | "homepage": "https://github.com/slamdata/purescript-webdriver", 8 | "authors": [ 9 | "Maxim Zimaliev ", 10 | "Jon Sterling " 11 | ], 12 | "description": "A PureScript interface to Selenium's Node Webdriver", 13 | "keywords": [ 14 | "purescript", 15 | "webdriver", 16 | "selenium" 17 | ], 18 | "license": "Apache-2.0", 19 | "dependencies": { 20 | "purescript-aff": "^5.0.0", 21 | "purescript-aff-reattempt": "^5.0.0", 22 | "purescript-aff-promise": "^2.0.0", 23 | "purescript-web-html": "^1.0.0", 24 | "purescript-web-uievents": "^1.0.0" 25 | }, 26 | "devDependencies": { 27 | "purescript-console": "^4.1.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf output && rimraf .pulp-cache", 5 | "build": "pulp build -- --censor-lib --strict", 6 | "test": "pulp test" 7 | }, 8 | "dependencies": { 9 | "chromedriver": "^2.29.0", 10 | "selenium-webdriver": "3.4.0" 11 | }, 12 | "devDependencies": { 13 | "pulp": "^12.3.0", 14 | "purescript-psa": "^0.6.0", 15 | "purescript": "^0.12.0", 16 | "rimraf": "^2.6.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Selenium.js: -------------------------------------------------------------------------------- 1 | // module Selenium 2 | 3 | require("chromedriver"); 4 | 5 | var webdriver = require("selenium-webdriver"), 6 | By = webdriver.By, 7 | fs = require("fs"), 8 | path = require("path"); 9 | 10 | exports._get = function(driver) { 11 | return function(url) { 12 | return function(eb, cb) { 13 | driver.get(url).then(cb, eb); 14 | }; 15 | }; 16 | }; 17 | 18 | exports._setFileDetector = function(driver) { 19 | return function(detector) { 20 | return function (eb, cb) { 21 | try { 22 | driver.setFileDetector(detector); 23 | return cb(); 24 | } catch (e) { 25 | return eb(e); 26 | } 27 | }; 28 | }; 29 | }; 30 | 31 | exports._wait = function(promise) { 32 | return function(timeout) { 33 | return function(driver) { 34 | return function(eb, cb) { 35 | try { 36 | driver.wait(promise, timeout) 37 | .then(function() { 38 | promise().then(function(res) { 39 | if (res) { 40 | cb(); 41 | } else { 42 | eb(new Error("wait promise has returned false")); 43 | } 44 | }, eb); 45 | }, eb); 46 | } 47 | catch (e) { 48 | eb(e); 49 | } 50 | }; 51 | }; 52 | }; 53 | }; 54 | 55 | exports._quit = function(driver) { 56 | return function(eb, cb) { 57 | driver.quit().then(cb,eb); 58 | }; 59 | }; 60 | 61 | exports._byClassName = function(className) { 62 | return function(eb, cb) { 63 | try { 64 | return cb(By.className(className)); 65 | } 66 | catch (e) { 67 | return eb(e); 68 | } 69 | }; 70 | }; 71 | 72 | exports._byCss = function(selector) { 73 | return function(eb, cb) { 74 | try { 75 | return cb(By.css(selector)); 76 | } 77 | catch (e) { 78 | return eb(e); 79 | } 80 | }; 81 | }; 82 | 83 | exports._byId = function(id) { 84 | return function(eb, cb) { 85 | try { 86 | return cb(By.id(id)); 87 | } 88 | catch (e) { 89 | return eb(e); 90 | } 91 | }; 92 | }; 93 | 94 | exports._byName = function(name) { 95 | return function(eb, cb) { 96 | try { 97 | return cb(By.name(name)); 98 | } 99 | catch (e) { 100 | return eb(e); 101 | } 102 | }; 103 | }; 104 | 105 | exports._byXPath = function(xpath) { 106 | return function(eb, cb) { 107 | try { 108 | return cb(By.xpath(xpath)); 109 | } 110 | catch (e) { 111 | return eb(e); 112 | } 113 | }; 114 | }; 115 | 116 | function _find(nothing) { 117 | return function(just) { 118 | return function(driver) { 119 | return function(by) { 120 | return function(eb, cb) { 121 | driver.findElement(by).then(function(el) { 122 | return cb(just(el)); 123 | }, function() { 124 | return cb(nothing); 125 | }); 126 | }; 127 | }; 128 | }; 129 | }; 130 | } 131 | 132 | function _exact(driver) { 133 | return function(by) { 134 | return function(eb, cb) { 135 | driver.findElement(by).then(cb, eb); 136 | }; 137 | }; 138 | } 139 | 140 | exports.showLocator = function(locator) { 141 | return locator.toString(); 142 | } 143 | 144 | exports._findExact = _exact; 145 | exports._childExact = _exact; 146 | 147 | exports._findElement = _find; 148 | 149 | exports._findChild = _find; 150 | 151 | function _finds(parent) { 152 | return function(by) { 153 | return function(eb, cb) { 154 | return parent.findElements(by).then(function(children) { 155 | return cb(children); 156 | }, eb) 157 | }; 158 | }; 159 | } 160 | 161 | exports._findElements = _finds; 162 | exports._findChildren = _finds; 163 | 164 | exports._sendKeysEl = function(keys) { 165 | return function(el) { 166 | return function(eb, cb) { 167 | el.sendKeys(keys).then(cb, eb); 168 | }; 169 | }; 170 | }; 171 | 172 | exports._getCssValue = function(el) { 173 | return function(str) { 174 | return function(eb, cb) { 175 | return el.getCssValue(str).then(cb, eb); 176 | }; 177 | }; 178 | }; 179 | 180 | exports._getAttribute = function(nothing) { 181 | return function(just) { 182 | return function(el) { 183 | return function(str) { 184 | return function(eb, cb) { 185 | return el.getAttribute(str).then(function(attr) { 186 | if (attr === null) { 187 | cb(nothing); 188 | } else { 189 | cb(just(attr)); 190 | } 191 | }, eb); 192 | }; 193 | }; 194 | }; 195 | }; 196 | }; 197 | 198 | exports._getText = function(el) { 199 | return function(eb, cb) { 200 | return el.getText().then(cb, eb); 201 | }; 202 | }; 203 | 204 | exports._isDisplayed = function(el) { 205 | return function(eb, cb) { 206 | return el.isDisplayed().then(function(is) { 207 | return cb(is); 208 | }, eb); 209 | }; 210 | }; 211 | 212 | exports._isEnabled = function(el) { 213 | return function(eb, cb) { 214 | return el.isEnabled().then(function(is) { 215 | return cb(is); 216 | }, eb); 217 | }; 218 | }; 219 | 220 | exports._getCurrentUrl = function(driver) { 221 | return function(eb, cb) { 222 | return driver.getCurrentUrl().then(cb, eb); 223 | }; 224 | }; 225 | 226 | exports._getTitle = function(driver) { 227 | return function(eb, cb) { 228 | return driver.getTitle().then(cb, eb); 229 | }; 230 | }; 231 | 232 | exports._navigateBack = function(driver) { 233 | return function(eb, cb) { 234 | var n = new webdriver.WebDriver.Navigation(driver); 235 | return n.back().then(cb, eb); 236 | }; 237 | }; 238 | 239 | exports._navigateForward = function(driver) { 240 | return function(eb, cb) { 241 | var n = new webdriver.WebDriver.Navigation(driver); 242 | return n.forward().then(cb, eb); 243 | }; 244 | }; 245 | 246 | exports._refresh = function(driver) { 247 | return function(eb, cb) { 248 | var n = new webdriver.WebDriver.Navigation(driver); 249 | return n.refresh().then(cb, eb); 250 | }; 251 | }; 252 | 253 | exports._navigateTo = function(url) { 254 | return function(driver) { 255 | return function(eb, cb) { 256 | var n = new webdriver.WebDriver.Navigation(driver); 257 | return n.to(url).then(cb, eb); 258 | }; 259 | }; 260 | }; 261 | 262 | 263 | exports._getInnerHtml = function(el) { 264 | return function(eb, cb) { 265 | el.getInnerHtml().then(cb, eb); 266 | }; 267 | }; 268 | 269 | exports._getSize = function(el) { 270 | return function(eb, cb) { 271 | el.getSize().then(cb, eb); 272 | }; 273 | }; 274 | 275 | exports._getLocation = function(el) { 276 | return function(eb, cb) { 277 | el.getLocation().then(cb, eb); 278 | }; 279 | }; 280 | 281 | function execute(driver) { 282 | return function(action) { 283 | return function(eb, cb) { 284 | driver.executeScript(action).then(cb, eb); 285 | }; 286 | }; 287 | } 288 | 289 | exports._executeStr = execute; 290 | 291 | exports._affLocator = function(elementToAff) { 292 | return function(eb, cb) { 293 | return cb(function(el) { 294 | return elementToAff(el)(); 295 | }); 296 | }; 297 | }; 298 | 299 | exports._clearEl = function(el) { 300 | return function(eb, cb) { 301 | el.clear().then(cb, eb); 302 | }; 303 | }; 304 | 305 | exports._clickEl = function(el) { 306 | return function(eb, cb) { 307 | el.click().then(cb, eb); 308 | }; 309 | }; 310 | 311 | 312 | exports._takeScreenshot = function(driver) { 313 | return function(eb, cb) { 314 | driver.takeScreenshot() 315 | .then(cb, eb); 316 | }; 317 | }; 318 | 319 | 320 | exports._saveScreenshot = function(fileName) { 321 | return function(driver) { 322 | return function(eb, cb) { 323 | driver.takeScreenshot() 324 | .then(function(str) { 325 | fs.writeFile(path.resolve(fileName), 326 | str.replace(/^data:image\/png;base64,/,""), 327 | { 328 | encoding: "base64", 329 | flag: "w+" 330 | }, 331 | function(err) { 332 | if (err) return eb(err); 333 | return cb(); 334 | }); 335 | }, eb); 336 | }; 337 | }; 338 | }; 339 | 340 | 341 | exports._getWindow = function(window) { 342 | return function(eb, cb) { 343 | try { 344 | return cb(window.manage().window()); 345 | } 346 | catch (e) { 347 | return eb(e); 348 | } 349 | }; 350 | }; 351 | 352 | exports._getWindowPosition = function(window) { 353 | return function(eb, cb) { 354 | return window.getPosition() 355 | .then(cb, eb); 356 | }; 357 | }; 358 | 359 | exports._getWindowSize = function(window) { 360 | return function(eb, cb) { 361 | return window.getSize() 362 | .then(cb, eb); 363 | }; 364 | }; 365 | 366 | exports._maximizeWindow = function(window) { 367 | return function(eb, cb) { 368 | return window.maximize() 369 | .then(cb, eb); 370 | }; 371 | }; 372 | 373 | exports._setWindowPosition = function(loc) { 374 | return function(window) { 375 | return function(eb, cb) { 376 | return window.setPosition(loc.x, loc.y) 377 | .then(cb, eb); 378 | }; 379 | }; 380 | }; 381 | 382 | exports._setWindowSize = function(size) { 383 | return function(window) { 384 | return function(eb, cb) { 385 | return window.setSize(size.width, size.height) 386 | .then(cb, eb); 387 | }; 388 | }; 389 | }; 390 | 391 | exports._getWindowScroll = function(driver) { 392 | return function(eb, cb) { 393 | driver.executeScript(function() { 394 | return { 395 | x: window.scrollX, 396 | y: window.scrollY 397 | }; 398 | }).then(cb, eb); 399 | }; 400 | }; 401 | 402 | exports._getWindowHandle = function(driver) { 403 | return function(eb, cb) { 404 | driver.getWindowHandle().then(cb, eb); 405 | }; 406 | }; 407 | 408 | exports._getAllWindowHandles = function(driver) { 409 | return function(eb, cb) { 410 | driver.getAllWindowHandles().then(cb, eb); 411 | }; 412 | }; 413 | 414 | exports._switchTo = function(handle) { 415 | return function(driver) { 416 | return function(eb, cb) { 417 | return driver.switchTo().window(handle).then(cb, eb); 418 | }; 419 | }; 420 | }; 421 | 422 | exports._close = function(driver) { 423 | return function(eb, cb) { 424 | return driver.close().then(cb, eb); 425 | }; 426 | }; 427 | -------------------------------------------------------------------------------- /src/Selenium.purs: -------------------------------------------------------------------------------- 1 | module Selenium 2 | ( get 3 | , wait 4 | , quit 5 | , byClassName 6 | , byCss 7 | , byId 8 | , byName 9 | , byXPath 10 | , affLocator 11 | , findElement 12 | , loseElement 13 | , findElements 14 | , findChild 15 | , findChildren 16 | , findExact 17 | , showLocator 18 | , childExact 19 | , navigateBack 20 | , navigateForward 21 | , refresh 22 | , navigateTo 23 | , getCurrentUrl 24 | , executeStr 25 | , sendKeysEl 26 | , clickEl 27 | , getCssValue 28 | , getAttribute 29 | , getText 30 | , getTitle 31 | , isDisplayed 32 | , isEnabled 33 | , getInnerHtml 34 | , getSize 35 | , getLocation 36 | , clearEl 37 | , setFileDetector 38 | , takeScreenshot 39 | , saveScreenshot 40 | , setWindowSize 41 | , getWindowSize 42 | , maximizeWindow 43 | , setWindowPosition 44 | , getWindowPosition 45 | , getWindow 46 | , getWindowScroll 47 | , getWindowHandle 48 | , getAllWindowHandles 49 | , switchTo 50 | , close 51 | ) where 52 | 53 | import Prelude 54 | 55 | import Effect.Aff (Aff, attempt) 56 | import Effect.Aff.Compat (EffectFnAff, fromEffectFnAff) 57 | import Effect (Effect) 58 | import Effect.Exception (error) 59 | import Control.Monad.Error.Class (throwError) 60 | import Control.Promise (Promise) 61 | import Control.Promise (fromAff) as Promise 62 | import Data.Array (uncons) 63 | import Data.Either (either) 64 | import Foreign (Foreign) 65 | import Data.Maybe (Maybe(..)) 66 | import Data.Time.Duration (Milliseconds) 67 | import Data.Tuple (Tuple(..)) 68 | import Data.Unfoldable (class Unfoldable, unfoldr) 69 | import Selenium.Types (Driver, WindowHandle, Location, Window, Size, Element, FileDetector, Locator) 70 | 71 | foreign import _get 72 | ∷ Driver 73 | → String 74 | → EffectFnAff Unit 75 | 76 | foreign import _wait 77 | ∷ Effect (Promise Boolean) 78 | → Milliseconds 79 | → Driver 80 | → EffectFnAff Unit 81 | 82 | foreign import _quit 83 | ∷ Driver 84 | → EffectFnAff Unit 85 | 86 | get 87 | ∷ Driver 88 | → String 89 | → Aff Unit 90 | get driver str = fromEffectFnAff $ _get driver str 91 | 92 | -- | Wait until first argument returns 'true'. If it returns false an error will be raised 93 | wait 94 | ∷ Aff Boolean 95 | → Milliseconds 96 | → Driver 97 | → Aff Unit 98 | wait action time driver = fromEffectFnAff $ _wait (Promise.fromAff action) time driver 99 | 100 | -- | Finalizer 101 | quit 102 | ∷ Driver 103 | → Aff Unit 104 | quit driver = fromEffectFnAff $ _quit driver 105 | 106 | -- LOCATOR BUILDERS 107 | foreign import _byClassName 108 | ∷ String → EffectFnAff Locator 109 | foreign import _byCss 110 | ∷ String → EffectFnAff Locator 111 | foreign import _byId 112 | ∷ String → EffectFnAff Locator 113 | foreign import _byName 114 | ∷ String → EffectFnAff Locator 115 | foreign import _byXPath 116 | ∷ String → EffectFnAff Locator 117 | 118 | foreign import _affLocator 119 | ∷ (Element → Effect (Promise Element)) 120 | → EffectFnAff Locator 121 | 122 | foreign import showLocator 123 | ∷ Locator 124 | → String 125 | 126 | foreign import _findElement 127 | ∷ ∀ a 128 | . Maybe a 129 | → (a → Maybe a) 130 | → Driver 131 | → Locator 132 | → EffectFnAff (Maybe Element) 133 | 134 | foreign import _findChild 135 | ∷ ∀ a 136 | . Maybe a 137 | → (a → Maybe a) 138 | → Element 139 | → Locator 140 | → EffectFnAff (Maybe Element) 141 | 142 | foreign import _findElements 143 | ∷ Driver 144 | → Locator 145 | → EffectFnAff (Array Element) 146 | 147 | foreign import _findChildren 148 | ∷ Element 149 | → Locator 150 | → EffectFnAff (Array Element) 151 | 152 | foreign import _findExact 153 | ∷ Driver 154 | → Locator 155 | → EffectFnAff Element 156 | 157 | foreign import _childExact 158 | ∷ Element 159 | → Locator 160 | → EffectFnAff Element 161 | 162 | -- LOCATOR BUILDERS 163 | byClassName 164 | ∷ String → Aff Locator 165 | byClassName className = fromEffectFnAff $ _byClassName className 166 | 167 | byCss 168 | ∷ String → Aff Locator 169 | byCss css = fromEffectFnAff $ _byCss css 170 | 171 | byId 172 | ∷ String → Aff Locator 173 | byId id = fromEffectFnAff $ _byId id 174 | 175 | byName 176 | ∷ String → Aff Locator 177 | byName name = fromEffectFnAff $ _byName name 178 | 179 | byXPath 180 | ∷ String → Aff Locator 181 | byXPath xpath = fromEffectFnAff $ _byXPath xpath 182 | 183 | -- | Build locator from asynchronous function returning element. 184 | -- | I.e. this locator will find first visible element with `.common-element` class 185 | -- | ```purescript 186 | -- | affLocator \el → do 187 | -- | commonElements ← byCss ".common-element" >>= findElements el 188 | -- | flagedElements ← traverse (\el → Tuple el <$> isVisible el) commonElements 189 | -- | maybe err pure $ foldl foldFn Nothing flagedElements 190 | -- | where 191 | -- | err = throwError $ error "all common elements are not visible" 192 | -- | foldFn Nothing (Tuple el true) = Just el 193 | -- | foldFn a _ = a 194 | -- | ``` 195 | affLocator 196 | ∷ (Element → Aff Element) 197 | → Aff Locator 198 | affLocator locator = fromEffectFnAff $ _affLocator (Promise.fromAff <<< locator) 199 | 200 | -- | Tries to find an element starting from `document`; will return `Nothing` if there 201 | -- | is no element can be found by locator 202 | findElement 203 | ∷ Driver → Locator → Aff (Maybe Element) 204 | findElement driver = 205 | fromEffectFnAff <<< _findElement Nothing Just driver 206 | 207 | -- | Same as `findElement` but starts searching from custom element 208 | findChild 209 | ∷ Element 210 | → Locator 211 | → Aff (Maybe Element) 212 | findChild element = fromEffectFnAff <<< _findChild Nothing Just element 213 | 214 | findExact 215 | ∷ Driver 216 | → Locator 217 | → Aff Element 218 | findExact driver = fromEffectFnAff <<< _findExact driver 219 | 220 | childExact 221 | ∷ Element 222 | → Locator 223 | → Aff Element 224 | childExact element locator = fromEffectFnAff $ _childExact element locator 225 | 226 | -- | Tries to find element and throws an error if it succeeds. 227 | loseElement 228 | ∷ Driver 229 | → Locator 230 | → Aff Unit 231 | loseElement driver locator = do 232 | result ← attempt $ findExact driver locator 233 | either (const $ pure unit) (const $ throwError $ error failMessage) result 234 | where 235 | failMessage = "Found element with locator: " <> showLocator locator 236 | 237 | -- | Finds elements by locator from `document` 238 | findElements 239 | ∷ ∀ f. (Unfoldable f) ⇒ Driver → Locator → Aff (f Element) 240 | findElements driver locator = 241 | map fromArray $ fromEffectFnAff $ _findElements driver locator 242 | 243 | -- | Same a `findElements` but starts searching from custom element 244 | findChildren 245 | ∷ ∀ f 246 | . (Unfoldable f) 247 | ⇒ Element 248 | → Locator 249 | → Aff (f Element) 250 | findChildren el locator = 251 | map fromArray $ fromEffectFnAff $ _findChildren el locator 252 | 253 | foreign import _setFileDetector 254 | ∷ Driver → FileDetector → EffectFnAff Unit 255 | foreign import _navigateBack 256 | ∷ Driver → EffectFnAff Unit 257 | foreign import _navigateForward 258 | ∷ Driver → EffectFnAff Unit 259 | foreign import _refresh 260 | ∷ Driver → EffectFnAff Unit 261 | foreign import _navigateTo 262 | ∷ String → Driver → EffectFnAff Unit 263 | foreign import _getCurrentUrl 264 | ∷ Driver → EffectFnAff String 265 | foreign import _getTitle 266 | ∷ Driver → EffectFnAff String 267 | foreign import _executeStr 268 | ∷ Driver → String → EffectFnAff Foreign 269 | foreign import _sendKeysEl 270 | ∷ String → Element → EffectFnAff Unit 271 | foreign import _clickEl 272 | ∷ Element → EffectFnAff Unit 273 | foreign import _getCssValue 274 | ∷ Element → String → EffectFnAff String 275 | foreign import _getAttribute 276 | ∷ ∀ a 277 | . Maybe a 278 | → (a → Maybe a) 279 | → Element 280 | → String 281 | → EffectFnAff (Maybe String) 282 | 283 | setFileDetector 284 | ∷ Driver → FileDetector → Aff Unit 285 | setFileDetector driver detector = fromEffectFnAff $ _setFileDetector driver detector 286 | 287 | navigateBack 288 | ∷ Driver → Aff Unit 289 | navigateBack driver = fromEffectFnAff $ _navigateBack driver 290 | 291 | navigateForward 292 | ∷ Driver → Aff Unit 293 | navigateForward driver = fromEffectFnAff $ _navigateForward driver 294 | 295 | refresh 296 | ∷ Driver → Aff Unit 297 | refresh driver = fromEffectFnAff $ _refresh driver 298 | 299 | navigateTo 300 | ∷ String → Driver → Aff Unit 301 | navigateTo uri driver = fromEffectFnAff $ _navigateTo uri driver 302 | 303 | getCurrentUrl 304 | ∷ Driver → Aff String 305 | getCurrentUrl driver = fromEffectFnAff $ _getCurrentUrl driver 306 | 307 | getTitle 308 | ∷ Driver → Aff String 309 | getTitle driver = fromEffectFnAff $ _getTitle driver 310 | 311 | -- | Executes javascript script from `String` argument. 312 | executeStr 313 | ∷ Driver → String → Aff Foreign 314 | executeStr driver strToExecute = fromEffectFnAff $ _executeStr driver strToExecute 315 | 316 | sendKeysEl 317 | ∷ String → Element → Aff Unit 318 | sendKeysEl key element = fromEffectFnAff $ _sendKeysEl key element 319 | 320 | clickEl 321 | ∷ Element → Aff Unit 322 | clickEl element = fromEffectFnAff $ _clickEl element 323 | 324 | getCssValue 325 | ∷ Element → String → Aff String 326 | getCssValue element attribute = fromEffectFnAff $ _getCssValue element attribute 327 | 328 | -- | Tries to find an element starting from `document`; will return `Nothing` if there 329 | -- | is no element can be found by locator 330 | getAttribute 331 | ∷ Element → String → Aff (Maybe String) 332 | getAttribute element string = fromEffectFnAff $ _getAttribute Nothing Just element string 333 | 334 | foreign import _getText 335 | ∷ Element → EffectFnAff String 336 | foreign import _isDisplayed 337 | ∷ Element → EffectFnAff Boolean 338 | foreign import _isEnabled 339 | ∷ Element → EffectFnAff Boolean 340 | foreign import _getInnerHtml 341 | ∷ Element → EffectFnAff String 342 | foreign import _getSize 343 | ∷ Element → EffectFnAff Size 344 | foreign import _getLocation 345 | ∷ Element → EffectFnAff Location 346 | foreign import _clearEl 347 | ∷ Element → EffectFnAff Unit 348 | 349 | foreign import _takeScreenshot 350 | ∷ Driver → EffectFnAff String 351 | foreign import _saveScreenshot 352 | ∷ String → Driver → EffectFnAff Unit 353 | 354 | foreign import _getWindow 355 | ∷ Driver → EffectFnAff Window 356 | foreign import _getWindowPosition 357 | ∷ Window → EffectFnAff Location 358 | foreign import _getWindowSize 359 | ∷ Window → EffectFnAff Size 360 | foreign import _maximizeWindow 361 | ∷ Window → EffectFnAff Unit 362 | foreign import _setWindowPosition 363 | ∷ Location → Window → EffectFnAff Unit 364 | foreign import _setWindowSize 365 | ∷ Size → Window → EffectFnAff Unit 366 | foreign import _getWindowScroll 367 | ∷ Driver → EffectFnAff Location 368 | foreign import _getWindowHandle 369 | ∷ Driver → EffectFnAff WindowHandle 370 | foreign import _getAllWindowHandles 371 | ∷ Driver → EffectFnAff (Array WindowHandle) 372 | 373 | getText 374 | ∷ Element → Aff String 375 | getText element = fromEffectFnAff $ _getText element 376 | 377 | isDisplayed 378 | ∷ Element → Aff Boolean 379 | isDisplayed element = fromEffectFnAff $ _isDisplayed element 380 | 381 | isEnabled 382 | ∷ Element → Aff Boolean 383 | isEnabled element = fromEffectFnAff $ _isEnabled element 384 | 385 | getInnerHtml 386 | ∷ Element → Aff String 387 | getInnerHtml element = fromEffectFnAff $ _getInnerHtml element 388 | 389 | getSize 390 | ∷ Element → Aff Size 391 | getSize element = fromEffectFnAff $ _getSize element 392 | 393 | getLocation 394 | ∷ Element → Aff Location 395 | getLocation element = fromEffectFnAff $ _getLocation element 396 | 397 | -- | Clear `value` of element, if it has no value will do nothing. 398 | -- | If `value` is weakly referenced by `virtual-dom` (`purescript-halogen`) 399 | -- | will not work -- to clear such inputs one should use direct signal from 400 | -- | `Selenium.ActionSequence` 401 | clearEl 402 | ∷ Element → Aff Unit 403 | clearEl element = fromEffectFnAff $ _clearEl element 404 | 405 | -- | Returns png base64 encoded png image 406 | takeScreenshot 407 | ∷ Driver → Aff String 408 | takeScreenshot driver = fromEffectFnAff $ _takeScreenshot driver 409 | 410 | -- | Saves screenshot to path 411 | saveScreenshot 412 | ∷ String → Driver → Aff Unit 413 | saveScreenshot name driver = fromEffectFnAff $ _saveScreenshot name driver 414 | 415 | getWindow 416 | ∷ Driver → Aff Window 417 | getWindow driver = fromEffectFnAff $ _getWindow driver 418 | 419 | getWindowPosition 420 | ∷ Window → Aff Location 421 | getWindowPosition window = fromEffectFnAff $ _getWindowPosition window 422 | 423 | getWindowSize 424 | ∷ Window → Aff Size 425 | getWindowSize window = fromEffectFnAff $ _getWindowSize window 426 | 427 | maximizeWindow 428 | ∷ Window → Aff Unit 429 | maximizeWindow window = fromEffectFnAff $ _maximizeWindow window 430 | 431 | setWindowPosition 432 | ∷ Location → Window → Aff Unit 433 | setWindowPosition location window = fromEffectFnAff $ _setWindowPosition location window 434 | 435 | setWindowSize 436 | ∷ Size → Window → Aff Unit 437 | setWindowSize size window = fromEffectFnAff $ _setWindowSize size window 438 | 439 | getWindowScroll 440 | ∷ Driver → Aff Location 441 | getWindowScroll driver = fromEffectFnAff $ _getWindowScroll driver 442 | 443 | getWindowHandle 444 | ∷ Driver → Aff WindowHandle 445 | getWindowHandle driver = fromEffectFnAff $ _getWindowHandle driver 446 | 447 | getAllWindowHandles 448 | ∷ ∀ f 449 | . (Unfoldable f) 450 | ⇒ Driver 451 | → Aff (f WindowHandle) 452 | getAllWindowHandles driver = 453 | map fromArray $ fromEffectFnAff $ _getAllWindowHandles driver 454 | 455 | fromArray 456 | ∷ ∀ a f. (Unfoldable f) ⇒ Array a → f a 457 | fromArray = 458 | unfoldr (\xs → (\rec → Tuple rec.head rec.tail) <$> uncons xs) 459 | 460 | foreign import _switchTo 461 | ∷ WindowHandle → Driver → EffectFnAff Unit 462 | foreign import _close 463 | ∷ Driver → EffectFnAff Unit 464 | 465 | switchTo 466 | ∷ WindowHandle → Driver → Aff Unit 467 | switchTo handle driver = fromEffectFnAff $ _switchTo handle driver 468 | 469 | close 470 | ∷ Driver → Aff Unit 471 | close driver = fromEffectFnAff $ _close driver 472 | -------------------------------------------------------------------------------- /src/Selenium/ActionSequence.js: -------------------------------------------------------------------------------- 1 | // module Selenium.ActionSequence 2 | var webdriver = require("selenium-webdriver"); 3 | 4 | exports._newSequence = function(driver) { 5 | return function(eb, cb) { 6 | try { 7 | return cb(new webdriver.ActionSequence(driver)); 8 | } 9 | catch (e) { 10 | return eb(e); 11 | } 12 | }; 13 | }; 14 | 15 | exports._performSequence = function(sequence) { 16 | return function(eb, cb) { 17 | try { 18 | return sequence.perform().then(cb, eb); 19 | } 20 | catch (e) { 21 | return eb(e); 22 | } 23 | }; 24 | }; 25 | 26 | exports._click = function(seq, btn, el) { 27 | return seq.click(el, btn); 28 | }; 29 | exports._doubleClick = function(seq, btn, el) { 30 | return seq.doubleClick(el, btn); 31 | }; 32 | exports._mouseToElement = function(seq, el) { 33 | return seq.mouseMove(el); 34 | }; 35 | exports._mouseToLocation = function(seq, loc) { 36 | return seq.mouseMove(loc); 37 | }; 38 | exports._mouseDown = function(seq, btn, el) { 39 | return seq.mouseDown(el, btn); 40 | }; 41 | exports._mouseUp = function(seq, btn, el) { 42 | return seq.mouseUp(el, btn); 43 | }; 44 | exports._keyDown = function(seq, key) { 45 | return seq.keyDown(key); 46 | }; 47 | exports._keyUp = function(seq, key) { 48 | return seq.keyUp(key); 49 | }; 50 | exports._sendKeys = function(seq, keys) { 51 | return seq.sendKeys(keys); 52 | }; 53 | exports._dndToElement = function(seq, el, tgt) { 54 | return seq.dragAndDrop(el, tgt); 55 | }; 56 | exports._dndToLocation = function(seq, el, tgt) { 57 | return seq.dragAndDrop(el, tgt); 58 | }; 59 | -------------------------------------------------------------------------------- /src/Selenium/ActionSequence.purs: -------------------------------------------------------------------------------- 1 | -- | DSL for building action sequences 2 | module Selenium.ActionSequence 3 | ( sequence 4 | , keyUp 5 | , keyDown 6 | , mouseToLocation 7 | , dndToElement 8 | , dndToLocation 9 | , sendKeys 10 | , mouseUp 11 | , mouseDown 12 | , hover 13 | , doubleClick 14 | , leftClick 15 | , click 16 | , Sequence 17 | ) where 18 | 19 | import Prelude 20 | 21 | import Effect.Aff (Aff) 22 | import Effect.Aff.Compat (EffectFnAff, fromEffectFnAff) 23 | import Control.Monad.Writer (Writer, execWriter) 24 | import Control.Monad.Writer.Class (tell) 25 | import Data.Foldable (foldl) 26 | import Data.Function.Uncurried (Fn3, Fn2, runFn3, runFn2) 27 | import Data.List (List, singleton) 28 | import Selenium.MouseButton (leftButton) 29 | import Selenium.Types (ActionSequence, Location, Element, ControlKey, MouseButton, Driver) 30 | 31 | data Command 32 | = Click MouseButton Element 33 | | DoubleClick MouseButton Element 34 | | KeyDown ControlKey 35 | | KeyUp ControlKey 36 | | MouseDown MouseButton Element 37 | | MouseToElement Element 38 | | MouseToLocation Location 39 | | MouseUp MouseButton Element 40 | | DnDToElement Element Element 41 | | DnDToLocation Element Location 42 | | SendKeys String 43 | 44 | newtype Sequence a = Sequence (Writer (List Command) a) 45 | 46 | unSequence ∷ ∀ a. Sequence a → Writer (List Command) a 47 | unSequence (Sequence a) = a 48 | 49 | instance functorSequence ∷ Functor Sequence where 50 | map f (Sequence a) = Sequence $ f <$> a 51 | 52 | instance applySequence ∷ Apply Sequence where 53 | apply (Sequence f) (Sequence w) = Sequence $ f <*> w 54 | 55 | instance bindSequence ∷ Bind Sequence where 56 | bind (Sequence w) f = Sequence $ w >>= unSequence <<< f 57 | 58 | instance applicativeSequence ∷ Applicative Sequence where 59 | pure = Sequence <<< pure 60 | 61 | instance monadSequence ∷ Monad Sequence 62 | 63 | rule ∷ Command → Sequence Unit 64 | rule = Sequence <<< tell <<< singleton 65 | 66 | 67 | click ∷ MouseButton → Element → Sequence Unit 68 | click btn el = rule $ Click btn el 69 | 70 | leftClick ∷ Element → Sequence Unit 71 | leftClick = click leftButton 72 | 73 | doubleClick ∷ MouseButton → Element → Sequence Unit 74 | doubleClick btn el = rule $ DoubleClick btn el 75 | 76 | hover ∷ Element → Sequence Unit 77 | hover el = rule $ MouseToElement el 78 | 79 | mouseDown ∷ MouseButton → Element → Sequence Unit 80 | mouseDown btn el = rule $ MouseDown btn el 81 | 82 | mouseUp ∷ MouseButton → Element → Sequence Unit 83 | mouseUp btn el = rule $ MouseUp btn el 84 | 85 | sendKeys ∷ String → Sequence Unit 86 | sendKeys keys = rule $ SendKeys keys 87 | 88 | mouseToLocation ∷ Location → Sequence Unit 89 | mouseToLocation loc = rule $ MouseToLocation loc 90 | 91 | -- | This function is used only with special keys (META, CONTROL, etc) 92 | -- | It doesn't emulate __keyDown__ event 93 | keyDown ∷ ControlKey → Sequence Unit 94 | keyDown k = rule $ KeyDown k 95 | -- | This function is used only with special keys (META, CONTROL, etc) 96 | -- | It doesn't emulate __keyUp__ event 97 | keyUp ∷ ControlKey → Sequence Unit 98 | keyUp k = rule $ KeyUp k 99 | 100 | dndToElement ∷ Element → Element → Sequence Unit 101 | dndToElement el tgt = rule $ DnDToElement el tgt 102 | 103 | dndToLocation ∷ Element → Location → Sequence Unit 104 | dndToLocation el tgt = rule $ DnDToLocation el tgt 105 | 106 | sequence ∷ Driver → Sequence Unit → Aff Unit 107 | sequence driver commands = do 108 | seq ← newSequence driver 109 | performSequence $ interpret (execWriter $ unSequence commands) seq 110 | 111 | interpret ∷ List Command → ActionSequence → ActionSequence 112 | interpret commands initSeq = 113 | foldl foldFn initSeq commands 114 | where 115 | foldFn ∷ ActionSequence → Command → ActionSequence 116 | foldFn seq (Click btn el) = runFn3 _click seq btn el 117 | foldFn seq (DoubleClick btn el) = runFn3 _doubleClick seq btn el 118 | foldFn seq (MouseToElement el) = runFn2 _mouseToElement seq el 119 | foldFn seq (MouseToLocation loc) = runFn2 _mouseToLocation seq loc 120 | foldFn seq (MouseDown btn el) = runFn3 _mouseDown seq btn el 121 | foldFn seq (MouseUp btn el) = runFn3 _mouseUp seq btn el 122 | foldFn seq (KeyDown k) = runFn2 _keyDown seq k 123 | foldFn seq (KeyUp k) = runFn2 _keyUp seq k 124 | foldFn seq (SendKeys ks) = runFn2 _sendKeys seq ks 125 | foldFn seq (DnDToElement el tgt) = runFn3 _dndToElement seq el tgt 126 | foldFn seq (DnDToLocation el tgt) = runFn3 _dndToLocation seq el tgt 127 | 128 | 129 | foreign import _newSequence ∷ Driver → EffectFnAff ActionSequence 130 | foreign import _performSequence ∷ ActionSequence → EffectFnAff Unit 131 | 132 | newSequence ∷ Driver → Aff ActionSequence 133 | newSequence = fromEffectFnAff <<< _newSequence 134 | 135 | performSequence ∷ ActionSequence → Aff Unit 136 | performSequence = fromEffectFnAff <<< _performSequence 137 | 138 | foreign import _click ∷ Fn3 ActionSequence MouseButton Element ActionSequence 139 | foreign import _doubleClick ∷ Fn3 ActionSequence MouseButton Element ActionSequence 140 | foreign import _mouseToElement ∷ Fn2 ActionSequence Element ActionSequence 141 | foreign import _mouseToLocation ∷ Fn2 ActionSequence Location ActionSequence 142 | foreign import _mouseDown ∷ Fn3 ActionSequence MouseButton Element ActionSequence 143 | foreign import _mouseUp ∷ Fn3 ActionSequence MouseButton Element ActionSequence 144 | foreign import _keyDown ∷ Fn2 ActionSequence ControlKey ActionSequence 145 | foreign import _keyUp ∷ Fn2 ActionSequence ControlKey ActionSequence 146 | foreign import _sendKeys ∷ Fn2 ActionSequence String ActionSequence 147 | foreign import _dndToElement ∷ Fn3 ActionSequence Element Element ActionSequence 148 | foreign import _dndToLocation ∷ Fn3 ActionSequence Element Location ActionSequence 149 | -------------------------------------------------------------------------------- /src/Selenium/Browser.js: -------------------------------------------------------------------------------- 1 | // module Selenium.Browser 2 | 3 | exports._browserCapabilities = function(br) { 4 | return {browserName: br}; 5 | }; 6 | 7 | exports.versionCapabilities = function(v) { 8 | return {version: v}; 9 | }; 10 | 11 | exports.platformCapabilities = function(p) { 12 | return {platform: p}; 13 | }; 14 | -------------------------------------------------------------------------------- /src/Selenium/Browser.purs: -------------------------------------------------------------------------------- 1 | module Selenium.Browser 2 | ( Browser(..) 3 | , browser2str 4 | , str2browser 5 | , browserCapabilities 6 | , versionCapabilities 7 | , platformCapabilities 8 | ) where 9 | 10 | import Prelude 11 | 12 | import Data.Maybe (Maybe(..)) 13 | 14 | import Selenium.Capabilities (Capabilities) 15 | 16 | data Browser 17 | = PhantomJS 18 | | Chrome 19 | | FireFox 20 | | IE 21 | | Opera 22 | | Safari 23 | 24 | browser2str ∷ Browser → String 25 | browser2str PhantomJS = "phantomjs" 26 | browser2str Chrome = "chrome" 27 | browser2str FireFox = "firefox" 28 | browser2str Opera = "opera" 29 | browser2str Safari = "safari" 30 | browser2str IE = "ie" 31 | 32 | str2browser ∷ String → Maybe Browser 33 | str2browser "phantomjs" = pure PhantomJS 34 | str2browser "chrome" = pure Chrome 35 | str2browser "firefox" = pure FireFox 36 | str2browser "ie" = pure IE 37 | str2browser "opera" = pure Opera 38 | str2browser "safari" = pure Safari 39 | str2browser _ = Nothing 40 | 41 | foreign import _browserCapabilities ∷ String → Capabilities 42 | 43 | browserCapabilities ∷ Browser → Capabilities 44 | browserCapabilities = browser2str >>> _browserCapabilities 45 | 46 | foreign import versionCapabilities ∷ String → Capabilities 47 | foreign import platformCapabilities ∷ String → Capabilities 48 | -------------------------------------------------------------------------------- /src/Selenium/Builder.js: -------------------------------------------------------------------------------- 1 | // module Selenium.Builder 2 | 3 | var webdriver = require("selenium-webdriver"); 4 | 5 | exports._newBuilder = function(eb, cb) { 6 | try { 7 | return cb(new webdriver.Builder()); 8 | } 9 | catch (e) { 10 | return eb(e); 11 | } 12 | }; 13 | 14 | exports._build = function(builder) { 15 | return function(eb, cb) { 16 | builder.build().then(cb, eb); 17 | }; 18 | }; 19 | 20 | exports._usingServer = function(b, s) { 21 | return b.usingServer(s); 22 | }; 23 | 24 | exports._setScrollBehaviour = function(b, bh) { 25 | return b.setScrollBehaviour(bh); 26 | }; 27 | 28 | exports._withCapabilities = function(b, c) { 29 | return b.withCapabilities(c); 30 | }; 31 | -------------------------------------------------------------------------------- /src/Selenium/Builder.purs: -------------------------------------------------------------------------------- 1 | module Selenium.Builder 2 | ( build 3 | , browser 4 | , version 5 | , platform 6 | , usingServer 7 | , scrollBehaviour 8 | , withCapabilities 9 | , Build 10 | ) where 11 | 12 | import Prelude 13 | 14 | import Effect.Aff (Aff) 15 | import Effect.Aff.Compat (EffectFnAff, fromEffectFnAff) 16 | import Control.Monad.Writer (Writer, execWriter) 17 | import Control.Monad.Writer.Class (tell) 18 | import Data.Foldable (foldl) 19 | import Data.Function.Uncurried (Fn2, runFn2) 20 | import Data.List (List(..), singleton) 21 | import Data.Tuple (Tuple(..)) 22 | import Selenium.Browser (Browser, browserCapabilities, platformCapabilities, versionCapabilities) 23 | import Selenium.Capabilities (Capabilities, emptyCapabilities) 24 | import Selenium.Types (Builder, ScrollBehaviour, Driver, SafariOptions, ProxyConfig, OperaOptions, LoggingPrefs, IEOptions, FirefoxOptions, ControlFlow, ChromeOptions) 25 | 26 | data Command 27 | = SetChromeOptions ChromeOptions 28 | | SetControlFlow ControlFlow 29 | | SetEnableNativeEvents Boolean 30 | | SetFirefoxOptions FirefoxOptions 31 | | SetIeOptions IEOptions 32 | | SetLoggingPrefs LoggingPrefs 33 | | SetOperaOptions OperaOptions 34 | | SetProxy ProxyConfig 35 | | SetSafariOptions SafariOptions 36 | | SetScrollBehaviour ScrollBehaviour 37 | | UsingServer String 38 | 39 | newtype Build a = Build (Writer (Tuple Capabilities (List Command)) a) 40 | 41 | unBuild ∷ ∀ a. Build a → Writer (Tuple Capabilities (List Command)) a 42 | unBuild (Build a) = a 43 | 44 | instance functorBuild ∷ Functor Build where 45 | map f (Build a) = Build $ f <$> a 46 | 47 | instance applyBuild ∷ Apply Build where 48 | apply (Build f) (Build w) = Build $ f <*> w 49 | 50 | instance bindBuild ∷ Bind Build where 51 | bind (Build w) f = Build $ w >>= unBuild <<< f 52 | 53 | instance applicativeBuild ∷ Applicative Build where 54 | pure = Build <<< pure 55 | 56 | instance monadBuild ∷ Monad Build 57 | 58 | rule ∷ Command → Build Unit 59 | rule = Build <<< tell <<< Tuple emptyCapabilities <<< singleton 60 | 61 | version ∷ String → Build Unit 62 | version = withCapabilities <<< versionCapabilities 63 | 64 | platform ∷ String → Build Unit 65 | platform = withCapabilities <<< platformCapabilities 66 | 67 | usingServer ∷ String → Build Unit 68 | usingServer = rule <<< UsingServer 69 | 70 | scrollBehaviour ∷ ScrollBehaviour → Build Unit 71 | scrollBehaviour = rule <<< SetScrollBehaviour 72 | 73 | withCapabilities ∷ Capabilities → Build Unit 74 | withCapabilities c = Build $ tell $ Tuple c noRules 75 | where 76 | noRules ∷ List Command 77 | noRules = Nil 78 | 79 | browser ∷ Browser → Build Unit 80 | browser = withCapabilities <<< browserCapabilities 81 | 82 | build ∷ Build Unit → Aff Driver 83 | build dsl = do 84 | builder ← fromEffectFnAff _newBuilder 85 | case execWriter $ unBuild dsl of 86 | Tuple capabilities commands → 87 | fromEffectFnAff $ _build $ runFn2 _withCapabilities (interpret commands builder) capabilities 88 | 89 | interpret ∷ List Command → Builder → Builder 90 | interpret commands initialBuilder = foldl foldFn initialBuilder commands 91 | where 92 | foldFn ∷ Builder → Command → Builder 93 | foldFn b (UsingServer s) = runFn2 _usingServer b s 94 | foldFn b (SetScrollBehaviour bh) = runFn2 _setScrollBehaviour b bh 95 | foldFn b _ = b 96 | 97 | 98 | foreign import _newBuilder ∷ EffectFnAff Builder 99 | foreign import _build ∷ Builder → EffectFnAff Driver 100 | 101 | foreign import _usingServer ∷ Fn2 Builder String Builder 102 | foreign import _setScrollBehaviour ∷ Fn2 Builder ScrollBehaviour Builder 103 | foreign import _withCapabilities ∷ Fn2 Builder Capabilities Builder 104 | -------------------------------------------------------------------------------- /src/Selenium/Capabilities.js: -------------------------------------------------------------------------------- 1 | // module Selenium.Capabilities 2 | 3 | exports.emptyCapabilities = {}; 4 | 5 | exports.appendCapabilities = function(first) { 6 | return function(second) { 7 | var i, key, 8 | firstKeys = Object.keys(first), 9 | secondKeys = Object.keys(second), 10 | result = {}; 11 | for (i = 0; i < firstKeys.length; i++) { 12 | key = firstKeys[i]; 13 | if (first.hasOwnProperty(key)) { 14 | result[key] = first[key]; 15 | } 16 | } 17 | for (i = 0; i < secondKeys.length; i++) { 18 | key = secondKeys[i]; 19 | if (second.hasOwnProperty(key)) { 20 | result[key] = second[key]; 21 | } 22 | } 23 | return result; 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/Selenium/Capabilities.purs: -------------------------------------------------------------------------------- 1 | module Selenium.Capabilities where 2 | 3 | import Prelude 4 | 5 | foreign import data Capabilities ∷ Type 6 | foreign import emptyCapabilities ∷ Capabilities 7 | foreign import appendCapabilities ∷ Capabilities → Capabilities → Capabilities 8 | 9 | instance semigroupCapabilities ∷ Semigroup Capabilities where 10 | append = appendCapabilities 11 | 12 | instance monoidCapabilities ∷ Monoid Capabilities where 13 | mempty = emptyCapabilities 14 | -------------------------------------------------------------------------------- /src/Selenium/Combinators.purs: -------------------------------------------------------------------------------- 1 | module Selenium.Combinators where 2 | 3 | import Prelude 4 | import Control.Alt ((<|>)) 5 | import Effect.Exception (error) 6 | import Control.Monad.Error.Class (throwError) 7 | import Control.Monad.Trans.Class (lift) 8 | import Data.Either (Either(..), either) 9 | import Data.Maybe (Maybe, isJust, maybe) 10 | import Data.Time.Duration (class Duration, Milliseconds(..)) 11 | import Selenium.Monad (Selenium, getCurrentUrl, wait, attempt, findExact, tryRepeatedlyTo, tryRepeatedlyTo', findElement, byCss, later, byClassName, byName, byId, byXPath) 12 | import Selenium.Types (Element, Locator) 13 | 14 | -- | Retry computation until it successed but not more then `n` times 15 | retry ∷ ∀ e o a. Int → Selenium e o a → Selenium e o a 16 | retry n action = do 17 | res ← attempt action 18 | case res of 19 | Left e 20 | | n > one → retry (n - one) action 21 | | otherwise → lift $ throwError $ error "To many retries" 22 | Right r → pure r 23 | 24 | -- | Tries to find element by string checks: css, xpath, id, name and classname 25 | tryFind ∷ ∀ e o. String → Selenium e o Element 26 | tryFind probablyLocator = 27 | (byCss probablyLocator >>= findExact) <|> 28 | (byXPath probablyLocator >>= findExact) <|> 29 | (byId probablyLocator >>= findExact) <|> 30 | (byName probablyLocator >>= findExact) <|> 31 | (byClassName probablyLocator >>= findExact) 32 | 33 | waitUntilJust ∷ ∀ d e o a. Duration d ⇒ Selenium e o (Maybe a) → d → Selenium e o a 34 | waitUntilJust check time = do 35 | wait (checker $ isJust <$> check) time 36 | check >>= maybe (throwError $ error $ "Maybe was not Just after waiting for isJust") pure 37 | 38 | -- Tries to evaluate `Selenium` if it returns `false` after 500ms 39 | checker ∷ ∀ e o. Selenium e o Boolean → Selenium e o Boolean 40 | checker check = 41 | check >>= if _ 42 | then pure true 43 | else later (Milliseconds 500.0) $ checker check 44 | 45 | getElementByCss ∷ ∀ e o. String → Selenium e o Element 46 | getElementByCss cls = 47 | byCss cls 48 | >>= findElement 49 | >>= maybe (throwError $ error $ "There is no element matching css: " <> cls) pure 50 | 51 | checkNotExistsByCss ∷ ∀ e o. String → Selenium e o Unit 52 | checkNotExistsByCss = contra <<< getElementByCss 53 | 54 | contra ∷ ∀ e o a. Selenium e o a → Selenium e o Unit 55 | contra check = do 56 | eR ← attempt check 57 | either 58 | (const $ pure unit) 59 | (const $ throwError $ error "check successed in contra") eR 60 | 61 | -- | Repeatedly attempts to find an element using the provided selector until the 62 | -- | provided timeout elapses. 63 | tryToFind' ∷ ∀ d e o. Duration d ⇒ d → Selenium e o Locator → Selenium e o Element 64 | tryToFind' timeout locator = tryRepeatedlyTo' timeout $ locator >>= findExact 65 | 66 | -- | Repeatedly tries to find an element using the provided selector until 67 | -- | the provided `Selenium`'s `defaultTimeout` elapses. 68 | tryToFind ∷ ∀ e o. Selenium e o Locator → Selenium e o Element 69 | tryToFind locator = tryRepeatedlyTo $ locator >>= findExact 70 | 71 | -- | Repeatedly tries to evaluate check (third arg) for timeout ms (first arg) 72 | -- | finishes when check evaluates to true. 73 | -- | If there is an error during check or it constantly returns `false` 74 | -- | throws error with message (second arg) 75 | await ∷ ∀ d e o. Duration d ⇒ d → Selenium e o Boolean → Selenium e o Unit 76 | await timeout check = do 77 | ei ← attempt $ wait (checker check) timeout 78 | case ei of 79 | Left _ → throwError $ error "await has no success" 80 | Right _ → pure unit 81 | 82 | awaitUrlChanged ∷ ∀ e o. String → Selenium e o Boolean 83 | awaitUrlChanged oldURL = checker $ (oldURL /= _) <$> getCurrentUrl 84 | -------------------------------------------------------------------------------- /src/Selenium/FFProfile.js: -------------------------------------------------------------------------------- 1 | // module Selenium.FFProfile 2 | 3 | exports._newFFProfile = function(eb, cb) { 4 | var FirefoxProfile = require('selenium-webdriver/firefox/profile.js').Profile; 5 | return cb(new FirefoxProfile()); 6 | }; 7 | 8 | exports._setFFPreference = function(key) { 9 | return function(val) { 10 | return function(p) { 11 | p.setPreference(key, val); 12 | return p; 13 | }; 14 | }; 15 | }; 16 | 17 | exports._encode = function(p) { 18 | return function(eb, cb) { 19 | p.encode() 20 | .then(function(c) { return cb({firefox_profile: c});}, eb); 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/Selenium/FFProfile.purs: -------------------------------------------------------------------------------- 1 | module Selenium.FFProfile 2 | ( FFProfileBuild 3 | , FFPreference 4 | , buildFFProfile 5 | , setPreference 6 | , setStringPreference 7 | , setIntPreference 8 | , setNumberPreference 9 | , setBoolPreference 10 | , intToFFPreference 11 | , numberToFFPreference 12 | , stringToFFPreference 13 | , boolToFFPreference 14 | ) where 15 | 16 | import Prelude 17 | 18 | import Effect.Aff (Aff) 19 | import Effect.Aff.Compat (EffectFnAff, fromEffectFnAff) 20 | import Control.Monad.Writer (Writer, execWriter) 21 | import Control.Monad.Writer.Class (tell) 22 | import Data.Foldable (foldl) 23 | import Foreign (Foreign) 24 | import Data.List (List, singleton) 25 | import Selenium.Capabilities (Capabilities) 26 | import Unsafe.Coerce (unsafeCoerce) 27 | 28 | foreign import data FFProfile ∷ Type 29 | foreign import data FFPreference ∷ Type 30 | 31 | data Command 32 | = SetPreference String FFPreference 33 | 34 | newtype FFProfileBuild a = FFProfileBuild (Writer (List Command) a) 35 | 36 | unFFProfileBuild ∷ ∀ a. FFProfileBuild a → Writer (List Command) a 37 | unFFProfileBuild (FFProfileBuild a) = a 38 | 39 | instance functorFFProfileBuild ∷ Functor FFProfileBuild where 40 | map f (FFProfileBuild a) = FFProfileBuild $ f <$> a 41 | 42 | instance applyFFProfileBuild ∷ Apply FFProfileBuild where 43 | apply (FFProfileBuild f) (FFProfileBuild w) = FFProfileBuild $ f <*> w 44 | 45 | instance bindFFProfileBuild ∷ Bind FFProfileBuild where 46 | bind (FFProfileBuild w) f = FFProfileBuild $ w >>= unFFProfileBuild <<< f 47 | 48 | instance applicativeFFProfileBuild ∷ Applicative FFProfileBuild where 49 | pure = FFProfileBuild <<< pure 50 | 51 | instance monadFFProfileBuild ∷ Monad FFProfileBuild 52 | 53 | rule ∷ Command → FFProfileBuild Unit 54 | rule = FFProfileBuild <<< tell <<< singleton 55 | 56 | setPreference ∷ String → FFPreference → FFProfileBuild Unit 57 | setPreference key val = rule $ SetPreference key val 58 | 59 | setStringPreference ∷ String → String → FFProfileBuild Unit 60 | setStringPreference key = setPreference key <<< stringToFFPreference 61 | 62 | setIntPreference ∷ String → Int → FFProfileBuild Unit 63 | setIntPreference key = setPreference key <<< intToFFPreference 64 | 65 | setNumberPreference ∷ String → Number → FFProfileBuild Unit 66 | setNumberPreference key = setPreference key <<< numberToFFPreference 67 | 68 | setBoolPreference ∷ String → Boolean → FFProfileBuild Unit 69 | setBoolPreference key = setPreference key <<< boolToFFPreference 70 | 71 | buildFFProfile ∷ FFProfileBuild Unit → Aff Capabilities 72 | buildFFProfile commands = do 73 | profile ← interpret (execWriter $ unFFProfileBuild commands) <$> fromEffectFnAff _newFFProfile 74 | fromEffectFnAff $ _encode profile 75 | 76 | interpret ∷ List Command → FFProfile→ FFProfile 77 | interpret commands b = foldl foldFn b commands 78 | where 79 | foldFn ∷ FFProfile → Command → FFProfile 80 | foldFn p (SetPreference k v) = _setFFPreference k v p 81 | 82 | 83 | foreign import _setFFPreference ∷ String → FFPreference → FFProfile → FFProfile 84 | foreign import _newFFProfile ∷ EffectFnAff FFProfile 85 | foreign import _encode ∷ FFProfile → EffectFnAff Capabilities 86 | 87 | 88 | intToFFPreference ∷ Int → FFPreference 89 | intToFFPreference = unsafeCoerce 90 | 91 | numberToFFPreference ∷ Number → FFPreference 92 | numberToFFPreference = unsafeCoerce 93 | 94 | stringToFFPreference ∷ String → FFPreference 95 | stringToFFPreference = unsafeCoerce 96 | 97 | boolToFFPreference ∷ Boolean → FFPreference 98 | boolToFFPreference = unsafeCoerce 99 | 100 | foreignToFFPreference ∷ Foreign → FFPreference 101 | foreignToFFPreference = unsafeCoerce 102 | -------------------------------------------------------------------------------- /src/Selenium/Key.js: -------------------------------------------------------------------------------- 1 | // module Selenium.Key 2 | 3 | var k = require("selenium-webdriver").Key; 4 | 5 | exports.altKey = k.ALT; 6 | exports.controlKey = k.CONTROL; 7 | exports.shiftKey = k.SHIFT; 8 | exports.commandKey = k.COMMAND; 9 | exports.metaKey = k.META; 10 | -------------------------------------------------------------------------------- /src/Selenium/Key.purs: -------------------------------------------------------------------------------- 1 | module Selenium.Key where 2 | 3 | import Selenium.Types 4 | 5 | -- TODO: port all `Key` enum 6 | foreign import altKey ∷ ControlKey 7 | foreign import controlKey ∷ ControlKey 8 | foreign import shiftKey ∷ ControlKey 9 | foreign import commandKey ∷ ControlKey 10 | foreign import metaKey ∷ ControlKey 11 | -------------------------------------------------------------------------------- /src/Selenium/Monad.purs: -------------------------------------------------------------------------------- 1 | -- | Most functions of `Selenium` use `Driver` as an argument 2 | -- | This module supposed to make code a bit cleaner through 3 | -- | putting `Driver` to `ReaderT` 4 | module Selenium.Monad where 5 | 6 | import Prelude 7 | import Effect.Aff as A 8 | import Effect.Aff.Reattempt (reattempt) 9 | import Effect.Exception (Error) 10 | import Control.Monad.Reader.Trans (ReaderT(..), lift, ask, runReaderT) 11 | import Data.Either (Either) 12 | import Foreign (Foreign) 13 | import Data.Int as Int 14 | import Data.List (List, fromFoldable) 15 | import Data.Maybe (Maybe) 16 | import Data.Time.Duration (class Duration, fromDuration, Milliseconds(..)) 17 | import Selenium as S 18 | import Selenium.ActionSequence as AS 19 | import Selenium.Types (Driver, Element, FileDetector, Location, Locator, Size, Window, WindowHandle, XHRStats) 20 | import Selenium.XHR as XHR 21 | 22 | -- | `Driver` is field of `ReaderT` context 23 | -- | Usually selenium tests are run with tons of configs (i.e. xpath locators, 24 | -- | timeouts) all those configs can be putted to `Selenium e o a` 25 | type Selenium e o = 26 | ReaderT 27 | {driver ∷ Driver, defaultTimeout ∷ Milliseconds |o} 28 | A.Aff 29 | 30 | -- | get driver from context 31 | getDriver ∷ ∀ e o. Selenium e o Driver 32 | getDriver = _.driver <$> ask 33 | 34 | getWindow ∷ ∀ e o. Selenium e o Window 35 | getWindow = getDriver >>= lift <<< S.getWindow 36 | 37 | getWindowPosition ∷ ∀ e o. Selenium e o Location 38 | getWindowPosition = getWindow >>= lift <<< S.getWindowPosition 39 | 40 | getWindowSize ∷ ∀ e o. Selenium e o Size 41 | getWindowSize = getWindow >>= lift <<< S.getWindowSize 42 | 43 | maximizeWindow ∷ ∀ e o. Selenium e o Unit 44 | maximizeWindow = getWindow >>= lift <<< S.maximizeWindow 45 | 46 | setWindowPosition ∷ ∀ e o. Location → Selenium e o Unit 47 | setWindowPosition pos = getWindow >>= S.setWindowPosition pos >>> lift 48 | 49 | setWindowSize ∷ ∀ e o. Size → Selenium e o Unit 50 | setWindowSize size = getWindow >>= S.setWindowSize size >>> lift 51 | 52 | getWindowScroll ∷ ∀ e o. Selenium e o Location 53 | getWindowScroll = getDriver >>= S.getWindowScroll >>> lift 54 | 55 | -- LIFT `Aff` combinators to `Selenium.Monad` 56 | apathize ∷ ∀ e o a. Selenium e o a → Selenium e o Unit 57 | apathize check = ReaderT \r → 58 | A.apathize $ runReaderT check r 59 | 60 | attempt ∷ ∀ e o a. Selenium e o a → Selenium e o (Either Error a) 61 | attempt check = ReaderT \r → 62 | A.attempt $ runReaderT check r 63 | 64 | later ∷ ∀ d e o a. Duration d ⇒ d → Selenium e o a → Selenium e o a 65 | later time check = ReaderT \r → do 66 | A.delay (fromDuration time) 67 | runReaderT check r 68 | 69 | -- LIFT `Selenium` funcs to `Selenium.Monad` 70 | get ∷ ∀ e o. String → Selenium e o Unit 71 | get url = 72 | getDriver >>= lift <<< flip S.get url 73 | 74 | wait ∷ ∀ d e o. Duration d ⇒ Selenium e o Boolean → d → Selenium e o Unit 75 | wait check time = ReaderT \r → 76 | S.wait (runReaderT check r) (fromDuration time) r.driver 77 | 78 | -- | Tries the provided Selenium computation repeatedly until the provided timeout expires 79 | tryRepeatedlyTo' ∷ ∀ d a e o. Duration d ⇒ d → Selenium e o a → Selenium e o a 80 | tryRepeatedlyTo' time selenium = ReaderT \r → 81 | reattempt (fromDuration time) (runReaderT selenium r) 82 | 83 | -- | Tries the provided Selenium computation repeatedly until `Selenium`'s defaultTimeout expires 84 | tryRepeatedlyTo ∷ ∀ a e o. Selenium e o a → Selenium e o a 85 | tryRepeatedlyTo selenium = ask >>= \r → 86 | tryRepeatedlyTo' r.defaultTimeout selenium 87 | 88 | byCss ∷ ∀ e o. String → Selenium e o Locator 89 | byCss = lift <<< S.byCss 90 | 91 | byXPath ∷ ∀ e o. String → Selenium e o Locator 92 | byXPath = lift <<< S.byXPath 93 | 94 | byId ∷ ∀ e o. String → Selenium e o Locator 95 | byId = lift <<< S.byId 96 | 97 | byName ∷ ∀ e o. String → Selenium e o Locator 98 | byName = lift <<< S.byName 99 | 100 | byClassName ∷ ∀ e o. String → Selenium e o Locator 101 | byClassName = lift <<< S.byClassName 102 | 103 | -- | get element by action returning an element 104 | -- | ```purescript 105 | -- | locator \el → do 106 | -- | commonElements ← byCss ".common-element" >>= findElements el 107 | -- | flaggedElements ← traverse (\el → Tuple el <$> isVisible el) commonElements 108 | -- | maybe err pure $ foldl foldFn Nothing flaggedElements 109 | -- | where 110 | -- | err = throwError $ error "all common elements are not visible" 111 | -- | foldFn Nothing (Tuple el true) = Just el 112 | -- | foldFn a _ = a 113 | -- | ``` 114 | locator ∷ ∀ e o. (Element → Selenium e o Element) → Selenium e o Locator 115 | locator checkFn = ReaderT \r → 116 | S.affLocator (\el → runReaderT (checkFn el) r) 117 | 118 | -- | Tries to find element and return it wrapped in `Just` 119 | findElement ∷ ∀ e o. Locator → Selenium e o (Maybe Element) 120 | findElement l = 121 | getDriver >>= lift <<< flip S.findElement l 122 | 123 | findElements ∷ ∀ e o. Locator → Selenium e o (List Element) 124 | findElements l = 125 | getDriver >>= lift <<< flip S.findElements l 126 | 127 | -- | Tries to find child and return it wrapped in `Just` 128 | findChild ∷ ∀ e o. Element → Locator → Selenium e o (Maybe Element) 129 | findChild el loc = lift $ S.findChild el loc 130 | 131 | findChildren ∷ ∀ e o. Element → Locator → Selenium e o (List Element) 132 | findChildren el loc = lift $ S.findChildren el loc 133 | 134 | getInnerHtml ∷ ∀ e o. Element → Selenium e o String 135 | getInnerHtml = lift <<< S.getInnerHtml 136 | 137 | getSize ∷ ∀ e o. Element → Selenium e o Size 138 | getSize = lift <<< S.getSize 139 | 140 | getLocation ∷ ∀ e o. Element → Selenium e o Location 141 | getLocation = lift <<< S.getLocation 142 | 143 | isDisplayed ∷ ∀ e o. Element → Selenium e o Boolean 144 | isDisplayed = lift <<< S.isDisplayed 145 | 146 | isEnabled ∷ ∀ e o. Element → Selenium e o Boolean 147 | isEnabled = lift <<< S.isEnabled 148 | 149 | getCssValue ∷ ∀ e o. Element → String → Selenium e o String 150 | getCssValue el key = lift $ S.getCssValue el key 151 | 152 | getAttribute ∷ ∀ e o. Element → String → Selenium e o (Maybe String) 153 | getAttribute el attr = lift $ S.getAttribute el attr 154 | 155 | getText ∷ ∀ e o. Element → Selenium e o String 156 | getText el = lift $ S.getText el 157 | 158 | clearEl ∷ ∀ e o. Element → Selenium e o Unit 159 | clearEl = lift <<< S.clearEl 160 | 161 | clickEl ∷ ∀ e o. Element → Selenium e o Unit 162 | clickEl = lift <<< S.clickEl 163 | 164 | sendKeysEl ∷ ∀ e o. String → Element → Selenium e o Unit 165 | sendKeysEl ks el = lift $ S.sendKeysEl ks el 166 | 167 | script ∷ ∀ e o. String → Selenium e o Foreign 168 | script str = 169 | getDriver >>= flip S.executeStr str >>> lift 170 | 171 | getCurrentUrl ∷ ∀ e o. Selenium e o String 172 | getCurrentUrl = getDriver >>= S.getCurrentUrl >>> lift 173 | 174 | navigateBack ∷ ∀ e o. Selenium e o Unit 175 | navigateBack = getDriver >>= S.navigateBack >>> lift 176 | 177 | navigateForward ∷ ∀ e o. Selenium e o Unit 178 | navigateForward = getDriver >>= S.navigateForward >>> lift 179 | 180 | navigateTo ∷ ∀ e o. String → Selenium e o Unit 181 | navigateTo url = getDriver >>= S.navigateTo url >>> lift 182 | 183 | setFileDetector ∷ ∀ e o. FileDetector → Selenium e o Unit 184 | setFileDetector fd = getDriver >>= flip S.setFileDetector fd >>> lift 185 | 186 | getTitle ∷ ∀ e o. Selenium e o String 187 | getTitle = getDriver >>= S.getTitle >>> lift 188 | 189 | 190 | -- | Run sequence of actions 191 | sequence ∷ ∀ e o. AS.Sequence Unit → Selenium e o Unit 192 | sequence seq = do 193 | getDriver >>= lift <<< flip AS.sequence seq 194 | 195 | -- | Same as `sequence` but takes function of `ReaderT` as an argument 196 | actions 197 | ∷ ∀ e o 198 | . ({ driver ∷ Driver, defaultTimeout ∷ Milliseconds | o } → AS.Sequence Unit) 199 | → Selenium e o Unit 200 | actions seqFn = do 201 | ctx ← ask 202 | sequence $ seqFn ctx 203 | 204 | -- | Stop computations 205 | stop ∷ ∀ e o. Selenium e o Unit 206 | stop = wait (later msMax $ pure false) msMax 207 | where 208 | msMax = Milliseconds (Int.toNumber (top ∷ Int)) 209 | 210 | refresh ∷ ∀ e o. Selenium e o Unit 211 | refresh = getDriver >>= S.refresh >>> lift 212 | 213 | quit ∷ ∀ e o. Selenium e o Unit 214 | quit = getDriver >>= S.quit >>> lift 215 | 216 | takeScreenshot ∷ ∀ e o. Selenium e o String 217 | takeScreenshot = getDriver >>= S.takeScreenshot >>> lift 218 | 219 | saveScreenshot ∷ ∀ e o. String → Selenium e o Unit 220 | saveScreenshot name = getDriver >>= S.saveScreenshot name >>> lift 221 | 222 | -- | Tries to find element, if has no success throws an error 223 | findExact ∷ ∀ e o. Locator → Selenium e o Element 224 | findExact loc = getDriver >>= flip S.findExact loc >>> lift 225 | 226 | -- | Tries to find element and throws an error if it succeeds. 227 | loseElement ∷ ∀ e o. Locator → Selenium e o Unit 228 | loseElement loc = getDriver >>= flip S.loseElement loc >>> lift 229 | 230 | -- | Tries to find child, if has no success throws an error 231 | childExact ∷ ∀ e o. Element → Locator → Selenium e o Element 232 | childExact el loc = lift $ S.childExact el loc 233 | 234 | startSpying ∷ ∀ e o. Selenium e o Unit 235 | startSpying = getDriver >>= XHR.startSpying >>> lift 236 | 237 | stopSpying ∷ ∀ e o. Selenium e o Unit 238 | stopSpying = getDriver >>= XHR.stopSpying >>> lift 239 | 240 | clearLog ∷ ∀ e o. Selenium e o Unit 241 | clearLog = getDriver >>= XHR.clearLog >>> lift 242 | 243 | getXHRStats ∷ ∀ e o. Selenium e o (List XHRStats) 244 | getXHRStats = getDriver >>= XHR.getStats >>> map fromFoldable >>> lift 245 | 246 | 247 | getWindowHandle ∷ ∀ e o. Selenium e o WindowHandle 248 | getWindowHandle = getDriver >>= S.getWindowHandle >>> lift 249 | 250 | getAllWindowHandles ∷ ∀ e o. Selenium e o (List WindowHandle) 251 | getAllWindowHandles = getDriver >>= S.getAllWindowHandles >>> lift 252 | 253 | switchTo ∷ ∀ e o. WindowHandle → Selenium e o Unit 254 | switchTo w = getDriver >>= S.switchTo w >>> lift 255 | 256 | closeWindow ∷ ∀ e o. Selenium e o Unit 257 | closeWindow = getDriver >>= S.close >>> lift 258 | -------------------------------------------------------------------------------- /src/Selenium/MouseButton.js: -------------------------------------------------------------------------------- 1 | // module Selenium.MouseButton 2 | 3 | var b = require("selenium-webdriver").Button; 4 | 5 | exports.leftButton = b.LEFT; 6 | exports.rightButton = b.RIGHT; 7 | exports.middleButton = b.MIDDLE; 8 | -------------------------------------------------------------------------------- /src/Selenium/MouseButton.purs: -------------------------------------------------------------------------------- 1 | module Selenium.MouseButton where 2 | 3 | import Selenium.Types 4 | 5 | foreign import leftButton ∷ MouseButton 6 | foreign import rightButton ∷ MouseButton 7 | foreign import middleButton ∷ MouseButton 8 | -------------------------------------------------------------------------------- /src/Selenium/Remote.js: -------------------------------------------------------------------------------- 1 | // module Selenium.Remote 2 | 3 | var remote = require('selenium-webdriver/remote'); 4 | 5 | exports.fileDetector = function () { 6 | return new remote.FileDetector(); 7 | }; 8 | -------------------------------------------------------------------------------- /src/Selenium/Remote.purs: -------------------------------------------------------------------------------- 1 | module Selenium.Remote where 2 | 3 | import Effect (Effect) 4 | import Selenium.Types 5 | 6 | foreign import fileDetector ∷ Effect FileDetector 7 | -------------------------------------------------------------------------------- /src/Selenium/ScrollBehaviour.js: -------------------------------------------------------------------------------- 1 | // module Selenium.ScrollBehaviour 2 | 3 | exports.top = 0; 4 | exports.bottom = 1; 5 | -------------------------------------------------------------------------------- /src/Selenium/ScrollBehaviour.purs: -------------------------------------------------------------------------------- 1 | module Selenium.ScrollBehaviour where 2 | 3 | import Selenium.Types 4 | 5 | foreign import top ∷ ScrollBehaviour 6 | foreign import bottom ∷ ScrollBehaviour 7 | -------------------------------------------------------------------------------- /src/Selenium/Types.purs: -------------------------------------------------------------------------------- 1 | module Selenium.Types where 2 | 3 | import Prelude 4 | import Control.Monad.Error.Class (throwError) 5 | import Foreign (F, Foreign, ForeignError(..), readString) 6 | import Data.List.NonEmpty as NEL 7 | import Data.Maybe (Maybe) 8 | import Data.String (toLower) 9 | 10 | foreign import data Builder ∷ Type 11 | foreign import data Driver ∷ Type 12 | foreign import data Window ∷ Type 13 | foreign import data Until ∷ Type 14 | foreign import data Element ∷ Type 15 | foreign import data Locator ∷ Type 16 | foreign import data ActionSequence ∷ Type 17 | foreign import data MouseButton ∷ Type 18 | foreign import data ChromeOptions ∷ Type 19 | foreign import data ControlFlow ∷ Type 20 | foreign import data FirefoxOptions ∷ Type 21 | foreign import data IEOptions ∷ Type 22 | foreign import data LoggingPrefs ∷ Type 23 | foreign import data OperaOptions ∷ Type 24 | foreign import data ProxyConfig ∷ Type 25 | foreign import data SafariOptions ∷ Type 26 | foreign import data ScrollBehaviour ∷ Type 27 | foreign import data FileDetector ∷ Type 28 | foreign import data WindowHandle ∷ Type 29 | 30 | -- | Copied from `purescript-affjax` because the only thing we 31 | -- | need from `affjax` is `Method` 32 | data Method 33 | = DELETE 34 | | GET 35 | | HEAD 36 | | OPTIONS 37 | | PATCH 38 | | POST 39 | | PUT 40 | | MOVE 41 | | COPY 42 | | CustomMethod String 43 | 44 | derive instance eqMethod ∷ Eq Method 45 | 46 | readMethod ∷ Foreign → F Method 47 | readMethod f = do 48 | str ← readString f 49 | pure $ case toLower str of 50 | "delete" → DELETE 51 | "get" → GET 52 | "head" → HEAD 53 | "options" → OPTIONS 54 | "patch" → PATCH 55 | "post" → POST 56 | "put" → PUT 57 | "move" → MOVE 58 | "copy" → COPY 59 | a → CustomMethod a 60 | 61 | data XHRState 62 | = Stale 63 | | Opened 64 | | Loaded 65 | 66 | derive instance eqXHRState ∷ Eq XHRState 67 | 68 | readXHRState ∷ Foreign → F XHRState 69 | readXHRState f = do 70 | str ← readString f 71 | case str of 72 | "stale" → pure Stale 73 | "opened" → pure Opened 74 | "loaded" → pure Loaded 75 | _ → throwError $ NEL.singleton $ TypeMismatch "xhr state" "string" 76 | 77 | 78 | type Location = 79 | { x ∷ Int 80 | , y ∷ Int 81 | } 82 | 83 | type Size = 84 | { width ∷ Int 85 | , height ∷ Int 86 | } 87 | 88 | newtype ControlKey = ControlKey String 89 | 90 | type XHRStats = 91 | { method ∷ Method 92 | , url ∷ String 93 | , async ∷ Boolean 94 | , user ∷ Maybe String 95 | , password ∷ Maybe String 96 | , state ∷ XHRState 97 | } 98 | -------------------------------------------------------------------------------- /src/Selenium/XHR.purs: -------------------------------------------------------------------------------- 1 | module Selenium.XHR where 2 | 3 | import Prelude 4 | import Effect.Aff (Aff) 5 | import Effect.Exception (error) 6 | import Control.Monad.Error.Class (throwError) 7 | import Control.Monad.Except (runExcept) 8 | import Data.Either (either, Either(..)) 9 | import Foreign (isUndefined, readArray, readBoolean, readNullOrUndefined, readString) 10 | import Foreign.Index (readProp) 11 | import Data.Traversable (traverse, for) 12 | import Selenium (executeStr) 13 | import Selenium.Types (Driver, XHRStats, readMethod, readXHRState) 14 | 15 | -- | Start spy on xhrs. It defines global variable in browser 16 | -- | and put information about to it. 17 | startSpying ∷ Driver → Aff Unit 18 | startSpying driver = void $ 19 | executeStr driver """ 20 | "use strict" 21 | // If we have activated spying 22 | if (window.__SELENIUM__) { 23 | // and it stopped 24 | if (!window.__SELENIUM__.isActive) { 25 | window.__SELENIUM__.spy(); 26 | } 27 | } else { 28 | var Selenium = { 29 | isActive: false, 30 | log: [], 31 | count: 0, 32 | spy: function() { 33 | // monkey patch 34 | var open = XMLHttpRequest.prototype.open; 35 | window.XMLHttpRequest.prototype.open = 36 | function(method, url, async, user, password) { 37 | // we need this mark to update log after 38 | // request is finished 39 | this.__id = Selenium.count; 40 | Selenium.log[this.__id] = { 41 | method: method, 42 | url: url, 43 | async: async, 44 | user: user, 45 | password: password, 46 | state: "stale" 47 | }; 48 | Selenium.count++; 49 | open.apply(this, arguments); 50 | }; 51 | // another monkey patch 52 | var send = XMLHttpRequest.prototype.send; 53 | window.XMLHttpRequest.prototype.send = 54 | function(data) { 55 | // this request can be deleted (this.clean() i.e.) 56 | if (Selenium.log[this.__id]) { 57 | Selenium.log[this.__id].state = "opened"; 58 | } 59 | // monkey pathc `onload` (I suppose it's useless to fire xhr 60 | // without `onload` handler, but to be sure there is check for 61 | // type of current value 62 | var m = this.onload; 63 | this.onload = function() { 64 | if (Selenium.log[this.__id]) { 65 | Selenium.log[this.__id].state = "loaded"; 66 | } 67 | if (typeof m == 'function') { 68 | m(); 69 | } 70 | }; 71 | send.apply(this, arguments); 72 | }; 73 | // monkey patch `abort` 74 | var abort = window.XMLHttpRequest.prototype.abort; 75 | window.XMLHttpRequest.prototype.abort = function() { 76 | if (Selenium.log[this.__id]) { 77 | Selenium.log[this.__id].state = "aborted"; 78 | } 79 | abort.apply(this, arguments); 80 | }; 81 | this.isActive = true; 82 | // if we define it here we need not to make `send` global 83 | Selenium.unspy = function() { 84 | this.active = false; 85 | window.XMLHttpRequest.send = send; 86 | window.XMLHttpRequest.open = open; 87 | window.XMLHttpRequest.abort = abort; 88 | }; 89 | }, 90 | // just clean log 91 | clean: function() { 92 | this.log = []; 93 | } 94 | }; 95 | window.__SELENIUM__ = Selenium; 96 | Selenium.spy(); 97 | } 98 | """ 99 | 100 | -- | Return xhr's method to initial. Will not raise an error if hasn't been initiated 101 | stopSpying ∷ Driver → Aff Unit 102 | stopSpying driver = void $ executeStr driver """ 103 | if (window.__SELENIUM__) { 104 | window.__SELENIUM__.unspy(); 105 | } 106 | """ 107 | 108 | -- | Clean log. Will raise an error if spying hasn't been initiated 109 | clearLog ∷ Driver → Aff Unit 110 | clearLog driver = do 111 | success ← executeStr driver """ 112 | if (!window.__SELENIUM__) { 113 | return false; 114 | } 115 | else { 116 | window.__SELENIUM__.clean(); 117 | return true; 118 | } 119 | """ 120 | case runExcept $ readBoolean success of 121 | Right true → pure unit 122 | _ → throwError $ error "spying is inactive" 123 | 124 | -- | Get recorded xhr stats. If spying has not been set will raise an error 125 | getStats ∷ Driver → Aff (Array XHRStats) 126 | getStats driver = do 127 | log ← executeStr driver """ 128 | if (!window.__SELENIUM__) { 129 | return undefined; 130 | } 131 | else { 132 | return window.__SELENIUM__.log; 133 | } 134 | """ 135 | when (isUndefined log) 136 | $ throwError $ error "spying is inactive" 137 | 138 | either (const $ throwError $ error "incorrect log") pure $ runExcept do 139 | arr ← readArray log 140 | for arr \el → do 141 | state ← readXHRState =<< readProp "state" el 142 | method ← readMethod =<< readProp "method" el 143 | url ← readString =<< readProp "url" el 144 | async ← readBoolean =<< readProp "async" el 145 | password ← traverse readString =<< readNullOrUndefined =<< readProp "password" el 146 | user ← traverse readString =<< readNullOrUndefined =<< readProp "user" el 147 | pure { state: state 148 | , method: method 149 | , url: url 150 | , async: async 151 | , password: password 152 | , user: user 153 | } 154 | -------------------------------------------------------------------------------- /test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (maybe) 6 | import Data.Time.Duration (Milliseconds(..)) 7 | import Effect (Effect) 8 | import Effect.Aff (launchAff, delay) 9 | import Effect.Class (liftEffect) 10 | import Effect.Console (log) 11 | import Selenium (byCss, byName, clickEl, findElement, get, getTitle, quit, sendKeysEl, wait) 12 | import Selenium.Browser (Browser(..)) 13 | import Selenium.Builder (browser, build) 14 | 15 | main :: Effect Unit 16 | main = do 17 | void $ launchAff do 18 | driver <- build $ browser Chrome 19 | get driver "http://google.com/ncr" 20 | byName "q" >>= 21 | findElement driver >>= 22 | maybe noInput (goInput driver) 23 | where 24 | noInput = void (liftEffect (log "No input, sorry :(")) 25 | 26 | goInput driver el = do 27 | sendKeysEl "webdriver" el 28 | byCss ".ds .lsbb button.lsb" >>= 29 | findElement driver >>= 30 | maybe noButton (goButton driver) 31 | 32 | noButton = void $ liftEffect (log "No submit button") 33 | 34 | goButton driver button = do 35 | clickEl button 36 | wait (titleAff driver) (Milliseconds 1000.0) driver 37 | quit driver 38 | 39 | titleAff driver = do 40 | title <- getTitle driver 41 | if title == "webdriver - Google Search" 42 | then pure true 43 | else do 44 | delay (Milliseconds 50.0) 45 | titleAff driver 46 | --------------------------------------------------------------------------------