├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .ruby-version ├── .travis.yml ├── Gemfile ├── LICENSE ├── README.md ├── docs ├── _config.yml ├── contributing.md ├── how-tos │ ├── execute-async-script.md │ └── use-headless-chrome.md ├── index.md └── travis-ci.md ├── jsdoc.conf ├── package-lock.json ├── package.json ├── src ├── application-state.js ├── browser-manager.js ├── browser-models │ ├── browser.js │ ├── driver-config.js │ ├── local-browser.js │ └── saucelabs-browser.js ├── download-manager.js ├── index.js ├── local-browsers │ ├── chrome.js │ ├── firefox.js │ └── safari.js ├── saucelabs-browsers │ ├── chrome.js │ ├── edge.js │ ├── firefox.js │ ├── ie.js │ └── safari.js └── webdriver-config │ ├── chrome.js │ ├── edge.js │ ├── firefox.js │ ├── ie.js │ └── safari.js └── test ├── .eslintrc ├── application-state.js ├── browser-usage.js ├── data └── example-site │ └── index.html ├── downloading-browsers.js ├── expiration-behavior.js ├── helpers └── test-server.js ├── local-browser.js ├── saucelabs-usage.js └── selenium-assistant.js /.eslintignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | coverage/ 3 | node_modules/ 4 | npm-debug.log 5 | geckodriver 6 | test/test-output/ 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "google"], 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | }, 7 | "rules": { 8 | "no-var": 2 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | npm-debug.log 4 | geckodriver 5 | test/test-output/ 6 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.2.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: node_js 4 | 5 | env: 6 | global: 7 | - COVERALLS_PARALLEL=true 8 | 9 | notifications: 10 | webhooks: https://coveralls.io/webhook?repo_token=COVERALLS_REPO_TOKEN 11 | 12 | install: 13 | - npm ci 14 | 15 | script: 16 | - npm run test 17 | 18 | matrix: 19 | include: 20 | - name: "Linux (Latest Node)" 21 | os: linux 22 | dist: bionic 23 | # See https://docs.travis-ci.com/user/gui-and-headless-browsers/#using-services 24 | services: 25 | - xvfb 26 | node_js: 27 | - 10 28 | - name: "OS X (Latest Node)" 29 | os: osx 30 | node_js: 31 | - 10 32 | before_script: 33 | # See https://github.com/GoogleChromeLabs/selenium-assistant/issues/118 34 | - "sudo /usr/bin/safaridriver --enable" 35 | - "sudo defaults write com.apple.Safari AllowRemoteAutomation 1" 36 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'jekyll', '>=3.1.6' 4 | gem 'kramdown' 5 | gem 'jekyll-github-metadata' 6 | -------------------------------------------------------------------------------- /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 | [![Travis Build Status](https://travis-ci.org/GoogleChromeLabs/selenium-assistant.svg?branch=master)](https://travis-ci.org/GoogleChromeLabs/selenium-assistant) [![Coverage Status](https://coveralls.io/repos/github/GoogleChromeLabs/selenium-assistant/badge.svg?branch=master)](https://coveralls.io/github/GoogleChromeLabs/selenium-assistant?branch=master) [![Dependency Status](https://david-dm.org/googlechromelabs/selenium-assistant.svg)](https://david-dm.org/googlechromelabs/selenium-assistant) [![devDependency Status](https://david-dm.org/googlechromelabs/selenium-assistant/dev-status.svg)](https://david-dm.org/googlechromelabs/selenium-assistant?type=dev) 2 | 3 | # selenium-assistant 4 | 5 | It can be a challenge to manage which browsers you can / can't run your tests against 6 | as well as blocking those that have known issues. 7 | 8 | This library is a simple set of helpers that helps find the browsers available 9 | in the current environment the tests are run on and adding a set of simple 10 | methods to check for versions and print useful info for logs. 11 | 12 |

13 | View Docs Here 14 |

15 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | source: . 2 | layouts_dir: ./themes/jekyll/_layouts/ 3 | includes_dir: ./themes/jekyll/_includes/ 4 | 5 | # This adds github pages data 6 | gems: ['jekyll-github-metadata'] 7 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: index 3 | title: "Contributing" 4 | navigation_weight: 2 5 | --- 6 | # Contributing 7 | 8 | If you wish to help with this project, please feel free to raise an issue, 9 | raise a PR or add tests to prove bad / incorrect behaviour. 10 | 11 | To run tests locally it's simply: 12 | 13 | npm run test 14 | -------------------------------------------------------------------------------- /docs/how-tos/execute-async-script.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: index 3 | title: "Execute Async Script" 4 | navigation_weight: 1 5 | --- 6 | If you run code in the browser, `executeScript()` expects the 7 | code to return immediately. 8 | 9 | `executeAsyncScript()` allows you to run code in the browser 10 | but expect an asynchronous result (i.e. it waits for a 11 | callback); 12 | 13 | ```javascript 14 | const chromeBrowser = seleniumAssistant.getLocalBrowser('chrome', 'stable'); 15 | const driver = await chromeBrowser.getSeleniumDriver(); 16 | const value = driver.executeAsyncScript((inputString, ...args) => { 17 | const cb = args[args.length - 1]; 18 | 19 | setTimeout(() => { 20 | cb(inputString); 21 | }, 2000); 22 | }, 'example-string'); 23 | ``` 24 | 25 | If the async script takes too long to execute, you can extend it with 26 | the following: 27 | 28 | ```javascript 29 | const chromeBrowser = seleniumAssistant.getLocalBrowser('chrome', 'stable'); 30 | const driver = await chromeBrowser.getSeleniumDriver(); 31 | driver.manage().timeouts().setScriptTimeout(60 * 1000); 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/how-tos/use-headless-chrome.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: index 3 | title: "Use Headless Chrome" 4 | navigation_weight: 1 5 | --- 6 | It's possible to use headless Chrome by setting a simple option. 7 | 8 | ```javascript 9 | const chromeBrowser = seleniumAssistant.getLocalBrowser('chrome', 'stable'); 10 | const options = chromeBrowser.getSeleniumOptions(); 11 | options.addArguments('--headless'); 12 | ``` 13 | 14 | You can learn more [here](https://developers.google.com/web/updates/2017/04/headless-chrome). 15 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: index 3 | title: "Selenium Assistant" 4 | navigation_weight: 0 5 | left_column: | 6 | # Why 7 | 8 | This library is designed to make using selenium a little easier in terms 9 | of finding different releases of a particular browser and 10 | generating a web driver instance for it. 11 | right_column: | 12 | # Install 13 | 14 | Installing of this module is simply: 15 | 16 | npm install selenium-assistant --save-dev 17 | 18 | --- 19 | # Usage 20 | 21 | Depending on the browsers you wish to test against you'll need to add 22 | the drivers for them. 23 | 24 | To use **Google Chrome**: 25 | 26 | npm install chromedriver --save-dev 27 | 28 | To use **Firefox**: 29 | 30 | npm install geckodriver --save-dev 31 | 32 | ## Local Browsers 33 | 34 | The most basic / common use of this library to get the available browsers 35 | on the current machine, filter out any browsers you may not want and 36 | then get a web driver instance for that browser, this can be done like so: 37 | 38 | ```javascript 39 | const selenium = require('selenium-webdriver'); 40 | const seleniumAssistant = require('selenium-assistant'); 41 | 42 | const browsers = seleniumAssistant.getLocalBrowsers(); 43 | browsers.forEach(browser => { 44 | // Skip if the browser isn't stable. 45 | if (browser.getReleaseName() !== 'stable') { 46 | return; 47 | } 48 | 49 | // Print out the browsers name. 50 | console.log(browser.getPrettyName()); 51 | 52 | browser.getSeleniumDriver() 53 | .then(webdriverInstance => 54 | webdriverInstance.get('https://google.com/') 55 | .then(_ => webdriverInstance.wait(selenium.until.titleIs('Google'), 1000)) 56 | ); 57 | }); 58 | ``` 59 | 60 | Make sure you checkout the reference docs for all the available APIs. 61 | 62 | For documentation on how to use the `webdriverInstace` [check out the 63 | selenium docs](http://seleniumhq.github.io/selenium/docs/api/javascript/). 64 | 65 | ## Saucelabs Browsers 66 | 67 | There will come a point where you'll want to test on browsers you don't have 68 | on your local machine (i.e. Internet Explorer or Safari). 69 | 70 | Selenium-assistant can help with this, exposing methods so that you can: 71 | 72 | 1. Set your Saucelabs username and access key. 73 | 1. Use Saucelabs connect so Saucelabs browsers can access local servers. 74 | 3. Get a working driver instance (i.e. have the correct configuration). 75 | 76 | ```javascript 77 | const seleniumAssistant = require('selenium-assistant'); 78 | 79 | seleniumAssistant.setSaucelabsDetails( 80 | SAUCELABS_USERNAME, 81 | SAUCELABS_ACCESS_KEY); 82 | 83 | seleniumAssistant.startSaucelabsConnect() 84 | .then(() => { 85 | return seleniumAssistant.getSauceLabsBrowser('microsoftedge', 'latest'); 86 | }) 87 | .then((browserInstance) => { 88 | return browserInstance.getSeleniumDriver(); 89 | }) 90 | .then((driver) => { 91 | return driver.get('http://localhost:8080/') 92 | .then(() => { 93 | // Do any testing with the driver. 94 | }) 95 | .then(() => { 96 | return seleniumAssistant.killWebDriver(driver); 97 | }); 98 | }) 99 | .then(() => { 100 | return seleniumAssistant.stopSaucelabsConnect(); 101 | }); 102 | ``` 103 | 104 | > **Note:** You don't need Saucelabs connect if you are testing a publicly 105 | > available URL. 106 | -------------------------------------------------------------------------------- /docs/travis-ci.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: index 3 | title: "Travis CI Support" 4 | navigation_weight: 1 5 | --- 6 | Selenium-Assistant can be easily used with Travis. 7 | 8 | All you need to do is create a "Virtual Display", which is required for the 9 | browsers to work: 10 | 11 | In your `.travis.yml` file, run: 12 | 13 | # Read more here: https://docs.travis-ci.com/user/gui-and-headless-browsers/#Using-xvfb-to-Run-Tests-That-Require-a-GUI 14 | before_script: 15 | - "export DISPLAY=:99.0" 16 | - "sh -e /etc/init.d/xvfb start || echo \"Unable to start virtual display.\"" 17 | - sleep 3 # give xvfb some time to start 18 | 19 | If you want to speed up your travis times and your downloading browsers, add 20 | the following to your `.travis.yml` file and the browser downloads should 21 | be cached between runs. 22 | 23 | cache: 24 | directories: 25 | - node_modules 26 | - ~/.selenium-assistant 27 | -------------------------------------------------------------------------------- /jsdoc.conf: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "include": [ 4 | "./src" 5 | ] 6 | }, 7 | "templates": { 8 | "default": { 9 | "outputSourceFiles": false 10 | } 11 | }, 12 | "opts": { 13 | "template": "./node_modules/npm-publish-scripts/build/themes/jsdoc", 14 | "recurse": true 15 | }, 16 | "markdown": { 17 | "idInHeadings": true 18 | }, 19 | "plugins": ["plugins/markdown"] 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "selenium-assistant", 3 | "version": "6.1.0", 4 | "description": "A node module to help with use of selenium driver", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "coveralls": "cat ./coverage/lcov.info | coveralls", 8 | "istanbul": "npm run lint && istanbul cover _mocha", 9 | "lint": "eslint '.'", 10 | "mocha": "mocha", 11 | "pretest": "npm install --no-save chromedriver geckodriver", 12 | "test": "npm run istanbul && npm run coveralls" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/GoogleChrome/selenium-assistant.git" 17 | }, 18 | "engines": { 19 | "node": ">= 8.9.0" 20 | }, 21 | "keywords": [ 22 | "selenium", 23 | "webdriver" 24 | ], 25 | "author": "Matt Gaunt", 26 | "license": "Apache-2.0", 27 | "bugs": { 28 | "url": "https://github.com/GoogleChrome/selenium-assistant/issues" 29 | }, 30 | "homepage": "https://github.com/GoogleChrome/selenium-assistant#readme", 31 | "devDependencies": { 32 | "chai": "^4.2.0", 33 | "coveralls": "^3.0.5", 34 | "eslint": "^6.1.0", 35 | "eslint-config-google": "^0.13.0", 36 | "express": "^4.17.1", 37 | "firebase": "^6.3.3", 38 | "glob": "^7.1.4", 39 | "istanbul": "^0.4.5", 40 | "jsdoc": "^3.6.3", 41 | "mocha": "^6.2.0", 42 | "proxyquire": "^2.1.1", 43 | "sinon": "^7.3.2" 44 | }, 45 | "dependencies": { 46 | "chalk": "^2.4.2", 47 | "dmg": "^0.1.0", 48 | "fs-extra": "^8.1.0", 49 | "mkdirp": "^0.5.1", 50 | "node-localstorage": "^1.3.1", 51 | "request": "^2.88.0", 52 | "sauce-connect-launcher": "^1.2.7", 53 | "selenium-webdriver": "^4.0.0-alpha.7", 54 | "semver": "^6.3.0", 55 | "which": "^1.3.1", 56 | "yauzl": "^2.10.0" 57 | }, 58 | "files": [ 59 | "src/" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /src/application-state.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const path = require('path'); 20 | const sauceConnectLauncher = require('sauce-connect-launcher'); 21 | 22 | /** 23 | * This class is a super basic class that stores shared state across the 24 | * classes in this library / module. 25 | * 26 | * @private 27 | */ 28 | class ApplicationState { 29 | /** 30 | * This constructor is never used directly, but this class stores the 31 | * overall current state of selenium-assistant while in use. 32 | */ 33 | constructor() { 34 | this._installDir = this.getDefaultInstallLocation(); 35 | } 36 | 37 | /** 38 | * To define where browsers should be installed and searched for, 39 | * define the path by calling this method. 40 | * @param {String} newInstallDir The path to install new browsers in to. 41 | */ 42 | setInstallDirectory(newInstallDir) { 43 | if (newInstallDir) { 44 | this._installDir = path.resolve(newInstallDir); 45 | } else { 46 | this._installDir = this.getDefaultInstallLocation(); 47 | } 48 | } 49 | 50 | /** 51 | * To get ther current path of where Browsers are installed and searched for, 52 | * call this method. 53 | * @return {String} The current path for installed browsers. 54 | */ 55 | getInstallDirectory() { 56 | return this._installDir; 57 | } 58 | 59 | /** 60 | * When this library is used a default path is used for installing and 61 | * searched for browsers. This allows multiple projects using this library 62 | * to share the same browsers, saving space on the users machine. 63 | * @return {String} The default path for installed browsers. 64 | */ 65 | getDefaultInstallLocation() { 66 | let installLocation; 67 | const homeLocation = process.env.HOME || process.env.USERPROFILE; 68 | if (homeLocation) { 69 | installLocation = homeLocation; 70 | } else { 71 | installLocation = '.'; 72 | } 73 | 74 | const folderName = process.platform === 'win32' ? 75 | 'selenium-assistant' : '.selenium-assistant'; 76 | return path.join(installLocation, folderName); 77 | } 78 | 79 | /** 80 | * Before attempting to use a Sauce Labs browser, you must 81 | * call this method with your Sauce Labs Username and Access Key. 82 | * @param {String} username Your Sauce Labs username. 83 | * @param {String} accessKey Your Sauce Labs accessKey. 84 | */ 85 | setSaucelabsDetails(username, accessKey) { 86 | this._saucelabs = this._saucelabs || {}; 87 | this._saucelabs.username = username; 88 | this._saucelabs.accessKey = accessKey; 89 | } 90 | 91 | /** 92 | * @return {Object} Returns an object containing the username and accessKey 93 | * for saucelabs. 94 | */ 95 | getSaucelabsDetails() { 96 | if (!this._saucelabs) { 97 | throw new Error('Saucelab details not defined.'); 98 | } 99 | 100 | return this._saucelabs; 101 | } 102 | 103 | /** 104 | * @return {Promise} Returns a promise that resolves once the connection is 105 | * open. 106 | */ 107 | startSaucelabsConnect() { 108 | if (this._sauceConnect) { 109 | return Promise.resolve(); 110 | } 111 | 112 | return new Promise((resolve, reject) => { 113 | const options = { 114 | username: this._saucelabs.username, 115 | accessKey: this._saucelabs.accessKey, 116 | // Using tunnelIdentifier means all Sauce Labs browsers must define 117 | // this capability 118 | // tunnelIdentifier: 119 | // // Slashes in the identifier breaks a pidfile requirement 120 | // `selenium-assistant_sauce-connect-tunnel_${Date.now()}`, 121 | connectRetries: 3, 122 | }; 123 | 124 | sauceConnectLauncher(options, (err, sauceConnectProcess) => { 125 | if (err) { 126 | reject(err); 127 | return; 128 | } 129 | 130 | resolve(sauceConnectProcess); 131 | }); 132 | }) 133 | .then((sauceConnectProcess) => { 134 | this._sauceConnect = sauceConnectProcess; 135 | }); 136 | } 137 | 138 | /** 139 | * @return {Promise} A promise that resolves once the connection is closed. 140 | */ 141 | stopSaucelabsConnect() { 142 | if (!this._sauceConnect) { 143 | return Promise.resolve(); 144 | } 145 | 146 | return new Promise((resolve) => { 147 | this._sauceConnect.close(resolve); 148 | }) 149 | .then(() => { 150 | this._sauceConnect = null; 151 | }); 152 | } 153 | } 154 | 155 | module.exports = new ApplicationState(); 156 | -------------------------------------------------------------------------------- /src/browser-manager.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const LocalChromeBrowser = require('./local-browsers/chrome'); 20 | const LocalFirefoxBrowser = require('./local-browsers/firefox'); 21 | const LocalSafariBrowser = require('./local-browsers/safari'); 22 | const SaucelabsChromeBrowser = require('./saucelabs-browsers/chrome'); 23 | const SaucelabsEdgeBrowser = require('./saucelabs-browsers/edge'); 24 | const SaucelabsFirefoxBrowser = require('./saucelabs-browsers/firefox'); 25 | const SaucelabsIEBrowser = require('./saucelabs-browsers/ie'); 26 | const SaucelabsSafariBrowser = require('./saucelabs-browsers/safari'); 27 | 28 | /** 29 | * This class is a simple helper to define the possible permutations of 30 | * browsers and create the objects which are returned by 31 | * {@link SeleniumAssistant}. 32 | * 33 | * @private 34 | */ 35 | class BrowserManager { 36 | // Details from: 37 | // https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-BrowserVersion 38 | /** 39 | * This method will return a browser instance tied to a Sauce Labs hosted 40 | * browser. 41 | * 42 | * @param {String} browserId The browser ID you wish to control. 43 | * @param {String} browserVersion The browser verions you wish to target. 44 | * This is the Sauce Labs version, not release name. Can be "latest", 45 | * "latest-1", "latest-2", "45.0" 46 | * @return {WebDriverBrowser} browser A WebDriverBrowser instance pointing 47 | * to a Sauce Labs hosted browser. 48 | */ 49 | getSauceLabsBrowser(browserId, browserVersion) { 50 | switch (browserId) { 51 | case 'chrome': 52 | return new SaucelabsChromeBrowser(browserVersion); 53 | case 'microsoftedge': 54 | return new SaucelabsEdgeBrowser(browserVersion); 55 | case 'firefox': 56 | return new SaucelabsFirefoxBrowser(browserVersion); 57 | case 'internet explorer': 58 | return new SaucelabsIEBrowser(browserVersion); 59 | case 'safari': 60 | return new SaucelabsSafariBrowser(browserVersion); 61 | default: 62 | throw new Error('Unknown Saucelabs browser request: ', browserId); 63 | } 64 | } 65 | 66 | /** 67 | *

This method returns the full list of browsers this library supports, 68 | * regardless of whether the current environment has access to them or not. 69 | *

70 | * 71 | *

As more browsers are specifically added, this list will grow.

72 | * 73 | * @return {Array} An array of all the possible browsers 74 | * this library supports. 75 | */ 76 | getSupportedBrowsers() { 77 | return [ 78 | this.getLocalBrowser('chrome', 'stable'), 79 | this.getLocalBrowser('chrome', 'beta'), 80 | 81 | this.getLocalBrowser('firefox', 'stable'), 82 | this.getLocalBrowser('firefox', 'beta'), 83 | this.getLocalBrowser('firefox', 'unstable'), 84 | 85 | this.getLocalBrowser('safari', 'stable'), 86 | this.getLocalBrowser('safari', 'beta'), 87 | ]; 88 | } 89 | 90 | /** 91 | *

A very simple method to create a {@link WebDriverBrowser} instance 92 | * with the current config based on minimal config.

93 | * 94 | *

This method will throw if you request a browser that this 95 | * library doesn't support.

96 | * 97 | * @param {String} browserId The selenium browser Id 'chrome', 'firefox', etc 98 | * @param {String} release The release you want the browser to be on 99 | * 'stable', 'beta', 'unstable' or 'saucelabs' 100 | * @return {WebDriverBrowser} An instance of the browser you requested. 101 | */ 102 | getLocalBrowser(browserId, release) { 103 | if (release !== 'stable' && 104 | release !== 'beta' && 105 | release !== 'unstable') { 106 | throw new Error('Unknown release type.'); 107 | } 108 | 109 | switch (browserId) { 110 | case 'chrome': 111 | return new LocalChromeBrowser(release); 112 | case 'firefox': 113 | return new LocalFirefoxBrowser(release); 114 | case 'safari': 115 | return new LocalSafariBrowser(release); 116 | default: 117 | throw new Error('Unknown web driver browser request: ', browserId); 118 | } 119 | } 120 | } 121 | 122 | module.exports = new BrowserManager(); 123 | -------------------------------------------------------------------------------- /src/browser-models/browser.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const seleniumWebdriver = require('selenium-webdriver'); 20 | 21 | /** 22 | * A base class which all "types" of browser models extend. 23 | * 24 | * For example: {@link LocalBrowser|LocalBrowser} and 25 | * {@link SauceLabsBrowser|SauceLabsBrowser} 26 | */ 27 | class Browser { 28 | /** 29 | * @param {Object} config The required config for the browser 30 | * this instance should represent. 31 | */ 32 | constructor(config) { 33 | if (typeof config === 'undefined' || config === null || 34 | typeof config !== 'object') { 35 | throw new Error('No browser config provided.'); 36 | } 37 | 38 | this._config = config; 39 | this._capabilities = new seleniumWebdriver.Capabilities(); 40 | 41 | this.addCapability(seleniumWebdriver.Capability.BROWSER_NAME, this.getId()); 42 | } 43 | 44 | /** 45 | * You can define capabilities here that will be 46 | * given to the WebDriver builder when 47 | * [getSeleniumDriverBuilder()]{@link Browser#getSeleniumDriverBuilder} or 48 | * [getSeleniumDriver()]{@link Browser#getSeleniumDriver} is called. 49 | * 50 | * @param {String} key The capability key. 51 | * @param {String} value The capability value. 52 | */ 53 | addCapability(key, value) { 54 | this._capabilities.set(key, value); 55 | } 56 | 57 | /** 58 | * Get the Selenium ID of the browser (i.e. 'chrome', 'firefox', 59 | * 'microsoftedge' etc.). 60 | * @return {String} Selenium ID of this browser. 61 | */ 62 | getId() { 63 | return this._config._id; 64 | } 65 | 66 | /** 67 | * Some browsers require an executable be available on the current path 68 | * to work with Selenium. If this is the case, there may be an NPM module 69 | * that can download and add the executable to the path. This method returns 70 | * the name of the appropriate NPM module for this browser. 71 | * 72 | * For example, Chrome uses the 'chromedriver' npm module and Firefox uses 73 | * the 'geckodriver' module. 74 | * 75 | * @return {String|null} The name of the NPM driver module to use with this 76 | * browser. Null is returned if there is no driver module. 77 | */ 78 | getDriverModule() { 79 | return this._config._driverModule; 80 | } 81 | 82 | /** 83 | * This is simple a user friendly name for the browser. This is largely 84 | * used for printing friendly debug info in tests. 85 | * @return {String} A user friendly name for the browser 86 | */ 87 | getPrettyName() { 88 | return this._config._prettyName; 89 | } 90 | 91 | /** 92 | * The selenium options passed to WebDriver's Builder. 93 | * 94 | * For Chrome, this will return an instance of `selenium-webdriver/chrome`, 95 | * for Firefox it would return an instance of `selenium-webdriver/firefox`. 96 | * 97 | * @return {SeleniumOptions} An instance of the appropriate 98 | * `selenium-webdriver` options for this browser. 99 | */ 100 | getSeleniumOptions() { 101 | return this._config._options; 102 | } 103 | 104 | /** 105 | * If changes are made to the selenium options, call this method to 106 | * set them before calling 107 | * [getSeleniumDriver()]{@link Browser#getSeleniumDriver}. 108 | * 109 | * @param {SeleniumOptions} options An instance of a `selenium-webdriver` 110 | * options class. 111 | */ 112 | setSeleniumOptions(options) { 113 | this._config._options = options; 114 | } 115 | 116 | /* eslint-disable valid-jsdoc */ 117 | /** 118 | * This method returns the preconfigured WebDriver Builder. 119 | * 120 | * This is useful if you wish to customise the builder with additional 121 | * options (i.e. customise the proxy of the driver.) 122 | * @return {WebDriverBuilder} A WebDriver Builder instance, see [selenium-webdriver.Builder]{@link http://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_Builder.html} for more info. 123 | */ 124 | getSeleniumDriverBuilder() { 125 | throw new Error('getSeleniumDriverBuilder() must be overriden by ' + 126 | 'subclasses'); 127 | } 128 | 129 | /** 130 | * This method resolves to a raw WebDriver instance. 131 | * 132 | * @return {Promise} A WebDriver Instance, see [selenium-webdriver.ThenableWebDriver]{@link http://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_ThenableWebDriver.html} for more info. 133 | */ 134 | getSeleniumDriver() { 135 | throw new Error('getSeleniumDriver() must be overriden by ' + 136 | 'subclasses'); 137 | } 138 | /* eslint-enable valid-jsdoc */ 139 | } 140 | 141 | module.exports = Browser; 142 | -------------------------------------------------------------------------------- /src/browser-models/driver-config.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /** 20 | * @private 21 | */ 22 | class DriverConfig { 23 | /** 24 | * @param {String} id The Browser ID. This is what selenium will expect. 25 | * @param {Object} seleniumOptions This is the Selenium Options object 26 | * @param {String} prettyName This is the pretty name for the browser. 27 | * @param {String} driverModuleName This is the name of the driver module 28 | * if one exists (i.e. 'chromedriver', 'geckodriver', etc). 29 | */ 30 | constructor(id, seleniumOptions, prettyName, driverModuleName) { 31 | this._id = id; 32 | this._options = seleniumOptions; 33 | this._prettyName = prettyName; 34 | this._driverModule = driverModuleName; 35 | } 36 | } 37 | 38 | module.exports = DriverConfig; 39 | -------------------------------------------------------------------------------- /src/browser-models/local-browser.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const fs = require('fs'); 20 | const execSync = require('child_process').execSync; 21 | const Browser = require('./browser.js'); 22 | 23 | /** 24 | * The LocalBrowser class is an abstract class that is overriden by 25 | * browser classes that are supported (Chrome, Firefox and Safari). 26 | * @extends Browser 27 | */ 28 | class LocalBrowser extends Browser { 29 | /** 30 | * Construct a new local browser. 31 | * 32 | * @param {DriverConfig} config The config for the browser. 33 | * @param {String} release Release name of the browser, must be 'stable', 34 | * 'beta' or 'unstable'. 35 | * @param {Object} denylist This is a list of browser versions => driver 36 | * versions which is used to deny a browser from being made available. This 37 | * is not assurance of a browser working but may be used more actively 38 | * to block bad browser support in the future. 39 | */ 40 | constructor(config, release, denylist) { 41 | super(config); 42 | 43 | if (typeof config._prettyName !== 'string' || 44 | config._prettyName.length === 0) { 45 | throw new Error('Invalid prettyName value: ', config._prettyName); 46 | } 47 | 48 | if (release !== 'stable' && release !== 'beta' && release !== 'unstable') { 49 | throw new Error('Unexpected browser release given: ', release); 50 | } 51 | 52 | this._prettyName = `${config._prettyName}`; 53 | 54 | const releaseNames = this.constructor.getPrettyReleaseNames(); 55 | if (releaseNames[release]) { 56 | this._prettyName += ` ${releaseNames[release]}`; 57 | } 58 | 59 | this._release = release; 60 | this._denylist = denylist; 61 | } 62 | 63 | /** 64 | * This method returns true if the browser executable can be found and the 65 | * browser version is expected to work with the current installed browser 66 | * driver, otherwise false is returned. 67 | * 68 | * @return {Boolean} True if a WebDriver can be built and used. 69 | */ 70 | isValid() { 71 | const executablePath = this.getExecutablePath(); 72 | if (!executablePath) { 73 | return false; 74 | } 75 | 76 | try { 77 | // This will throw if it's not found 78 | fs.lstatSync(executablePath); 79 | 80 | const minVersion = this._getMinSupportedVersion(); 81 | if (minVersion) { 82 | const browserVersion = this.getVersionNumber(); 83 | if (browserVersion !== -1 && browserVersion < minVersion) { 84 | return false; 85 | } 86 | } 87 | 88 | if (this.isDenyListed()) { 89 | return false; 90 | } 91 | 92 | return true; 93 | } catch (error) { 94 | // NOOP 95 | } 96 | 97 | return false; 98 | } 99 | 100 | /** 101 | * This method is largely used internally to determine if a browser should 102 | * be made available or not. 103 | * 104 | * This method will only denylist a browser if there is a known bad browser 105 | * + driver module combination. 106 | * 107 | * @return {Boolean} Whether this browser is denylisted or not from being 108 | * included in available browsers. 109 | */ 110 | isDenyListed() { 111 | return false; 112 | } 113 | 114 | /** 115 | * A user friendly name for the browser. This is largely useful for 116 | * console logging. LocalBrowsers will also include the appropriate release 117 | * name. 118 | * @return {String} A user friendly name for the browser. 119 | */ 120 | getPrettyName() { 121 | return this._prettyName; 122 | } 123 | 124 | /** 125 | * This is the raw output of a browsers version number (i.e. running the 126 | * browser executable with `--version`). 127 | * 128 | * This provides more information than major release number returned by 129 | * [getVersionNumber()]{@link LocalBrowser#getVersionNumber}. 130 | * 131 | * @return {String} Raw string of the browser version. 132 | */ 133 | getRawVersionString() { 134 | if (this._rawVerstionString) { 135 | return this._rawVerstionString; 136 | } 137 | 138 | const executablePath = this.getExecutablePath(); 139 | if (!executablePath) { 140 | return null; 141 | } 142 | 143 | this._rawVerstionString = null; 144 | 145 | try { 146 | this._rawVerstionString = execSync(`"${executablePath}" --version`) 147 | .toString(); 148 | } catch (err) { 149 | // NOOP 150 | } 151 | 152 | return this._rawVerstionString; 153 | } 154 | 155 | /** 156 | * This method resolves to a raw WebDriver instance. 157 | * 158 | * @return {Promise} A WebDriver Instance, see [selenium-webdriver.ThenableWebDriver]{@link http://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_ThenableWebDriver.html} for more info. 159 | */ 160 | getSeleniumDriver() { 161 | if (this.getDriverModule()) { 162 | try { 163 | // This will require the necessary driver module that will add the 164 | // driver executable to the current path. 165 | require(this.getDriverModule()); 166 | } catch (err) { 167 | // NOOP 168 | } 169 | } 170 | 171 | try { 172 | const builder = this.getSeleniumDriverBuilder(); 173 | let buildResult = builder.build(); 174 | if (!buildResult.then) { 175 | buildResult = Promise.resolve(buildResult); 176 | } 177 | 178 | return buildResult 179 | .then((driver) => { 180 | // Enable async execution out of the box. 181 | const timeout = 30 * 1000; 182 | driver.manage().setTimeouts({ 183 | implicit: timeout, 184 | pageLoad: timeout, 185 | script: timeout, 186 | }); 187 | 188 | return driver; 189 | }); 190 | } catch (err) { 191 | return Promise.reject(err); 192 | } 193 | } 194 | 195 | /** 196 | * @private 197 | * @return {number} The minimum supported version number. 198 | */ 199 | _getMinSupportedVersion() { 200 | return false; 201 | } 202 | 203 | /** 204 | * The release name for this browser. This will be either 'stable', 'beta' or 205 | * 'unstable'. 206 | * 207 | * Useful if you only want to test, or not test, on a particular 208 | * release type, i.e. only test of stable releases of browsers. 209 | * 210 | * @return {String} Release name of this browser. 'stable', 'beta' or 211 | * 'unstable'. 212 | */ 213 | getReleaseName() { 214 | return this._release; 215 | } 216 | 217 | // 218 | // Disabling eslint for JSDoc here as I want to document the return 219 | // types of the LocalBrowser without labelling the methods as 220 | // abstract in the docs. 221 | // 222 | 223 | /* eslint-disable valid-jsdoc */ 224 | /** 225 | * Get the path of the browser executable if known. 226 | * @return {String|null} The path of the browsers executable. Null if 227 | * it's unknown. 228 | */ 229 | getExecutablePath() { 230 | throw new Error('getExecutablePath() must be overriden by subclasses'); 231 | } 232 | 233 | /** 234 | * This returns an object consisting of the supported releases and the 235 | * matching browser release name. 236 | * 237 | * For example, Chrome is: 238 | * `{ stable: 'Stable', beta: 'Beta' }` 239 | * 240 | * @return {Object} Returns an object containing release names as keys and 241 | * a user friendly release name as the value. 242 | */ 243 | static getPrettyReleaseNames() { 244 | throw new Error('getPrettyReleaseNames() must be overriden by ' + 245 | 'subclasses'); 246 | } 247 | 248 | /** 249 | * The major version of the browser if known. 250 | * @return {Integer} Major version number of this browser if it can be found, 251 | * -1 otherwise. 252 | */ 253 | getVersionNumber() { 254 | throw new Error('getVersionNumber() must be overriden by subclasses'); 255 | } 256 | /* eslint-enable valid-jsdoc */ 257 | } 258 | 259 | module.exports = LocalBrowser; 260 | -------------------------------------------------------------------------------- /src/browser-models/saucelabs-browser.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const Browser = require('./browser.js'); 20 | const applicationState = require('../application-state'); 21 | const webdriver = require('selenium-webdriver'); 22 | 23 | // To find out the configuration you can use see: 24 | // https://wiki.saucelabs.com/display/DOCS/Platform+Configurator#/ 25 | 26 | /** 27 | * The SauceLabsBrowser class is an abstract class that is overriden by browser 28 | * classes that are supported on Sauce Labs (Chrome, Edge, Firefox, IE, 29 | * and Safari). 30 | * @extends Browser 31 | */ 32 | class SauceLabsBrowser extends Browser { 33 | /** 34 | * Constructs new Sauce Labs Browser. 35 | * @param {DriverConfig} config The config for the browser. 36 | * @param {String} version Version name to be given to Sauce Labs. 37 | */ 38 | constructor(config, version) { 39 | super(config); 40 | 41 | if (typeof config._prettyName !== 'string' || 42 | config._prettyName.length === 0) { 43 | throw new Error('Invalid prettyName value: ', config._prettyName); 44 | } 45 | 46 | this._prettyName = `${config._prettyName} - [${version}]`; 47 | this._version = version; 48 | } 49 | 50 | /** 51 | * A user friendly name for the browser. This is largely useful for 52 | * console logging. SauceLabsBrowsers will also include the version passed 53 | * into the constructor. 54 | * @return {String} A user friendly name for the browser. 55 | */ 56 | getPrettyName() { 57 | return this._prettyName; 58 | } 59 | 60 | /** 61 | * This method resolves to a raw WebDriver instance. 62 | * 63 | * @return {Promise} A WebDriver Instance, see [selenium-webdriver.ThenableWebDriver]{@link http://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_ThenableWebDriver.html} for more info. 64 | */ 65 | getSeleniumDriver() { 66 | if (this.getDriverModule()) { 67 | try { 68 | // This will require the necessary driver module that will add the 69 | // driver executable to the current path. 70 | require(this.getDriverModule()); 71 | } catch (err) { 72 | // NOOP 73 | } 74 | } 75 | 76 | try { 77 | const builder = this.getSeleniumDriverBuilder(); 78 | const buildResult = builder.build(); 79 | if (buildResult.then) { 80 | return buildResult; 81 | } 82 | return Promise.resolve(buildResult); 83 | } catch (err) { 84 | return Promise.reject(err); 85 | } 86 | } 87 | 88 | /** 89 | * This is the version passed into the constructor which is ultimately used 90 | * by Sauce Labs (i.e. 'latest', 'latest-2', '48'). 91 | * 92 | * @return {String} The Sauce Labs browser version. 93 | */ 94 | getVersion() { 95 | return this._version; 96 | } 97 | 98 | /** 99 | * This method returns the preconfigured WebDriver Builder. 100 | * 101 | * This is useful if you wish to customise the builder with additional 102 | * options (i.e. customise the proxy of the driver.) 103 | * 104 | * @return {WebDriverBuilder} A WebDriver Builder instance, see [selenium-webdriver.Builder]{@link http://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_Builder.html} for more info. 105 | */ 106 | getSeleniumDriverBuilder() { 107 | const saucelabsDetails = applicationState.getSaucelabsDetails(); 108 | this.addCapability('username', saucelabsDetails.username); 109 | this.addCapability('accessKey', saucelabsDetails.accessKey); 110 | 111 | const builder = new webdriver 112 | .Builder(); 113 | 114 | return builder.usingServer('https://' + saucelabsDetails.username + ':' + 115 | saucelabsDetails.accessKey + '@ondemand.saucelabs.com:443/wd/hub'); 116 | } 117 | } 118 | 119 | module.exports = SauceLabsBrowser; 120 | -------------------------------------------------------------------------------- /src/download-manager.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const spawn = require('child_process').spawn; 20 | const path = require('path'); 21 | const fs = require('fs'); 22 | const request = require('request'); 23 | const mkdirp = require('mkdirp'); 24 | const dmg = require('dmg'); 25 | const fse = require('fs-extra'); 26 | const LocalStorage = require('node-localstorage').LocalStorage; 27 | 28 | const application = require('./application-state.js'); 29 | const browserManager = require('./browser-manager.js'); 30 | 31 | /** 32 | * The download manager's sole job is to download browsers and drivers. 33 | * The executable paths for these downloaded browsers will be discovered 34 | * by the individual {@link WebDriverBrowser} instances. 35 | * 36 | * @private 37 | */ 38 | class DownloadManager { 39 | /** 40 | * Get the default expiration for downloaded browsers. 41 | * @return {number} Returns the default expiration of 24 hours. 42 | */ 43 | get defaultExpiration() { 44 | return 24; 45 | } 46 | 47 | /** 48 | * This method will download a browser if it is needed (i.e. can't be found 49 | * in the usual system location or in the install directory). 50 | * @param {String} browserId This is the Selenium ID of the browser you wish 51 | * to download ('chrome', 'firefox'). 52 | * @param {String} release This downloads the browser on a particular track 53 | * and can be 'stable', 'beta' or 'unstable' 54 | * @param {Number} [expirationInHours=24] This is how long until a browser 55 | * download is regarded and expired and Should 56 | * be updated. A value of 0 will force a download. 57 | * If you want to install the browser regardless 58 | * of any existing installs of the process, pass 59 | * in true. 60 | * @return {Promise} Promise resolves once the browser has been 61 | * downloaded and ready for use. 62 | */ 63 | downloadLocalBrowser(browserId, release, expirationInHours) { 64 | const installDir = application.getInstallDirectory(); 65 | const storageKey = `${browserId}:${release}`; 66 | const localstoragePath = 67 | path.join(application.getInstallDirectory(), 'localstorage'); 68 | let localStorage = null; 69 | 70 | return new Promise((resolve, reject) => { 71 | // TODO: Switch to fs-extra 72 | mkdirp(localstoragePath, (err) => { 73 | if (err) { 74 | return reject(err); 75 | } 76 | resolve(); 77 | }); 78 | }) 79 | .then(() => { 80 | localStorage = new LocalStorage(localstoragePath); 81 | const lastBrowserUpdate = localStorage.getItem(storageKey); 82 | if (lastBrowserUpdate) { 83 | if (typeof expirationInHours === 'undefined') { 84 | expirationInHours = this.defaultExpiration; 85 | } 86 | 87 | const expirationInMillis = expirationInHours * 60 * 60 * 1000; 88 | const dateComparison = Date.now() - expirationInMillis; 89 | 90 | if (parseInt(lastBrowserUpdate, 10) > dateComparison) { 91 | const browserInstance = browserManager 92 | .getLocalBrowser(browserId, release); 93 | return !browserInstance.isValid(); 94 | } 95 | } 96 | 97 | return true; 98 | }) 99 | .catch((err) => { 100 | // In case of error download browser. 101 | return true; 102 | }) 103 | .then((browserNeedsDownloading) => { 104 | if (!browserNeedsDownloading) { 105 | return; 106 | } 107 | 108 | let downloadPromise; 109 | switch (browserId) { 110 | case 'chrome': 111 | downloadPromise = this._downloadChrome(release, installDir); 112 | break; 113 | case 'firefox': 114 | downloadPromise = this._downloadFirefox(release, installDir); 115 | break; 116 | default: 117 | throw new Error(`Apologies, but ${browserId} can't be ` + 118 | `downloaded with this tool`); 119 | } 120 | 121 | return downloadPromise.then(() => { 122 | if (localStorage) { 123 | return localStorage.setItem(storageKey, Date.now()); 124 | } 125 | }); 126 | }); 127 | } 128 | 129 | /** 130 | * Download a version of Chrome to a specific directory. 131 | * @param {String} release This should be 'stable' or 'beta'. 132 | * @param {String} installDir The path to install Chrome into. 133 | * @return {Promise} Promise that resolves once the download has completed. 134 | */ 135 | _downloadChrome(release, installDir) { 136 | let downloadUrl; 137 | let fileExtension = null; 138 | let chromeProduct = null; 139 | let chromeOSXAppName = null; 140 | 141 | switch (release) { 142 | case 'stable': 143 | chromeProduct = 'google-chrome-stable'; 144 | break; 145 | case 'beta': 146 | chromeProduct = 'google-chrome-beta'; 147 | break; 148 | default: 149 | throw new Error(`Unknown release: '${release}'`); 150 | } 151 | 152 | switch (process.platform) { 153 | case 'linux': { 154 | fileExtension = 'deb'; 155 | downloadUrl = `https://dl.google.com/linux/direct/${chromeProduct}_current_amd64.deb`; 156 | break; 157 | } 158 | case 'darwin': 159 | fileExtension = 'dmg'; 160 | switch (release) { 161 | case 'stable': 162 | // Must leave in ggro (brand) as without it, the dmg will be for an 163 | // old version of Chrome 164 | downloadUrl = `https://dl.google.com/chrome/mac/universal/stable/ggro/` + 165 | `googlechrome.dmg`; 166 | chromeOSXAppName = 'Google Chrome.app'; 167 | break; 168 | case 'beta': 169 | downloadUrl = `https://dl.google.com/chrome/mac/beta/` + 170 | `GoogleChrome.dmg`; 171 | chromeOSXAppName = 'Google Chrome.app'; 172 | break; 173 | default: 174 | throw new Error(`Unknown release: '${release}'`); 175 | } 176 | break; 177 | default: 178 | throw new Error('Unsupported platform.', process.platform); 179 | } 180 | 181 | const finalBrowserPath = path.join(installDir, 'chrome', release); 182 | return new Promise((resolve, reject) => { 183 | mkdirp(installDir, (err) => { 184 | if (err) { 185 | return reject(err); 186 | } 187 | resolve(); 188 | }); 189 | }) 190 | .then(() => { 191 | return new Promise((resolve, reject) => { 192 | const filePath = path.join(installDir, chromeProduct + '.' + 193 | fileExtension); 194 | const file = fs.createWriteStream(filePath); 195 | request(downloadUrl, (err) => { 196 | if (err) { 197 | return reject(err); 198 | } 199 | 200 | resolve(filePath); 201 | }) 202 | .pipe(file); 203 | }); 204 | }) 205 | .then((filePath) => { 206 | return new Promise((resolve, reject) => { 207 | mkdirp(finalBrowserPath, (err) => { 208 | if (err) { 209 | return reject(err); 210 | } 211 | resolve(filePath); 212 | }); 213 | }); 214 | }) 215 | .then((filePath) => { 216 | switch (fileExtension) { 217 | case 'deb': 218 | return new Promise(function(resolve, reject) { 219 | // dpkg -x app.deb /path/to/target/dir/ 220 | const dpkgProcess = spawn('dpkg', [ 221 | '-x', 222 | filePath, 223 | finalBrowserPath, 224 | ], {stdio: 'inherit'}); 225 | 226 | dpkgProcess.on('exit', (code) => { 227 | if (code === 0) { 228 | return resolve(filePath); 229 | } 230 | 231 | reject(new Error('Unable to extract deb')); 232 | }); 233 | }); 234 | case 'dmg': 235 | return new Promise((resolve, reject) => { 236 | dmg.mount(filePath, (err, mountedPath) => { 237 | if (err) { 238 | return reject(err); 239 | } 240 | 241 | fse.copySync( 242 | path.join(mountedPath, chromeOSXAppName), 243 | path.join(installDir, 'chrome', release, 244 | chromeOSXAppName), 245 | {dereference: true} 246 | ); 247 | 248 | dmg.unmount(mountedPath, (err) => { 249 | if (err) { 250 | reject(err); 251 | } 252 | 253 | resolve(); 254 | }); 255 | }); 256 | }); 257 | default: 258 | throw new Error('Unknown file extension: ', fileExtension); 259 | } 260 | }) 261 | .then((filePath) => { 262 | if (filePath) { 263 | return fse.remove(filePath); 264 | } 265 | }); 266 | } 267 | 268 | /** 269 | * Download a version of Firefox to a specific directory. 270 | * @param {String} release This should be 'stable', 'beta' or 'unstable'. 271 | * @param {String} installDir The path to install Firefox into. 272 | * @return {Promise} Promise that resolves once the download has completed. 273 | */ 274 | _downloadFirefox(release, installDir) { 275 | let ffProduct = null; 276 | let ffPlatformId = null; 277 | let fileExtension = null; 278 | let firefoxMacApp = null; 279 | 280 | switch (release) { 281 | case 'stable': 282 | firefoxMacApp = 'Firefox.app'; 283 | ffProduct = 'firefox-latest'; 284 | break; 285 | case 'beta': 286 | firefoxMacApp = 'Firefox.app'; 287 | ffProduct = 'firefox-beta-latest'; 288 | break; 289 | case 'unstable': 290 | firefoxMacApp = 'Firefox Nightly.app'; 291 | ffProduct = 'firefox-nightly-latest'; 292 | break; 293 | default: 294 | throw new Error(`Unknown release: '${release}'`); 295 | } 296 | 297 | switch (process.platform) { 298 | case 'linux': 299 | ffPlatformId = 'linux64'; 300 | fileExtension = '.tar.gz'; 301 | break; 302 | case 'darwin': 303 | ffPlatformId = 'osx'; 304 | fileExtension = '.dmg'; 305 | break; 306 | default: 307 | throw new Error('Unsupport platform.', process.platform); 308 | } 309 | 310 | const downloadUrl = `https://download.mozilla.org/?product=${ffProduct}&lang=en-US&os=${ffPlatformId}`; 311 | return new Promise((resolve, reject) => { 312 | mkdirp(installDir, (err) => { 313 | if (err) { 314 | return reject(err); 315 | } 316 | resolve(); 317 | }); 318 | }) 319 | .then(() => { 320 | return new Promise((resolve, reject) => { 321 | const filePath = path.join(installDir, ffProduct + fileExtension); 322 | const file = fs.createWriteStream(filePath); 323 | request(downloadUrl, (err) => { 324 | if (err) { 325 | return reject(err); 326 | } 327 | 328 | resolve(filePath); 329 | }) 330 | .pipe(file); 331 | }); 332 | }) 333 | .then((filePath) => { 334 | return new Promise((resolve, reject) => { 335 | mkdirp(path.join(installDir, 'firefox', release), (err) => { 336 | if (err) { 337 | return reject(err); 338 | } 339 | resolve(filePath); 340 | }); 341 | }); 342 | }) 343 | .then((filePath) => { 344 | if (fileExtension === '.tar.gz') { 345 | return new Promise((resolve, reject) => { 346 | const untarProcess = spawn('tar', [ 347 | 'xvjf', 348 | filePath, 349 | '--directory', 350 | path.join(installDir, 'firefox', release), 351 | '--strip-components', 352 | 1, 353 | ]); 354 | 355 | untarProcess.on('exit', (code) => { 356 | if (code === 0) { 357 | return resolve(filePath); 358 | } 359 | 360 | reject(new Error('Unable to extract tar')); 361 | }); 362 | }); 363 | } else if (fileExtension === '.dmg') { 364 | return new Promise((resolve, reject) => { 365 | dmg.mount(filePath, (err, mountedPath) => { 366 | if (err) { 367 | return reject(err); 368 | } 369 | 370 | fse.copySync( 371 | path.join(mountedPath, firefoxMacApp), 372 | path.join(installDir, 'firefox', release, firefoxMacApp), 373 | {dereference: true} 374 | ); 375 | 376 | dmg.unmount(mountedPath, (err) => { 377 | if (err) { 378 | reject(err); 379 | } 380 | 381 | resolve(); 382 | }); 383 | }); 384 | }); 385 | } 386 | 387 | throw new Error('Unable to handle downloaded file: ', downloadUrl); 388 | }) 389 | .then((filePath) => { 390 | if (filePath) { 391 | return fse.remove(filePath); 392 | } 393 | }); 394 | } 395 | } 396 | 397 | module.exports = new DownloadManager(); 398 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const chalk = require('chalk'); 20 | 21 | const application = require('./application-state.js'); 22 | const browserManager = require('./browser-manager.js'); 23 | const downloadManager = require('./download-manager.js'); 24 | 25 | /** 26 | * When you require in the `selenium-assistant` module an instance of this 27 | * SeleniumAssistant class will be returned. 28 | * 29 | * This method gives you the require APIs to manage downloading of browsers, 30 | * accessing required browsers and making use of SaucesLabs. 31 | * 32 | * @example Usage in Node 33 | * const seleniumAssistant = require('selenium-assistant'); 34 | * 35 | * const browsers = seleniumAssistant.getLocalBrowsers(); 36 | * browsers.map(browser => { 37 | * console.log(browser.getPrettyName()); 38 | * console.log(browser.getReleaseName()); 39 | * 40 | * return browser.getSeleniumDriver() 41 | * .then((driver) => { 42 | * return driver.get('https://google.com/') 43 | * .then(() => { 44 | * return seleniumAssistant.killWebDriver(driver); 45 | * }); 46 | * }); 47 | * }); 48 | */ 49 | class SeleniumAssistant { 50 | /** 51 | * Returns the browser download path. 52 | * @return {String} Path of downloaded browsers 53 | */ 54 | getBrowserInstallDir() { 55 | return application.getInstallDirectory(); 56 | } 57 | 58 | /** 59 | * To change where browsers are downloaded to, call this method 60 | * before calling 61 | * [downloadLocalBrowser()]{@link SeleniumAssistant#downloadLocalBrowser} and 62 | * [getLocalBrowsers()]{@link SeleniumAssistant#getLocalBrowsers}. 63 | * 64 | * By default, this will install under `.selenium-assistant` in 65 | * your home directory on OS X and Linux, or just `selenium-assistant` 66 | * in your home directory on Windows. 67 | * 68 | * @param {String} newInstallDir Path to download browsers to. Pass in 69 | * null to use default path. 70 | */ 71 | setBrowserInstallDir(newInstallDir) { 72 | application.setInstallDirectory(newInstallDir); 73 | } 74 | 75 | /** 76 | * This downloads a browser with browser ID of 'chrome' or 'firefox' and 77 | * a release type of 'stable', 'beta', 'unstable'. SeleniumAssistant 78 | * will download the browser and keep a track of when it was last downloaded. 79 | * 80 | * The next time a download is requested, seleniumAssistant will check if the 81 | * browser is within the `expirationInHours` parameter and if it is, resolve 82 | * the promise. 83 | * 84 | * Any programs using selenium-assistant will share the same browser 85 | * downloads reducing overall download time (unless 86 | * [setBrowserInstallDir()]{@link SeleniumAssistant#setBrowserInstallDir} 87 | * is called with a unique directory). 88 | * 89 | * @param {String} browserId The selenium ID of the browser you wish 90 | * to download. 91 | * @param {String} release The release channel of the browser. Can be 92 | * 'stable', 'beta' or 'unstable' 93 | * @param {Number} [expirationInHours=24] This is how long until a browser 94 | * download is regarded as expired and should be updated. 95 | * A value of 0 will force a download. 96 | * @return {Promise} The promise resolves once the browser has been 97 | * downloaded. 98 | * 99 | * @example 100 | * return Promise.all([ 101 | * seleniumAssistant.downloadLocalBrowser('chrome', 'stable', 48), 102 | * seleniumAssistant.downloadLocalBrowser('chrome', 'beta', 48), 103 | * seleniumAssistant.downloadLocalBrowser('firefox', 'stable', 48), 104 | * seleniumAssistant.downloadLocalBrowser('firefox', 'beta', 48), 105 | * seleniumAssistant.downloadLocalBrowser('firefox', 'unstable', 48), 106 | * ]) 107 | * .then(() => { 108 | * console.log('Browser download complete.'); 109 | * }) 110 | * .catch((err) => { 111 | * console.error('Browser download failed.'); 112 | * }); 113 | * 114 | */ 115 | downloadLocalBrowser(browserId, release, expirationInHours) { 116 | return downloadManager.downloadLocalBrowser( 117 | browserId, release, expirationInHours); 118 | } 119 | 120 | /** 121 | * Most users of this library will want to make use of 122 | * {@link getLocalBrowsers} to get all available browsers in the current 123 | * environment. 124 | * 125 | * If you need a specific browser use this method to retrieve it. Use 126 | * [LocalBrowser.isValid()]{@link LocalBrowser#isValid} to check if the 127 | * browser is available on the current environment. 128 | * 129 | * @param {String} browserId The selenium id of the browser you want. 130 | * @param {String} release The release of the browser you want. Either 131 | * 'stable', 'beta' or 'unstable.' 132 | * @return {LocalBrowser} A LocalBrowser instance that represents 133 | * your request. 134 | */ 135 | getLocalBrowser(browserId, release) { 136 | return browserManager.getLocalBrowser(browserId, release); 137 | } 138 | 139 | /** 140 | * This method returns a list of available browsers in the current 141 | * environment. 142 | * 143 | * This method will throw an error if run on a platform other than 144 | * OS X and Linux. 145 | * 146 | * @return {Array} Array of browsers discovered in the 147 | * current environment. 148 | */ 149 | getLocalBrowsers() { 150 | if (process.platform !== 'darwin' && process.platform !== 'linux') { 151 | throw new Error('Sorry this library only supports OS X and Linux.'); 152 | } 153 | 154 | let webdriverBrowsers = browserManager.getSupportedBrowsers(); 155 | webdriverBrowsers = webdriverBrowsers.filter((webdriverBrowser) => { 156 | return webdriverBrowser.isValid(); 157 | }); 158 | 159 | return webdriverBrowsers; 160 | } 161 | 162 | /** 163 | * This method prints out a table of info for all available browsers 164 | * on the current environment. 165 | * 166 | * Useful if you are testing on Travis and want to see what tests 167 | * should be running, but be cautious to print this only at the start 168 | * of your tests to avoid excessive logging. 169 | * 170 | * @param {Boolean} [printToConsole=true] - If you wish to prevent 171 | * the table being printed to the console, you can suppress it by 172 | * passing in false and simply get the string response. 173 | * @return {String} Returns table of information as a string. 174 | */ 175 | printAvailableBrowserInfo(printToConsole) { 176 | if (typeof printToConsole === 'undefined') { 177 | printToConsole = true; 178 | } 179 | 180 | const rows = []; 181 | rows.push([ 182 | 'Browser Name', 183 | 'Browser Version', 184 | 'Path', 185 | ]); 186 | 187 | const browsers = this.getLocalBrowsers(); 188 | browsers.forEach((browser) => { 189 | rows.push([ 190 | browser.getPrettyName(), 191 | browser.getVersionNumber().toString(), 192 | browser.getExecutablePath(), 193 | ]); 194 | }); 195 | 196 | const noOfColumns = rows[0].length; 197 | const rowLengths = []; 198 | for (let i = 0; i < noOfColumns; i++) { 199 | let currentRowMaxLength = 0; 200 | rows.forEach((row) => { 201 | currentRowMaxLength = Math.max( 202 | currentRowMaxLength, row[i].length); 203 | }); 204 | rowLengths[i] = currentRowMaxLength; 205 | } 206 | 207 | let totalRowLength = rowLengths.reduce((a, b) => a + b, 0); 208 | 209 | // Account for spaces and markers 210 | totalRowLength += (noOfColumns * 3) + 1; 211 | 212 | let outputString = chalk.gray('-'.repeat(totalRowLength)) + '\n'; 213 | rows.forEach((row, rowIndex) => { 214 | const color = rowIndex === 0 ? chalk.bold : chalk.blue; 215 | const coloredRows = row.map((column, columnIndex) => { 216 | const padding = rowLengths[columnIndex] - column.length; 217 | if (padding > 0) { 218 | return color(column) + ' '.repeat(padding); 219 | } 220 | return color(column); 221 | }); 222 | 223 | const rowString = coloredRows.join(' | '); 224 | 225 | outputString += '| ' + rowString + ' |\n'; 226 | }); 227 | 228 | outputString += chalk.gray('-'.repeat(totalRowLength)) + '\n'; 229 | 230 | if (printToConsole) { 231 | /* eslint-disable no-console */ 232 | console.log(outputString); 233 | /* eslint-enable no-console */ 234 | } 235 | 236 | return outputString; 237 | } 238 | 239 | /** 240 | * If you wish to use Sauce Labs to host the browser instances you can 241 | * do so by setting your saucelab details with this method before calling 242 | * [getSauceLabsBrowser()]{@link SeleniumAssistant#getSauceLabsBrowser}. 243 | * @param {String} username The Sauce Labs username. 244 | * @param {String} accessKey The Sauce Labs access key. 245 | */ 246 | setSaucelabsDetails(username, accessKey) { 247 | application.setSaucelabsDetails(username, accessKey); 248 | } 249 | 250 | /** 251 | * Get a Sauce Labs hosted browser for a particular browser ID and a 252 | * particular browser version. 253 | * @param {String} browserId The selenium browser ID. 254 | * @param {String} browserVersion The Sauce Labs browser version, i.e. 255 | * "latest", "latest-2", "48.0". 256 | * @param {Object} options Any options that you wish to set on the browser 257 | * that are for Sauce Labs rather than configuration of the browser. 258 | * @return {SauceLabsBrowser} A selenium-assistant web driver instance. 259 | * 260 | * @example 261 | * seleniumAssistant.setSaucelabsDetails(myusername, myaccesskey); 262 | * seleniumAssistant.startSaucelabsConnect() 263 | * .then(() => { 264 | * return seleniumAssistant.getSauceLabsBrowser('microsoftedge', 'latest'); 265 | * }) 266 | * .then((browserInstance) => { 267 | * return browserInstance.getSeleniumDriver(); 268 | * }) 269 | * .then((driver) => { 270 | * return driver.get('http://localhost:8080/') 271 | * .then(() => { 272 | * return seleniumAssistant.killWebDriver(driver); 273 | * }); 274 | * }) 275 | * .then(() => { 276 | * return seleniumAssistant.stopSaucelabsConnect(); 277 | * }); 278 | */ 279 | getSauceLabsBrowser(browserId, browserVersion, options) { 280 | if (!options) { 281 | options = {}; 282 | } 283 | 284 | if (!options.saucelabs || !options.saucelabs.username || 285 | !options.saucelabs.accessKey) { 286 | options.saucelabs = application.getSaucelabsDetails(); 287 | } 288 | 289 | return browserManager.getSauceLabsBrowser(browserId, browserVersion, 290 | options); 291 | } 292 | 293 | /** 294 | * The Sauce Labs proxy allows a browser running on Sauce Labs to load 295 | * a localhost site. 296 | * 297 | * Calling this method will start the Sauce Labs connect proxy. 298 | * 299 | * @return {Promise} Returns a promise that resolves once the proxy is 300 | * set up. 301 | */ 302 | startSaucelabsConnect() { 303 | return application.startSaucelabsConnect(); 304 | } 305 | 306 | /** 307 | * The Sauce Labs proxy allows a browser running on Sauce Labs to load 308 | * a localhost site. 309 | * 310 | * Calling this method will stop the Sauce Labs connect proxy. 311 | * 312 | * @return {Promise} Returns a promise that resolves once the proxy is closed. 313 | */ 314 | stopSaucelabsConnect() { 315 | return application.stopSaucelabsConnect(); 316 | } 317 | 318 | /** 319 | * Once a web driver is no longer needed, call this method to kill it. 320 | * 321 | * This is a basic helper that adds a timeout to the end of killling the 322 | * driver to account for shutdown time and the issues that can be caused 323 | * if a new driver is launched too soon before the previous end of a driver. 324 | * 325 | * @param {WebDriver} driver An instance of {@link https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html|WebDriver} 326 | * @return {Promise} Promise that resolves once the browser is killed. 327 | */ 328 | killWebDriver(driver) { 329 | if (typeof driver === 'undefined' || driver === null) { 330 | return Promise.resolve(); 331 | } 332 | 333 | if (!driver.quit || typeof driver.quit !== 'function') { 334 | return Promise.reject(new Error('Unable to find a quit method on the ' + 335 | 'web driver.')); 336 | } 337 | 338 | // Sometimes calling driver.quit() on Chrome, doesn't work, 339 | // so this timeout offers a semi-decent fallback 340 | let quitTimeout; 341 | return new Promise((resolve) => { 342 | quitTimeout = setTimeout(resolve, 2000); 343 | 344 | driver.close() 345 | .then(() => driver.quit(), () => driver.quit()) 346 | .then(resolve, resolve); 347 | }) 348 | .then(() => { 349 | clearTimeout(quitTimeout); 350 | 351 | return new Promise((resolve, reject) => { 352 | setTimeout(resolve, 2000); 353 | }); 354 | }); 355 | } 356 | } 357 | 358 | /** 359 | * Requiring the SeleniumAssistant node module will give you an instance of 360 | * the {@link SeleniumAssistant} class. 361 | * 362 | * @module selenium-assistant 363 | * 364 | * @example Usage in Node 365 | * const seleniumAssistant = require('selenium-assistant'); 366 | * 367 | * const browsers = seleniumAssistant.getLocalBrowsers(); 368 | * browsers.map(browser => { 369 | * console.log(browsers.getPrettyName()); 370 | * console.log(browsers.getReleaseName()); 371 | * 372 | * return browser.getSeleniumDriver() 373 | * .then((driver) => { 374 | * return driver.get('https://google.com/') 375 | * .then(() => { 376 | * return seleniumAssistant.killWebDriver(driver); 377 | * }); 378 | * }); 379 | * }); 380 | */ 381 | module.exports = new SeleniumAssistant(); 382 | -------------------------------------------------------------------------------- /src/local-browsers/chrome.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const fs = require('fs'); 20 | const path = require('path'); 21 | const which = require('which'); 22 | const webdriver = require('selenium-webdriver'); 23 | 24 | const LocalBrowser = require('../browser-models/local-browser.js'); 25 | const application = require('../application-state.js'); 26 | const ChromeConfig = require('../webdriver-config/chrome.js'); 27 | 28 | /** 29 | *

Handles the prettyName and executable path for Chrome browser.

30 | * 31 | * @private 32 | * @extends WebDriverBrowser 33 | */ 34 | class LocalChromeBrowser extends LocalBrowser { 35 | /** 36 | * Create a Chrome representation of a {@link WebDriverBrowser} 37 | * instance on a specific channel. 38 | * @param {String} release The release name for this browser instance. 39 | */ 40 | constructor(release) { 41 | super(new ChromeConfig(), release); 42 | } 43 | 44 | /** 45 | *

This method returns the preconfigured builder used by 46 | * getSeleniumDriver().

47 | * 48 | *

This is useful if you wish to customise the builder with additional 49 | * options (i.e. customise the proxy of the driver.)

50 | * 51 | *

For more info, see: 52 | * {@link https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_Builder.html | WebDriverBuilder Docs}

53 | * 54 | * @return {WebDriverBuilder} Builder that resolves to a webdriver instance. 55 | */ 56 | getSeleniumDriverBuilder() { 57 | const seleniumOptions = this.getSeleniumOptions(); 58 | seleniumOptions.setChromeBinaryPath(this.getExecutablePath()); 59 | 60 | const builder = new webdriver 61 | .Builder() 62 | .withCapabilities(this._capabilities) 63 | .forBrowser(this.getId()) 64 | .setChromeOptions(seleniumOptions); 65 | 66 | return builder; 67 | } 68 | 69 | /** 70 | * @return {string|null} The install directory with selenium-assistant's 71 | * reserved directory for installing browsers and operating files. 72 | */ 73 | _findInInstallDir() { 74 | const defaultDir = application.getInstallDirectory(); 75 | let expectedPath; 76 | if (process.platform === 'linux') { 77 | let chromeSubPath = 'chrome/google-chrome'; 78 | if (this._release === 'beta') { 79 | chromeSubPath = 'chrome-beta/google-chrome-beta'; 80 | } 81 | 82 | expectedPath = path.join( 83 | defaultDir, 'chrome', this._release, 'opt/google/', 84 | chromeSubPath); 85 | } else if (process.platform === 'darwin') { 86 | let chromeAppName = 'Google Chrome'; 87 | if (this._release === 'beta') { 88 | chromeAppName = 'Google Chrome'; 89 | } 90 | 91 | expectedPath = path.join( 92 | defaultDir, 'chrome', this._release, chromeAppName + '.app', 93 | 'Contents/MacOS/' + chromeAppName 94 | ); 95 | } 96 | 97 | try { 98 | // This will throw if it's not found 99 | fs.lstatSync(expectedPath); 100 | return expectedPath; 101 | } catch (error) { 102 | // NOOP 103 | } 104 | 105 | return null; 106 | } 107 | 108 | /** 109 | * Returns the executable for the browser 110 | * @return {String} Path of executable 111 | */ 112 | getExecutablePath() { 113 | const installDirExecutable = this._findInInstallDir(); 114 | if (installDirExecutable) { 115 | // We have a path for the browser 116 | return installDirExecutable; 117 | } 118 | 119 | try { 120 | switch (process.platform) { 121 | case 'darwin': 122 | // Chrome on OS X 123 | switch (this._release) { 124 | case 'stable': 125 | return '/Applications/Google Chrome.app/' + 126 | 'Contents/MacOS/Google Chrome'; 127 | case 'beta': 128 | return '/Applications/Google Chrome Beta.app/' + 129 | 'Contents/MacOS/Google Chrome Beta'; 130 | default: 131 | throw new Error('Unknown release: ' + this._release); 132 | } 133 | case 'linux': 134 | // Chrome on linux 135 | switch (this._release) { 136 | case 'stable': 137 | return which.sync('google-chrome'); 138 | case 'beta': 139 | return which.sync('google-chrome-beta'); 140 | default: 141 | throw new Error('Unknown release: ' + this._release); 142 | } 143 | default: 144 | throw new Error('Sorry, this platform isn\'t supported'); 145 | } 146 | } catch (err) { 147 | // NOOP 148 | } 149 | 150 | return null; 151 | } 152 | 153 | /** 154 | * A version number for the browser. This is the major version number 155 | * (i.e. for 48.0.1293, this would return 18) 156 | * @return {Integer} The major version number of this browser 157 | */ 158 | getVersionNumber() { 159 | const chromeVersion = this.getRawVersionString(); 160 | if (!chromeVersion) { 161 | return -1; 162 | } 163 | 164 | const regexMatch = chromeVersion.match(/(\d+)\.\d+\.\d+\.\d+/); 165 | if (regexMatch === null) { 166 | return -1; 167 | } 168 | 169 | return parseInt(regexMatch[1], 10); 170 | } 171 | 172 | /** 173 | * Get the minimum support version of Chrome with selenium-assistant. 174 | * @return {number} Minimum supported Chrome version. 175 | */ 176 | _getMinSupportedVersion() { 177 | // ChromeDriver only works on Chrome 47+ 178 | return 47; 179 | } 180 | 181 | /** 182 | * This method returns the pretty names for each browser release. 183 | * @return {Object} An object containing on or move of 'stable', 'beta' or 184 | * 'unstable' keys with a matching name for that release. 185 | */ 186 | static getPrettyReleaseNames() { 187 | return { 188 | stable: 'Stable', 189 | beta: 'Beta', 190 | }; 191 | } 192 | } 193 | 194 | module.exports = LocalChromeBrowser; 195 | -------------------------------------------------------------------------------- /src/local-browsers/firefox.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const fs = require('fs'); 20 | const path = require('path'); 21 | const which = require('which'); 22 | const webdriver = require('selenium-webdriver'); 23 | 24 | const LocalBrowser = require('../browser-models/local-browser.js'); 25 | const application = require('../application-state.js'); 26 | const FirefoxConfig = require('../webdriver-config/firefox.js'); 27 | 28 | /** 29 | *

Handles the prettyName and executable path for Chrome browser.

30 | * 31 | * @private 32 | * @extends WebDriverBrowser 33 | */ 34 | class LocalChromeBrowser extends LocalBrowser { 35 | /** 36 | * Create a Chrome representation of a {@link WebDriverBrowser} 37 | * instance on a specific channel. 38 | * @param {String} release The release name for this browser instance. 39 | */ 40 | constructor(release) { 41 | super(new FirefoxConfig(), release); 42 | } 43 | 44 | /** 45 | *

This method returns the preconfigured builder used by 46 | * getSeleniumDriver().

47 | * 48 | *

This is useful if you wish to customise the builder with additional 49 | * options (i.e. customise the proxy of the driver.)

50 | * 51 | *

For more info, see: 52 | * {@link https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_Builder.html | WebDriverBuilder Docs}

53 | * 54 | * @return {WebDriverBuilder} Builder that resolves to a webdriver instance. 55 | */ 56 | getSeleniumDriverBuilder() { 57 | const seleniumOptions = this.getSeleniumOptions(); 58 | seleniumOptions.setBinary(this.getExecutablePath()); 59 | 60 | const builder = new webdriver 61 | .Builder() 62 | .withCapabilities(this._capabilities) 63 | .forBrowser(this.getId()) 64 | .setFirefoxOptions(seleniumOptions); 65 | 66 | return builder; 67 | } 68 | 69 | /** 70 | * @return {string|null} The install directory with selenium-assistant's 71 | * reserved directory for installing browsers and operating files. 72 | */ 73 | _findInInstallDir() { 74 | const defaultDir = application.getInstallDirectory(); 75 | if (process.platform === 'linux') { 76 | const expectedPath = path.join( 77 | defaultDir, 'firefox', this._release, 'firefox'); 78 | 79 | try { 80 | // This will throw if it's not found 81 | fs.lstatSync(expectedPath); 82 | return expectedPath; 83 | } catch (error) { 84 | // NOOP 85 | } 86 | } else if (process.platform === 'darwin') { 87 | // Find OS X expected path 88 | let firefoxAppName; 89 | if (this._release === 'unstable') { 90 | firefoxAppName = 'Firefox Nightly.app'; 91 | } else { 92 | firefoxAppName = 'Firefox.app'; 93 | } 94 | 95 | const expectedPath = path.join( 96 | defaultDir, 'firefox', this._release, firefoxAppName, 97 | 'Contents/MacOS/firefox' 98 | ); 99 | 100 | try { 101 | // This will throw if it's not found 102 | fs.lstatSync(expectedPath); 103 | return expectedPath; 104 | } catch (error) { 105 | // NOOP 106 | } 107 | } 108 | 109 | return null; 110 | } 111 | 112 | /** 113 | * Returns the executable for the browser 114 | * @return {String} Path of executable 115 | */ 116 | getExecutablePath() { 117 | const installDirExecutable = this._findInInstallDir(); 118 | if (installDirExecutable) { 119 | // We have a path for the browser 120 | return installDirExecutable; 121 | } 122 | 123 | try { 124 | switch (process.platform) { 125 | case 'darwin': 126 | // Firefox Beta on OS X overrides Firefox stable, so realistically 127 | // this location could return ff stable as beta, but at least it will 128 | // only be returned once. 129 | if (this._release === 'stable') { 130 | return '/Applications/Firefox.app/Contents/MacOS/firefox'; 131 | } else if (this._release === 'unstable') { 132 | return '/Applications/Firefox Nightly.app/Contents/MacOS/firefox'; 133 | } 134 | break; 135 | case 'linux': 136 | // Stable firefox on Linux is the only known location we can find 137 | // otherwise it's jsut a .tar.gz that users have to put anywhere 138 | if (this._release === 'stable') { 139 | return which.sync('firefox'); 140 | } 141 | break; 142 | default: 143 | throw new Error('Sorry, this platform isn\'t supported'); 144 | } 145 | } catch (err) { 146 | // NOOP 147 | } 148 | 149 | return null; 150 | } 151 | 152 | /** 153 | * A version number for the browser. This is the major version number 154 | * (i.e. for 48.0.1293, this would return 18) 155 | * @return {Integer} The major version number of this browser 156 | */ 157 | getVersionNumber() { 158 | const firefoxVersion = this.getRawVersionString(); 159 | if (!firefoxVersion) { 160 | return -1; 161 | } 162 | 163 | const regexMatch = firefoxVersion.match(/(\d+)\.\d/); 164 | if (regexMatch === null) { 165 | return -1; 166 | } 167 | 168 | return parseInt(regexMatch[1], 10); 169 | } 170 | 171 | /** 172 | * Get the minimum support version of Firefox with selenium-assistant. 173 | * @return {number} Minimum supported Firefox version. 174 | */ 175 | _getMinSupportedVersion() { 176 | // Firefox Marionette only works on Firefox 47+ 177 | return 47; 178 | } 179 | 180 | /** 181 | * This method returns the pretty names for each browser release. 182 | * @return {Object} An object containing on or move of 'stable', 'beta' or 183 | * 'unstable' keys with a matching name for that release. 184 | */ 185 | static getPrettyReleaseNames() { 186 | return { 187 | stable: 'Stable', 188 | beta: 'Beta', 189 | unstable: 'Nightly', 190 | }; 191 | } 192 | } 193 | 194 | module.exports = LocalChromeBrowser; 195 | -------------------------------------------------------------------------------- /src/local-browsers/safari.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const fs = require('fs'); 20 | const webdriver = require('selenium-webdriver'); 21 | const safari = require('selenium-webdriver/safari'); 22 | 23 | const LocalBrowser = require('../browser-models/local-browser.js'); 24 | const SafariConfig = require('../webdriver-config/safari.js'); 25 | 26 | /** 27 | *

Handles the prettyName and executable path for Chrome browser.

28 | * 29 | * @private 30 | * @extends WebDriverBrowser 31 | */ 32 | class LocalSafariBrowser extends LocalBrowser { 33 | /** 34 | * Create a Chrome representation of a {@link WebDriverBrowser} 35 | * instance on a specific channel. 36 | * @param {String} release The release name for this browser instance. 37 | */ 38 | constructor(release) { 39 | super(new SafariConfig(), release); 40 | } 41 | 42 | /** 43 | *

This method returns the preconfigured builder used by 44 | * getSeleniumDriver().

45 | * 46 | *

This is useful if you wish to customise the builder with additional 47 | * options (i.e. customise the proxy of the driver.)

48 | * 49 | *

For more info, see: 50 | * {@link https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_Builder.html | WebDriverBuilder Docs}

51 | * 52 | * @return {WebDriverBuilder} Builder that resolves to a webdriver instance. 53 | */ 54 | getSeleniumDriverBuilder() { 55 | const seleniumOptions = this.getSeleniumOptions(); 56 | seleniumOptions.setTechnologyPreview((this._release === 'beta')); 57 | 58 | let builder = new webdriver 59 | .Builder() 60 | .withCapabilities(this._capabilities) 61 | .forBrowser(this.getId()) 62 | .setSafariOptions(seleniumOptions); 63 | 64 | // Run Safari 12 in legacy mode due to 65 | // https://github.com/SeleniumHQ/selenium/issues/6026 66 | // This is not needed in Safari 13+. 67 | if (this.getVersionNumber() === 12) { 68 | builder = builder.usingServer( 69 | new safari.ServiceBuilder().addArguments('--legacy').build().start()); 70 | } 71 | 72 | return builder; 73 | } 74 | 75 | /** 76 | * @return {string|null} The install directory with selenium-assistant's 77 | * reserved directory for installing browsers and operating files. 78 | */ 79 | _findInInstallDir() { 80 | return null; 81 | } 82 | 83 | /** 84 | * Returns the executable for the browser 85 | * @return {String} Path of executable 86 | */ 87 | getExecutablePath() { 88 | const installDirExecutable = this._findInInstallDir(); 89 | if (installDirExecutable) { 90 | // We have a path for the browser 91 | return installDirExecutable; 92 | } 93 | 94 | try { 95 | if (this._release === 'stable') { 96 | if (process.platform === 'darwin') { 97 | return '/Applications/Safari.app/' + 98 | 'Contents/MacOS/Safari'; 99 | } 100 | } else if (this._release === 'beta') { 101 | if (process.platform === 'darwin') { 102 | return '/Applications/Safari Technology Preview.app/' + 103 | 'Contents/MacOS/Safari Technology Preview'; 104 | } 105 | } 106 | } catch (err) { 107 | // NOOP 108 | } 109 | 110 | return null; 111 | } 112 | 113 | /** 114 | * Get the version string from the browser itself. From Safari this is from 115 | * `version.plist` file. 116 | * @return {String} The version string for this Safari release. 117 | */ 118 | getRawVersionString() { 119 | if (this._rawVerstionString) { 120 | return this._rawVerstionString; 121 | } 122 | 123 | const executablePath = this.getExecutablePath(); 124 | if (!executablePath) { 125 | return null; 126 | } 127 | 128 | this._rawVerstionString = null; 129 | 130 | let versionListPath; 131 | if (this._release === 'stable') { 132 | versionListPath = '/Applications/Safari.app/Contents/version.plist'; 133 | } else if (this._release === 'beta') { 134 | versionListPath = '/Applications/Safari Technology Preview.app/' + 135 | 'Contents/version.plist'; 136 | } 137 | try { 138 | const versionDoc = fs.readFileSync(versionListPath).toString(); 139 | /* eslint-disable no-useless-escape */ 140 | const results = new RegExp( 141 | 'CFBundleShortVersionString' + 142 | '[\\s]+([\\d]+.[\\d]+(?:.[\\d]+)?)', 'g') 143 | .exec(versionDoc); 144 | /* eslint-enable no-useless-escape */ 145 | if (results) { 146 | this._rawVerstionString = results[1]; 147 | } 148 | } catch (err) { 149 | // NOOP 150 | } 151 | 152 | return this._rawVerstionString; 153 | } 154 | 155 | /** 156 | * A version number for the browser. This is the major version number 157 | * (i.e. for 48.0.1293, this would return 18) 158 | * @return {Integer} The major version number of this browser 159 | */ 160 | getVersionNumber() { 161 | const safariVersion = this.getRawVersionString(); 162 | if (!safariVersion) { 163 | return -1; 164 | } 165 | 166 | const regexMatch = safariVersion.match(/(\d+)\.\d+(?:\.\d+)?/); 167 | if (regexMatch === null) { 168 | return -1; 169 | } 170 | 171 | return parseInt(regexMatch[1], 10); 172 | } 173 | 174 | /** 175 | * Get the minimum support version of Safari with selenium-assistant. 176 | * @return {number} Minimum supported Safari version. 177 | */ 178 | _getMinSupportedVersion() { 179 | // Latest SafariDriver only works on Safari 10+ 180 | return 10; 181 | } 182 | 183 | /** 184 | * This method returns the pretty names for each browser release. 185 | * @return {Object} An object containing on or move of 'stable', 'beta' or 186 | * 'unstable' keys with a matching name for that release. 187 | */ 188 | static getPrettyReleaseNames() { 189 | return { 190 | stable: 'Stable', 191 | beta: 'Technology Preview', 192 | }; 193 | } 194 | } 195 | 196 | module.exports = LocalSafariBrowser; 197 | -------------------------------------------------------------------------------- /src/saucelabs-browsers/chrome.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const SauceLabsBrowser = require('../browser-models/saucelabs-browser'); 20 | const ChromeConfig = require('../webdriver-config/chrome'); 21 | /** 22 | *

Handles the prettyName and executable path for Chrome browser.

23 | * 24 | * @private 25 | * @extends WebDriverBrowser 26 | */ 27 | class ChromeWebDriverBrowser extends SauceLabsBrowser { 28 | /** 29 | * Create a Chrome representation of a {@link WebDriverBrowser} 30 | * instance on a specific channel. 31 | * @param {String} version The release name for this browser instance. 32 | */ 33 | constructor(version) { 34 | super(new ChromeConfig(), version); 35 | 36 | // Set default platform to windows 10 otherwise it will be an out of 37 | // date version of Chrome. 38 | this.addCapability('platform', 'Windows 10'); 39 | } 40 | 41 | /** 42 | *

This method returns the preconfigured builder used by 43 | * getSeleniumDriver().

44 | * 45 | *

This is useful if you wish to customise the builder with additional 46 | * options (i.e. customise the proxy of the driver.)

47 | * 48 | *

For more info, see: 49 | * {@link https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_Builder.html | WebDriverBuilder Docs}

50 | * 51 | * @return {WebDriverBuilder} Builder that resolves to a webdriver instance. 52 | */ 53 | getSeleniumDriverBuilder() { 54 | let builder = super.getSeleniumDriverBuilder(); 55 | 56 | const seleniumOptions = this.getSeleniumOptions(); 57 | const capabilities = seleniumOptions.toCapabilities(this._capabilities); 58 | 59 | builder = builder.withCapabilities(capabilities); 60 | 61 | return builder; 62 | } 63 | } 64 | 65 | module.exports = ChromeWebDriverBrowser; 66 | -------------------------------------------------------------------------------- /src/saucelabs-browsers/edge.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const SauceLabsBrowser = require('../browser-models/saucelabs-browser'); 20 | const EdgeConfig = require('../webdriver-config/edge'); 21 | 22 | /** 23 | *

Handles the prettyName and executable path for Chrome browser.

24 | * 25 | *

Note that Edge gets cranky about which ports are used for localhost + 26 | * saucelabs.

27 | * 28 | *

https://support.saucelabs.com/customer/portal/questions/14368823-requests-to-localhost-on-microsoft-edge-are-failing-over-sauce-connect

29 | * 30 | * @private 31 | * @extends WebDriverBrowser 32 | */ 33 | class EdgeWebDriverBrowser extends SauceLabsBrowser { 34 | /** 35 | * Create a Chrome representation of a {@link WebDriverBrowser} 36 | * instance on a specific channel. 37 | * @param {String} version The version name for this browser instance. 38 | */ 39 | constructor(version) { 40 | super(new EdgeConfig(), version); 41 | 42 | // Set default platform to windows 10 otherwise it will fail. 43 | this.addCapability('platform', 'Windows 10'); 44 | } 45 | 46 | /** 47 | *

This method returns the preconfigured builder used by 48 | * getSeleniumDriver().

49 | * 50 | *

This is useful if you wish to customise the builder with additional 51 | * options (i.e. customise the proxy of the driver.)

52 | * 53 | *

For more info, see: 54 | * {@link https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_Builder.html | WebDriverBuilder Docs}

55 | * 56 | * @return {WebDriverBuilder} Builder that resolves to a webdriver instance. 57 | */ 58 | getSeleniumDriverBuilder() { 59 | let builder = super.getSeleniumDriverBuilder(); 60 | const seleniumOptions = this.getSeleniumOptions(); 61 | const capabilities = seleniumOptions.toCapabilities(this._capabilities); 62 | 63 | builder = builder 64 | .withCapabilities(capabilities); 65 | 66 | return builder; 67 | } 68 | } 69 | 70 | module.exports = EdgeWebDriverBrowser; 71 | -------------------------------------------------------------------------------- /src/saucelabs-browsers/firefox.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const SauceLabsBrowser = require('../browser-models/saucelabs-browser'); 20 | const FirefoxConfig = require('../webdriver-config/firefox'); 21 | 22 | /** 23 | *

Handles the prettyName and executable path for Chrome browser.

24 | * 25 | * @private 26 | * @extends WebDriverBrowser 27 | */ 28 | class FirefoxWebDriverBrowser extends SauceLabsBrowser { 29 | /** 30 | * Create a Chrome representation of a {@link WebDriverBrowser} 31 | * instance on a specific channel. 32 | * @param {String} version The release name for this browser instance. 33 | */ 34 | constructor(version) { 35 | super(new FirefoxConfig(), version); 36 | 37 | // Set default platform to windows 10 otherwise it will be an out of 38 | // date version of Firefox. 39 | this.addCapability('platform', 'Windows 10'); 40 | } 41 | 42 | /** 43 | *

This method returns the preconfigured builder used by 44 | * getSeleniumDriver().

45 | * 46 | *

This is useful if you wish to customise the builder with additional 47 | * options (i.e. customise the proxy of the driver.)

48 | * 49 | *

For more info, see: 50 | * {@link https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_Builder.html | WebDriverBuilder Docs}

51 | * 52 | * @return {WebDriverBuilder} Builder that resolves to a webdriver instance. 53 | */ 54 | getSeleniumDriverBuilder() { 55 | let builder = super.getSeleniumDriverBuilder(); 56 | 57 | const seleniumOptions = this.getSeleniumOptions(); 58 | // FirefoxOptions.toCapabilities() don't take in options. 59 | const capabilities = seleniumOptions.toCapabilities() 60 | .merge(this._capabilities); 61 | 62 | builder = builder 63 | .withCapabilities(capabilities); 64 | 65 | return builder; 66 | 67 | /** let builder = super.getSeleniumDriverBuilder(); 68 | builder = builder 69 | // Sauce Labs + Firefox is simple broken if I pass in the options 70 | // .setFirefoxOptions(this.getSeleniumOptions()) 71 | .withCapabilities(this._capabilities) 72 | .forBrowser(this.getId()); 73 | 74 | return builder;**/ 75 | } 76 | } 77 | 78 | module.exports = FirefoxWebDriverBrowser; 79 | -------------------------------------------------------------------------------- /src/saucelabs-browsers/ie.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const SauceLabsBrowser = require('../browser-models/saucelabs-browser'); 20 | const IEConfig = require('../webdriver-config/ie'); 21 | 22 | /** 23 | *

Handles the prettyName and executable path for Chrome browser.

24 | * 25 | * @private 26 | * @extends WebDriverBrowser 27 | */ 28 | class IEWebDriverBrowser extends SauceLabsBrowser { 29 | /** 30 | * Create a Chrome representation of a {@link WebDriverBrowser} 31 | * instance on a specific channel. 32 | * @param {String} version The release name for this browser instance. 33 | */ 34 | constructor(version) { 35 | super(new IEConfig(), version); 36 | } 37 | 38 | /** 39 | *

This method returns the preconfigured builder used by 40 | * getSeleniumDriver().

41 | * 42 | *

This is useful if you wish to customise the builder with additional 43 | * options (i.e. customise the proxy of the driver.)

44 | * 45 | *

For more info, see: 46 | * {@link https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_Builder.html | WebDriverBuilder Docs}

47 | * 48 | * @return {WebDriverBuilder} Builder that resolves to a webdriver instance. 49 | */ 50 | getSeleniumDriverBuilder() { 51 | let builder = super.getSeleniumDriverBuilder(); 52 | const seleniumOptions = this.getSeleniumOptions(); 53 | const capabilities = seleniumOptions.toCapabilities(this._capabilities); 54 | 55 | builder = builder 56 | .withCapabilities(capabilities); 57 | 58 | return builder; 59 | } 60 | } 61 | 62 | module.exports = IEWebDriverBrowser; 63 | -------------------------------------------------------------------------------- /src/saucelabs-browsers/safari.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const SauceLabsBrowser = require('../browser-models/saucelabs-browser'); 20 | const SafariConfig = require('../webdriver-config/safari'); 21 | 22 | /** 23 | *

Handles the prettyName and executable path for Chrome browser.

24 | * 25 | * @private 26 | * @extends WebDriverBrowser 27 | */ 28 | class SafariDriverBrowser extends SauceLabsBrowser { 29 | /** 30 | * Create a Chrome representation of a {@link WebDriverBrowser} 31 | * instance on a specific channel. 32 | * @param {String} version The release name for this browser instance. 33 | */ 34 | constructor(version) { 35 | super(new SafariConfig(), version); 36 | } 37 | 38 | /** 39 | *

This method returns the preconfigured builder used by 40 | * getSeleniumDriver().

41 | * 42 | *

This is useful if you wish to customise the builder with additional 43 | * options (i.e. customise the proxy of the driver.)

44 | * 45 | *

For more info, see: 46 | * {@link https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_Builder.html | WebDriverBuilder Docs}

47 | * 48 | * @return {WebDriverBuilder} Builder that resolves to a webdriver instance. 49 | */ 50 | getSeleniumDriverBuilder() { 51 | let builder = super.getSeleniumDriverBuilder(); 52 | const seleniumOptions = this.getSeleniumOptions(); 53 | const capabilities = seleniumOptions.toCapabilities(this._capabilities); 54 | 55 | builder = builder 56 | .withCapabilities(capabilities); 57 | 58 | return builder; 59 | } 60 | } 61 | 62 | module.exports = SafariDriverBrowser; 63 | -------------------------------------------------------------------------------- /src/webdriver-config/chrome.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const DriverConfig = require('../browser-models/driver-config'); 20 | const seleniumChrome = require('selenium-webdriver/chrome'); 21 | 22 | /** 23 | * @private 24 | */ 25 | class ChromeConfig extends DriverConfig { 26 | /** 27 | * 28 | */ 29 | constructor() { 30 | super( 31 | 'chrome', 32 | new seleniumChrome.Options(), 33 | 'Google Chrome', 34 | 'chromedriver' 35 | ); 36 | } 37 | } 38 | 39 | module.exports = ChromeConfig; 40 | -------------------------------------------------------------------------------- /src/webdriver-config/edge.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const DriverConfig = require('../browser-models/driver-config'); 20 | const seleniumEdge = require('selenium-webdriver/edge'); 21 | 22 | /** 23 | * @private 24 | */ 25 | class Edge extends DriverConfig { 26 | /** 27 | * 28 | */ 29 | constructor() { 30 | super( 31 | 'microsoftedge', 32 | new seleniumEdge.Options(), 33 | 'Microsoft Edge' 34 | ); 35 | } 36 | } 37 | 38 | module.exports = Edge; 39 | -------------------------------------------------------------------------------- /src/webdriver-config/firefox.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const DriverConfig = require('../browser-models/driver-config'); 20 | const seleniumFirefox = require('selenium-webdriver/firefox'); 21 | 22 | /** 23 | * @private 24 | */ 25 | class Firefox extends DriverConfig { 26 | /** 27 | * 28 | */ 29 | constructor() { 30 | super( 31 | 'firefox', 32 | new seleniumFirefox.Options(), 33 | 'Firefox', 34 | 'geckodriver' 35 | ); 36 | } 37 | } 38 | 39 | module.exports = Firefox; 40 | -------------------------------------------------------------------------------- /src/webdriver-config/ie.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const DriverConfig = require('../browser-models/driver-config'); 20 | const seleniumIE = require('selenium-webdriver/ie'); 21 | 22 | /** 23 | * @private 24 | */ 25 | class IE extends DriverConfig { 26 | /** 27 | * 28 | */ 29 | constructor() { 30 | super( 31 | 'internet explorer', 32 | new seleniumIE.Options(), 33 | 'Internet Explorer' 34 | ); 35 | } 36 | } 37 | 38 | module.exports = IE; 39 | -------------------------------------------------------------------------------- /src/webdriver-config/safari.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const DriverConfig = require('../browser-models/driver-config'); 20 | const seleniumSafari = require('selenium-webdriver/safari'); 21 | 22 | /** 23 | * @private 24 | */ 25 | class Safari extends DriverConfig { 26 | /** 27 | * 28 | */ 29 | constructor() { 30 | super( 31 | 'safari', 32 | new seleniumSafari.Options(), 33 | 'Safari' 34 | ); 35 | } 36 | } 37 | 38 | module.exports = Safari; 39 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "mocha": true 5 | }, 6 | "rules": { 7 | "no-console": 0, 8 | "no-new": 0, 9 | "max-len": 0, 10 | "no-unused-expression": 0, 11 | "max-nested-callbacks": 0, 12 | "require-jsdoc": 0, 13 | "no-invalid-this": 0 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/application-state.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const path = require('path'); 20 | 21 | require('chai').should(); 22 | 23 | describe('Application State', function() { 24 | it('should be able to get default install location', function() { 25 | const applicationState = require('../src/application-state.js'); 26 | const installLocation = applicationState.getDefaultInstallLocation(); 27 | 28 | (typeof installLocation).should.equal('string'); 29 | (installLocation.length).should.be.gt(1); 30 | }); 31 | 32 | it('should return the current directory if no useful environment variables exist', function() { 33 | const originalHomeValue = process.env.HOME; 34 | const originalUserProfValue = process.env.USERPROFILE; 35 | 36 | delete process.env.HOME; 37 | delete process.env.USERPROFILE; 38 | 39 | const applicationState = require('../src/application-state.js'); 40 | const installLocation = applicationState.getDefaultInstallLocation(); 41 | 42 | process.env.HOME = originalHomeValue; 43 | process.env.USERPROFILE = originalUserProfValue; 44 | 45 | (typeof installLocation).should.equal('string'); 46 | (installLocation).should.equal('.selenium-assistant'); 47 | }); 48 | 49 | it('should start with default install directory', function() { 50 | const applicationState = require('../src/application-state.js'); 51 | applicationState.getInstallDirectory().should.equal( 52 | applicationState.getDefaultInstallLocation() 53 | ); 54 | }); 55 | 56 | it('should be able to get a change install directory', function() { 57 | const applicationState = require('../src/application-state.js'); 58 | const newPath = './test/test-output/'; 59 | applicationState.setInstallDirectory(newPath); 60 | applicationState.getInstallDirectory().should.equal(path.resolve(newPath)); 61 | }); 62 | 63 | it('should be able to pass in null to reset state to default install location', function() { 64 | const applicationState = require('../src/application-state.js'); 65 | 66 | const newPath = './test/test-output/'; 67 | applicationState.setInstallDirectory(newPath); 68 | applicationState.getInstallDirectory().should.equal(path.resolve(newPath)); 69 | 70 | applicationState.setInstallDirectory(null); 71 | applicationState.getInstallDirectory().should.equal( 72 | applicationState.getDefaultInstallLocation() 73 | ); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/browser-usage.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const fs = require('fs'); 20 | const path = require('path'); 21 | const sinon = require('sinon'); 22 | const selenium = require('selenium-webdriver'); 23 | const seleniumAssistant = require('../src/index.js'); 24 | const TestServer = require('./helpers/test-server.js'); 25 | const expect = require('chai').expect; 26 | 27 | require('chai').should(); 28 | 29 | const TIMEOUT = 5 * 60 * 1000; 30 | const RETRIES = 3; 31 | 32 | describe('Test Usage of Browsers', function() { 33 | this.timeout(TIMEOUT); 34 | this.retries(RETRIES); 35 | 36 | const sandbox = sinon.createSandbox(); 37 | let globalDriver = null; 38 | const globalServer = new TestServer(false); 39 | let localURL = ''; 40 | 41 | function testNormalSeleniumUsage(specificBrowser) { 42 | return specificBrowser.getSeleniumDriver() 43 | .then((driver) => { 44 | globalDriver = driver; 45 | }) 46 | .then(() => { 47 | return globalDriver.get(localURL) 48 | .then(() => { 49 | return globalDriver.wait(selenium.until.titleIs('Example Site'), 1000); 50 | }); 51 | }) 52 | .then(() => seleniumAssistant.killWebDriver(globalDriver)) 53 | .then(() => { 54 | globalDriver = null; 55 | }); 56 | } 57 | 58 | function testBuilderSeleniumUsage(specificBrowser) { 59 | const builder = specificBrowser.getSeleniumDriverBuilder(); 60 | 61 | return builder.build() 62 | .then((driver) => { 63 | globalDriver = driver; 64 | }) 65 | .then(() => { 66 | return globalDriver.get(localURL); 67 | }) 68 | .then(() => { 69 | return globalDriver.wait(selenium.until.titleIs('Example Site'), 1000); 70 | }) 71 | .then(() => { 72 | return seleniumAssistant.killWebDriver(globalDriver); 73 | }) 74 | .then(() => { 75 | globalDriver = null; 76 | }); 77 | } 78 | 79 | function testBrowserInfo(specificBrowser) { 80 | const versionString = specificBrowser.getRawVersionString(); 81 | (typeof versionString).should.equal('string'); 82 | (versionString === null).should.equal(false); 83 | versionString.length.should.gt(0); 84 | 85 | const versionNumber = specificBrowser.getVersionNumber(); 86 | (typeof versionNumber).should.equal('number'); 87 | if (versionString) { 88 | versionNumber.should.not.equal(-1); 89 | } 90 | 91 | const prettyName = specificBrowser.getPrettyName(); 92 | prettyName.length.should.gt(1); 93 | 94 | const options = specificBrowser.getSeleniumOptions(); 95 | expect(options).to.exist; 96 | 97 | const executablePath = specificBrowser.getExecutablePath(); 98 | (typeof executablePath).should.equal('string'); 99 | } 100 | 101 | function setupTest(localBrowser) { 102 | describe(`Test Usage of ${localBrowser.getPrettyName()}`, function() { 103 | it(`should be able to use ${localBrowser.getId()} - ${localBrowser.getReleaseName()}`, function() { 104 | if (localBrowser.isDenyListed()) { 105 | console.warn(`Browser is denylisted ${localBrowser.getId()} - ${localBrowser.getReleaseName()}`); 106 | return; 107 | } 108 | 109 | if (!localBrowser.isValid()) { 110 | console.warn(`Browser is invalid ${localBrowser.getId()} - ${localBrowser.getReleaseName()}`); 111 | return; 112 | } 113 | 114 | testBrowserInfo(localBrowser); 115 | return testNormalSeleniumUsage(localBrowser) 116 | .then(() => testBuilderSeleniumUsage(localBrowser)); 117 | }); 118 | 119 | it('should get null for raw version output if no executable found', function() { 120 | sandbox.stub(localBrowser, 'getExecutablePath').callsFake(() => { 121 | return null; 122 | }); 123 | 124 | // To overcome version string caching 125 | localBrowser._rawVerstionString = null; 126 | 127 | const rawString = localBrowser.getRawVersionString(); 128 | (rawString === null).should.equal(true); 129 | }); 130 | 131 | it('should get -1 for version number if no executable found', function() { 132 | sandbox.stub(localBrowser, 'getExecutablePath').callsFake(() => { 133 | return null; 134 | }); 135 | 136 | // To overcome version string caching 137 | localBrowser._rawVerstionString = null; 138 | 139 | const versionNumber = localBrowser.getVersionNumber(); 140 | versionNumber.should.equal(-1); 141 | }); 142 | 143 | it('should get -1 for an unexpected raw version string', function() { 144 | sandbox.stub(localBrowser, 'getRawVersionString').callsFake(() => { 145 | return 'ImTotallyMadeUp 12345678.asdf.12345678.asdf'; 146 | }); 147 | 148 | // To overcome version string caching 149 | localBrowser._rawVerstionString = null; 150 | 151 | const versionNumber = localBrowser.getVersionNumber(); 152 | versionNumber.should.equal(-1); 153 | }); 154 | }); 155 | } 156 | 157 | before(function() { 158 | seleniumAssistant.setBrowserInstallDir(null); 159 | 160 | const expiration = process.env.TRAVIS ? 0 : 24; 161 | console.log('Downloading browsers....'); 162 | return Promise.all([ 163 | seleniumAssistant.downloadLocalBrowser('chrome', 'stable', expiration), 164 | seleniumAssistant.downloadLocalBrowser('chrome', 'beta', expiration), 165 | seleniumAssistant.downloadLocalBrowser('firefox', 'stable', expiration), 166 | seleniumAssistant.downloadLocalBrowser('firefox', 'beta', expiration), 167 | seleniumAssistant.downloadLocalBrowser('firefox', 'unstable', expiration), 168 | ]) 169 | .catch((err) => { 170 | console.warn('There was an issue downloading the browsers: ', err); 171 | }) 172 | .then(() => { 173 | console.log('Download of browsers complete.'); 174 | 175 | const serverPath = path.join(__dirname, 'data', 'example-site'); 176 | return globalServer.startServer(serverPath); 177 | }) 178 | .then((portNumber) => { 179 | localURL = `http://localhost:${portNumber}/`; 180 | }); 181 | }); 182 | 183 | afterEach(function() { 184 | sandbox.restore(); 185 | 186 | return seleniumAssistant.killWebDriver(globalDriver).catch(() => {}) 187 | .then(() => { 188 | globalDriver = null; 189 | }); 190 | }); 191 | 192 | after(function() { 193 | return globalServer.killServer(); 194 | }); 195 | 196 | const localBrowserFiles = fs.readdirSync( 197 | path.join(__dirname, '..', 'src', 'local-browsers')); 198 | localBrowserFiles.forEach((localBrowserFile) => { 199 | const LocalBrowserClass = require(`./../src/local-browsers/${localBrowserFile}`); 200 | const browserReleases = LocalBrowserClass.getPrettyReleaseNames(); 201 | Object.keys(browserReleases).forEach((release) => { 202 | const localBrowser = new LocalBrowserClass(release); 203 | setupTest(localBrowser); 204 | }); 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /test/data/example-site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Example Site 4 | 5 | 6 |

Hello From Your Tests :)

7 | 8 | 9 | -------------------------------------------------------------------------------- /test/downloading-browsers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra'); 4 | const seleniumAssistant = require('../src/index.js'); 5 | 6 | const TIMEOUT = 5 * 60 * 1000; 7 | const RETRIES = 3; 8 | 9 | describe('Test Download Manager - Browser Download', function() { 10 | this.timeout(TIMEOUT); 11 | this.retries(RETRIES); 12 | 13 | before(function() { 14 | // Reset Install Directory 15 | seleniumAssistant.setBrowserInstallDir(null); 16 | 17 | return fs.remove(seleniumAssistant.getBrowserInstallDir()); 18 | }); 19 | 20 | const setupDownloadTest = (browserId, release) => { 21 | it(`should download ${browserId} - ${release} from the network`, function() { 22 | this.timeout(5 * 60 * 1000); 23 | return seleniumAssistant.downloadLocalBrowser(browserId, release, 0); 24 | }); 25 | }; 26 | 27 | const browsers = [ 28 | 'firefox', 29 | 'chrome', 30 | ]; 31 | const releases = [ 32 | 'stable', 33 | 'beta', 34 | 'unstable', 35 | ]; 36 | 37 | browsers.forEach((browserId) => { 38 | releases.forEach((release) => { 39 | if (browserId === 'chrome' && release === 'unstable') { 40 | return; 41 | } 42 | setupDownloadTest(browserId, release); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/expiration-behavior.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fse = require('fs-extra'); 4 | const sinon = require('sinon'); 5 | const LocalStorage = require('node-localstorage').LocalStorage; 6 | const path = require('path'); 7 | const mkdirp = require('mkdirp'); 8 | require('chai').should(); 9 | 10 | const LocalBrowser = require( 11 | '../src/browser-models/local-browser.js'); 12 | const seleniumAssistant = require('../src/index.js'); 13 | const downloadManager = require('../src/download-manager.js'); 14 | 15 | const TIMEOUT = 5 * 60 * 1000; 16 | const RETRIES = 3; 17 | 18 | const testPath = './test/test-output'; 19 | const localStoragePath = path.join(testPath, 'localstorage'); 20 | let browserDownloads; 21 | 22 | describe('Test Download Manager - Browser Expiration', function() { 23 | const sandbox = sinon.createSandbox(); 24 | 25 | this.timeout(TIMEOUT); 26 | this.retries(RETRIES); 27 | 28 | const performTest = (browserId, releases) => { 29 | releases.forEach((release) => { 30 | it(`should download ${browserId} - ${release} with no expiration.`, function() { 31 | return downloadManager.downloadLocalBrowser(browserId, release) 32 | .then(() => { 33 | browserDownloads[browserId][release].should.equal(true); 34 | browserDownloads[browserId][release] = false; 35 | return downloadManager.downloadLocalBrowser(browserId, release); 36 | }) 37 | .then(() => { 38 | // The default should be 24 hours, so manipulate the DB to say older 39 | // than 24 hours 40 | const localStorage = new LocalStorage(localStoragePath); 41 | const storageKey = `${browserId}:${release}`; 42 | const lastUpdate = Date.now() - 43 | (downloadManager.defaultExpiration * 60 * 60 * 1000); 44 | localStorage.setItem(storageKey, lastUpdate); 45 | }) 46 | .then(() => { 47 | return downloadManager.downloadLocalBrowser(browserId, release, 0); 48 | }) 49 | .then(() => { 50 | browserDownloads[browserId][release].should.equal(true); 51 | }); 52 | }); 53 | 54 | it(`should download ${browserId} - ${release} with 0 hour expiration (Force download).`, function() { 55 | return downloadManager.downloadLocalBrowser(browserId, release, 0) 56 | .then(() => { 57 | browserDownloads[browserId][release].should.equal(true); 58 | 59 | browserDownloads[browserId][release] = false; 60 | return downloadManager.downloadLocalBrowser(browserId, release, 0); 61 | }) 62 | .then(() => { 63 | browserDownloads[browserId][release].should.equal(true); 64 | }); 65 | }); 66 | 67 | it(`should download ${browserId} - ${release} with 1 hour expiration and not re-download.`, function() { 68 | const EXPIRATION_TIME = 1; 69 | 70 | return downloadManager.downloadLocalBrowser(browserId, release, 71 | EXPIRATION_TIME) 72 | .then(() => { 73 | browserDownloads[browserId][release].should.equal(true); 74 | 75 | // Reset download for next step 76 | browserDownloads[browserId][release] = false; 77 | 78 | return downloadManager.downloadLocalBrowser(browserId, release, 79 | EXPIRATION_TIME); 80 | }) 81 | .then(() => { 82 | browserDownloads[browserId][release].should.equal(false); 83 | 84 | // Alter DB so browser is expired and check it downloads immediately 85 | const storageKey = `${browserId}:${release}`; 86 | const localStorage = new LocalStorage(localStoragePath); 87 | const lastUpdate = Date.now() - (EXPIRATION_TIME * 60 * 60 * 1000); 88 | localStorage.setItem(storageKey, lastUpdate); 89 | }) 90 | .then(() => { 91 | // Reset download for next step 92 | browserDownloads[browserId][release] = false; 93 | 94 | return downloadManager.downloadLocalBrowser(browserId, release, 95 | EXPIRATION_TIME); 96 | }) 97 | .then(() => { 98 | browserDownloads[browserId][release].should.equal(true); 99 | }); 100 | }); 101 | }); 102 | }; 103 | 104 | before(function() { 105 | seleniumAssistant.setBrowserInstallDir(testPath); 106 | 107 | sandbox.stub(downloadManager, '_downloadChrome') 108 | .callsFake((release, installDir) => { 109 | browserDownloads.chrome[release] = true; 110 | return Promise.resolve(); 111 | }); 112 | 113 | sandbox.stub(downloadManager, '_downloadFirefox') 114 | .callsFake((release, installDir) => { 115 | browserDownloads.firefox[release] = true; 116 | return Promise.resolve(); 117 | }); 118 | 119 | sandbox.stub(LocalBrowser.prototype, 'isValid') 120 | .callsFake(() => { 121 | return true; 122 | }); 123 | 124 | return mkdirp(localStoragePath); 125 | }); 126 | 127 | beforeEach(function() { 128 | browserDownloads = {}; 129 | browserDownloads.chrome = { 130 | stable: false, 131 | beta: false, 132 | }; 133 | browserDownloads.firefox = { 134 | stable: false, 135 | beta: false, 136 | unstable: false, 137 | }; 138 | }); 139 | 140 | after(function() { 141 | this.timeout(6000); 142 | 143 | sandbox.restore(); 144 | 145 | return fse.remove(seleniumAssistant.getBrowserInstallDir()); 146 | }); 147 | 148 | beforeEach(function() { 149 | this.timeout(6000); 150 | 151 | // Ensure the test output is clear at the start 152 | return fse.remove(seleniumAssistant.getBrowserInstallDir()); 153 | }); 154 | 155 | const browsers = [ 156 | 'firefox', 157 | 'chrome', 158 | ]; 159 | const releases = [ 160 | 'stable', 161 | 'beta', 162 | 'unstable', 163 | ]; 164 | 165 | browsers.forEach((browserId) => { 166 | performTest(browserId, releases); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /test/helpers/test-server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /* eslint-env node */ 21 | 22 | const express = require('express'); 23 | 24 | /** 25 | *

A super simple class that will start and stop an 26 | * express server with a few nice defaults and just removes boilerplate 27 | * to server up static files.

28 | * 29 | *

NOTE: This should never be used as a production 30 | * web server.

31 | * 32 | * @example 33 | * const TestServer = require('sw-testing-helpers').TestServer; 34 | * 35 | * let testServer = new TestServer(); 36 | * testServer.startServer(path.join(__dirname, '..'), 8888) 37 | * .then(portNumber => { 38 | * console.log('http://localhost:' + portNumber); 39 | * }); 40 | * 41 | * // To kill at a later stage... 42 | * testServer.killServer(); 43 | */ 44 | class TestServer { 45 | /** 46 | * Create a new TestServer instace 47 | * @param {Boolean} addDefaultRoutes Passing in true will create a redirect 48 | * for '/' -> '/test/browser-tests/' and add a 'Service-Worker-Allowed' header 49 | * with a value of '/'. 50 | */ 51 | constructor(addDefaultRoutes) { 52 | if ( 53 | typeof addDefaultRoutes !== 'undefined' && 54 | typeof addDefaultRoutes !== 'boolean' 55 | ) { 56 | throw new Error('addDefaultRoutes must be a boolean value'); 57 | } 58 | 59 | this._server = null; 60 | this._app = express(); 61 | this._useDefaults = false; 62 | 63 | if (typeof addDefaultRoutes === 'undefined' || addDefaultRoutes) { 64 | this._addDefaultRoutes(); 65 | } 66 | } 67 | 68 | _addDefaultRoutes() { 69 | this._useDefaults = true; 70 | 71 | // If the user tries to go to the root of the server, redirect them 72 | // to the browser test path 73 | this._app.get('/', function(req, res) { 74 | res.redirect('/test/browser-tests/'); 75 | }); 76 | } 77 | 78 | /** 79 | * If you need to extend the routes on the test server, you can access 80 | * the express app with this method. 81 | * @return {ExpressApp} The express app used to respond to requests. 82 | */ 83 | getExpressApp() { 84 | return this._app; 85 | } 86 | 87 | /** 88 | * This will start the express server with the provided port and host. 89 | * 90 | * @param {String} path Path to start the server on (i.e. './') 91 | * @param {Number} [portNumber=0] portNumber Optional parameter, by default will pick 92 | * a random available port. 93 | * @param {String} [host='localhost'] host Optional parameter, a host to bind 94 | * the express server to, by default this is localhost. 95 | * @return {Promise} Promise that resolves when the 96 | * server is started resolving with the port used. 97 | */ 98 | startServer(path, portNumber, host) { 99 | if (this._server) { 100 | this._server.close(); 101 | } 102 | 103 | // 0 will pick a random port number 104 | if (typeof portNumber === 'undefined') { 105 | portNumber = 0; 106 | } 107 | 108 | if (typeof host === 'undefined') { 109 | host = 'localhost'; 110 | } 111 | 112 | this._app.use('/', express.static(path, { 113 | setHeaders: function(res) { 114 | res.setHeader('Service-Worker-Allowed', '/'); 115 | }, 116 | })); 117 | 118 | return new Promise((resolve) => { 119 | // Start service on desired port 120 | this._server = this._app.listen(portNumber, host, () => { 121 | resolve(this._server.address().port); 122 | }); 123 | }); 124 | } 125 | 126 | /** 127 | * This method can be used to stop the express server 128 | */ 129 | killServer() { 130 | if (this._server) { 131 | this._server.close(); 132 | this._server = null; 133 | } 134 | } 135 | } 136 | 137 | module.exports = TestServer; 138 | -------------------------------------------------------------------------------- /test/local-browser.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const expect = require('chai').expect; 20 | const proxyquire = require('proxyquire'); 21 | const sinon = require('sinon'); 22 | const seleniumFF = require('selenium-webdriver/firefox'); 23 | const LocalBrowser = require('./../src/browser-models/local-browser.js'); 24 | 25 | require('chai').should(); 26 | 27 | describe('LocalBrowser', function() { 28 | const EXAMPLE_CONFIG = { 29 | _id: 'example', 30 | _options: { 31 | selenium: 'example-options', 32 | }, 33 | _prettyName: 'Example Pretty Name', 34 | }; 35 | 36 | const EXAMPLE_DENYLIST = { 37 | 999: '1.0.0', 38 | }; 39 | 40 | const sandbox = sinon.createSandbox(); 41 | let releaseNames = {}; 42 | 43 | beforeEach(function() { 44 | releaseNames = {}; 45 | 46 | // getPrettyReleaseNames needs to be stubbed since we are instantiating 47 | // LocalBrowser directly rather than extending it. 48 | sandbox.stub(LocalBrowser, 'getPrettyReleaseNames') 49 | .callsFake(() => { 50 | return releaseNames; 51 | }); 52 | }); 53 | 54 | afterEach(function() { 55 | sandbox.restore(); 56 | }); 57 | 58 | it('should instantiate with valid input', function() { 59 | new LocalBrowser( 60 | EXAMPLE_CONFIG, 61 | 'stable', 62 | EXAMPLE_DENYLIST 63 | ); 64 | }); 65 | 66 | it('should fail on null for pretty name input', function() { 67 | expect(() => { 68 | const options = JSON.parse(JSON.stringify(EXAMPLE_CONFIG)); 69 | options._prettyName = null; 70 | 71 | new LocalBrowser( 72 | options, 73 | 'stable', 74 | EXAMPLE_DENYLIST 75 | ); 76 | }).to.throw('Invalid prettyName'); 77 | }); 78 | 79 | it('should fail on empty string for pretty name input', function() { 80 | expect(() => { 81 | const options = JSON.parse(JSON.stringify(EXAMPLE_CONFIG)); 82 | options._prettyName = ''; 83 | 84 | new LocalBrowser( 85 | options, 86 | 'stable', 87 | EXAMPLE_DENYLIST 88 | ); 89 | }).to.throw('Invalid prettyName'); 90 | }); 91 | 92 | it('should fail on invalid release input', function() { 93 | expect(() => { 94 | new LocalBrowser( 95 | EXAMPLE_CONFIG, 96 | 'notarelease' 97 | ); 98 | }).to.throw('Unexpected browser release'); 99 | }); 100 | 101 | it('should fail on no selenium options', function() { 102 | expect(() => { 103 | new LocalBrowser( 104 | null, 105 | 'stable' 106 | ); 107 | }).to.throw('No browser config provided.'); 108 | }); 109 | 110 | it('should return the pretty name value', function() { 111 | const prettyName = 'PrettyName' + Date.now(); 112 | const options = JSON.parse(JSON.stringify(EXAMPLE_CONFIG)); 113 | options._prettyName = prettyName; 114 | 115 | const stableReleaseName = 'Injected Stable Browser Name'; 116 | releaseNames = { 117 | stable: stableReleaseName, 118 | }; 119 | 120 | const localBrowser = new LocalBrowser( 121 | options, 122 | 'stable' 123 | ); 124 | localBrowser.getPrettyName().should.equal(`${prettyName} ${stableReleaseName}`); 125 | }); 126 | 127 | it('should return the release value', function() { 128 | const stableBrowser = new LocalBrowser( 129 | EXAMPLE_CONFIG, 130 | 'stable' 131 | ); 132 | stableBrowser.getReleaseName().should.equal('stable'); 133 | 134 | const betaBrowser = new LocalBrowser( 135 | EXAMPLE_CONFIG, 136 | 'beta' 137 | ); 138 | betaBrowser.getReleaseName().should.equal('beta'); 139 | 140 | const unstableBrowser = new LocalBrowser( 141 | EXAMPLE_CONFIG, 142 | 'unstable' 143 | ); 144 | unstableBrowser.getReleaseName().should.equal('unstable'); 145 | }); 146 | 147 | it('should return the correct selenium browser ID', function() { 148 | const localBrowser = new LocalBrowser( 149 | EXAMPLE_CONFIG, 150 | 'stable' 151 | ); 152 | localBrowser.getId().should.equal(EXAMPLE_CONFIG._id); 153 | }); 154 | 155 | it('should return the correct selenium options', function() { 156 | const localBrowser = new LocalBrowser( 157 | EXAMPLE_CONFIG, 158 | 'stable' 159 | ); 160 | localBrowser.getSeleniumOptions().should.equal(EXAMPLE_CONFIG._options); 161 | }); 162 | 163 | it('should be able to set the selenium options', function() { 164 | const options = JSON.parse(JSON.stringify(EXAMPLE_CONFIG)); 165 | 166 | const ffOptions = new seleniumFF.Options(); 167 | 168 | const localBrowser = new LocalBrowser( 169 | options, 170 | 'stable' 171 | ); 172 | localBrowser.setSeleniumOptions(ffOptions); 173 | localBrowser.getSeleniumOptions().should.equal(ffOptions); 174 | }); 175 | 176 | it('should throw for non-overriden getExecutablePath()', function() { 177 | expect(() => { 178 | const localBrowser = new LocalBrowser( 179 | EXAMPLE_CONFIG, 180 | 'stable' 181 | ); 182 | 183 | localBrowser.getExecutablePath(); 184 | }).to.throw('overriden'); 185 | }); 186 | 187 | it('should throw for non-overriden getRawVersionString()', function() { 188 | expect(() => { 189 | const localBrowser = new LocalBrowser( 190 | EXAMPLE_CONFIG, 191 | 'stable' 192 | ); 193 | 194 | localBrowser.getRawVersionString(); 195 | }).to.throw('overriden'); 196 | }); 197 | 198 | it('should throw for non-overriden getVersionNumber()', function() { 199 | expect(() => { 200 | const localBrowser = new LocalBrowser( 201 | EXAMPLE_CONFIG, 202 | 'stable' 203 | ); 204 | 205 | localBrowser.getVersionNumber(); 206 | }).to.throw('overriden'); 207 | }); 208 | 209 | it('should throw for isValid when non-overriden method is used', function() { 210 | expect(() => { 211 | const localBrowser = new LocalBrowser( 212 | EXAMPLE_CONFIG, 213 | 'stable' 214 | ); 215 | 216 | localBrowser.isValid(); 217 | }).to.throw('overriden'); 218 | }); 219 | 220 | it('should throw when getting a builder when non-overriden method is used', function() { 221 | expect(() => { 222 | const localBrowser = new LocalBrowser( 223 | EXAMPLE_CONFIG, 224 | 'stable' 225 | ); 226 | 227 | localBrowser.getSeleniumDriverBuilder(); 228 | }).to.throw('overriden'); 229 | }); 230 | 231 | it('should reject when building a driver which isn\'t a subclass', function() { 232 | const localBrowser = new LocalBrowser( 233 | EXAMPLE_CONFIG, 234 | 'stable' 235 | ); 236 | 237 | return localBrowser.getSeleniumDriver() 238 | .then(() => { 239 | throw new Error('Unexpected promise resolve'); 240 | }, (err) => { 241 | (err.message.indexOf('overriden')).should.not.equal(-1); 242 | }); 243 | }); 244 | 245 | it('should handle missing driver module', function() { 246 | const options = JSON.parse(JSON.stringify(EXAMPLE_CONFIG)); 247 | options._driverModule = 'exampledriver'; 248 | 249 | const ProxiedLocalBrowser = proxyquire('../src/browser-models/local-browser.js', { 250 | 'exampledriver': null, 251 | }); 252 | 253 | sandbox.stub(ProxiedLocalBrowser, 'getPrettyReleaseNames') 254 | .callsFake(() => { 255 | return releaseNames; 256 | }); 257 | 258 | const localBrowser = new ProxiedLocalBrowser( 259 | options, 260 | 'stable' 261 | ); 262 | 263 | return localBrowser.getSeleniumDriver() 264 | .then(() => { 265 | throw new Error('Unexpected promise resolve'); 266 | }, (err) => { 267 | (err.message.indexOf('overriden')).should.not.equal(-1); 268 | }); 269 | }); 270 | 271 | it('should require driver module', function() { 272 | const options = JSON.parse(JSON.stringify(EXAMPLE_CONFIG)); 273 | options._driverModule = 'exampledriver'; 274 | 275 | const ProxiedLocalBrowser = proxyquire('../src/browser-models/local-browser.js', { 276 | 'exampledriver': { 277 | '@noCallThru': true, 278 | }, 279 | }); 280 | 281 | sandbox.stub(ProxiedLocalBrowser, 'getPrettyReleaseNames') 282 | .callsFake(() => { 283 | return releaseNames; 284 | }); 285 | 286 | const localBrowser = new ProxiedLocalBrowser( 287 | options, 288 | 'stable' 289 | ); 290 | 291 | return localBrowser.getSeleniumDriver() 292 | .then(() => { 293 | throw new Error('Unexpected promise resolve'); 294 | }, (err) => { 295 | (err.message.indexOf('overriden')).should.not.equal(-1); 296 | }); 297 | }); 298 | 299 | // TODO denylist test 300 | }); 301 | -------------------------------------------------------------------------------- /test/saucelabs-usage.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const TestServer = require('./helpers/test-server.js'); 3 | const seleniumAssistant = require('../src/index.js'); 4 | const selenium = require('selenium-webdriver'); 5 | 6 | if (!process.env['SAUCELABS_USERNAME'] || 7 | !process.env['SAUCELABS_ACCESS_KEY']) { 8 | console.warn('Skipping Sauce Labs tests due to no credentials in environment'); 9 | return; 10 | } 11 | 12 | const TIMEOUT = 10 * 60 * 1000; 13 | const RETRIES = 1; 14 | 15 | const SAUCELABS_USERNAME = process.env['SAUCELABS_USERNAME']; 16 | const SAUCELABS_ACCESS_KEY = process.env['SAUCELABS_ACCESS_KEY']; 17 | 18 | const RELEASES = [ 19 | 'latest', 20 | ]; 21 | 22 | describe('Test Saucelabs', function() { 23 | this.timeout(TIMEOUT); 24 | this.retries(RETRIES); 25 | 26 | let globalDriver; 27 | const globalServer = new TestServer(false); 28 | let localURL; 29 | 30 | before(function() { 31 | const serverPath = path.join(__dirname, 'data', 'example-site'); 32 | return globalServer.startServer(serverPath, 7000) 33 | .then((portNumber) => { 34 | localURL = `http://localhost:${portNumber}/`; 35 | }) 36 | .then(() => { 37 | seleniumAssistant.setSaucelabsDetails( 38 | SAUCELABS_USERNAME, 39 | SAUCELABS_ACCESS_KEY); 40 | return seleniumAssistant.startSaucelabsConnect(); 41 | }); 42 | }); 43 | 44 | after(function() { 45 | return seleniumAssistant.killWebDriver(globalDriver).catch(() => {}) 46 | .then(() => { 47 | return seleniumAssistant.stopSaucelabsConnect(); 48 | }) 49 | .then(() => { 50 | return globalServer.killServer(); 51 | }); 52 | }); 53 | 54 | function testNormalSeleniumUsage(specificBrowser) { 55 | return specificBrowser.getSeleniumDriver() 56 | .then((driver) => { 57 | globalDriver = driver; 58 | }) 59 | .then(() => { 60 | return globalDriver.get(localURL) 61 | .then(() => { 62 | return globalDriver.wait(selenium.until.titleIs('Example Site'), 5 * 60 * 1000); 63 | }); 64 | }) 65 | .then(() => seleniumAssistant.killWebDriver(globalDriver)) 66 | .then(() => { 67 | globalDriver = null; 68 | }); 69 | } 70 | 71 | it('should reject for bad saucelab details', function() { 72 | 73 | }); 74 | 75 | it('should reject when no saucelab details', function() { 76 | 77 | }); 78 | 79 | it('should reject on bad browser input', function() { 80 | 81 | }); 82 | 83 | function setupTest(browserId, browserVersion) { 84 | it(`should be able to use saucelab browser ${browserId} - ${browserVersion}`, function() { 85 | this.timeout(5 * 60 * 1000); 86 | return testNormalSeleniumUsage( 87 | seleniumAssistant.getSauceLabsBrowser(browserId, browserVersion) 88 | ); 89 | }); 90 | } 91 | 92 | const browserIds = [ 93 | 'chrome', 94 | 'microsoftedge', 95 | 'firefox', 96 | 'internet explorer', 97 | 'safari', 98 | ]; 99 | browserIds.forEach((browserId) => { 100 | RELEASES.forEach((release) => { 101 | setupTest(browserId, release); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /test/selenium-assistant.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const expect = require('chai').expect; 20 | const sinon = require('sinon'); 21 | const path = require('path'); 22 | 23 | const browserManager = require('../src/browser-manager.js'); 24 | 25 | require('chai').should(); 26 | 27 | const NUM_OF_BROWSERS = browserManager.getSupportedBrowsers().length; 28 | const sandbox = sinon.createSandbox(); 29 | 30 | describe('SeleniumAssistant', function() { 31 | const seleniumAssistant = require('../src/index.js'); 32 | 33 | afterEach(function() { 34 | sandbox.restore(); 35 | }); 36 | 37 | it('should be instantiated', function() { 38 | (typeof seleniumAssistant !== 'undefined').should.equal(true); 39 | }); 40 | 41 | it('should be able to get an array of available browsers', function() { 42 | this.timeout(NUM_OF_BROWSERS * 3000 * 1000); 43 | 44 | const browsers = seleniumAssistant.getLocalBrowsers(); 45 | (browsers instanceof Array).should.equal(true); 46 | 47 | browsers.forEach((browser) => { 48 | browser.isValid().should.equal(true); 49 | }); 50 | }); 51 | 52 | it('should return a browser for valid browser and release names', function() { 53 | const possibleBrowsers = ['chrome', 'firefox']; 54 | const releases = ['stable', 'beta', 'unstable']; 55 | possibleBrowsers.forEach((browserId) => { 56 | releases.forEach((release) => { 57 | if (browserId === 'chrome' && release === 'stable') { 58 | return; 59 | } 60 | const browser = seleniumAssistant.getLocalBrowser(browserId, release); 61 | (typeof browser).should.equal('object'); 62 | }); 63 | }); 64 | }); 65 | 66 | it('should throw for an invalid browser name in getBrowser', function() { 67 | expect(function() { 68 | seleniumAssistant.getLocalBrowser('made-up', 'stable'); 69 | }).to.throw(); 70 | }); 71 | 72 | it('should throw for an null browser name in getBrowser', function() { 73 | expect(function() { 74 | seleniumAssistant.getLocalBrowser(null, 'stable'); 75 | }).to.throw(); 76 | }); 77 | 78 | it('should throw for an invalid release name in getBrowser', function() { 79 | expect(function() { 80 | seleniumAssistant.getLocalBrowser('chrome', 'made-up'); 81 | }).to.throw(); 82 | }); 83 | 84 | it('should throw for an null release name in getBrowser', function() { 85 | expect(function() { 86 | seleniumAssistant.getLocalBrowser('chrome', null); 87 | }).to.throw(); 88 | }); 89 | 90 | it('should throw for no release name in getBrowser', function() { 91 | expect(function() { 92 | seleniumAssistant.getLocalBrowser('chrome'); 93 | }).to.throw(); 94 | }); 95 | 96 | it('should throw for no arguments in getBrowser', function() { 97 | expect(function() { 98 | seleniumAssistant.getLocalBrowser(); 99 | }).to.throw(); 100 | }); 101 | 102 | it('should be able to print available browsers', function() { 103 | this.timeout(NUM_OF_BROWSERS * 3000 * 1000); 104 | 105 | let consoleCalls = 0; 106 | sandbox.stub(console, 'log').callsFake((input) => { 107 | consoleCalls++; 108 | }); 109 | 110 | // Console Test 111 | const output = seleniumAssistant.printAvailableBrowserInfo(); 112 | 113 | sandbox.restore(); 114 | 115 | (typeof output).should.equal('string'); 116 | consoleCalls.should.equal(1); 117 | }); 118 | 119 | it('should not print table to console', function() { 120 | this.timeout(NUM_OF_BROWSERS * 3000 * 1000); 121 | 122 | let consoleCalls = 0; 123 | sandbox.stub(console, 'log').callsFake(() => { 124 | consoleCalls++; 125 | }); 126 | 127 | // Console Test 128 | const output = seleniumAssistant.printAvailableBrowserInfo(false); 129 | 130 | sandbox.restore(); 131 | 132 | (typeof output).should.equal('string'); 133 | consoleCalls.should.equal(0); 134 | }); 135 | 136 | it('should handle undefined in killWebDriver()', function() { 137 | const killPromise = seleniumAssistant.killWebDriver(); 138 | (killPromise instanceof Promise).should.equal(true); 139 | return killPromise; 140 | }); 141 | 142 | it('should handle null in killWebDriver()', function() { 143 | const killPromise = seleniumAssistant.killWebDriver(null); 144 | (killPromise instanceof Promise).should.equal(true); 145 | return killPromise; 146 | }); 147 | 148 | it('should throw an error on an object without a quit function', function() { 149 | const killPromise = seleniumAssistant.killWebDriver({}); 150 | (killPromise instanceof Promise).should.equal(true); 151 | return killPromise.then(() => { 152 | throw new Error('Expected to throw error'); 153 | }, () => { 154 | // NOOP, this should throw 155 | }); 156 | }); 157 | 158 | it('should resolve when a driver with quit method is injected', function() { 159 | // Driver quit waits 2 seconds for driver to completely finish 160 | this.timeout(3000); 161 | 162 | const killPromise = seleniumAssistant.killWebDriver({ 163 | close: () => { 164 | return Promise.resolve(); 165 | }, 166 | quit: () => { 167 | return Promise.resolve(); 168 | }, 169 | }); 170 | (killPromise instanceof Promise).should.equal(true); 171 | return killPromise; 172 | }); 173 | 174 | it('should resolve when a driver with quit method that rejects is injected', function() { 175 | // Driver quit waits 2 seconds for driver to completely finish 176 | this.timeout(3000); 177 | 178 | const killPromise = seleniumAssistant.killWebDriver({ 179 | close: () => { 180 | return Promise.resolve(); 181 | }, 182 | quit: () => { 183 | return Promise.reject(new Error()); 184 | }, 185 | }); 186 | (killPromise instanceof Promise).should.equal(true); 187 | return killPromise; 188 | }); 189 | 190 | it('should resolve when a driver with quit method that never resolves is injected', function() { 191 | // Driver quit waits up to 4 seconds + additional setup time for driver to completely finish 192 | this.timeout(5000); 193 | 194 | const killPromise = seleniumAssistant.killWebDriver({ 195 | close: () => { 196 | return Promise.resolve(); 197 | }, 198 | quit: () => { 199 | return new Promise((resolve, reject) => {}); 200 | }, 201 | }); 202 | (killPromise instanceof Promise).should.equal(true); 203 | return killPromise; 204 | }); 205 | 206 | it('should return a default value for install dir', function() { 207 | const directory = seleniumAssistant.getBrowserInstallDir(); 208 | (typeof directory).should.equal('string'); 209 | directory.length.should.be.gt(1); 210 | }); 211 | 212 | it('should return the specified path for install dir', function() { 213 | const installPath = './test-browsers/'; 214 | seleniumAssistant.setBrowserInstallDir(installPath); 215 | const directory = seleniumAssistant.getBrowserInstallDir(); 216 | (typeof directory).should.equal('string'); 217 | directory.should.equal(path.resolve(installPath)); 218 | }); 219 | }); 220 | --------------------------------------------------------------------------------