├── .circleci └── config.yml ├── .clang-format ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin └── webdriver-manager ├── config.json ├── docs ├── README.md ├── mobile.md ├── protractor.md └── versions.md ├── e2e_spec ├── android_spec.ts ├── server_spec.ts └── support │ ├── full.json │ └── headless.json ├── gulpfile.js ├── lib ├── binaries │ ├── android_sdk.ts │ ├── appium.ts │ ├── binary.ts │ ├── chrome_driver.ts │ ├── chrome_xml.ts │ ├── config_source.ts │ ├── gecko_driver.ts │ ├── gecko_driver_github.ts │ ├── iedriver.ts │ ├── iedriver_xml.ts │ ├── index.ts │ ├── standalone.ts │ └── standalone_xml.ts ├── cli │ ├── cli.ts │ ├── index.ts │ ├── logger.ts │ ├── options.ts │ └── programs.ts ├── cli_instance.ts ├── cmds │ ├── clean.ts │ ├── index.ts │ ├── initialize.ts │ ├── opts.ts │ ├── shutdown.ts │ ├── start.ts │ ├── status.ts │ ├── update.ts │ └── version.ts ├── config.ts ├── files │ ├── downloaded_binary.ts │ ├── downloader.ts │ ├── file_manager.ts │ └── index.ts ├── http_utils.ts ├── utils.ts └── webdriver.ts ├── package-lock.json ├── package.json ├── release.md ├── spec ├── binaries │ ├── chrome_driver_spec.ts │ ├── chrome_xml_spec.ts │ ├── config_source_spec.ts │ ├── gecko_driver_github_spec.ts │ ├── gecko_driver_spec.ts │ ├── iedriver_spec.ts │ ├── iedriver_xml_spec.ts │ ├── standalone_spec.ts │ └── standalone_xml_spec.ts ├── cli │ ├── options_spec.ts │ └── programs_spec.ts ├── cmds │ ├── status_spec.ts │ └── update_spec.ts ├── files │ ├── downloader_spec.ts │ └── file_manager_spec.ts ├── http_utils_spec.ts ├── support │ └── jasmine.json └── webdriver_spec.ts └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | var_1: &docker_image circleci/node:8-browsers 2 | 3 | anchor_1: &job_defaults 4 | working_directory: ~/workspace 5 | docker: 6 | - image: *docker_image 7 | 8 | version: 2 9 | jobs: 10 | build_and_test: 11 | <<: *job_defaults 12 | steps: 13 | - checkout 14 | - run: node --version 15 | - run: npm --version 16 | - run: npm install 17 | - run: npm run format-enforce 18 | - run: npm run test-unit 19 | 20 | workflows: 21 | version: 2 22 | default_workflow: 23 | jobs: 24 | - build_and_test -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | Language: JavaScript 2 | BasedOnStyle: Google 3 | ColumnLimit: 100 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | built/ 2 | node_modules/ 3 | selenium/ 4 | selenium_test/ 5 | typings/ 6 | .idea/ 7 | npm-debug.log 8 | .vscode 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | selenium/ 3 | spec/ 4 | built/spec/ 5 | e2e_spec/ 6 | built/e2e_spec/ 7 | 8 | .clang-format 9 | .gitignore 10 | .gitattributes 11 | .github/ 12 | .idea/ 13 | .npmignore 14 | circle.yml 15 | npm-debug.log 16 | release.md 17 | tsconfig.json 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Questions 5 | --------- 6 | 7 | Please ask support questions on [StackOverflow](http://stackoverflow.com/questions/tagged/webdriver-manager) or [Gitter](https://gitter.im/angular/webdriver-manager). 8 | 9 | Any questions posted to webdriver-manager's Github Issues will be closed with this note: 10 | 11 | Please direct general support questions like this one to an appropriate support channel, see https://github.com/angular/webdriver-manager/blob/master/CONTRIBUTING.md#questions. Thank you! 12 | 13 | Issues 14 | ====== 15 | 16 | If you have a bug or feature request, please file an issue. When submitting an issue, please include a reproducible case that we can actually run. 17 | 18 | Please format code and markup in your issue using [github markdown](https://help.github.com/articles/github-flavored-markdown). 19 | 20 | 21 | Contributing to Source Code (Pull Requests) 22 | =========================================== 23 | 24 | Loosely, follow the [Angular contribution rules](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md). 25 | 26 | * If your PR changes any behavior or fixes an issue, it should have an associated test. 27 | * New features should be general and as simple as possible. 28 | * Breaking changes should be avoided if possible. 29 | * All pull requests require review. No PR will be submitted without a comment from a team member stating LGTM (Looks good to me). 30 | 31 | Webdriver-manager specific rules 32 | ------------------------- 33 | 34 | * JavaScript style should generally follow the [Google JS style guide](http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml). 35 | * Wrap code at 100 chars. 36 | * Document public methods with jsdoc. 37 | * Be consistent with the code around you! 38 | 39 | Commit Messages 40 | --------------- 41 | 42 | Please write meaningful commit messages - they are used to generate the changelog, so the commit message should tell a user everything they need to know about a commit. Webdriver-manager follows AngularJS's [commit message format](https://docs.google.com/a/google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#heading=h.z8a3t6ehl060). 43 | 44 | In summary, this style is 45 | 46 | (): 47 | 48 | 49 | 50 | Where `` is one of [feat, fix, docs, refactor, test, chore, deps] and 51 | `` is a quick descriptor of the location of the change, such as cli, clientSideScripts, element. 52 | 53 | Testing your changes 54 | -------------------- 55 | 56 | Test your changes on your machine by running `npm test` to run the test suite. 57 | 58 | When you submit a PR, tests will also be run on the Continuous Integration environment 59 | through Travis. If your tests fail on Travis, take a look at the logs - if the failures 60 | are known flakes in Internet Explorer or Safari you can ignore them, but otherwise 61 | Travis should pass. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016-2017 Google, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Webdriver Manager [![CircleCI Status](https://circleci.com/gh/angular/webdriver-manager.svg?style=shield)](https://circleci.com/gh/angular/webdriver-manager) [![Join the chat at https://gitter.im/angular/webdriver-manager](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/angular/webdriver-manager?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 3 | ================= 4 | 5 | A selenium server and browser driver manager for your end to end tests. This is the same tool as `webdriver-manager` from the [Protractor](https://github.com/angular/protractor) repository. 6 | 7 | **Note:** Version 9 and lower please reference [pose/webdriver-manager](https://github.com/pose/webdriver-manager). If there are features that existed in version 9 and lower, please open up an issue with the missing feature or a create a pull request. 8 | 9 | Getting Started 10 | --------------- 11 | 12 | ``` 13 | npm install -g webdriver-manager 14 | ``` 15 | 16 | Setting up a Selenium Server 17 | ---------------------------- 18 | 19 | Prior to starting the selenium server, download the selenium server jar and driver binaries. By default it will download the selenium server jar and chromedriver binary. 20 | 21 | ``` 22 | webdriver-manager update 23 | ``` 24 | 25 | Starting the Selenium Server 26 | ---------------------------- 27 | 28 | By default, the selenium server will run on `http://localhost:4444/wd/hub`. 29 | 30 | 31 | ``` 32 | webdriver-manager start 33 | ``` 34 | 35 | Other useful commands 36 | --------------------- 37 | 38 | View different versions of server and driver files: 39 | 40 | ``` 41 | webdriver-manager status 42 | ``` 43 | 44 | Clear out the server and driver files. If `webdriver-manager start` does not work, try to clear out the saved files. 45 | 46 | ``` 47 | webdriver-manager clean 48 | ``` 49 | 50 | Running / stopping server in background process (stopping is not yet supported on standalone server 3.x.x): 51 | 52 | ``` 53 | webdriver-manager start --detach 54 | webdriver-manager shutdown 55 | ``` 56 | 57 | Help commands 58 | ------------- 59 | 60 | Wedriver-manager has a main help option: `webdriver-manager help`. There are also other built in help menus for each of the commands. So for example, if you would like to look up all the flag options you can set in `update`, you could run `webdriver-manager update help`. 61 | 62 | Here are a list of all the commands with help: 63 | 64 | ``` 65 | webdriver-manager update help 66 | webdriver-manager start help 67 | webdriver-manager clean help 68 | webdriver-manager status help 69 | ``` 70 | 71 | Other topics: 72 | -------------- 73 | 74 | - [mobile browser support](docs/mobile.md) 75 | - [protractor support](docs/protractor.md) 76 | - [set specific versions](docs/versions.md) -------------------------------------------------------------------------------- /bin/webdriver-manager: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var cwd = process.cwd(); 6 | 7 | var nodeModuleName = 'webdriver-manager'; 8 | var localInstall = path.resolve(cwd, 'node_modules', nodeModuleName); 9 | var parentPath = path.resolve(cwd, '..'); 10 | var dir = __dirname; 11 | var folder = cwd.replace(parentPath, '').substring(1); 12 | 13 | var isProjectVersion = folder === nodeModuleName; 14 | var isLocalVersion = false; 15 | 16 | var printCyan, printMagenta, printGreen; 17 | 18 | printCyan = printMagenta = printGreen = function(message) { 19 | return message; 20 | }; 21 | 22 | try { 23 | var chalk = require('chalk'); 24 | printMagenta = chalk.magenta; 25 | printCyan = chalk.cyan; 26 | printGreen = chalk.green; 27 | } 28 | catch (e) { 29 | console.log("Error loading chalk, output will be boring."); 30 | } 31 | 32 | try { 33 | isLocalVersion = fs.statSync(localInstall).isDirectory(); 34 | } catch(e) { 35 | } 36 | 37 | // project version 38 | if (folder === nodeModuleName) { 39 | console.log(nodeModuleName + ': using ' + printMagenta('project version ' + 40 | require(path.resolve('package.json')).version)); 41 | require(path.resolve('built/lib/webdriver')); 42 | } 43 | 44 | // local version 45 | else if (isLocalVersion) { 46 | console.log(nodeModuleName + ': using ' + printCyan('local installed version ' + 47 | require(path.resolve(localInstall, 'package.json')).version)); 48 | require(path.resolve(localInstall, 'built/lib/webdriver')); 49 | } 50 | 51 | // global version 52 | else { 53 | console.log(nodeModuleName + ': using ' + printGreen('global installed version ' + 54 | require(path.resolve(dir, '../package.json')).version)); 55 | require(path.resolve(dir, '../built/lib/webdriver')); 56 | } 57 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "webdriverVersions": { 3 | "selenium": "2.53.1", 4 | "chromedriver": "2.27", 5 | "maxChromedriver": "77", 6 | "geckodriver": "v0.13.0", 7 | "iedriver": "2.53.1", 8 | "androidsdk": "24.4.1", 9 | "appium": "1.6.5" 10 | }, 11 | "cdnUrls": { 12 | "selenium": "https://selenium-release.storage.googleapis.com/", 13 | "chromedriver": "https://chromedriver.storage.googleapis.com/", 14 | "geckodriver": "https://github.com/mozilla/geckodriver/releases/download/", 15 | "iedriver": "https://selenium-release.storage.googleapis.com/", 16 | "androidsdk": "http://dl.google.com/android/" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Contents 2 | 3 | - [main page](../README.md) 4 | - [mobile browser support](mobile.md) 5 | - [protractor support](protractor.md) 6 | - [set specific versions](versions.md) 7 | -------------------------------------------------------------------------------- /docs/mobile.md: -------------------------------------------------------------------------------- 1 | Mobile Browser Support 2 | ====================== 3 | 4 | Support for mobile browsers is provided via [appium](https://github.com/appium/appium). If you 5 | have used `webdriver-manager update --android` or `webdriver-manager update --ios`, when you run 6 | `webdriver-manager start`, Appium will automatically start on the port specified by `--appium-port`. 7 | 8 | 9 | Android SDK 10 | ----------- 11 | 12 | `webdriver-manager` will not install the android SDK by default. If you want to test on android, 13 | run `webdriver-manager update --android`. This will download the android SDK, Appium, and set up 14 | some virtual android devices for you to run tests against. By default, this will create only an 15 | android device running version 24 on x86-64. If you need a different device, you must use the 16 | `--android-api-levels` and `--android-abis` flags. So you might run a command like this: 17 | 18 | ``` 19 | webdriver-manager update --android --android-api-levels 24 --android-archs armeabi-v7a 20 | ``` 21 | 22 | Valid values for the `--android-api-levels` flag are: `24` and `25`. You *can* specify a lower 23 | API level, but the virtual device create will not have Chrome installed. 24 | 25 | Valid values for the `--android-archs` flag are: 26 | 27 | * `x86` 28 | * `x86_64` 29 | * `armeabi-v7a` (or possibly some other 32-bit ARM architecture) 30 | * `arm64-v8a` (or possibly some other 64-bit ARM architecture) 31 | * `mips` 32 | 33 | Note that we always use the `google_apis/*` ABIs, since only those versions comes with chrome. So 34 | if you specify `--android-archs x86_64`, this tool will use the ABI `google_apis/x86_64`. If you 35 | wish to use a different platform (i.e. `android-wear`, `android-tv` or `default`), you can do so 36 | with the `--android-platforms` flag. But only the `google_apis` version comes with Chrome. 37 | 38 | 39 | As a practical matter, if you don't want to manually accept the license agreements, you can use 40 | `--android-accept-licenses`, which will accept them on your behalf. 41 | 42 | Once you have installed the Android SDK with the virtual devices you need, use 43 | `webdriver-manager start --android` to boot up Appium and begin emulating your android device(s). 44 | By default `webdriver-manager` will emulate all available android devices. If you would rather 45 | emulate a specific device, use `--avds`. So you might use: 46 | 47 | ``` 48 | webdriver-manager start --android --avds android-23-default-x86_64 49 | ``` 50 | 51 | If you would prefer not to emulate any android virtual devices, use `--avds none`. 52 | 53 | If you need to specify the ports used by the android virtual devices, use `--avd_port`. The port 54 | you specify will be used for the console of the first device, and the port one higher will be used 55 | for its ADB. The second device will use the next two ports, and so on. 56 | 57 | 58 | ### "Welcome to Chome" UI 59 | 60 | Every time appium boots up chrome, a "Welcome to Chrome" dialog will pop up. It won't interfere 61 | with your tests, since the webpage still renders underneath the dialog, and those tests access the 62 | webpage directly. However, you won't be able to watch what's going on unless you manually click 63 | "Accept & Continue". Also, since webdriver boots up a fresh chrome instance every time, chrome 64 | won't remember that you've clicked "Accept & Continue" between tests. Any time you want to actually 65 | watch the test as it's run, you'll need to click through the dialog again. For now, there is no 66 | good way around this sadly (https://github.com/appium/appium/issues/6618). 67 | 68 | iOS 69 | --------- 70 | 71 | When you run `webdriver-manager update --ios`, `webdriver-manager` will install Appium and check 72 | your computer for iOS simulation capabilities. `webdriver-manager` cannot download the xcode 73 | commandline tools for you however, nor can it agree to Apple's user agreement. The xcode 74 | commandline tools come with several virtual devices pre-setup. If you need more, run 75 | `xcrun simctl` for help doing that. 76 | 77 | Once you have installed Appium, `webdriver-manager` will launch it automatically when you run 78 | `webdriver-manager start`. Appium will automatically handle starting iOS device emulation as 79 | needed. 80 | -------------------------------------------------------------------------------- /docs/protractor.md: -------------------------------------------------------------------------------- 1 | # Protractor support topics 2 | 3 | ## Supported browsers 4 | 5 | ### Tests with Protractor test suite (v 4.0.13) 6 | 7 | The following are the supported browsers / drivers based on running the 8 | Protractor test suite. Since the test suite checks only firefox and chrome, 9 | these are the only browsers reported. 10 | 11 | ### Current supported browsers / drivers 12 | 13 | | selenium standalone | firefox | chromedriver | chrome | 14 | | ------------------- | ------- | ------------ | ------ | 15 | | 2.53.1 | 47.0.1 | 2.26 | 54 | 16 | 17 | 18 | ### Investigated 19 | 20 | | selenium standalone | firefox 47.0.1 | firefox 49.0.1 | 21 | | ------------------- | -------------- | -------------- | 22 | | 2.53.1 | pass | fail | 23 | | 3.0.0 | fail | fail | 24 | 25 | 26 | ## Driver providers 27 | 28 | There are a couple cases where Protractor launches downloaded binaries 29 | (from webdriver-manager) without specifying which version to use: 30 | 31 | - local driver providers - when no `seleniumAddress`, `directConnect`, saucelabs 32 | or browser stack configuration is set 33 | - direct connect - when setting `directConnect` to true 34 | 35 | Protractor knows which downloaded binaries to use based on the 36 | `update-config.json`. The `update-config.json` is written during the `update` 37 | command and provides the path to the current downloaded version and a list of 38 | all the binaries previously downloaded. 39 | 40 | During the launch of either local driver provider or direct connect, Protractor 41 | will launch the last downloaded binary. 42 | -------------------------------------------------------------------------------- /docs/versions.md: -------------------------------------------------------------------------------- 1 | # update command: downloading specific binaries 2 | 3 | The help command for update. The flags shown here are OS specific. If you are not on a Windows machine, the `--ie` flags are not shown. 4 | 5 | ``` 6 | webdriver-manager update help 7 | ``` 8 | 9 | Overview when you call `update`: 10 | 11 | * The output directory is created if it does not exist (`out_dir`) 12 | * If this is the first time downloading, it will make web requests to selenium-release.storage.googleapis.com, chromedriver.storage.googleapis.com, and api.github.com. The responses will be stored in the `out_dir` as a cache. 13 | * If this is not the first time downloading, it will look at the cached files in `out_dir` and if the file is older than an hour, it will make a request for new files. Note: api.github.com is rate limited by the ip address making the request. This is an issue for common CI systems like Travis or Circle and have an issue opened for this. 14 | * The response will then be parsed for the url to get the latest version if no version is provided via command line. The latest version will be based on what is available for your OS type and architecture. 15 | * Next we download the file from the url. If the file already exists (based on content length), we cancel the download. 16 | * We uncompress zip and tar files and for linux and mac, we set the permissions. Currently, we always uncompress the files even if the uncompressed files already exist. 17 | * Finally we write the file locations to the `update-config.json` which is also located in the `out_dir`. 18 | 19 | ## Download defaults --chrome, --gecko, --standalone flags 20 | 21 | Let's say you want to download just chromedriver... 22 | 23 | ``` 24 | webdriver-manager update --gecko false --standalone false 25 | ``` 26 | 27 | Why do we set `--gecko` and `--standalone` to false? By default chromedriver, geckodriver and selenium standalone server download with no flags set. Setting `--gecko` and `--standalone` to false will prevent them from downloading and will just download chromedriver. 28 | 29 | ## Download using --ie 30 | 31 | When setting the `--ie` flag, this will download IEDriver 32 bit. Why 32-bit? We prefer using the 32-bit version because the 64-bit version has been previously reported to be very slow and flaky. 32 | 33 | ## Download a specific version 34 | 35 | Let's say there is an issue with the latest version of chromedriver and we need version 2.20: 36 | 37 | ``` 38 | webdriver-manager update --versions.chrome 2.20 39 | ``` 40 | 41 | So the only thing different here is that instead of parsing for the latest version, we are parsing for version 2.20. We could then verify that it was the last item downloaded by calling `webdriver-manager status`. 42 | 43 | What about selenium standalone 2.53.1? This is similar but instead of the flag would be `--versions.standalone 2.53.1`. What if the selenium standalone server has a beta in the name? The idea is similar: 44 | 45 | ``` 46 | webdriver-manager update --versions.standalone 3.0.0-beta1 47 | ``` 48 | 49 | # start command: setting specific versions 50 | 51 | The help command for start. 52 | 53 | ``` 54 | webdriver-manager start help 55 | ``` 56 | 57 | When starting selenium standalone server with `webdriver-manager start`, it will use the latest version that is found in the cache unless you specify the version. Why? The idea here is that if you run `webdriver-manager update` without flags, you are using the latest binary versions, so the start command would be similar `webdriver-manager start`. 58 | 59 | ## Starting a specific version 60 | 61 | So let's say you downloaded chromedriver 2.20 and let's also say that the latest version is 2.29. If you have never downloaded 2.29, then chromedriver will not be associated with your selenium standalone server. In order for you to get 2.20 working: 62 | 63 | ``` 64 | webdriver-manager start --versions.chrome 2.20 65 | ``` 66 | 67 | What about chrome version 2.20, selenium standalone server 3.0.0-beta1, and not start with gecko driver? 68 | 69 | ``` 70 | webdriver-manager start --versions.chrome 2.20 --versions.standalone 3.0.0-beta1 --gecko false 71 | ``` 72 | 73 | ## Starting with --ie 74 | 75 | Since IEDriver is not set by default, you'll still need to pass this field if you would like to start the selenium standalone server with the IEDriver. 76 | 77 | ``` 78 | webdriver-manager start --ie 79 | ``` 80 | 81 | ## Starting with --edge 82 | 83 | We make the assumption that you already have the EdgeDriver downloaded and installed. To get this working pass the full path to the `--edge` flag 84 | 85 | ``` 86 | webdriver-manager start --edge "C:\Program Files (x86)\Microsoft Web Driver\MicrosoftWebDriver.exe" 87 | ``` 88 | -------------------------------------------------------------------------------- /e2e_spec/android_spec.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | import * as webdriver from 'selenium-webdriver'; 3 | import {AndroidSDK} from '../lib/binaries' 4 | 5 | let versions: {androidsdk: string, appium: string} = require('../config.json').webdriverVersions; 6 | 7 | describe('browser smoke tests', () => { 8 | it('should be able to boot up android chrome', (done) => { 9 | let driver = 10 | new webdriver.Builder() 11 | .usingServer('http://localhost:4723/wd/hub') 12 | .withCapabilities({ 13 | browserName: 'chrome', 14 | platformName: 'Android', 15 | platformVersion: 16 | AndroidSDK.VERSIONS[parseInt(AndroidSDK.DEFAULT_API_LEVELS.split(',')[0])], 17 | deviceName: 'Android Emulator' 18 | }) 19 | .build(); 20 | driver.get('http://10.0.2.2:4723/wd/hub/status') 21 | .then(() => { 22 | return driver.getPageSource(); 23 | }) 24 | .then((source: string) => { 25 | expect(source).toContain('"status":0'); 26 | return driver.quit(); 27 | }) 28 | .then(() => { 29 | done(); 30 | }); 31 | }, 10 * 60 * 1000); 32 | }); 33 | -------------------------------------------------------------------------------- /e2e_spec/server_spec.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | 3 | 4 | describe('sever smoke tests', () => { 5 | it('should be able to ping selenium server', (done) => { 6 | http.get('http://localhost:4444/wd/hub/status', (resp) => { 7 | expect(resp.statusCode).toBe(200); 8 | let logs = ''; 9 | resp.on('data', (chunk) => logs += chunk); 10 | resp.on('end', () => { 11 | expect(logs).toContain('"ready": true'); 12 | done() 13 | }); 14 | }); 15 | }); 16 | 17 | it('should be able to ping appium server', (done) => { 18 | http.get('http://localhost:4723/wd/hub/status', (resp) => { 19 | expect(resp.statusCode).toBe(200); 20 | let data = ''; 21 | resp.on('data', (chunk) => data += chunk); 22 | resp.on('end', () => { 23 | expect(JSON.parse(data).status).toBe(0); 24 | done() 25 | }); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /e2e_spec/support/full.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "built/e2e_spec", 3 | "spec_files": [ 4 | "**/*_spec.js" 5 | ], 6 | "stopSpecOnExpectationFailure": false, 7 | "random": false 8 | } 9 | -------------------------------------------------------------------------------- /e2e_spec/support/headless.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "built/e2e_spec", 3 | "spec_files": [ 4 | "**/server_spec.js" 5 | ], 6 | "stopSpecOnExpectationFailure": false, 7 | "random": false 8 | } 9 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const gulp = require('gulp'); 4 | const tsGlobs = ['lib/**/*.ts', '*spec/**/*.ts']; 5 | 6 | let runSpawn = (task, args, done) => { 7 | done = done || function() {}; 8 | const spawn = require('child_process').spawn; 9 | let child = spawn(task, args, {stdio: 'inherit'}); 10 | let running = false; 11 | child.on('close', (code) => { 12 | if (!running) { 13 | running = true; 14 | done(code); 15 | } 16 | }); 17 | child.on('error', (err) => { 18 | if (!running) { 19 | console.error('gulp encountered a child error'); 20 | running = true; 21 | done(err || 1); 22 | } 23 | }); 24 | return child; 25 | }; 26 | 27 | gulp.task('copy', function() { 28 | return gulp.src(['config.json', 'package.json']) 29 | .pipe(gulp.dest('built/')); 30 | }); 31 | 32 | gulp.task('format:enforce', () => { 33 | const format = require('gulp-clang-format'); 34 | const clangFormat = require('clang-format'); 35 | return gulp.src(tsGlobs).pipe( 36 | format.checkFormat('file', clangFormat, {verbose: true, fail: true})); 37 | }); 38 | 39 | gulp.task('format', () => { 40 | const format = require('gulp-clang-format'); 41 | const clangFormat = require('clang-format'); 42 | return gulp.src(tsGlobs, { base: '.' }).pipe( 43 | format.format('file', clangFormat)).pipe(gulp.dest('.')); 44 | }); -------------------------------------------------------------------------------- /lib/binaries/android_sdk.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as rimraf from 'rimraf'; 3 | 4 | import {Config} from '../config'; 5 | import {spawnSync} from '../utils'; 6 | 7 | import {Binary, BinaryUrl, OS} from './binary'; 8 | 9 | function getAndroidArch(): string { 10 | switch (Config.osArch()) { 11 | case 'arm': 12 | return 'armeabi-v7a'; 13 | case 'arm64': 14 | return 'arm64-v8a'; 15 | case 'x86': 16 | case 'x32': 17 | case 'ia32': 18 | case 'ppc': 19 | return 'x86'; 20 | case 'x86-64': 21 | case 'x64': 22 | case 'ia64': 23 | case 'ppc64': 24 | return 'x86_64'; 25 | default: 26 | return Config.osArch(); 27 | } 28 | } 29 | 30 | /** 31 | * The android sdk binary. 32 | */ 33 | export class AndroidSDK extends Binary { 34 | static os = [OS.Windows_NT, OS.Linux, OS.Darwin]; 35 | static id = 'android'; 36 | static versionDefault = Config.binaryVersions().android; 37 | static isDefault = false; 38 | static DEFAULT_API_LEVELS = '24'; 39 | static DEFAULT_ARCHITECTURES = getAndroidArch(); 40 | static DEFAULT_PLATFORMS = 'google_apis'; 41 | static VERSIONS: {[api_level: number]: string} = { 42 | // Before 24 is not supported 43 | 24: '7.0', 44 | 25: '7.1' 45 | } 46 | 47 | constructor(alternateCDN?: string) { 48 | super(alternateCDN || Config.cdnUrls().android); 49 | 50 | this.name = 'android-sdk'; 51 | this.versionCustom = AndroidSDK.versionDefault; 52 | } 53 | 54 | id(): string { 55 | return AndroidSDK.id; 56 | } 57 | 58 | prefix(): string { 59 | return 'android-sdk_r'; 60 | } 61 | 62 | suffix(): string { 63 | if (this.ostype === 'Darwin') { 64 | return '-macosx.zip'; 65 | } else if (this.ostype === 'Linux') { 66 | return '-linux.tgz'; 67 | } else if (this.ostype === 'Windows_NT') { 68 | return '-windows.zip'; 69 | } 70 | } 71 | 72 | getUrl(): Promise { 73 | return Promise.resolve({url: this.cdn + this.filename(), version: this.versionCustom}); 74 | } 75 | 76 | getVersionList(): Promise { 77 | return null; 78 | } 79 | 80 | url(ostype: string): string { 81 | return this.cdn + this.filename(); 82 | } 83 | 84 | zipContentName(): string { 85 | if (this.ostype === 'Darwin') { 86 | return this.name + '-macosx'; 87 | } else if (this.ostype === 'Linux') { 88 | return this.name + '-linux'; 89 | } else if (this.ostype === 'Windows_NT') { 90 | return this.name + '-windows'; 91 | } 92 | } 93 | 94 | executableSuffix(): string { 95 | return ''; 96 | } 97 | 98 | remove(sdkPath: string): void { 99 | try { 100 | let avds = require(path.resolve(sdkPath, 'available_avds.json')); 101 | let version = path.basename(sdkPath).slice(this.prefix().length); 102 | avds.forEach((avd: string) => { 103 | spawnSync( 104 | path.resolve(sdkPath, 'tools', 'android'), 105 | ['delete', 'avd', '-n', avd + '-v' + version + '-wd-manager']); 106 | }); 107 | } catch (e) { 108 | } 109 | rimraf.sync(sdkPath); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/binaries/appium.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as rimraf from 'rimraf'; 3 | 4 | import {Config} from '../config'; 5 | 6 | import {Binary, BinaryUrl, OS} from './binary'; 7 | 8 | 9 | /** 10 | * The appium binary. 11 | */ 12 | export class Appium extends Binary { 13 | static os = [OS.Windows_NT, OS.Linux, OS.Darwin]; 14 | static id = 'appium'; 15 | static versionDefault = Config.binaryVersions().appium; 16 | static isDefault = false; 17 | 18 | constructor(alternateCDN?: string) { 19 | super(alternateCDN || Config.cdnUrls().appium); 20 | this.name = 'appium'; 21 | this.versionCustom = Appium.versionDefault; 22 | } 23 | 24 | id(): string { 25 | return Appium.id; 26 | } 27 | 28 | prefix(): string { 29 | return 'appium-'; 30 | } 31 | 32 | suffix(): string { 33 | return ''; 34 | } 35 | 36 | executableSuffix(): string { 37 | return ''; 38 | } 39 | 40 | getUrl(version?: string): Promise { 41 | return Promise.resolve({url: '', version: this.versionCustom}); 42 | } 43 | 44 | getVersionList(): Promise { 45 | return null; 46 | } 47 | 48 | remove(sdkPath: string): void { 49 | rimraf.sync(sdkPath); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/binaries/binary.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import {Config} from '../config'; 3 | import {ConfigSource} from './config_source'; 4 | 5 | /** 6 | * operating system enum 7 | */ 8 | export enum OS { 9 | Windows_NT, 10 | Linux, 11 | Darwin 12 | } 13 | 14 | export interface BinaryUrl { 15 | url: string; 16 | version: string; 17 | } 18 | 19 | /** 20 | * Dictionary to map the binary's id to the binary object 21 | */ 22 | export interface BinaryMap { [id: string]: T; } 23 | 24 | export abstract class Binary { 25 | static os: OS[]; 26 | 27 | configSource: ConfigSource; 28 | 29 | ostype: string = Config.osType(); 30 | osarch: string = Config.osArch(); 31 | 32 | // TODO(cnishina): downloading the latest requires parsing XML or JSON that is assumed to be 33 | // published and formatted in a specific way. If this is not available, let the user download 34 | // the file from an alternative download URL. 35 | alternativeDownloadUrl: string; 36 | 37 | cdn: string; // The url host for XML reading or the base path to the url. 38 | 39 | name: string; 40 | versionDefault: string; 41 | versionCustom: string; 42 | 43 | constructor(opt_alternativeCdn?: string) { 44 | this.cdn = opt_alternativeCdn; 45 | } 46 | 47 | abstract prefix(): string; 48 | abstract suffix(): string; 49 | 50 | executableSuffix(): string { 51 | if (this.ostype == 'Windows_NT') { 52 | return '.exe'; 53 | } else { 54 | return ''; 55 | } 56 | } 57 | 58 | version(): string { 59 | return this.versionCustom; 60 | } 61 | 62 | filename(): string { 63 | return this.prefix() + this.version() + this.suffix(); 64 | } 65 | 66 | /** 67 | * @param ostype The operating system. 68 | * @returns The file name for the executable. 69 | */ 70 | executableFilename(): string { 71 | return this.prefix() + this.version() + this.executableSuffix(); 72 | } 73 | 74 | /** 75 | * Gets the id of the binary. 76 | */ 77 | abstract id(): string; 78 | 79 | /** 80 | * Gets the url to download the file set by the version. This will use the XML if available. 81 | * If not, it will download from an existing url. 82 | * 83 | * @param {string} version The version we are looking for. This could also be 'latest'. 84 | */ 85 | getUrl(version?: string): Promise { 86 | if (this.alternativeDownloadUrl != null) { 87 | return Promise.resolve({url: '', version: ''}); 88 | } else { 89 | return this.getVersionList().then(() => { 90 | version = version || Config.binaryVersions()[this.id()]; 91 | return this.configSource.getUrl(version).then(binaryUrl => { 92 | this.versionCustom = binaryUrl.version; 93 | return {url: binaryUrl.url, version: binaryUrl.version}; 94 | }); 95 | }); 96 | } 97 | } 98 | 99 | /** 100 | * Gets the list of available versions available based on the xml. If no XML exists, return an 101 | * empty list. 102 | */ 103 | abstract getVersionList(): Promise; 104 | 105 | /** 106 | * Delete an instance of this binary from the file system 107 | */ 108 | remove(filename: string): void { 109 | fs.unlinkSync(filename); 110 | } 111 | 112 | /** 113 | * @param ostype The operating system. 114 | * @returns The file name for the file inside the downloaded zip file 115 | */ 116 | zipContentName(): string { 117 | return this.name + this.executableSuffix(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/binaries/chrome_driver.ts: -------------------------------------------------------------------------------- 1 | import {Config} from '../config'; 2 | 3 | import {Binary, BinaryUrl, OS} from './binary'; 4 | import {ChromeXml} from './chrome_xml'; 5 | 6 | export class ChromeDriver extends Binary { 7 | static id = 'chrome'; 8 | static isDefault = true; 9 | static os = [OS.Windows_NT, OS.Linux, OS.Darwin]; 10 | static versionDefault = Config.binaryVersions().chrome; 11 | 12 | constructor(opt_alternativeCdn?: string) { 13 | super(opt_alternativeCdn || Config.cdnUrls().chrome); 14 | this.configSource = new ChromeXml(); 15 | this.name = 'chromedriver'; 16 | this.versionDefault = ChromeDriver.versionDefault; 17 | this.versionCustom = this.versionDefault; 18 | } 19 | 20 | id(): string { 21 | return ChromeDriver.id; 22 | } 23 | 24 | prefix(): string { 25 | return 'chromedriver_'; 26 | } 27 | 28 | suffix(): string { 29 | return '.zip'; 30 | } 31 | 32 | getVersionList(): Promise { 33 | // If an alternative cdn is set, return an empty list. 34 | if (this.alternativeDownloadUrl != null) { 35 | Promise.resolve([]); 36 | } else { 37 | return this.configSource.getVersionList(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/binaries/chrome_xml.ts: -------------------------------------------------------------------------------- 1 | import * as semver from 'semver'; 2 | 3 | import {Config} from '../config'; 4 | import {requestBody} from '../http_utils'; 5 | 6 | import {BinaryUrl} from './binary'; 7 | import {XmlConfigSource} from './config_source'; 8 | 9 | export class ChromeXml extends XmlConfigSource { 10 | maxVersion = Config.binaryVersions().maxChrome; 11 | 12 | constructor() { 13 | super('chrome', Config.cdnUrls()['chrome']); 14 | } 15 | 16 | getUrl(version: string): Promise { 17 | if (version === 'latest') { 18 | return this.getLatestChromeDriverVersion(); 19 | } else { 20 | return this.getSpecificChromeDriverVersion(version); 21 | } 22 | } 23 | 24 | /** 25 | * Get a list of chrome drivers paths available for the configuration OS type and architecture. 26 | */ 27 | getVersionList(): Promise { 28 | return this.getXml().then(xml => { 29 | let versionPaths: string[] = []; 30 | let osType = this.getOsTypeName(); 31 | 32 | for (let content of xml.ListBucketResult.Contents) { 33 | let contentKey: string = content.Key[0]; 34 | 35 | if ( 36 | // Filter for 32-bit devices, make sure x64 is not an option 37 | (this.osarch.includes('64') || !contentKey.includes('64')) && 38 | // Filter for x86 macs, make sure m1 is not an option 39 | ((this.ostype === 'Darwin' && this.osarch === 'arm64') || !contentKey.includes('m1'))) { 40 | // Filter for only the osType 41 | if (contentKey.includes(osType)) { 42 | versionPaths.push(contentKey); 43 | } 44 | } 45 | } 46 | return versionPaths; 47 | }); 48 | } 49 | 50 | /** 51 | * Helper method, gets the ostype and gets the name used by the XML 52 | */ 53 | getOsTypeName(): string { 54 | // Get the os type name. 55 | if (this.ostype === 'Darwin') { 56 | return 'mac'; 57 | } else if (this.ostype === 'Windows_NT') { 58 | return 'win'; 59 | } else { 60 | return 'linux'; 61 | } 62 | } 63 | 64 | /** 65 | * Gets the latest item from the XML. 66 | */ 67 | private getLatestChromeDriverVersion(): Promise { 68 | const latestReleaseUrl = 'https://chromedriver.storage.googleapis.com/LATEST_RELEASE'; 69 | return requestBody(latestReleaseUrl).then(latestVersion => { 70 | return this.getSpecificChromeDriverVersion(latestVersion); 71 | }); 72 | } 73 | 74 | /** 75 | * Gets a specific item from the XML. 76 | */ 77 | private getSpecificChromeDriverVersion(inputVersion: string): Promise { 78 | return this.getVersionList().then(list => { 79 | const specificVersion = getValidSemver(inputVersion); 80 | if (specificVersion === '') { 81 | throw new Error(`version ${inputVersion} ChromeDriver does not exist`) 82 | } 83 | let itemFound = ''; 84 | for (let item of list) { 85 | // Get a semantic version. 86 | let version = item.split('/')[0]; 87 | if (semver.valid(version) == null) { 88 | const lookUpVersion = getValidSemver(version); 89 | 90 | if (semver.valid(lookUpVersion)) { 91 | // Check to see if the specified version matches. 92 | if (lookUpVersion === specificVersion) { 93 | // When item found is null, check the os arch 94 | // 64-bit version works OR not 64-bit version and the path does not have '64' 95 | if (itemFound == '') { 96 | if (this.osarch === 'x64' || 97 | (this.osarch !== 'x64' && !item.includes(this.getOsTypeName() + '64'))) { 98 | itemFound = item; 99 | } 100 | if (this.osarch === 'arm64' && this.ostype === 'Darwin' && item.includes('m1')) { 101 | itemFound = item; 102 | } 103 | } 104 | // If the semantic version is the same, check os arch. 105 | // For 64-bit systems, prefer the 64-bit version. 106 | else if (this.osarch === 'x64') { 107 | // No win64 version exists, so even on x64 we need to look for win32 108 | const osTypeNameAndArch = 109 | this.getOsTypeName() + (this.getOsTypeName() === 'win' ? '32' : '64'); 110 | 111 | if (item.includes(osTypeNameAndArch)) { 112 | itemFound = item; 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | if (itemFound == '') { 120 | return {url: '', version: inputVersion}; 121 | } else { 122 | return {url: Config.cdnUrls().chrome + itemFound, version: inputVersion}; 123 | } 124 | }); 125 | } 126 | } 127 | 128 | /** 129 | * Chromedriver is the only binary that does not conform to semantic versioning 130 | * and either has too little number of digits or too many. To get this to be in 131 | * semver, we will either add a '.0' at the end or chop off the last set of 132 | * digits. This is so we can compare to find the latest and greatest. 133 | * 134 | * Example: 135 | * 2.46 -> 2.46.0 136 | * 75.0.3770.8 -> 75.0.3770 137 | * 138 | * @param version 139 | */ 140 | export function getValidSemver(version: string): string { 141 | let lookUpVersion = ''; 142 | // This supports downloading 2.46 143 | try { 144 | const oldRegex = /(\d+.\d+)/g; 145 | const exec = oldRegex.exec(version); 146 | if (exec) { 147 | lookUpVersion = exec[1] + '.0'; 148 | } 149 | } catch (_) { 150 | // no-op: is this is not valid, do not throw here. 151 | } 152 | // This supports downloading 74.0.3729.6 153 | try { 154 | const newRegex = /(\d+.\d+.\d+).\d+/g; 155 | const exec = newRegex.exec(version); 156 | if (exec) { 157 | lookUpVersion = exec[1]; 158 | } 159 | } catch (_) { 160 | // no-op: if this does not work, use the other regex pattern. 161 | } 162 | return lookUpVersion; 163 | } 164 | -------------------------------------------------------------------------------- /lib/binaries/config_source.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as os from 'os'; 3 | import * as path from 'path'; 4 | import * as request from 'request'; 5 | import * as url from 'url'; 6 | import * as xml2js from 'xml2js'; 7 | 8 | import {Logger} from '../cli/logger'; 9 | import {Config} from '../config'; 10 | import {HttpUtils} from '../http_utils'; 11 | 12 | let logger = new Logger('config_source'); 13 | 14 | export abstract class ConfigSource { 15 | ostype = Config.osType(); 16 | osarch = Config.osArch(); 17 | out_dir: string = Config.getSeleniumDir(); 18 | 19 | abstract getUrl(version: string): Promise<{url: string, version: string}>; 20 | abstract getVersionList(): Promise; 21 | } 22 | 23 | export abstract class XmlConfigSource extends ConfigSource { 24 | constructor(public name: string, public xmlUrl: string) { 25 | super(); 26 | } 27 | 28 | protected getFileName(): string { 29 | try { 30 | fs.statSync(this.out_dir); 31 | } catch (e) { 32 | fs.mkdirSync(this.out_dir); 33 | } 34 | return path.resolve(this.out_dir, this.name + '-response.xml'); 35 | } 36 | 37 | protected getXml(): Promise { 38 | let fileName = this.getFileName(); 39 | let content = this.readResponse(); 40 | if (content) { 41 | return Promise.resolve(content); 42 | } else { 43 | return this.requestXml().then(text => { 44 | let xml = this.convertXml2js(text); 45 | fs.writeFileSync(fileName, text); 46 | return xml; 47 | }); 48 | } 49 | } 50 | 51 | private readResponse(): any { 52 | let fileName = this.getFileName(); 53 | try { 54 | let contents = fs.readFileSync(fileName).toString(); 55 | let timestamp = new Date(fs.statSync(fileName).mtime).getTime(); 56 | let size = fs.statSync(fileName).size; 57 | let now = Date.now(); 58 | 59 | // On start, read the file. If not on start, check use the cache as long as the 60 | // size > 0 and within the cache time. 61 | // 60 minutes * 60 seconds / minute * 1000 ms / second 62 | if (Config.runCommand === 'start' || (size > 0 && (now - (60 * 60 * 1000) < timestamp))) { 63 | return this.convertXml2js(contents); 64 | } else { 65 | return null; 66 | } 67 | } catch (err) { 68 | return null; 69 | } 70 | } 71 | 72 | private requestXml(): Promise { 73 | return new Promise((resolve, reject) => { 74 | let options = HttpUtils.initOptions(this.xmlUrl); 75 | 76 | let curl = this.getFileName() + ' ' + options.url; 77 | if (HttpUtils.requestOpts.proxy) { 78 | let pathUrl = url.parse(options.url.toString()).path; 79 | let host = url.parse(options.url.toString()).host; 80 | let newFileUrl = url.resolve(HttpUtils.requestOpts.proxy, pathUrl); 81 | curl = this.getFileName() + ' \'' + newFileUrl + '\' -H \'host:' + host + '\''; 82 | } 83 | if (HttpUtils.requestOpts.ignoreSSL) { 84 | curl = 'k ' + curl; 85 | } 86 | logger.info('curl -o' + curl); 87 | 88 | let req = request(options); 89 | req.on('response', response => { 90 | if (response.statusCode === 200) { 91 | let output = ''; 92 | response.on('data', (data) => { 93 | output += data; 94 | }); 95 | response.on('end', () => { 96 | resolve(output); 97 | }); 98 | 99 | } else { 100 | reject(new Error('response status code is not 200')); 101 | } 102 | }); 103 | }); 104 | } 105 | 106 | private convertXml2js(xml: string): any { 107 | let retResult: any = null; 108 | xml2js.parseString(xml, (err, result) => { 109 | retResult = result; 110 | }); 111 | return retResult; 112 | } 113 | } 114 | 115 | export abstract class JsonConfigSource extends ConfigSource { 116 | constructor(public name: string, public jsonUrl: string) { 117 | super(); 118 | } 119 | 120 | protected getFileName(): string { 121 | try { 122 | fs.statSync(this.out_dir); 123 | } catch (e) { 124 | fs.mkdirSync(this.out_dir); 125 | } 126 | return path.resolve(this.out_dir, this.name + '-response.json'); 127 | } 128 | 129 | protected abstract getJson(): Promise; 130 | } 131 | 132 | export abstract class GithubApiConfigSource extends JsonConfigSource { 133 | constructor(name: string, url: string) { 134 | super(name, url); 135 | } 136 | 137 | /** 138 | * This is an unauthenticated request and since Github limits the rate, we will cache this 139 | * to a file. { timestamp: number, response: response }. We will check the timestamp and renew 140 | * this request if the file is older than an hour. 141 | */ 142 | getJson(): Promise { 143 | let fileName = this.getFileName(); 144 | let content = this.readResponse(); 145 | if (content) { 146 | return Promise.resolve(JSON.parse(content)); 147 | } else { 148 | return this.requestJson().then(body => { 149 | let json = JSON.parse(body); 150 | fs.writeFileSync(fileName, JSON.stringify(json, null, ' ')); 151 | return json; 152 | }); 153 | } 154 | } 155 | 156 | private requestJson(): Promise { 157 | return new Promise((resolve, reject) => { 158 | let options = HttpUtils.initOptions(this.jsonUrl); 159 | options = HttpUtils.optionsHeader(options, 'Host', 'api.github.com'); 160 | options = HttpUtils.optionsHeader(options, 'User-Agent', 'request'); 161 | 162 | let curl = this.getFileName() + ' ' + options.url; 163 | if (HttpUtils.requestOpts.proxy) { 164 | let pathUrl = url.parse(options.url.toString()).path; 165 | let host = url.parse(options.url.toString()).host; 166 | let newFileUrl = url.resolve(HttpUtils.requestOpts.proxy, pathUrl); 167 | curl = this.getFileName() + ' \'' + newFileUrl + '\' -H \'host:' + host + '\''; 168 | } 169 | if (HttpUtils.requestOpts.ignoreSSL) { 170 | curl = 'k ' + curl; 171 | } 172 | logger.info('curl -o' + curl); 173 | 174 | let req = request(options); 175 | req.on('response', response => { 176 | if (response.statusCode === 200) { 177 | let output = ''; 178 | response.on('data', (data) => { 179 | output += data; 180 | }); 181 | response.on('end', () => { 182 | resolve(output); 183 | }); 184 | 185 | } else if (response.statusCode == 403 && response.headers['x-ratelimit-remaining'] == '0') { 186 | reject(new Error('Failed to make Github request, rate limit reached.')); 187 | } else { 188 | reject(new Error('response status code is not 200. It was ' + response.statusCode)); 189 | } 190 | }) 191 | }); 192 | } 193 | 194 | private readResponse(): any { 195 | let fileName = this.getFileName(); 196 | try { 197 | let contents = fs.readFileSync(fileName).toString(); 198 | let timestamp = new Date(fs.statSync(fileName).mtime).getTime(); 199 | let size = fs.statSync(fileName).size; 200 | let now = Date.now(); 201 | 202 | // On start, read the file. If not on start, check use the cache as long as the 203 | // size > 0 and within the cache time. 204 | // 60 minutes * 60 seconds / minute * 1000 ms / second 205 | if (Config.runCommand === 'start' || (size > 0 && (now - (60 * 60 * 1000) < timestamp))) { 206 | return contents; 207 | } else { 208 | return null; 209 | } 210 | } catch (err) { 211 | return null; 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /lib/binaries/gecko_driver.ts: -------------------------------------------------------------------------------- 1 | import {Config} from '../config'; 2 | 3 | import {Binary, BinaryUrl, OS} from './binary'; 4 | import {GeckoDriverGithub} from './gecko_driver_github'; 5 | 6 | type StringMap = { 7 | [key: string]: string 8 | }; 9 | type SuffixMap = { 10 | [key: string]: StringMap 11 | }; 12 | 13 | export class GeckoDriver extends Binary { 14 | static id = 'gecko'; 15 | static isDefault = true; 16 | static os = [OS.Windows_NT, OS.Linux, OS.Darwin]; 17 | static versionDefault = Config.binaryVersions().gecko; 18 | private static suffixes: SuffixMap = { 19 | 'Darwin': {'x64': '-macos.tar.gz'}, 20 | 'Linux': {'x64': '-linux64.tar.gz', 'ia32': '-linux32.tar.gz'}, 21 | 'Windows_NT': { 22 | 'x64': '-win64.zip', 23 | 'ia32': '-win32.zip', 24 | } 25 | }; 26 | 27 | constructor(opt_alternativeCdn?: string) { 28 | super(opt_alternativeCdn || Config.cdnUrls().gecko); 29 | this.configSource = new GeckoDriverGithub(); 30 | this.name = 'geckodriver'; 31 | this.versionDefault = GeckoDriver.versionDefault; 32 | this.versionCustom = this.versionDefault; 33 | } 34 | 35 | id(): string { 36 | return GeckoDriver.id; 37 | } 38 | 39 | prefix(): string { 40 | return 'geckodriver-'; 41 | } 42 | 43 | suffix(): string { 44 | if (this.ostype === 'Windows_NT') { 45 | return '.zip'; 46 | } else { 47 | return '.tar.gz'; 48 | } 49 | } 50 | 51 | getVersionList(): Promise { 52 | if (this.alternativeDownloadUrl != null) { 53 | return Promise.resolve([]); 54 | } else { 55 | return this.configSource.getVersionList(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/binaries/gecko_driver_github.ts: -------------------------------------------------------------------------------- 1 | import * as semver from 'semver'; 2 | 3 | import {Config} from '../config'; 4 | 5 | import {BinaryUrl} from './binary'; 6 | import {GithubApiConfigSource} from './config_source'; 7 | 8 | export class GeckoDriverGithub extends GithubApiConfigSource { 9 | constructor() { 10 | super('gecko', 'https://api.github.com/repos/mozilla/geckodriver/releases'); 11 | } 12 | 13 | getUrl(version: string): Promise { 14 | if (version === 'latest') { 15 | return this.getLatestGeckoDriverVersion(); 16 | } else { 17 | return this.getSpecificGeckoDrierVersion(version); 18 | } 19 | } 20 | 21 | getVersionList(): Promise { 22 | return this.getJson().then(json => { 23 | let versions: string[] = []; 24 | for (let i = 0; i < json.length; i++) { 25 | let item = json[i]; 26 | versions.push(item.tag_name); 27 | } 28 | return versions; 29 | }); 30 | } 31 | 32 | getVersionsLookup(): Promise> { 33 | return this.getJson().then(json => { 34 | let versionsLookup: Array<{version: string, index: string}> = []; 35 | for (let i = 0; i < json.length; i++) { 36 | let item = json[i]; 37 | let index = i.toString(); 38 | versionsLookup.push({version: item.tag_name, index: index}); 39 | } 40 | return versionsLookup; 41 | }); 42 | } 43 | 44 | private getLatestGeckoDriverVersion(): Promise { 45 | return this.getJson().then(json => { 46 | return this.getVersionsLookup().then(versionsLookup => { 47 | let latest = ''; 48 | for (let item of versionsLookup) { 49 | let version = item.version.replace('v', ''); 50 | let assetsArray = json[item.index].assets; 51 | 52 | // check to make sure the version found has the OS 53 | for (let asset of assetsArray) { 54 | if ((asset.name as string).includes(this.oshelper())) { 55 | if (latest === '') { 56 | latest = version; 57 | } else if (semver.lt(latest, version)) { 58 | latest = version; 59 | } 60 | } 61 | } 62 | } 63 | return this.getSpecificGeckoDrierVersion('v' + latest); 64 | }); 65 | }); 66 | } 67 | 68 | private getSpecificGeckoDrierVersion(inputVersion: string): Promise { 69 | return this.getJson().then(json => { 70 | return this.getVersionsLookup().then(versionsLookup => { 71 | for (let item of versionsLookup) { 72 | // Get the asset from the matching version. 73 | if (item.version === inputVersion) { 74 | let assetsArray = json[item.index].assets; 75 | for (let asset of assetsArray) { 76 | if ((asset.name as string).includes(this.oshelper())) { 77 | return {url: asset.browser_download_url, version: inputVersion}; 78 | } 79 | } 80 | } 81 | } 82 | return null; 83 | }); 84 | }); 85 | } 86 | 87 | private oshelper(): string { 88 | // Get the os type name. 89 | if (this.ostype === 'Darwin') { 90 | return 'macos'; 91 | } else if (this.ostype === 'Windows_NT') { 92 | return this.osarch === 'x64' ? 'win64' : 'win32'; 93 | } else { 94 | return this.osarch === 'x64' ? 'linux64' : 'linux32'; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/binaries/iedriver.ts: -------------------------------------------------------------------------------- 1 | import {Config} from '../config'; 2 | 3 | import {Binary, BinaryUrl, OS} from './binary'; 4 | import {IEDriverXml} from './iedriver_xml'; 5 | 6 | export class IEDriver extends Binary { 7 | static id = 'ie'; 8 | static isDefault32 = false; 9 | static isDefault64 = false; 10 | static os = [OS.Windows_NT]; 11 | static versionDefault = Config.binaryVersions().ie; 12 | 13 | constructor(opt_alternativeCdn?: string) { 14 | super(opt_alternativeCdn || Config.cdnUrls().ie); 15 | this.configSource = new IEDriverXml(); 16 | this.name = 'IEDriverServer'; 17 | this.versionDefault = IEDriver.versionDefault; 18 | this.versionCustom = this.versionDefault; 19 | } 20 | 21 | id(): string { 22 | return IEDriver.id; 23 | } 24 | 25 | prefix(): string { 26 | return 'IEDriverServer'; 27 | } 28 | 29 | suffix(): string { 30 | return '.zip'; 31 | } 32 | 33 | getVersionList(): Promise { 34 | if (this.alternativeDownloadUrl != null) { 35 | return Promise.resolve([]); 36 | } else { 37 | return this.configSource.getVersionList(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/binaries/iedriver_xml.ts: -------------------------------------------------------------------------------- 1 | import * as semver from 'semver'; 2 | 3 | import {Config} from '../config'; 4 | 5 | import {BinaryUrl} from './binary'; 6 | import {XmlConfigSource} from './config_source'; 7 | 8 | export class IEDriverXml extends XmlConfigSource { 9 | constructor() { 10 | super('iedriver', Config.cdnUrls()['ie']); 11 | } 12 | 13 | getUrl(version: string): Promise { 14 | if (version === 'latest') { 15 | return this.getLatestIEDriverVersion(); 16 | } else { 17 | return this.getSpecificIEDriverVersion(version); 18 | } 19 | } 20 | 21 | getVersionList(): Promise { 22 | return this.getXml().then(xml => { 23 | let versionPaths: string[] = []; 24 | 25 | for (let content of xml.ListBucketResult.Contents) { 26 | let contentKey: string = content.Key[0]; 27 | 28 | // Filter For IEDriverServer win 32. Removing option to download x64 29 | if (contentKey.includes('IEDriverServer_Win32_')) { 30 | versionPaths.push(contentKey); 31 | } 32 | } 33 | return versionPaths; 34 | }); 35 | } 36 | 37 | private getLatestIEDriverVersion(): Promise { 38 | return this.getVersionList().then(list => { 39 | let latestVersion: string = null; 40 | let latest = ''; 41 | for (let item of list) { 42 | // Get a semantic version. 43 | let version = item.split('IEDriverServer_Win32_')[1].replace('.zip', ''); 44 | 45 | if (latestVersion == null) { 46 | // First time: use the version found. 47 | latestVersion = version; 48 | latest = item; 49 | } else if (semver.gt(version, latestVersion)) { 50 | // Get the latest. 51 | latestVersion = version; 52 | latest = item; 53 | } 54 | } 55 | return {url: Config.cdnUrls().ie + latest, version: latestVersion}; 56 | }); 57 | } 58 | 59 | private getSpecificIEDriverVersion(inputVersion: string): Promise { 60 | return this.getVersionList().then(list => { 61 | let itemFound = ''; 62 | 63 | for (let item of list) { 64 | // Get a semantic version. 65 | let version = item.split('IEDriverServer_Win32_')[1].replace('.zip', ''); 66 | 67 | // Check to see if the specified version matches. 68 | let firstPath = item.split('/')[0]; 69 | if (version === inputVersion) { 70 | return {url: Config.cdnUrls().ie + item, version: version}; 71 | } 72 | } 73 | return {url: '', version: inputVersion}; 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/binaries/index.ts: -------------------------------------------------------------------------------- 1 | export * from './android_sdk'; 2 | export * from './appium'; 3 | export * from './binary'; 4 | export * from './chrome_driver'; 5 | export * from './gecko_driver'; 6 | export * from './iedriver'; 7 | export * from './standalone'; 8 | -------------------------------------------------------------------------------- /lib/binaries/standalone.ts: -------------------------------------------------------------------------------- 1 | import {Config} from '../config'; 2 | 3 | import {Binary, BinaryUrl, OS} from './binary'; 4 | import {ConfigSource} from './config_source'; 5 | import {StandaloneXml} from './standalone_xml'; 6 | 7 | export class Standalone extends Binary { 8 | static id = 'standalone'; 9 | static isDefault = true; 10 | static os = [OS.Windows_NT, OS.Linux, OS.Darwin]; 11 | static versionDefault = Config.binaryVersions().selenium; 12 | 13 | constructor(opt_alternativeCdn?: string) { 14 | super(opt_alternativeCdn || Config.cdnUrls().selenium); 15 | this.configSource = new StandaloneXml(); 16 | this.name = 'selenium standalone'; 17 | this.versionDefault = Standalone.versionDefault; 18 | this.versionCustom = this.versionDefault; 19 | } 20 | 21 | id(): string { 22 | return Standalone.id; 23 | } 24 | 25 | prefix(): string { 26 | return 'selenium-server-standalone-'; 27 | } 28 | 29 | suffix(): string { 30 | return '.jar'; 31 | } 32 | 33 | executableSuffix(): string { 34 | return '.jar'; 35 | } 36 | 37 | getVersionList(): Promise { 38 | // If an alternative cdn is set, return an empty list. 39 | if (this.alternativeDownloadUrl != null) { 40 | return Promise.resolve([]); 41 | } else { 42 | return this.configSource.getVersionList(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/binaries/standalone_xml.ts: -------------------------------------------------------------------------------- 1 | import * as semver from 'semver'; 2 | 3 | import {Config} from '../config'; 4 | 5 | import {BinaryUrl} from './binary'; 6 | import {XmlConfigSource} from './config_source'; 7 | 8 | export class StandaloneXml extends XmlConfigSource { 9 | constructor() { 10 | super('standalone', Config.cdnUrls()['selenium']); 11 | } 12 | 13 | getUrl(version: string): Promise { 14 | if (version === 'latest') { 15 | return this.getLatestStandaloneVersion(); 16 | } else { 17 | return this.getSpecificStandaloneVersion(version); 18 | } 19 | } 20 | 21 | getVersionList(): Promise { 22 | return this.getXml().then(xml => { 23 | let versionPaths: string[] = []; 24 | 25 | for (let content of xml.ListBucketResult.Contents) { 26 | let contentKey: string = content.Key[0]; 27 | 28 | // Filter the selenium-server-standalone. 29 | if (contentKey.includes('selenium-server-standalone')) { 30 | versionPaths.push(contentKey); 31 | } 32 | } 33 | return versionPaths; 34 | }); 35 | } 36 | 37 | private getLatestStandaloneVersion(): Promise { 38 | return this.getVersionList().then(list => { 39 | let standaloneVersion: string = null; 40 | let latest = ''; 41 | let latestVersion = ''; 42 | // Use jar files that are not beta and not alpha versions. 43 | const jarList = list.filter((i) => { 44 | return i.endsWith('.jar') && !i.includes('beta') && !i.includes('alpha'); 45 | }); 46 | for (let item of jarList) { 47 | // Get a semantic version. 48 | let version = item.split('selenium-server-standalone-')[1].replace('.jar', ''); 49 | if (standaloneVersion == null) { 50 | // First time: use the version found. 51 | standaloneVersion = version; 52 | latest = item; 53 | latestVersion = version; 54 | } else if (semver.gt(version, standaloneVersion)) { 55 | // Get the latest. 56 | standaloneVersion = version; 57 | latest = item; 58 | latestVersion = version; 59 | } 60 | } 61 | return {url: Config.cdnUrls().selenium + latest, version: latestVersion}; 62 | }); 63 | } 64 | 65 | private getSpecificStandaloneVersion(inputVersion: string): Promise { 66 | return this.getVersionList().then(list => { 67 | let itemFound = ''; 68 | let standaloneVersion: string = null; 69 | 70 | for (let item of list) { 71 | // Get a semantic version. 72 | let version = item.split('selenium-server-standalone-')[1].replace('.jar', ''); 73 | 74 | // Check to see if the specified version matches. 75 | let firstPath = item.split('/')[0]; 76 | if (version === inputVersion) { 77 | // Check if the beta exists that we have the right version 78 | // Example: We will see that beta3 appears in the file and path 79 | // 3.0-beta3/selenium-server-standalone-3.0.0-beta3.jar 80 | // where this should not work: 81 | // 3.0-beta2/selenium-server-standalone-3.0.0-beta3.jar 82 | if (inputVersion.includes('beta')) { 83 | let betaInputVersion = inputVersion.replace('.jar', '').split('beta')[1]; 84 | if (item.split('/')[0].includes('beta' + betaInputVersion)) { 85 | return {url: Config.cdnUrls().selenium + item, version: version}; 86 | } 87 | } else { 88 | return {url: Config.cdnUrls().selenium + item, version: version}; 89 | } 90 | } 91 | } 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/cli/cli.ts: -------------------------------------------------------------------------------- 1 | import * as chalk from 'chalk'; 2 | import * as path from 'path'; 3 | 4 | import {Config} from '../config'; 5 | 6 | import {MinimistArgs, Options} from './options'; 7 | import {Program, Programs} from './programs'; 8 | 9 | 10 | 11 | /** 12 | * The Cli contains the usage and the collection of programs. 13 | * 14 | * Printing help for all the programs in the following order: 15 | * usage, commands, and options. If the options are used in multiple programs, 16 | * it will list it once. 17 | */ 18 | export class Cli { 19 | programs: Programs = {}; 20 | usageText: string; 21 | version: string; 22 | 23 | /** 24 | * Register a program to the command line interface. 25 | * @returns The cli for method chaining. 26 | */ 27 | program(prog: Program): Cli { 28 | this.programs[prog.cmd] = prog; 29 | return this; 30 | } 31 | 32 | /** 33 | * Add a usage for the command line interface. 34 | * @returns The cli for method chaining. 35 | */ 36 | usage(usageText: string): Cli { 37 | this.usageText = usageText; 38 | return this; 39 | } 40 | 41 | /** 42 | * Prints help for the programs registered to the cli. 43 | */ 44 | printHelp(): void { 45 | console.log('Usage: ' + this.usageText); 46 | console.log('\nCommands:'); 47 | let cmdDescriptionPos = this.posCmdDescription(); 48 | for (let cmd in this.programs) { 49 | let prog = this.programs[cmd]; 50 | prog.printCmd(cmdDescriptionPos); 51 | } 52 | let descriptionPos = this.posDescription(); 53 | let defaultPos = this.posDefault(); 54 | let extOptions: Options = {}; 55 | console.log('\nOptions:'); 56 | // print all options 57 | for (let cmd in this.programs) { 58 | let prog = this.programs[cmd]; 59 | prog.printOptions(descriptionPos, defaultPos, extOptions); 60 | } 61 | } 62 | 63 | /** 64 | * For commands, gets the position where the description should start so they 65 | * are aligned. 66 | * @returns The position where the command description should start. 67 | */ 68 | posCmdDescription(): number { 69 | let position = -1; 70 | for (let cmd in this.programs) { 71 | position = Math.max(position, cmd.length + 6); 72 | } 73 | return position; 74 | } 75 | 76 | /** 77 | * For options, gets the position where the description should start so they 78 | * are aligned. 79 | * @returns The position where the option description should start. 80 | */ 81 | posDescription(): number { 82 | let position = -1; 83 | for (let cmd in this.programs) { 84 | let prog = this.programs[cmd]; 85 | position = Math.max(position, prog.posDescription()); 86 | } 87 | return position; 88 | } 89 | 90 | /** 91 | * For options, get the position where the default values should start so they 92 | * are aligned. 93 | * @returns The position where the option default values should start. 94 | */ 95 | posDefault(): number { 96 | let position = -1; 97 | for (let cmd in this.programs) { 98 | let prog = this.programs[cmd]; 99 | position = Math.max(position, prog.posDefault()); 100 | } 101 | return position; 102 | } 103 | 104 | /** 105 | * Go through all programs and add options to the collection. 106 | * @returns The options used in the programs. 107 | */ 108 | getOptions(): Options { 109 | let allOptions: Options = {}; 110 | for (let cmd in this.programs) { 111 | let prog = this.programs[cmd]; 112 | allOptions = prog.getOptions_(allOptions); 113 | } 114 | return allOptions; 115 | } 116 | 117 | /** 118 | * Get the options used by the programs and create the minimist options 119 | * to ensure that minimist parses the values properly. 120 | * @returns The options for minimist. 121 | */ 122 | getMinimistOptions(): Object { 123 | let allOptions = this.getOptions(); 124 | let minimistOptions: MinimistArgs = {}; 125 | let minimistBoolean: string[] = []; 126 | let minimistString: string[] = []; 127 | let minimistNumber: string[] = []; 128 | let minimistDefault: any = {}; 129 | for (let opt in allOptions) { 130 | let option = allOptions[opt]; 131 | if (option.type === 'boolean') { 132 | minimistBoolean.push(option.opt); 133 | } else if (option.type === 'string') { 134 | minimistString.push(option.opt); 135 | } else if (option.type === 'number') { 136 | minimistNumber.push(option.opt); 137 | } 138 | if (typeof option.defaultValue !== 'undefined') { 139 | minimistDefault[option.opt] = option.defaultValue; 140 | } 141 | } 142 | minimistOptions['boolean'] = minimistBoolean; 143 | minimistOptions['string'] = minimistString; 144 | minimistOptions['number'] = minimistNumber; 145 | minimistOptions['default'] = minimistDefault; 146 | return minimistOptions; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /lib/cli/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cli'; 2 | export * from './options'; 3 | export * from './programs'; 4 | export * from './logger'; 5 | -------------------------------------------------------------------------------- /lib/cli/logger.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | // Will use chalk if chalk is available to add color to console logging 5 | let chalk: any; 6 | let printRed: Function; 7 | let printYellow: Function; 8 | let printGray: Function; 9 | 10 | try { 11 | chalk = require('chalk'); 12 | printRed = chalk.red; 13 | printYellow = chalk.yellow; 14 | printGray = chalk.gray; 15 | } catch (e) { 16 | printRed = printYellow = printGray = (msg: any) => { 17 | return msg; 18 | }; 19 | } 20 | 21 | export enum LogLevel { 22 | ERROR, 23 | WARN, 24 | INFO, 25 | DEBUG 26 | } 27 | 28 | export enum WriteTo { 29 | CONSOLE, 30 | FILE, 31 | BOTH, 32 | NONE 33 | } 34 | 35 | let logFile = 'webdriver.log'; // the default log file name 36 | 37 | /** 38 | * Logger class adds timestamp output, log levels, and identifiers to help 39 | * when debugging. Also could write to console, file, both, or none. 40 | */ 41 | export class Logger { 42 | static logLevel: LogLevel = LogLevel.INFO; 43 | static showTimestamp: boolean = true; 44 | static showId: boolean = true; 45 | static writeTo: WriteTo = WriteTo.CONSOLE; 46 | static fd: any; 47 | static firstWrite: boolean = false; 48 | 49 | /** 50 | * Set up the write location. If writing to a file, get the file descriptor. 51 | * @param writeTo The enum for where to write the logs. 52 | * @param opt_logFile An optional parameter to override the log file location. 53 | */ 54 | static setWrite(writeTo: WriteTo, opt_logFile?: string): void { 55 | if (opt_logFile) { 56 | logFile = opt_logFile; 57 | } 58 | Logger.writeTo = writeTo; 59 | if (Logger.writeTo == WriteTo.FILE || Logger.writeTo == WriteTo.BOTH) { 60 | Logger.fd = fs.openSync(path.resolve(logFile), 'a'); 61 | Logger.firstWrite = false; 62 | } 63 | } 64 | 65 | /** 66 | * Creates a logger instance with an ID for the logger. 67 | * @constructor 68 | */ 69 | constructor(private id: string) {} 70 | 71 | /** 72 | * Log INFO 73 | * @param ...msgs multiple arguments to be logged. 74 | */ 75 | info(...msgs: any[]): void { 76 | this.log_(LogLevel.INFO, msgs); 77 | } 78 | 79 | /** 80 | * Log DEBUG 81 | * @param ...msgs multiple arguments to be logged. 82 | */ 83 | debug(...msgs: any[]): void { 84 | this.log_(LogLevel.DEBUG, msgs); 85 | } 86 | 87 | /** 88 | * Log WARN 89 | * @param ...msgs multiple arguments to be logged. 90 | */ 91 | warn(...msgs: any[]): void { 92 | this.log_(LogLevel.WARN, msgs); 93 | } 94 | 95 | /** 96 | * Log ERROR 97 | * @param ...msgs multiple arguments to be logged. 98 | */ 99 | error(...msgs: any[]): void { 100 | this.log_(LogLevel.ERROR, msgs); 101 | } 102 | 103 | /** 104 | * For the log level set, check to see if the messages should be logged. 105 | * @param logLevel The log level of the message. 106 | * @param msgs The messages to be logged 107 | */ 108 | log_(logLevel: LogLevel, msgs: any[]): void { 109 | switch (Logger.logLevel) { 110 | case LogLevel.ERROR: 111 | if (logLevel <= LogLevel.ERROR) { 112 | this.print_(logLevel, msgs); 113 | } 114 | break; 115 | case LogLevel.WARN: 116 | if (logLevel <= LogLevel.WARN) { 117 | this.print_(logLevel, msgs); 118 | } 119 | break; 120 | case LogLevel.INFO: 121 | if (logLevel <= LogLevel.INFO) { 122 | this.print_(logLevel, msgs); 123 | } 124 | break; 125 | case LogLevel.DEBUG: 126 | if (logLevel <= LogLevel.DEBUG) { 127 | this.print_(logLevel, msgs); 128 | } 129 | break; 130 | default: 131 | throw new Error('Log level undefined'); 132 | } 133 | } 134 | 135 | /** 136 | * Format with timestamp, log level, identifier, and message and log to 137 | * specified medium (console, file, both, none). 138 | * @param logLevel The log level of the message. 139 | * @param msgs The messages to be logged. 140 | */ 141 | print_(logLevel: LogLevel, msgs: any[]): void { 142 | let consoleLog: string = ''; 143 | let fileLog: string = ''; 144 | 145 | if (Logger.showTimestamp) { 146 | consoleLog += Logger.timestamp_(WriteTo.CONSOLE); 147 | fileLog += Logger.timestamp_(WriteTo.FILE); 148 | } 149 | consoleLog += Logger.level_(logLevel, this.id, WriteTo.CONSOLE); 150 | fileLog += Logger.level_(logLevel, this.id, WriteTo.FILE); 151 | if (Logger.showId) { 152 | consoleLog += Logger.id_(logLevel, this.id, WriteTo.CONSOLE); 153 | fileLog += Logger.id_(logLevel, this.id, WriteTo.FILE); 154 | } 155 | consoleLog += ' -'; 156 | fileLog += ' - '; 157 | 158 | switch (Logger.writeTo) { 159 | case WriteTo.CONSOLE: 160 | msgs.unshift(consoleLog); 161 | console.log.apply(console, msgs); 162 | break; 163 | case WriteTo.FILE: 164 | // for the first line written to the file, add a space 165 | if (!Logger.firstWrite) { 166 | fs.writeSync(Logger.fd, '\n'); 167 | Logger.firstWrite = true; 168 | } 169 | fileLog += ' ' + Logger.msgToFile_(msgs); 170 | fs.writeSync(Logger.fd, fileLog + '\n'); 171 | break; 172 | case WriteTo.BOTH: 173 | // for the first line written to the file, add a space 174 | if (!Logger.firstWrite) { 175 | fs.writeSync(Logger.fd, '\n'); 176 | Logger.firstWrite = true; 177 | } 178 | fileLog += ' ' + Logger.msgToFile_(msgs); 179 | fs.writeSync(Logger.fd, fileLog + '\n'); 180 | msgs.unshift(consoleLog); 181 | console.log.apply(console, msgs); 182 | break; 183 | case WriteTo.NONE: 184 | break; 185 | } 186 | } 187 | 188 | /** 189 | * Get a timestamp formatted with [hh:mm:ss] 190 | * @param writeTo The enum for where to write the logs. 191 | * @return The string of the formatted timestamp 192 | */ 193 | static timestamp_(writeTo: WriteTo): string { 194 | let d = new Date(); 195 | let ts = '['; 196 | let hours = d.getHours() < 10 ? '0' + d.getHours() : d.getHours(); 197 | let minutes = d.getMinutes() < 10 ? '0' + d.getMinutes() : d.getMinutes(); 198 | let seconds = d.getSeconds() < 10 ? '0' + d.getSeconds() : d.getSeconds(); 199 | if (writeTo == WriteTo.CONSOLE) { 200 | ts += printGray(hours + ':' + minutes + ':' + seconds) + ']'; 201 | } else { 202 | ts += hours + ':' + minutes + ':' + seconds + ']'; 203 | } 204 | ts += ' '; 205 | return ts; 206 | } 207 | 208 | /** 209 | * Get the identifier of the logger as '/' 210 | * @param logLevel The log level of the message. 211 | * @param writeTo The enum for where to write the logs. 212 | * @return The string of the formatted id 213 | */ 214 | static id_(logLevel: LogLevel, id: string, writeTo: WriteTo): string { 215 | let level = LogLevel[logLevel].toString(); 216 | if (writeTo === WriteTo.FILE) { 217 | return '/' + id; 218 | } else if (logLevel === LogLevel.ERROR) { 219 | return printRed('/' + id); 220 | } else if (logLevel === LogLevel.WARN) { 221 | return printYellow('/' + id); 222 | } else { 223 | return '/' + id; 224 | } 225 | } 226 | 227 | /** 228 | * Get the log level formatted with the first letter. For info, it is I. 229 | * @param logLevel The log level of the message. 230 | * @param writeTo The enum for where to write the logs. 231 | * @return The string of the formatted log level 232 | */ 233 | static level_(logLevel: LogLevel, id: string, writeTo: WriteTo): string { 234 | let level = LogLevel[logLevel].toString(); 235 | if (writeTo === WriteTo.FILE) { 236 | return level[0]; 237 | } else if (logLevel === LogLevel.ERROR) { 238 | return printRed(level[0]); 239 | } else if (logLevel === LogLevel.WARN) { 240 | return printYellow(level[0]); 241 | } else { 242 | return level[0]; 243 | } 244 | } 245 | 246 | /** 247 | * Convert the list of messages to a single string message. 248 | * @param msgs The list of messages. 249 | * @return The string of the formatted messages 250 | */ 251 | static msgToFile_(msgs: any[]): string { 252 | let log = ''; 253 | for (let pos = 0; pos < msgs.length; pos++) { 254 | let msg = msgs[pos]; 255 | let ret: any; 256 | if (typeof msg === 'object') { 257 | ret = JSON.stringify(msg); 258 | } else { 259 | ret = msg; 260 | } 261 | if (pos !== msgs.length - 1) { 262 | ret += ' '; 263 | } 264 | log += ret; 265 | } 266 | return log; 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /lib/cli/options.ts: -------------------------------------------------------------------------------- 1 | export interface MinimistArgs { [opt: string]: string[] } 2 | 3 | export interface Args { [opt: string]: number|string|boolean } 4 | 5 | export interface Options { [opt: string]: Option; } 6 | 7 | export class Option { 8 | opt: string; 9 | description: string; 10 | type: string; 11 | defaultValue: number|string|boolean; 12 | value: number|string|boolean; 13 | 14 | constructor( 15 | opt: string, description: string, type: string, defaultValue?: number|string|boolean) { 16 | this.opt = opt; 17 | this.description = description; 18 | this.type = type; 19 | if (defaultValue != null) { 20 | this.defaultValue = defaultValue; 21 | } 22 | } 23 | 24 | getValue_(): number|string|boolean { 25 | if (typeof this.value !== 'undefined') { 26 | return this.value; 27 | } else { 28 | return this.defaultValue; 29 | } 30 | } 31 | 32 | getNumber(): number { 33 | let value = this.getValue_(); 34 | if (value != null && (typeof value === 'number' || typeof value === 'string')) { 35 | return +value; 36 | } else { 37 | return null; 38 | } 39 | } 40 | 41 | getString(): string { 42 | let value = this.getValue_(); 43 | if (value != null) { 44 | return '' + this.getValue_(); 45 | } else { 46 | return ''; 47 | } 48 | } 49 | 50 | getBoolean(): boolean { 51 | let value = this.getValue_(); 52 | if (value != null) { 53 | if (typeof value === 'string') { 54 | return !(value === '0' || value === 'false'); 55 | } else if (typeof value === 'number') { 56 | return value !== 0; 57 | } else { 58 | return value; 59 | } 60 | } 61 | return false; 62 | } 63 | } 64 | 65 | export function unparseOptions(options: Options) { 66 | var args: string[] = []; 67 | for (let name in options) { 68 | let value = options[name].getValue_(); 69 | if (value !== options[name].defaultValue) { 70 | args.push('--' + name, '' + value); 71 | } 72 | } 73 | return args; 74 | }; 75 | -------------------------------------------------------------------------------- /lib/cli/programs.ts: -------------------------------------------------------------------------------- 1 | import * as minimist from 'minimist'; 2 | 3 | import {Args, MinimistArgs, Option, Options} from './options'; 4 | 5 | 6 | /** 7 | * Dictionary that maps the command and the program. 8 | */ 9 | export interface Programs { [cmd: string]: Program; } 10 | 11 | /** 12 | * A program has a command, a description, options, and a run method 13 | */ 14 | export class Program { 15 | static MIN_SPACING: number = 4; 16 | cmd: string; 17 | cmdDescription: string 18 | options: Options = {}; 19 | runMethod: Function; 20 | helpDescription: string; 21 | version: string; 22 | 23 | /** 24 | * Register a command and the description. 25 | * @param cmd The command. 26 | * @param cmdDescription The description of the command. 27 | * @returns The program for method chaining. 28 | */ 29 | command(cmd: string, cmdDescription: string): Program { 30 | this.cmd = cmd; 31 | this.cmdDescription = cmdDescription; 32 | return this; 33 | } 34 | 35 | /** 36 | * Register a new option. 37 | * @param opt The option. 38 | * @param description The description of the option. 39 | * @param type The type of value expected: boolean, number, or string 40 | * @param defaultValue The option's default value. 41 | * @returns The program for method chaining. 42 | */ 43 | option(opt: string, description: string, type: string, opt_defaultValue?: number|string|boolean): 44 | Program { 45 | this.options[opt] = new Option(opt, description, type, opt_defaultValue); 46 | return this; 47 | } 48 | 49 | /** 50 | * Adds an option to the program. 51 | * @param option The option. 52 | * @returns The program for method chaining. 53 | */ 54 | addOption(option: Option): Program { 55 | this.options[option.opt] = option; 56 | return this; 57 | } 58 | 59 | /** 60 | * Registers a method that will be used to run the program. 61 | * @param runMethod The method that will be used to run the program. 62 | * @returns The program for method chaining. 63 | */ 64 | action(runMethod: Function): Program { 65 | this.runMethod = runMethod; 66 | return this; 67 | } 68 | 69 | /** 70 | * Adds the value to the options and passes the updated options to the run 71 | * method. 72 | * @param args The arguments that will be parsed to run the method. 73 | */ 74 | run(json: JSON): Promise { 75 | for (let opt in this.options) { 76 | this.options[opt].value = this.getValue_(opt, json); 77 | } 78 | return Promise.resolve(this.runMethod(this.options)); 79 | } 80 | 81 | private getValue_(key: string, json: JSON): number|boolean|string { 82 | let keyList: string[] = key.split('.'); 83 | let tempJson: any = json; 84 | while (keyList.length > 0) { 85 | let keyItem = keyList[0]; 86 | if (tempJson[keyItem] != null) { 87 | tempJson = tempJson[keyItem]; 88 | keyList = keyList.slice(1); 89 | } else { 90 | return undefined; 91 | } 92 | } 93 | return tempJson; 94 | } 95 | 96 | /** 97 | * Prints the command with the description. The description will have spaces 98 | * between the cmd so that the starting position is "posDescription". If the 99 | * gap between the cmd and the description is less than MIN_SPACING or 100 | * posDescription is undefined, the spacing will be MIN_SPACING. 101 | * 102 | * @param opt_postDescription Starting position of the description. 103 | */ 104 | printCmd(opt_posDescription?: number): void { 105 | let log = ' ' + this.cmd; 106 | let spacing = Program.MIN_SPACING; 107 | 108 | if (opt_posDescription) { 109 | let diff = opt_posDescription - log.length; 110 | if (diff < Program.MIN_SPACING) { 111 | spacing = Program.MIN_SPACING; 112 | } else { 113 | spacing = diff; 114 | } 115 | } 116 | log += Array(spacing).join(' ') + this.cmdDescription; 117 | console.log(log); 118 | } 119 | 120 | /** 121 | * Prints the options with the option descriptions and default values. 122 | * The posDescription and posDefault is the starting position for the option 123 | * description. If extOptions are provided, check to see if we have already 124 | * printed those options. Also, once we print the option, add them to the extOptions. 125 | * 126 | * @param posDescription Position to start logging the description. 127 | * @param posDefault Position to start logging the default value. 128 | * @param opt_extOptions A collection of options that will be updated. 129 | */ 130 | printOptions(posDescription: number, posDefault: number, opt_extOptions?: Options): void { 131 | for (let opt in this.options) { 132 | // we have already logged it 133 | if (opt_extOptions && opt_extOptions[opt]) { 134 | continue; 135 | } 136 | 137 | let option = this.options[opt]; 138 | let log = ' --' + option.opt; 139 | let spacing = Program.MIN_SPACING; 140 | 141 | // description 142 | let diff = posDescription - log.length; 143 | if (diff < Program.MIN_SPACING) { 144 | spacing = Program.MIN_SPACING; 145 | } else { 146 | spacing = diff; 147 | } 148 | log += Array(spacing).join(' ') + option.description; 149 | 150 | // default value 151 | if (option.defaultValue) { 152 | spacing = Program.MIN_SPACING; 153 | let diff = posDefault - log.length - 1; 154 | if (diff <= Program.MIN_SPACING) { 155 | spacing = Program.MIN_SPACING; 156 | } else { 157 | spacing = diff; 158 | } 159 | log += Array(spacing).join(' '); 160 | log += '[default: ' + option.defaultValue + ']'; 161 | } 162 | 163 | console.log(log); 164 | if (opt_extOptions) { 165 | opt_extOptions[option.opt] = option; 166 | } 167 | } 168 | } 169 | 170 | /** 171 | * Assuming that the this program can run by itself, to print out the program's 172 | * help. Also assuming that the commands are called cmd-run and cmd-help. 173 | */ 174 | printHelp(): void { 175 | console.log( 176 | '\n' + 177 | 'Usage: ' + this.cmd + ' [options]\n' + 178 | ' ' + this.cmd + ' help\n' + 179 | 'Description: ' + this.cmdDescription + '\n'); 180 | console.log('Options:'); 181 | this.printOptions(this.posDescription(), this.posDefault()); 182 | } 183 | 184 | posDescription(): number { 185 | return this.lengthOf_('opt') + 2 * Program.MIN_SPACING; 186 | } 187 | 188 | posDefault(): number { 189 | return this.posDescription() + this.lengthOf_('description') + Program.MIN_SPACING; 190 | } 191 | 192 | lengthOf_(param: string): number { 193 | let maxLength = -1; 194 | for (let opt in this.options) { 195 | let option = this.options[opt]; 196 | if (param === 'description') { 197 | maxLength = Math.max(maxLength, option.description.length); 198 | } else if (param === 'opt') { 199 | maxLength = Math.max(maxLength, option.opt.length); 200 | } 201 | } 202 | return maxLength; 203 | } 204 | 205 | /** 206 | * Create a collection of options used by this program. 207 | * @returns The options used in the programs. 208 | */ 209 | getOptions_(allOptions: Options): Options { 210 | for (let opt in this.options) { 211 | allOptions[opt] = this.options[opt]; 212 | } 213 | return allOptions; 214 | } 215 | 216 | /** 217 | * Get the options used by the program and create the minimist options 218 | * to ensure that minimist parses the values properly. 219 | * @returns The options for minimist. 220 | */ 221 | getMinimistOptions() { 222 | let allOptions: Options = {}; 223 | allOptions = this.getOptions_(allOptions); 224 | let minimistOptions: MinimistArgs = {}; 225 | let minimistBoolean: string[] = []; 226 | let minimistString: string[] = []; 227 | let minimistNumber: string[] = []; 228 | let minimistDefault: any = {}; 229 | for (let opt in allOptions) { 230 | let option = allOptions[opt]; 231 | if (option.type === 'boolean') { 232 | minimistBoolean.push(option.opt); 233 | } else if (option.type === 'string') { 234 | minimistString.push(option.opt); 235 | } else if (option.type === 'number') { 236 | minimistNumber.push(option.opt); 237 | } 238 | if (typeof option.defaultValue !== 'undefined') { 239 | minimistDefault[option.opt] = option.defaultValue; 240 | } 241 | } 242 | minimistOptions['boolean'] = minimistBoolean; 243 | minimistOptions['string'] = minimistString; 244 | minimistOptions['number'] = minimistNumber; 245 | minimistOptions['default'] = minimistDefault; 246 | return minimistOptions; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /lib/cli_instance.ts: -------------------------------------------------------------------------------- 1 | import {Cli} from './cli'; 2 | import * as clean from './cmds/clean'; 3 | import * as shutdown from './cmds/shutdown'; 4 | import * as start from './cmds/start'; 5 | import * as status from './cmds/status'; 6 | import * as update from './cmds/update'; 7 | import * as version from './cmds/version'; 8 | 9 | export let cli = new Cli() 10 | .usage('webdriver-manager [options]') 11 | .program(clean.program) 12 | .program(start.program) 13 | .program(shutdown.program) 14 | .program(status.program) 15 | .program(update.program) 16 | .program(version.program); 17 | -------------------------------------------------------------------------------- /lib/cmds/clean.ts: -------------------------------------------------------------------------------- 1 | import * as minimist from 'minimist'; 2 | import * as path from 'path'; 3 | 4 | import {Options, Program} from '../cli'; 5 | import {Config} from '../config'; 6 | import {FileManager} from '../files'; 7 | 8 | import * as Opt from './'; 9 | import {Opts} from './opts'; 10 | 11 | let prog = new Program() 12 | .command('clean', 'removes all downloaded driver files from the out_dir') 13 | .action(clean) 14 | .addOption(Opts[Opt.OUT_DIR]); 15 | 16 | export var program = prog; 17 | 18 | // stand alone runner 19 | let argv = minimist(process.argv.slice(2), prog.getMinimistOptions()); 20 | if (argv._[0] === 'clean-run') { 21 | prog.run(JSON.parse(JSON.stringify(argv))); 22 | } else if (argv._[0] === 'clean-help') { 23 | prog.printHelp(); 24 | } 25 | 26 | /** 27 | * Parses the options and cleans the output directory of binaries. 28 | * @param: options 29 | */ 30 | function clean(options: Options): void { 31 | let outputDir = Config.getSeleniumDir(); 32 | if (options[Opt.OUT_DIR].getString()) { 33 | if (path.isAbsolute(options[Opt.OUT_DIR].getString())) { 34 | outputDir = options[Opt.OUT_DIR].getString(); 35 | } else { 36 | outputDir = path.resolve(Config.getBaseDir(), options[Opt.OUT_DIR].getString()); 37 | } 38 | } 39 | FileManager.removeExistingFiles(outputDir); 40 | } 41 | -------------------------------------------------------------------------------- /lib/cmds/index.ts: -------------------------------------------------------------------------------- 1 | export * from './opts'; 2 | -------------------------------------------------------------------------------- /lib/cmds/initialize.ts: -------------------------------------------------------------------------------- 1 | import {ChildProcess, spawnSync} from 'child_process'; 2 | import * as fs from 'fs'; 3 | import * as glob from 'glob'; 4 | import * as ini from 'ini'; 5 | import * as path from 'path'; 6 | import * as q from 'q'; 7 | 8 | import {Logger} from '../cli'; 9 | import {Config} from '../config'; 10 | import {spawn} from '../utils'; 11 | 12 | 13 | const noop = () => {}; 14 | 15 | // Make a function which configures a child process to automatically respond 16 | // to a certain question 17 | function respondFactory(question: string, answer: string, verbose: boolean): Function { 18 | return (child: ChildProcess) => { 19 | (child.stdin).setDefaultEncoding('utf-8'); 20 | child.stdout.on('data', (data: Buffer|String) => { 21 | if (data != null) { 22 | if (verbose) { 23 | process.stdout.write(data as string); 24 | } 25 | if (data.toString().indexOf(question) != -1) { 26 | child.stdin.write(answer + '\n'); 27 | } 28 | } 29 | }); 30 | }; 31 | } 32 | 33 | // Run a command on the android SDK 34 | function runAndroidSDKCommand( 35 | sdkPath: string, cmd: string, args: string[], stdio?: string, 36 | config_fun?: Function): q.Promise { 37 | let child = spawn(path.resolve(sdkPath, 'tools', 'android'), [cmd].concat(args), stdio); 38 | 39 | if (config_fun) { 40 | config_fun(child); 41 | }; 42 | 43 | let deferred = q.defer() 44 | child.on('exit', (code: number) => { 45 | if (deferred != null) { 46 | if (code) { 47 | deferred.reject(code); 48 | } else { 49 | deferred.resolve(); 50 | } 51 | deferred = null; 52 | } 53 | }); 54 | child.on('error', (err: Error) => { 55 | if (deferred != null) { 56 | deferred.reject(err); 57 | deferred = null; 58 | } 59 | }); 60 | return deferred.promise; 61 | } 62 | 63 | // Download updates via the android SDK 64 | function downloadAndroidUpdates( 65 | sdkPath: string, targets: string[], search_all: boolean, auto_accept: boolean, 66 | verbose: boolean): q.Promise { 67 | return runAndroidSDKCommand( 68 | sdkPath, 'update', 69 | ['sdk', '-u'].concat(search_all ? ['-a'] : []).concat(['-t', targets.join(',')]), 70 | auto_accept ? 'pipe' : 'inherit', 71 | auto_accept ? respondFactory('Do you accept the license', 'y', verbose) : noop); 72 | } 73 | 74 | // Setup hardware acceleration for x86-64 emulation 75 | function setupHardwareAcceleration(sdkPath: string) { 76 | // TODO(sjelin): linux setup 77 | let toolDir = path.resolve(sdkPath, 'extras', 'intel', 'Hardware_Accelerated_Execution_Manager'); 78 | if (Config.osType() == 'Darwin') { 79 | console.log('Enabling hardware acceleration (requires root access)'); 80 | // We don't need the wrapped spawnSync because we know we're on OSX 81 | spawnSync('sudo', ['silent_install.sh'], {stdio: 'inherit', cwd: toolDir}); 82 | } else if (Config.osType() == 'Windows_NT') { 83 | console.log('Enabling hardware acceleration (requires admin access)'); 84 | // We don't need the wrapped spawnSync because we know we're on windows 85 | spawnSync('silent_install.bat', [], {stdio: 'inherit', cwd: toolDir}); 86 | } 87 | } 88 | 89 | // Get a list of all the SDK download targets for a given set of APIs, 90 | // architectures, and platforms 91 | function getAndroidSDKTargets( 92 | apiLevels: string[], architectures: string[], platforms: string[], 93 | oldAVDs: string[]): string[] { 94 | function getSysImgTarget(architecture: string, platform: string, level: string) { 95 | if (platform.toUpperCase() == 'DEFAULT') { 96 | platform = 'android'; 97 | } 98 | return 'sys-img-' + architecture + '-' + platform + '-' + level; 99 | } 100 | 101 | let targets = apiLevels 102 | .map((level) => { 103 | return 'android-' + level; 104 | }) 105 | .concat(architectures.reduce((targets, architecture) => { 106 | return targets.concat.apply(targets, platforms.map((platform) => { 107 | return apiLevels.map(getSysImgTarget.bind(null, architecture, platform)); 108 | })); 109 | }, [])); 110 | 111 | oldAVDs.forEach((name) => { 112 | let avd = new AVDDescriptor(name); 113 | if (targets.indexOf(avd.api) == -1) { 114 | targets.push(avd.api); 115 | } 116 | let sysImgTarget = 117 | getSysImgTarget(avd.architecture, avd.platform, avd.api.slice('android-'.length)); 118 | if (targets.indexOf(sysImgTarget) == -1) { 119 | targets.push(sysImgTarget); 120 | } 121 | }); 122 | 123 | return targets; 124 | } 125 | 126 | // All the information about an android virtual device 127 | class AVDDescriptor { 128 | api: string; 129 | platform: string; 130 | architecture: string; 131 | abi: string; 132 | name: string; 133 | 134 | constructor(api: string, platform?: string, architecture?: string) { 135 | if (platform != undefined) { 136 | this.api = api; 137 | this.platform = platform; 138 | this.architecture = architecture; 139 | this.name = [api, platform, architecture].join('-'); 140 | } else { 141 | this.name = api; 142 | let nameParts = this.name.split('-'); 143 | this.api = nameParts[0] + '-' + nameParts[1]; 144 | if (/v[0-9]+[a-z]+/.test(nameParts[nameParts.length - 1]) && 145 | (nameParts[nameParts.length - 2].slice(0, 3) == 'arm')) { 146 | this.architecture = nameParts[nameParts.length - 2] + '-' + nameParts[nameParts.length - 1]; 147 | } else { 148 | this.architecture = nameParts[nameParts.length - 1]; 149 | } 150 | this.platform = this.name.slice(this.api.length + 1, -this.architecture.length - 1); 151 | } 152 | this.abi = 153 | (this.platform.toUpperCase() == 'DEFAULT' ? '' : this.platform + '/') + this.architecture; 154 | } 155 | 156 | avdName(version: string): string { 157 | return this.name + '-v' + version + '-wd-manager'; 158 | } 159 | } 160 | 161 | // Gets the descriptors for all AVDs which are possible to make given the 162 | // SDKs which were downloaded 163 | function getAVDDescriptors(sdkPath: string): q.Promise { 164 | let deferred = q.defer(); 165 | // `glob` package always prefers patterns to use `/` 166 | glob('system-images/*/*/*', {cwd: sdkPath}, (err: Error, files: string[]) => { 167 | if (err) { 168 | deferred.reject(err); 169 | } else { 170 | deferred.resolve(files.map((file: string) => { 171 | // `file` could use `/` or `\`, so we use `path.normalize` 172 | let info = path.normalize(file).split(path.sep).slice(-3); 173 | return new AVDDescriptor(info[0], info[1], info[2]); 174 | })); 175 | } 176 | }); 177 | return deferred.promise; 178 | } 179 | 180 | function sequentialForEach(array: T[], func: (x: T) => q.Promise): q.Promise { 181 | let ret = q(null); 182 | 183 | array.forEach((x: T) => { 184 | ret = ret.then(() => { 185 | return func(x); 186 | }); 187 | }); 188 | 189 | return ret; 190 | } 191 | 192 | // Configures the hardware.ini file for a system image of a new AVD 193 | function configureAVDHardware(sdkPath: string, desc: AVDDescriptor): q.Promise { 194 | let file = path.resolve( 195 | sdkPath, 'system-images', desc.api, desc.platform, desc.architecture, 'hardware.ini'); 196 | return q.nfcall(fs.stat, file) 197 | .then( 198 | (stats: fs.Stats) => { 199 | return q.nfcall(fs.readFile, file); 200 | }, 201 | (err: Error) => { 202 | return q(''); 203 | }) 204 | .then((contents: string|Buffer) => { 205 | let config: any = ini.parse(contents.toString()); 206 | config['hw.keyboard'] = 'yes'; 207 | config['hw.battery'] = 'yes'; 208 | config['hw.ramSize'] = 1024; 209 | return q.nfcall(fs.writeFile, file, ini.stringify(config)); 210 | }); 211 | } 212 | 213 | // Make an android virtual device 214 | function makeAVD( 215 | sdkPath: string, desc: AVDDescriptor, version: string, verbose: boolean): q.Promise { 216 | return runAndroidSDKCommand(sdkPath, 'delete', ['avd', '--name', desc.avdName(version)]) 217 | .then(noop, noop) 218 | .then(() => { 219 | return runAndroidSDKCommand( 220 | sdkPath, 'create', 221 | ['avd', '--name', desc.avdName(version), '--target', desc.api, '--abi', desc.abi], 222 | 'pipe', 223 | respondFactory('Do you wish to create a custom hardware profile', 'no', verbose)); 224 | }); 225 | } 226 | 227 | // Initialize the android SDK 228 | export function android( 229 | sdkPath: string, apiLevels: string[], architectures: string[], platforms: string[], 230 | acceptLicenses: boolean, version: string, oldAVDs: string[], logger: Logger, 231 | verbose: boolean): void { 232 | let avdDescriptors: AVDDescriptor[]; 233 | let tools = ['platform-tool', 'tool']; 234 | if ((Config.osType() == 'Darwin') || (Config.osType() == 'Windows_NT')) { 235 | tools.push('extra-intel-Hardware_Accelerated_Execution_Manager'); 236 | } 237 | 238 | logger.info('android-sdk: Downloading additional SDK updates'); 239 | downloadAndroidUpdates(sdkPath, tools, false, acceptLicenses, verbose) 240 | .then(() => { 241 | return setupHardwareAcceleration(sdkPath); 242 | }) 243 | .then(() => { 244 | logger.info( 245 | 'android-sdk: Downloading more additional SDK updates ' + 246 | '(this may take a while)'); 247 | return downloadAndroidUpdates( 248 | sdkPath, 249 | ['build-tools-24.0.0'].concat( 250 | getAndroidSDKTargets(apiLevels, architectures, platforms, oldAVDs)), 251 | true, acceptLicenses, verbose); 252 | }) 253 | .then(() => { 254 | return getAVDDescriptors(sdkPath); 255 | }) 256 | .then((descriptors: AVDDescriptor[]) => { 257 | avdDescriptors = descriptors; 258 | logger.info('android-sdk: Configuring virtual device hardware'); 259 | return sequentialForEach(avdDescriptors, (descriptor: AVDDescriptor) => { 260 | return configureAVDHardware(sdkPath, descriptor); 261 | }); 262 | }) 263 | .then(() => { 264 | return sequentialForEach(avdDescriptors, (descriptor: AVDDescriptor) => { 265 | logger.info('android-sdk: Setting up virtual device "' + descriptor.name + '"'); 266 | return makeAVD(sdkPath, descriptor, version, verbose); 267 | }); 268 | }) 269 | .then(() => { 270 | return q.nfcall( 271 | fs.writeFile, path.resolve(sdkPath, 'available_avds.json'), 272 | JSON.stringify(avdDescriptors.map((descriptor: AVDDescriptor) => { 273 | return descriptor.name; 274 | }))); 275 | }) 276 | .then(() => { 277 | logger.info('android-sdk: Initialization complete'); 278 | }) 279 | .done(); 280 | }; 281 | 282 | export function iOS(logger: Logger) { 283 | if (Config.osType() != 'Darwin') { 284 | throw new Error('Must be on a Mac to simulate iOS devices.'); 285 | } 286 | try { 287 | fs.statSync('/Applications/Xcode.app'); 288 | } catch (e) { 289 | logger.warn('You must install the xcode commandline tools!'); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /lib/cmds/opts.ts: -------------------------------------------------------------------------------- 1 | import {AndroidSDK, Appium, ChromeDriver, GeckoDriver, IEDriver, Standalone} from '../binaries'; 2 | import {Cli, Option, Options} from '../cli'; 3 | import {Config} from '../config'; 4 | 5 | export const OUT_DIR = 'out_dir'; 6 | export const SELENIUM_PORT = 'seleniumPort'; 7 | export const APPIUM_PORT = 'appium-port'; 8 | export const AVD_PORT = 'avd-port'; 9 | export const IGNORE_SSL = 'ignore_ssl'; 10 | export const PROXY = 'proxy'; 11 | export const ALTERNATE_CDN = 'alternate_cdn'; 12 | export const STANDALONE = 'standalone'; 13 | export const CHROME = 'chrome'; 14 | export const IE = 'ie'; 15 | export const IE32 = 'ie32'; 16 | export const IE64 = 'ie64'; 17 | export const EDGE = 'edge'; 18 | export const GECKO = 'gecko'; 19 | export const ANDROID = 'android'; 20 | export const IOS = 'ios'; 21 | export const VERSIONS_CHROME = 'versions.chrome'; 22 | export const VERSIONS_GECKO = 'versions.gecko'; 23 | export const VERSIONS_STANDALONE = 'versions.standalone'; 24 | export const VERSIONS_IE = 'versions.ie'; 25 | export const VERSIONS_ANDROID = 'versions.android'; 26 | export const VERSIONS_APPIUM = 'versions.appium'; 27 | export const CHROME_LOGS = 'chrome_logs'; 28 | export const LOGGING = 'logging'; 29 | export const ANDROID_API_LEVELS = 'android-api-levels'; 30 | export const ANDROID_ARCHITECTURES = 'android-archs'; 31 | export const ANDROID_PLATFORMS = 'android-platorms'; 32 | export const ANDROID_ACCEPT_LICENSES = 'android-accept-licenses'; 33 | export const AVDS = 'avds'; 34 | export const AVD_USE_SNAPSHOTS = 'avd-use-snapshots'; 35 | export const STARTED_SIGNIFIER = 'started-signifier'; 36 | export const SIGNAL_VIA_IPC = 'signal-via-ipc'; 37 | export const DETACH = 'detach'; 38 | export const QUIET = 'quiet'; 39 | export const VERBOSE = 'verbose'; 40 | export const ALREADY_OFF_ERROR = 'already-off-error'; 41 | 42 | /** 43 | * The options used by the commands. 44 | */ 45 | var opts: Options = {}; 46 | opts[OUT_DIR] = new Option(OUT_DIR, 'Location to output/expect', 'string', Config.getSeleniumDir()); 47 | opts[SELENIUM_PORT] = 48 | new Option(SELENIUM_PORT, 'Optional port for the selenium standalone server', 'string', '4444'); 49 | opts[APPIUM_PORT] = 50 | new Option(APPIUM_PORT, 'Optional port for the appium server', 'string', '4723'); 51 | opts[AVD_PORT] = new Option( 52 | AVD_PORT, 'Optional port for android virtual devices. See mobile.md for details', 'number', 53 | 5554); 54 | opts[IGNORE_SSL] = new Option(IGNORE_SSL, 'Ignore SSL certificates', 'boolean', false); 55 | opts[PROXY] = new Option(PROXY, 'Proxy to use for the install or update command', 'string'); 56 | opts[ALTERNATE_CDN] = new Option(ALTERNATE_CDN, 'Alternate CDN to binaries', 'string'); 57 | opts[STANDALONE] = new Option( 58 | STANDALONE, 'Install or update selenium standalone', 'boolean', Standalone.isDefault); 59 | opts[CHROME] = 60 | new Option(CHROME, 'Install or update chromedriver', 'boolean', ChromeDriver.isDefault); 61 | opts[GECKO] = new Option(GECKO, 'Install or update geckodriver', 'boolean', GeckoDriver.isDefault); 62 | opts[IE] = new Option(IE, 'Install or update 32-bit ie driver', 'boolean', IEDriver.isDefault32); 63 | opts[IE32] = 64 | new Option(IE32, 'Install or update 32-bit ie driver', 'boolean', IEDriver.isDefault32); 65 | opts[IE64] = new Option( 66 | IE64, 'Update: install or update 64-bit IE driver. Start: use installed x64 IE driver.', 67 | 'boolean', IEDriver.isDefault64); 68 | opts[EDGE] = new Option( 69 | EDGE, 'Use installed Microsoft Edge driver', 'string', 70 | 'C:\\Program Files (x86)\\Microsoft Web Driver\\MicrosoftWebDriver.exe'); 71 | opts[ANDROID] = new Option(ANDROID, 'Update/use the android sdk', 'boolean', AndroidSDK.isDefault); 72 | opts[IOS] = new Option(IOS, 'Update the iOS sdk', 'boolean', false); 73 | opts[VERSIONS_CHROME] = new Option( 74 | VERSIONS_CHROME, 75 | 'Optional chrome driver version (use \'latest\' to get the most recent version)', 'string', 76 | 'latest'); 77 | opts[VERSIONS_GECKO] = 78 | new Option(VERSIONS_GECKO, 'Optional gecko driver version', 'string', 'latest'); 79 | opts[VERSIONS_ANDROID] = new Option( 80 | VERSIONS_ANDROID, 'Optional android sdk version', 'string', AndroidSDK.versionDefault); 81 | opts[VERSIONS_STANDALONE] = new Option( 82 | VERSIONS_STANDALONE, 83 | 'Optional seleniuim standalone server version (use \'latest\' to get the most recent version)', 84 | 'string', 'latest'); 85 | opts[VERSIONS_APPIUM] = 86 | new Option(VERSIONS_APPIUM, 'Optional appium version', 'string', Appium.versionDefault); 87 | opts[VERSIONS_IE] = new Option( 88 | VERSIONS_IE, 89 | 'Optional internet explorer driver version (use \'latest\' to get the most recent version)', 90 | 'string', 'latest'); 91 | opts[CHROME_LOGS] = new Option(CHROME_LOGS, 'File path to chrome logs', 'string', undefined); 92 | opts[LOGGING] = new Option(LOGGING, 'File path to logging properties file', 'string', undefined); 93 | opts[ANDROID_API_LEVELS] = new Option( 94 | ANDROID_API_LEVELS, 'Which versions of the android API you want to emulate', 'string', 95 | AndroidSDK.DEFAULT_API_LEVELS); 96 | opts[ANDROID_ARCHITECTURES] = new Option( 97 | ANDROID_ARCHITECTURES, 98 | 'Which architectures you want to use in android emulation. By default it will try to match os.arch()', 99 | 'string', AndroidSDK.DEFAULT_ARCHITECTURES); 100 | opts[ANDROID_PLATFORMS] = new Option( 101 | ANDROID_PLATFORMS, 'Which platforms you want to use in android emulation', 'string', 102 | AndroidSDK.DEFAULT_PLATFORMS); 103 | opts[ANDROID_ACCEPT_LICENSES] = 104 | new Option(ANDROID_ACCEPT_LICENSES, 'Automatically accept android licenses', 'boolean', false); 105 | opts[AVDS] = new Option( 106 | AVDS, 107 | 'Android virtual devices to emulate. Use "all" for emulating all possible devices, and "none" for no devices', 108 | 'string', 'all'); 109 | opts[AVD_USE_SNAPSHOTS] = new Option( 110 | AVD_USE_SNAPSHOTS, 111 | 'Rather than booting a new AVD every time, save/load snapshots of the last time it was used', 112 | 'boolean', true); 113 | opts[STARTED_SIGNIFIER] = new Option( 114 | STARTED_SIGNIFIER, 115 | 'A string to be outputted once the selenium server is up and running. Useful if you are writing a script which uses webdriver-manager.', 116 | 'string'); 117 | opts[SIGNAL_VIA_IPC] = new Option( 118 | SIGNAL_VIA_IPC, 119 | 'If you are using --' + STARTED_SIGNIFIER + 120 | ', this flag will emit the signal string using process.send(), rather than writing it to stdout', 121 | 'boolean', false); 122 | opts[DETACH] = new Option( 123 | DETACH, 124 | 'Once the selenium server is up and running, return control to the parent process and continue running the server in the background.', 125 | 'boolean', false); 126 | opts[VERBOSE] = new Option(VERBOSE, 'Extra console output', 'boolean', false); 127 | opts[QUIET] = new Option(QUIET, 'Minimal console output', 'boolean', false); 128 | opts[ALREADY_OFF_ERROR] = new Option( 129 | ALREADY_OFF_ERROR, 130 | 'Normally if you try to shut down a selenium which is not running, you will get a warning. This turns it into an error', 131 | 'boolean', false); 132 | 133 | export var Opts = opts; 134 | -------------------------------------------------------------------------------- /lib/cmds/shutdown.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import * as minimist from 'minimist'; 3 | 4 | import {Logger, Options, Program} from '../cli'; 5 | 6 | import * as Opt from './'; 7 | import {Opts} from './opts'; 8 | 9 | 10 | let logger = new Logger('shutdown'); 11 | let prog = new Program() 12 | .command('shutdown', 'shut down the selenium server') 13 | .action(shutdown) 14 | .addOption(Opts[Opt.SELENIUM_PORT]) 15 | .addOption(Opts[Opt.ALREADY_OFF_ERROR]); 16 | 17 | export var program = prog; 18 | 19 | // stand alone runner 20 | let argv = minimist(process.argv.slice(2), prog.getMinimistOptions()); 21 | if (argv._[0] === 'shutdown-run') { 22 | prog.run(JSON.parse(JSON.stringify(argv))); 23 | } else if (argv._[0] === 'shutdown-help') { 24 | prog.printHelp(); 25 | } 26 | 27 | /** 28 | * Parses the options and starts the selenium standalone server. 29 | * @param options 30 | */ 31 | function shutdown(options: Options) { 32 | logger.info('Attempting to shut down selenium nicely'); 33 | http.get( 34 | 'http://localhost:' + options[Opt.SELENIUM_PORT].getString() + 35 | '/selenium-server/driver/?cmd=shutDownSeleniumServer') 36 | .on('error', (e: NodeJS.ErrnoException) => { 37 | if ((e.code == 'ECONNREFUSED') && (e.syscall == 'connect')) { 38 | if (!options[Opt.ALREADY_OFF_ERROR].getBoolean()) { 39 | logger.warn('Server does not appear to be on'); 40 | } else { 41 | logger.error('Server unreachable, probably not running'); 42 | throw e; 43 | } 44 | } else { 45 | throw e; 46 | } 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /lib/cmds/status.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as minimist from 'minimist'; 3 | import * as path from 'path'; 4 | import * as semver from 'semver'; 5 | 6 | import {AndroidSDK, Appium, ChromeDriver, GeckoDriver, IEDriver, Standalone} from '../binaries'; 7 | import {getValidSemver} from '../binaries/chrome_xml'; 8 | import {Logger, Options, Program} from '../cli'; 9 | import {Config} from '../config'; 10 | import {FileManager} from '../files'; 11 | 12 | import * as Opt from './'; 13 | import {Opts} from './opts'; 14 | 15 | let logger = new Logger('status'); 16 | let prog = new Program() 17 | .command('status', 'list the current available drivers') 18 | .addOption(Opts[Opt.OUT_DIR]) 19 | .action(status); 20 | 21 | export var program = prog; 22 | 23 | // stand alone runner 24 | let argv = minimist(process.argv.slice(2), prog.getMinimistOptions()); 25 | if (argv._[0] === 'status-run') { 26 | prog.run(JSON.parse(JSON.stringify(argv))); 27 | } else if (argv._[0] === 'status-help') { 28 | prog.printHelp(); 29 | } 30 | 31 | /** 32 | * Parses the options and logs the status of the binaries downloaded. 33 | * @param options 34 | */ 35 | function status(options: Options) { 36 | let binaries = FileManager.setupBinaries(); 37 | let outputDir = Config.getSeleniumDir(); 38 | if (options[Opt.OUT_DIR].value) { 39 | if (path.isAbsolute(options[Opt.OUT_DIR].getString())) { 40 | outputDir = options[Opt.OUT_DIR].getString(); 41 | } else { 42 | outputDir = path.resolve(Config.getBaseDir(), options[Opt.OUT_DIR].getString()); 43 | } 44 | } 45 | 46 | try { 47 | // check if folder exists 48 | fs.statSync(outputDir).isDirectory(); 49 | } catch (e) { 50 | // if the folder does not exist, quit early. 51 | logger.warn('the out_dir path ' + outputDir + ' does not exist'); 52 | return; 53 | } 54 | 55 | // Try to get the update-config.json. This will be used for showing the last binary downloaded. 56 | let updateConfig: any = {}; 57 | try { 58 | updateConfig = 59 | JSON.parse(fs.readFileSync(path.resolve(outputDir, 'update-config.json')).toString()) || {}; 60 | } catch (err) { 61 | updateConfig = {}; 62 | } 63 | 64 | let downloadedBinaries = FileManager.downloadedBinaries(outputDir); 65 | 66 | // Log which binaries have been downloaded. 67 | for (let bin in downloadedBinaries) { 68 | let downloaded = downloadedBinaries[bin]; 69 | let log = downloaded.name + ' '; 70 | log += downloaded.versions.length == 1 ? 'version available: ' : 'versions available: '; 71 | 72 | // Get the "last" downloaded binary from the updateConfig. 73 | let last: string = null; 74 | if (downloaded.binary instanceof Appium && updateConfig[Appium.id]) { 75 | last = updateConfig[Appium.id]['last']; 76 | } else if (downloaded.binary instanceof AndroidSDK && updateConfig[AndroidSDK.id]) { 77 | last = updateConfig[AndroidSDK.id]['last']; 78 | } else if (downloaded.binary instanceof ChromeDriver && updateConfig[ChromeDriver.id]) { 79 | last = updateConfig[ChromeDriver.id]['last']; 80 | } else if (downloaded.binary instanceof GeckoDriver && updateConfig[GeckoDriver.id]) { 81 | last = updateConfig[GeckoDriver.id]['last']; 82 | } else if (downloaded.binary instanceof IEDriver && updateConfig[IEDriver.id]) { 83 | last = updateConfig[IEDriver.id]['last']; 84 | } else if (downloaded.binary instanceof Standalone && updateConfig[Standalone.id]) { 85 | last = updateConfig[Standalone.id]['last']; 86 | } 87 | 88 | // Sort the versions then log them: 89 | // - last: the last binary downloaded by webdriver-manager per the update-config.json 90 | downloaded.versions = downloaded.versions.sort((a: string, b: string): number => { 91 | if (!semver.valid(a)) { 92 | a = getValidSemver(a); 93 | b = getValidSemver(b); 94 | } 95 | if (semver.gt(a, b)) { 96 | return 1; 97 | } else { 98 | return 0; 99 | } 100 | }); 101 | for (let ver in downloaded.versions) { 102 | let version = downloaded.versions[ver]; 103 | log += version; 104 | if (last && last.indexOf(version) >= 0) { 105 | log += ' [last]' 106 | } 107 | if (+ver != downloaded.versions.length - 1) { 108 | log += ', '; 109 | } 110 | } 111 | logger.info(log); 112 | } 113 | // for binaries that are available for the operating system, show them here 114 | for (let bin in binaries) { 115 | if (downloadedBinaries[bin] == null) { 116 | logger.info(binaries[bin].name + ' is not present'); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/cmds/update.ts: -------------------------------------------------------------------------------- 1 | import * as AdmZip from 'adm-zip'; 2 | import * as child_process from 'child_process'; 3 | import * as fs from 'fs'; 4 | import * as minimist from 'minimist'; 5 | import * as path from 'path'; 6 | import * as q from 'q'; 7 | import * as rimraf from 'rimraf'; 8 | 9 | import {AndroidSDK, Appium, Binary, ChromeDriver, GeckoDriver, IEDriver, Standalone} from '../binaries'; 10 | import {Logger, Options, Program} from '../cli'; 11 | import {Config} from '../config'; 12 | import {Downloader, FileManager} from '../files'; 13 | import {HttpUtils} from '../http_utils'; 14 | import {spawn} from '../utils'; 15 | 16 | import * as Opt from './'; 17 | import {android as initializeAndroid, iOS as checkIOS} from './initialize'; 18 | import {Opts} from './opts'; 19 | 20 | Config.runCommand = 'update'; 21 | 22 | let logger = new Logger('update'); 23 | let prog = new Program() 24 | .command('update', 'install or update selected binaries') 25 | .action(update) 26 | .addOption(Opts[Opt.OUT_DIR]) 27 | .addOption(Opts[Opt.VERBOSE]) 28 | .addOption(Opts[Opt.IGNORE_SSL]) 29 | .addOption(Opts[Opt.PROXY]) 30 | .addOption(Opts[Opt.ALTERNATE_CDN]) 31 | .addOption(Opts[Opt.STANDALONE]) 32 | .addOption(Opts[Opt.CHROME]) 33 | .addOption(Opts[Opt.GECKO]) 34 | .addOption(Opts[Opt.ANDROID]) 35 | .addOption(Opts[Opt.ANDROID_API_LEVELS]) 36 | .addOption(Opts[Opt.ANDROID_ARCHITECTURES]) 37 | .addOption(Opts[Opt.ANDROID_PLATFORMS]) 38 | .addOption(Opts[Opt.ANDROID_ACCEPT_LICENSES]); 39 | 40 | if (Config.osType() === 'Darwin') { 41 | prog.addOption(Opts[Opt.IOS]); 42 | } 43 | 44 | if (Config.osType() === 'Windows_NT') { 45 | prog.addOption(Opts[Opt.IE]).addOption(Opts[Opt.IE32]).addOption(Opts[Opt.IE64]); 46 | } 47 | 48 | prog.addOption(Opts[Opt.VERSIONS_STANDALONE]) 49 | .addOption(Opts[Opt.VERSIONS_CHROME]) 50 | .addOption(Opts[Opt.VERSIONS_APPIUM]) 51 | .addOption(Opts[Opt.VERSIONS_ANDROID]) 52 | .addOption(Opts[Opt.VERSIONS_GECKO]); 53 | 54 | if (Config.osType() === 'Windows_NT') { 55 | prog.addOption(Opts[Opt.VERSIONS_IE]); 56 | } 57 | export let program = prog; 58 | 59 | // stand alone runner 60 | let argv = minimist(process.argv.slice(2), prog.getMinimistOptions()); 61 | if (argv._[0] === 'update-run') { 62 | prog.run(JSON.parse(JSON.stringify(argv))); 63 | } else if (argv._[0] === 'update-help') { 64 | prog.printHelp(); 65 | } 66 | 67 | let browserFile: BrowserFile; 68 | 69 | /** 70 | * Parses the options and downloads binaries if they do not exist. 71 | * @param options 72 | */ 73 | function update(options: Options): Promise { 74 | let promises: q.IPromise[] = []; 75 | let standalone = options[Opt.STANDALONE].getBoolean(); 76 | let chrome = options[Opt.CHROME].getBoolean(); 77 | let gecko = options[Opt.GECKO].getBoolean(); 78 | let ie32: boolean = false; 79 | let ie64: boolean = false; 80 | if (options[Opt.IE]) { 81 | ie32 = ie32 || options[Opt.IE].getBoolean(); 82 | } 83 | if (options[Opt.IE32]) { 84 | ie32 = ie32 || options[Opt.IE32].getBoolean(); 85 | } 86 | if (options[Opt.IE64]) { 87 | ie64 = options[Opt.IE64].getBoolean(); 88 | } 89 | let android: boolean = options[Opt.ANDROID].getBoolean(); 90 | let ios: boolean = false; 91 | if (options[Opt.IOS]) { 92 | ios = options[Opt.IOS].getBoolean(); 93 | } 94 | let outputDir = options[Opt.OUT_DIR].getString(); 95 | 96 | try { 97 | browserFile = 98 | JSON.parse(fs.readFileSync(path.resolve(outputDir, 'update-config.json')).toString()); 99 | } catch (err) { 100 | browserFile = {}; 101 | } 102 | 103 | let android_api_levels: string[] = options[Opt.ANDROID_API_LEVELS].getString().split(','); 104 | let android_architectures: string[] = options[Opt.ANDROID_ARCHITECTURES].getString().split(','); 105 | let android_platforms: string[] = options[Opt.ANDROID_PLATFORMS].getString().split(','); 106 | let android_accept_licenses: boolean = options[Opt.ANDROID_ACCEPT_LICENSES].getBoolean(); 107 | if (options[Opt.OUT_DIR].getString()) { 108 | if (path.isAbsolute(options[Opt.OUT_DIR].getString())) { 109 | outputDir = options[Opt.OUT_DIR].getString(); 110 | } else { 111 | outputDir = path.resolve(Config.getBaseDir(), options[Opt.OUT_DIR].getString()); 112 | } 113 | FileManager.makeOutputDirectory(outputDir); 114 | } 115 | let ignoreSSL = options[Opt.IGNORE_SSL].getBoolean(); 116 | let proxy = options[Opt.PROXY].getString(); 117 | HttpUtils.assignOptions({ignoreSSL, proxy}); 118 | let verbose = options[Opt.VERBOSE].getBoolean(); 119 | 120 | // setup versions for binaries 121 | let binaries = FileManager.setupBinaries(options[Opt.ALTERNATE_CDN].getString()); 122 | binaries[Standalone.id].versionCustom = options[Opt.VERSIONS_STANDALONE].getString(); 123 | binaries[ChromeDriver.id].versionCustom = options[Opt.VERSIONS_CHROME].getString(); 124 | if (options[Opt.VERSIONS_IE]) { 125 | binaries[IEDriver.id].versionCustom = options[Opt.VERSIONS_IE].getString(); 126 | } 127 | if (options[Opt.VERSIONS_GECKO]) { 128 | binaries[GeckoDriver.id].versionCustom = options[Opt.VERSIONS_GECKO].getString(); 129 | } 130 | binaries[AndroidSDK.id].versionCustom = options[Opt.VERSIONS_ANDROID].getString(); 131 | binaries[Appium.id].versionCustom = options[Opt.VERSIONS_APPIUM].getString(); 132 | 133 | // if the file has not been completely downloaded, download it 134 | // else if the file has already been downloaded, unzip the file, rename it, and give it 135 | // permissions 136 | if (standalone) { 137 | let binary: Standalone = binaries[Standalone.id]; 138 | promises.push(FileManager.downloadFile(binary, outputDir) 139 | .then((downloaded: boolean) => { 140 | if (!downloaded) { 141 | logger.info( 142 | binary.name + ': file exists ' + 143 | path.resolve(outputDir, binary.filename())); 144 | logger.info(binary.name + ': ' + binary.filename() + ' up to date'); 145 | } 146 | }) 147 | .then(() => { 148 | updateBrowserFile(binary, outputDir); 149 | })); 150 | } 151 | if (chrome) { 152 | let binary: ChromeDriver = binaries[ChromeDriver.id]; 153 | promises.push(updateBinary(binary, outputDir, proxy, ignoreSSL).then(() => { 154 | return Promise.resolve(updateBrowserFile(binary, outputDir)); 155 | })); 156 | } 157 | if (gecko) { 158 | let binary: GeckoDriver = binaries[GeckoDriver.id]; 159 | promises.push(updateBinary(binary, outputDir, proxy, ignoreSSL).then(() => { 160 | return Promise.resolve(updateBrowserFile(binary, outputDir)); 161 | })); 162 | } 163 | if (ie64) { 164 | let binary: IEDriver = binaries[IEDriver.id]; 165 | binary.osarch = Config.osArch(); // Win32 or x64 166 | promises.push(updateBinary(binary, outputDir, proxy, ignoreSSL).then(() => { 167 | return Promise.resolve(updateBrowserFile(binary, outputDir)); 168 | })); 169 | } 170 | if (ie32) { 171 | let binary: IEDriver = binaries[IEDriver.id]; 172 | binary.osarch = 'Win32'; 173 | promises.push(updateBinary(binary, outputDir, proxy, ignoreSSL).then(() => { 174 | return Promise.resolve(updateBrowserFile(binary, outputDir)); 175 | })); 176 | } 177 | if (android) { 178 | let binary = binaries[AndroidSDK.id]; 179 | let sdk_path = path.resolve(outputDir, binary.executableFilename()); 180 | let oldAVDList: string; 181 | 182 | updateBrowserFile(binary, outputDir); 183 | promises.push(q.nfcall(fs.readFile, path.resolve(sdk_path, 'available_avds.json')) 184 | .then( 185 | (oldAVDs: string) => { 186 | oldAVDList = oldAVDs; 187 | }, 188 | () => { 189 | oldAVDList = '[]'; 190 | }) 191 | .then(() => { 192 | return updateBinary(binary, outputDir, proxy, ignoreSSL); 193 | }) 194 | .then(() => { 195 | initializeAndroid( 196 | path.resolve(outputDir, binary.executableFilename()), 197 | android_api_levels, android_architectures, android_platforms, 198 | android_accept_licenses, binaries[AndroidSDK.id].versionCustom, 199 | JSON.parse(oldAVDList), logger, verbose); 200 | })); 201 | } 202 | if (ios) { 203 | checkIOS(logger); 204 | } 205 | if (android || ios) { 206 | installAppium(binaries[Appium.id], outputDir); 207 | updateBrowserFile(binaries[Appium.id], outputDir); 208 | } 209 | 210 | return Promise.all(promises).then(() => { 211 | writeBrowserFile(outputDir); 212 | }); 213 | } 214 | 215 | function updateBinary( 216 | binary: T, outputDir: string, proxy: string, ignoreSSL: boolean): Promise { 217 | return FileManager 218 | .downloadFile( 219 | binary, outputDir, 220 | (binary: Binary, outputDir: string, fileName: string) => { 221 | unzip(binary, outputDir, fileName); 222 | }) 223 | .then(downloaded => { 224 | if (!downloaded) { 225 | // The file did not have to download, we should unzip it. 226 | logger.info(binary.name + ': file exists ' + path.resolve(outputDir, binary.filename())); 227 | let fileName = binary.filename(); 228 | unzip(binary, outputDir, fileName); 229 | logger.info(binary.name + ': ' + binary.executableFilename() + ' up to date'); 230 | } 231 | }); 232 | } 233 | 234 | function unzip(binary: T, outputDir: string, fileName: string): void { 235 | // remove the previously saved file and unzip it 236 | let osType = Config.osType(); 237 | let mv = path.resolve(outputDir, binary.executableFilename()); 238 | try { 239 | fs.unlinkSync(mv); 240 | } catch (err) { 241 | try { 242 | rimraf.sync(mv); 243 | } catch (err2) { 244 | } 245 | } 246 | 247 | // unzip the file 248 | logger.info(binary.name + ': unzipping ' + fileName); 249 | if (fileName.slice(-4) == '.zip') { 250 | try { 251 | let zip = new AdmZip(path.resolve(outputDir, fileName)); 252 | zip.extractAllTo(outputDir, true); 253 | } catch (e) { 254 | throw new Error(`Invalid filename: ${path.resolve(outputDir, fileName)}`) 255 | } 256 | } else { 257 | // We will only ever get .tar files on linux 258 | child_process.spawnSync('tar', ['zxvf', path.resolve(outputDir, fileName), '-C', outputDir]); 259 | } 260 | 261 | // rename 262 | fs.renameSync(path.resolve(outputDir, binary.zipContentName()), mv); 263 | 264 | // set permissions 265 | if (osType !== 'Windows_NT') { 266 | logger.info(binary.name + ': setting permissions to 0755 for ' + mv); 267 | if (binary.id() !== AndroidSDK.id) { 268 | fs.chmodSync(mv, '0755'); 269 | } else { 270 | fs.chmodSync(path.resolve(mv, 'tools', 'android'), '0755'); 271 | fs.chmodSync(path.resolve(mv, 'tools', 'emulator'), '0755'); 272 | // TODO(sjelin): get 64 bit versions working 273 | } 274 | } 275 | } 276 | 277 | function installAppium(binary: Binary, outputDir: string): void { 278 | logger.info('appium: installing appium'); 279 | 280 | let folder = path.resolve(outputDir, binary.filename()); 281 | try { 282 | rimraf.sync(folder); 283 | } catch (err) { 284 | } 285 | 286 | fs.mkdirSync(folder); 287 | fs.writeFileSync( 288 | path.resolve(folder, 'package.json'), JSON.stringify({scripts: {appium: 'appium'}})); 289 | spawn('npm', ['install', 'appium@' + binary.version()], null, {cwd: folder}); 290 | } 291 | 292 | interface BinaryPath { 293 | last?: string, all?: string[] 294 | } 295 | 296 | interface BrowserFile { 297 | chrome?: BinaryPath, standalone?: BinaryPath, gecko?: BinaryPath, iedriver?: BinaryPath 298 | } 299 | 300 | function updateBrowserFile(binary: T, outputDir: string) { 301 | let currentDownload = path.resolve(outputDir, binary.executableFilename()); 302 | 303 | // if browserFile[id] exists, we should update it 304 | if ((browserFile as any)[binary.id()]) { 305 | let binaryPath: BinaryPath = (browserFile as any)[binary.id()]; 306 | if (binaryPath.last === currentDownload) { 307 | return; 308 | } else { 309 | binaryPath.last = currentDownload; 310 | for (let bin of binaryPath.all) { 311 | if (bin === currentDownload) { 312 | return; 313 | } 314 | } 315 | binaryPath.all.push(currentDownload); 316 | } 317 | } else { 318 | // The browserFile[id] does not exist / has not been downloaded previously. 319 | // We should create the entry. 320 | let binaryPath: BinaryPath = {last: currentDownload, all: [currentDownload]}; 321 | (browserFile as any)[binary.id()] = binaryPath; 322 | } 323 | } 324 | 325 | function writeBrowserFile(outputDir: string) { 326 | let filePath = path.resolve(outputDir, 'update-config.json'); 327 | fs.writeFileSync(filePath, JSON.stringify(browserFile)); 328 | } 329 | 330 | // for testing 331 | export function clearBrowserFile() { 332 | browserFile = {}; 333 | } 334 | -------------------------------------------------------------------------------- /lib/cmds/version.ts: -------------------------------------------------------------------------------- 1 | import * as minimist from 'minimist'; 2 | import {Logger, Options, Program} from '../cli'; 3 | import {Config} from '../config'; 4 | 5 | import * as Opt from './'; 6 | import {Opts} from './opts'; 7 | 8 | let logger = new Logger('version'); 9 | 10 | let prog = new Program().command('version', 'get the current version').action(getVersion); 11 | 12 | export let program = prog; 13 | 14 | // stand alone runner 15 | let argv = minimist(process.argv.slice(2), prog.getMinimistOptions()); 16 | if (argv._[0] === 'version-run') { 17 | prog.run(JSON.parse(JSON.stringify(argv))); 18 | } 19 | 20 | function getVersion(): void { 21 | logger.info('webdriver-manager', Config.getVersion()); 22 | } 23 | -------------------------------------------------------------------------------- /lib/config.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as os from 'os'; 3 | import * as path from 'path'; 4 | 5 | import {Logger} from './cli'; 6 | 7 | 8 | let logger = new Logger('config'); 9 | 10 | export interface ConfigFile { 11 | [key: string]: string; 12 | selenium?: string; 13 | chrome?: string; 14 | gecko?: string; 15 | ie?: string; 16 | android?: string; 17 | appium?: string; 18 | maxChrome?: string; 19 | } 20 | 21 | /** 22 | * The configuration for webdriver-manager 23 | * 24 | * The config.json, package.json, and selenium directory are found in the 25 | * same location at the root directory in webdriver-manager. 26 | * 27 | */ 28 | export class Config { 29 | static runCommand: string; 30 | 31 | static configFile: string = 'config.json'; 32 | static packageFile: string = 'package.json'; 33 | static nodeModuleName = 'webdriver-manager'; 34 | 35 | static cwd = process.cwd(); 36 | static localInstall: string; 37 | static parentPath = path.resolve(Config.cwd, '..'); 38 | static dir = __dirname; 39 | static folder = Config.cwd.replace(Config.parentPath, '').substring(1); 40 | 41 | static isProjectVersion = Config.folder === Config.nodeModuleName; 42 | static isLocalVersion = false; 43 | 44 | static osArch_ = os.arch(); 45 | static osType_ = os.type(); 46 | static noProxy_ = process.env.NO_PROXY || process.env.no_proxy; 47 | static httpsProxy_ = process.env.HTTPS_PROXY || process.env.https_proxy; 48 | static httpProxy_ = process.env.HTTP_PROXY || process.env.http_proxy; 49 | 50 | static osArch(): string { 51 | return Config.osArch_; 52 | } 53 | 54 | static osType(): string { 55 | return Config.osType_; 56 | } 57 | 58 | static noProxy(): string { 59 | return Config.noProxy_; 60 | } 61 | 62 | static httpProxy(): string { 63 | return Config.httpProxy_; 64 | } 65 | 66 | static httpsProxy(): string { 67 | return Config.httpsProxy_; 68 | } 69 | 70 | static getConfigFile_(): string { 71 | return path.resolve(Config.dir, '..', Config.configFile); 72 | } 73 | 74 | static getPackageFile_(): string { 75 | return path.resolve(Config.dir, '..', Config.packageFile) 76 | } 77 | 78 | static getSeleniumDir(): string { 79 | return path.resolve(Config.dir, '..', '..', 'selenium/'); 80 | } 81 | static getBaseDir(): string { 82 | return path.resolve(Config.dir, '..', '..'); 83 | } 84 | 85 | /** 86 | * Get the binary versions from the configuration file. 87 | * @returns A map of the versions defined in the configuration file. 88 | */ 89 | static binaryVersions(): ConfigFile { 90 | let configFile = require(Config.getConfigFile_()); 91 | let configVersions: ConfigFile = {}; 92 | configVersions.selenium = configFile.webdriverVersions.selenium; 93 | configVersions.chrome = configFile.webdriverVersions.chromedriver; 94 | configVersions.gecko = configFile.webdriverVersions.geckodriver; 95 | configVersions.ie = configFile.webdriverVersions.iedriver; 96 | configVersions.android = configFile.webdriverVersions.androidsdk; 97 | configVersions.appium = configFile.webdriverVersions.appium; 98 | configVersions.maxChrome = configFile.webdriverVersions.maxChromedriver; 99 | return configVersions; 100 | } 101 | 102 | /** 103 | * Get the CDN urls from the configuration file. 104 | * @returns A map of the CDN versions defined in the configuration file. 105 | */ 106 | static cdnUrls(): ConfigFile { 107 | let configFile = require(Config.getConfigFile_()); 108 | let configCdnUrls: ConfigFile = {}; 109 | configCdnUrls.selenium = configFile.cdnUrls.selenium; 110 | configCdnUrls.chrome = configFile.cdnUrls.chromedriver; 111 | configCdnUrls.gecko = configFile.cdnUrls.geckodriver; 112 | configCdnUrls.ie = configFile.cdnUrls.iedriver; 113 | configCdnUrls.android = configFile.cdnUrls.androidsdk; 114 | return configCdnUrls; 115 | } 116 | 117 | /** 118 | * Get the package version. 119 | */ 120 | static getVersion(): string { 121 | let packageFile = require(Config.getPackageFile_()); 122 | return packageFile.version; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/files/downloaded_binary.ts: -------------------------------------------------------------------------------- 1 | import {Binary, BinaryUrl} from '../binaries'; 2 | 3 | /** 4 | * The downloaded binary is the binary with the list of versions downloaded. 5 | */ 6 | export class DownloadedBinary extends Binary { 7 | versions: string[] = []; 8 | binary: Binary; 9 | 10 | constructor(binary: Binary) { 11 | super(); 12 | this.binary = binary; 13 | this.name = binary.name; 14 | this.versionCustom = binary.versionCustom; 15 | } 16 | 17 | id(): string { 18 | return this.binary.id(); 19 | } 20 | 21 | prefix(): string { 22 | return null; 23 | } 24 | suffix(): string { 25 | return null; 26 | } 27 | getUrl(): Promise { 28 | return null; 29 | } 30 | getVersionList(): Promise { 31 | return null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/files/downloader.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as q from 'q'; 4 | import * as request from 'request'; 5 | import * as url from 'url'; 6 | 7 | import {Binary} from '../binaries'; 8 | import {Logger} from '../cli'; 9 | import {Config} from '../config'; 10 | import {HttpUtils} from '../http_utils'; 11 | 12 | let logger = new Logger('downloader'); 13 | 14 | /** 15 | * The file downloader. 16 | */ 17 | export class Downloader { 18 | /** 19 | * Http get the file. Check the content length of the file before writing the file. 20 | * If the content length does not match, remove it and download the file. 21 | * 22 | * @param binary The binary of interest. 23 | * @param fileName The file name. 24 | * @param outputDir The directory where files are downloaded and stored. 25 | * @param contentLength The content length of the existing file. 26 | * @param opt_proxy The proxy for downloading files. 27 | * @param opt_callback Callback method to be executed after the file is downloaded. 28 | * @returns Promise Resolves true = downloaded. Resolves false = not downloaded. 29 | * Rejected with an error. 30 | */ 31 | static getFile( 32 | binary: Binary, fileUrl: string, fileName: string, outputDir: string, contentLength: number, 33 | callback?: Function): Promise { 34 | let filePath = path.resolve(outputDir, fileName); 35 | let file: any; 36 | 37 | let options = HttpUtils.initOptions(fileUrl); 38 | 39 | let req: request.Request = null; 40 | let resContentLength: number; 41 | 42 | return new Promise((resolve, reject) => { 43 | req = request(options); 44 | req.on('response', response => { 45 | if (response.statusCode === 200) { 46 | resContentLength = +response.headers['content-length']; 47 | if (contentLength === resContentLength) { 48 | // if the size is the same, do not download and stop here 49 | response.destroy(); 50 | resolve(false); 51 | } else { 52 | let curl = outputDir + '/' + fileName + ' ' + options.url; 53 | if (HttpUtils.requestOpts.proxy) { 54 | let pathUrl = url.parse(options.url.toString()).path; 55 | let host = url.parse(options.url.toString()).host; 56 | let newFileUrl = url.resolve(HttpUtils.requestOpts.proxy, pathUrl); 57 | curl = outputDir + '/' + fileName + ' \'' + newFileUrl + 58 | '\' -H \'host:' + host + '\''; 59 | } 60 | if (HttpUtils.requestOpts.ignoreSSL) { 61 | curl = 'k ' + curl; 62 | } 63 | logger.info('curl -o' + curl); 64 | 65 | // only pipe if the headers are different length 66 | file = fs.createWriteStream(filePath); 67 | req.pipe(file); 68 | file.on('close', () => { 69 | fs.stat(filePath, (error, stats) => { 70 | if (error) { 71 | (error as any).msg = 'Error: Got error ' + error + ' from ' + fileUrl; 72 | return reject(error); 73 | } 74 | if (stats.size != resContentLength) { 75 | (error as any).msg = 'Error: corrupt download for ' + fileName + 76 | '. Please re-run webdriver-manager update'; 77 | fs.unlinkSync(filePath); 78 | reject(error); 79 | } 80 | if (callback) { 81 | callback(binary, outputDir, fileName); 82 | } 83 | resolve(true); 84 | }); 85 | }); 86 | } 87 | 88 | } else { 89 | let error = new Error(); 90 | (error as any).msg = 91 | 'Expected response code 200, received: ' + response.statusCode; 92 | reject(error); 93 | } 94 | }); 95 | req.on('error', error => { 96 | if ((error as any).code === 'ETIMEDOUT') { 97 | (error as any).msg = 'Connection timeout downloading: ' + fileUrl + 98 | '. Default timeout is 4 minutes.'; 99 | } else if ((error as any).connect) { 100 | (error as any).msg = 'Could not connect to the server to download: ' + fileUrl; 101 | } 102 | reject(error); 103 | }); 104 | }) 105 | .catch(error => { 106 | logger.error((error as any).msg || (error as any).message); 107 | }); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/files/file_manager.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as q from 'q'; 4 | 5 | import {AndroidSDK, Appium, Binary, BinaryMap, ChromeDriver, GeckoDriver, IEDriver, OS, Standalone} from '../binaries'; 6 | import {Logger} from '../cli'; 7 | import {Config} from '../config'; 8 | 9 | import {DownloadedBinary} from './downloaded_binary'; 10 | import {Downloader} from './downloader'; 11 | 12 | let logger = new Logger('file_manager'); 13 | 14 | /** 15 | * The File Manager class is where the webdriver manager will compile a list of 16 | * binaries that could be downloaded and get a list of previously downloaded 17 | * file versions. 18 | */ 19 | export class FileManager { 20 | /** 21 | * Create a directory if it does not exist. 22 | * @param outputDir The directory to create. 23 | */ 24 | static makeOutputDirectory(outputDir: string) { 25 | try { 26 | fs.statSync(outputDir); 27 | } catch (e) { 28 | logger.info('creating folder ' + outputDir); 29 | fs.mkdirSync(outputDir); 30 | } 31 | } 32 | 33 | /** 34 | * For the operating system, check against the list of operating systems that the 35 | * binary is available for. 36 | * @param osType The operating system. 37 | * @param binary The class type to have access to the static properties. 38 | * @returns If the binary is available for the operating system. 39 | */ 40 | static checkOS_(osType: string, binary: typeof Binary): boolean { 41 | for (let os in binary.os) { 42 | if (OS[os] == osType) { 43 | return true; 44 | } 45 | } 46 | return false; 47 | } 48 | 49 | /** 50 | * For the operating system, create a list that includes the binaries 51 | * for selenium standalone, chrome, and internet explorer. 52 | * @param osType The operating system. 53 | * @param alternateCDN URL of the alternative CDN to be used instead of the default ones. 54 | * @returns A binary map that are available for the operating system. 55 | */ 56 | static compileBinaries_(osType: string, alternateCDN?: string): BinaryMap { 57 | let binaries: BinaryMap = {}; 58 | if (FileManager.checkOS_(osType, Standalone)) { 59 | binaries[Standalone.id] = new Standalone(alternateCDN); 60 | } 61 | if (FileManager.checkOS_(osType, ChromeDriver)) { 62 | binaries[ChromeDriver.id] = new ChromeDriver(alternateCDN); 63 | } 64 | if (FileManager.checkOS_(osType, GeckoDriver)) { 65 | binaries[GeckoDriver.id] = new GeckoDriver(alternateCDN); 66 | } 67 | if (FileManager.checkOS_(osType, IEDriver)) { 68 | binaries[IEDriver.id] = new IEDriver(alternateCDN); 69 | } 70 | if (FileManager.checkOS_(osType, AndroidSDK)) { 71 | binaries[AndroidSDK.id] = new AndroidSDK(alternateCDN); 72 | } 73 | if (FileManager.checkOS_(osType, Appium)) { 74 | binaries[Appium.id] = new Appium(alternateCDN); 75 | } 76 | return binaries; 77 | } 78 | 79 | /** 80 | * Look up the operating system and compile a list of binaries that are available 81 | * for the system. 82 | * @param alternateCDN URL of the alternative CDN to be used instead of the default ones. 83 | * @returns A binary map that is available for the operating system. 84 | */ 85 | static setupBinaries(alternateCDN?: string): BinaryMap { 86 | return FileManager.compileBinaries_(Config.osType(), alternateCDN); 87 | } 88 | 89 | /** 90 | * Get the list of existing files from the output directory 91 | * @param outputDir The directory where binaries are saved 92 | * @returns A list of existing files. 93 | */ 94 | static getExistingFiles(outputDir: string): string[] { 95 | try { 96 | return fs.readdirSync(outputDir); 97 | } catch (e) { 98 | return []; 99 | } 100 | } 101 | 102 | /** 103 | * For the binary, operating system, and system architecture, look through 104 | * the existing files and the downloaded binary 105 | * @param binary The binary of interest 106 | * @param osType The operating system. 107 | * @param existingFiles A list of existing files. 108 | * @returns The downloaded binary with all the versions found. 109 | */ 110 | static downloadedVersions_( 111 | binary: T, osType: string, arch: string, existingFiles: string[]): DownloadedBinary { 112 | let versions: string[] = []; 113 | for (let existPos in existingFiles) { 114 | let existFile: string = existingFiles[existPos]; 115 | // use only files that have a prefix and suffix that we care about 116 | if (existFile.indexOf(binary.prefix()) === 0) { 117 | let editExistFile = existFile.replace(binary.prefix(), ''); 118 | // if the suffix matches the executable suffix, add it 119 | if (binary.suffix() === binary.executableSuffix()) { 120 | versions.push(editExistFile.replace(binary.suffix(), '')); 121 | } 122 | // if the suffix does not match the executable, 123 | // the binary is something like: .exe and .zip 124 | // TODO(cnishina): fix implementation. Suffix method is dependent on the version number 125 | // example: chromedriver < 2.23 has a different suffix than 2.23+ (mac32.zip vs mac64.zip). 126 | else if ( 127 | !existFile.endsWith('.zip') && !existFile.endsWith('.tar.gz') && 128 | existFile.indexOf(binary.suffix()) === -1) { 129 | editExistFile = editExistFile.replace(binary.executableSuffix(), ''); 130 | editExistFile = editExistFile.indexOf('_') === 0 ? 131 | editExistFile.substring(1, editExistFile.length) : 132 | editExistFile; 133 | versions.push(editExistFile); 134 | } 135 | } 136 | } 137 | if (versions.length === 0) { 138 | return null; 139 | } 140 | let downloadedBinary = new DownloadedBinary(binary); 141 | downloadedBinary.versions = versions; 142 | return downloadedBinary; 143 | } 144 | 145 | /** 146 | * Finds all the downloaded binary versions stored in the output directory. 147 | * @param outputDir The directory where files are downloaded and stored. 148 | * @returns An dictionary map of all the downloaded binaries found in the output folder. 149 | */ 150 | static downloadedBinaries(outputDir: string): BinaryMap { 151 | let ostype = Config.osType(); 152 | let arch = Config.osArch(); 153 | let binaries = FileManager.setupBinaries(); 154 | let existingFiles = FileManager.getExistingFiles(outputDir); 155 | let downloaded: BinaryMap = {}; 156 | for (let bin in binaries) { 157 | let binary = FileManager.downloadedVersions_(binaries[bin], ostype, arch, existingFiles); 158 | if (binary != null) { 159 | downloaded[binary.id()] = binary; 160 | } 161 | } 162 | return downloaded; 163 | } 164 | 165 | /** 166 | * Try to download the binary version. 167 | * @param binary The binary of interest. 168 | * @param outputDir The directory where files are downloaded and stored. 169 | * @returns Promise resolved to true for files downloaded, resolved to false for files not 170 | * downloaded because they exist, rejected if there is an error. 171 | */ 172 | static downloadFile(binary: T, outputDir: string, callback?: Function): 173 | Promise { 174 | return new Promise((resolve, reject) => { 175 | let outDir = Config.getSeleniumDir(); 176 | let downloaded: BinaryMap = FileManager.downloadedBinaries(outputDir); 177 | let contentLength = 0; 178 | 179 | // Pass options down to binary to make request to get the latest version to download. 180 | binary.getUrl(binary.version()).then(fileUrl => { 181 | binary.versionCustom = fileUrl.version; 182 | let filePath = path.resolve(outputDir, binary.filename()); 183 | let fileName = binary.filename(); 184 | 185 | // If we have downloaded the file before, check the content length 186 | if (downloaded[binary.id()]) { 187 | let downloadedBinary = downloaded[binary.id()]; 188 | let versions = downloadedBinary.versions; 189 | let version = binary.versionCustom; 190 | 191 | for (let index in versions) { 192 | let v = versions[index]; 193 | if (v === version) { 194 | contentLength = fs.statSync(filePath).size; 195 | 196 | Downloader.getFile(binary, fileUrl.url, fileName, outputDir, contentLength, callback) 197 | .then(downloaded => { 198 | resolve(downloaded); 199 | }); 200 | } 201 | } 202 | } 203 | // We have not downloaded it before, or the version does not exist. Use the default content 204 | // length of zero and download the file. 205 | Downloader.getFile(binary, fileUrl.url, fileName, outputDir, contentLength, callback) 206 | .then(downloaded => { 207 | resolve(downloaded); 208 | }); 209 | }); 210 | }); 211 | } 212 | 213 | /** 214 | * Removes the existing files found in the output directory that match the 215 | * binary prefix names. 216 | * @param outputDir The directory where files are downloaded and stored. 217 | */ 218 | static removeExistingFiles(outputDir: string): void { 219 | try { 220 | fs.statSync(outputDir); 221 | } catch (e) { 222 | logger.warn('path does not exist ' + outputDir); 223 | return; 224 | } 225 | let existingFiles = FileManager.getExistingFiles(outputDir); 226 | if (existingFiles.length === 0) { 227 | logger.warn('no files found in path ' + outputDir); 228 | return; 229 | } 230 | 231 | let binaries = FileManager.setupBinaries(); 232 | existingFiles.forEach((file) => { 233 | for (let binPos in binaries) { 234 | let bin: Binary = binaries[binPos]; 235 | if (file.indexOf(bin.prefix()) !== -1) { 236 | bin.remove(path.resolve(outputDir, file)); 237 | logger.info('removed ' + file); 238 | } 239 | } 240 | }); 241 | 242 | let metaFiles = [ 243 | 'chrome-response.xml', 'gecko-response.json', 'iedriver-response.xml', 244 | 'standalone-response.xml', 'update-config.json' 245 | ]; 246 | for (let metaFile of metaFiles) { 247 | try { 248 | let metaFilePath = path.resolve(outputDir, metaFile); 249 | if (fs.statSync(metaFilePath)) { 250 | fs.unlinkSync(metaFilePath); 251 | logger.info('removed ' + metaFile); 252 | } 253 | } catch (e) { 254 | } 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /lib/files/index.ts: -------------------------------------------------------------------------------- 1 | export * from './downloaded_binary'; 2 | export * from './downloader'; 3 | export * from './file_manager'; 4 | -------------------------------------------------------------------------------- /lib/http_utils.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'request'; 2 | import {OptionsWithUrl} from 'request'; 3 | import * as url from 'url'; 4 | 5 | import {Logger} from './cli/logger'; 6 | import {Config} from './config'; 7 | 8 | let logger = new Logger('http_utils'); 9 | 10 | export declare interface RequestOptionsValue { 11 | proxy?: string; 12 | ignoreSSL?: boolean; 13 | } 14 | 15 | export class HttpUtils { 16 | static requestOpts: RequestOptionsValue = {}; 17 | static assignOptions(options: RequestOptionsValue): void { 18 | Object.assign(HttpUtils.requestOpts, options); 19 | } 20 | 21 | static initOptions(url: string, timeout?: number): OptionsWithUrl { 22 | let options: OptionsWithUrl = { 23 | url: url, 24 | // default Linux can be anywhere from 20-120 seconds 25 | // increasing this arbitrarily to 4 minutes 26 | timeout: 240000 27 | }; 28 | HttpUtils.optionsSSL(options, HttpUtils.requestOpts.ignoreSSL); 29 | HttpUtils.optionsProxy(options, url, HttpUtils.requestOpts.proxy); 30 | return options; 31 | } 32 | 33 | static optionsSSL(options: OptionsWithUrl, opt_ignoreSSL: boolean): OptionsWithUrl { 34 | if (opt_ignoreSSL) { 35 | logger.info('ignoring SSL certificate'); 36 | options.strictSSL = !opt_ignoreSSL; 37 | (options as any).rejectUnauthorized = !opt_ignoreSSL; 38 | } 39 | 40 | return options; 41 | } 42 | 43 | static optionsProxy(options: OptionsWithUrl, requestUrl: string, opt_proxy: string): 44 | OptionsWithUrl { 45 | if (opt_proxy) { 46 | options.proxy = HttpUtils.resolveProxy(requestUrl, opt_proxy); 47 | if (url.parse(requestUrl).protocol === 'https:') { 48 | options.url = requestUrl.replace('https:', 'http:'); 49 | } 50 | } 51 | return options; 52 | } 53 | 54 | static optionsHeader(options: OptionsWithUrl, key: string, value: string): OptionsWithUrl { 55 | if (options.headers == null) { 56 | options.headers = {}; 57 | } 58 | options.headers[key] = value; 59 | return options; 60 | } 61 | 62 | /** 63 | * Resolves proxy based on values set 64 | * @param fileUrl The url to download the file. 65 | * @param opt_proxy The proxy to connect to to download files. 66 | * @return Either undefined or the proxy. 67 | */ 68 | static resolveProxy(fileUrl: string, opt_proxy?: string): string { 69 | let protocol = url.parse(fileUrl).protocol; 70 | let hostname = url.parse(fileUrl).hostname; 71 | 72 | if (opt_proxy) { 73 | return opt_proxy; 74 | } else { 75 | // If the NO_PROXY environment variable exists and matches the host name, 76 | // to ignore the resolve proxy. 77 | // the checks to see if it exists and equal to empty string is to help with testing 78 | let noProxy: string = Config.noProxy(); 79 | if (noProxy) { 80 | // array of hostnames/domain names listed in the NO_PROXY environment variable 81 | let noProxyTokens = noProxy.split(','); 82 | // check if the fileUrl hostname part does not end with one of the 83 | // NO_PROXY environment variable's hostnames/domain names 84 | for (let noProxyToken of noProxyTokens) { 85 | if (hostname.indexOf(noProxyToken) !== -1) { 86 | return undefined; 87 | } 88 | } 89 | } 90 | 91 | // If the HTTPS_PROXY and HTTP_PROXY environment variable is set, use that as the proxy 92 | if (protocol === 'https:') { 93 | return Config.httpsProxy() || Config.httpProxy(); 94 | } else if (protocol === 'http:') { 95 | return Config.httpProxy(); 96 | } 97 | } 98 | return undefined; 99 | } 100 | } 101 | 102 | /** 103 | * Request the body from the url. 104 | * @param requestUrl The request url. 105 | * @returns A promise string of the response body. 106 | */ 107 | export function requestBody(requestUrl: string): Promise { 108 | const options = HttpUtils.initOptions(requestUrl); 109 | options.followRedirect = true; 110 | return new Promise((resolve, reject) => { 111 | const req = request(options); 112 | req.on('response', response => { 113 | if (response.statusCode === 200) { 114 | let output = ''; 115 | response.on('data', (data) => { 116 | output += data; 117 | }); 118 | response.on('end', () => { 119 | resolve(output); 120 | }); 121 | } else { 122 | reject(new Error('response status code is not 200')); 123 | } 124 | }); 125 | req.on('error', error => { 126 | reject(error); 127 | }); 128 | }); 129 | } 130 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from 'child_process'; 2 | import * as fs from 'fs'; 3 | import * as http from 'http'; 4 | import * as path from 'path'; 5 | 6 | import {Config} from './config'; 7 | 8 | function spawnFactory(sync: false): 9 | (cmd: string, args: string[], stdio?: any, opts?: child_process.SpawnOptions) => 10 | child_process.ChildProcess; 11 | function spawnFactory(sync: true): 12 | (cmd: string, args: string[], stdio?: any, opts?: child_process.SpawnSyncOptions) => 13 | child_process.SpawnSyncReturns; 14 | function spawnFactory(sync: boolean): 15 | (cmd: string, args: string[], stdio?: string, 16 | opts?: child_process.SpawnOptions|child_process.SpawnSyncOptions) => 17 | child_process.ChildProcess | child_process.SpawnSyncReturns { 18 | return (cmd: string, args: string[], stdio?: string, 19 | opts?: child_process.SpawnOptions|child_process.SpawnSyncOptions) => { 20 | if ((Config.osType() === 'Windows_NT') && (cmd.slice(-4) !== '.exe')) { 21 | if (fs.existsSync(cmd + '.exe')) { 22 | cmd += '.exe'; 23 | } else { 24 | args = ['/c'].concat([cmd], args); 25 | cmd = 'cmd'; 26 | } 27 | } 28 | if (stdio) { 29 | opts = opts || {}; 30 | opts.stdio = stdio; 31 | } 32 | if (sync) { 33 | return child_process.spawnSync(cmd, args, opts as child_process.SpawnOptions); 34 | } else { 35 | return child_process.spawn(cmd, args, opts as child_process.SpawnSyncOptions); 36 | } 37 | }; 38 | } 39 | 40 | export let spawn = spawnFactory(false); 41 | export let spawnSync = spawnFactory(true); 42 | 43 | export function request( 44 | method: string, port: string, path: string, timeout?: number, data?: any): Promise { 45 | let headers: {[key: string]: string} = {}; 46 | let hasContent = data && ((method == 'POST') || (method == 'PUT')); 47 | if (hasContent) { 48 | data = data ? JSON.stringify(data) : ''; 49 | headers['Content-Length'] = data.length; 50 | headers['Content-Type'] = 'application/json;charset=UTF-8'; 51 | } 52 | return new Promise((resolve, reject) => { 53 | let unexpectedEnd = () => { 54 | reject({code: 'UNKNOWN', message: 'Request ended unexpectedly'}); 55 | }; 56 | let req = http.request( 57 | {port: parseInt(port), method: method, path: path, headers: headers}, (res) => { 58 | req.removeListener('end', unexpectedEnd); 59 | if (res.statusCode !== 200) { 60 | reject({code: res.statusCode, message: res.statusMessage}); 61 | } else { 62 | let buffer: (string|Buffer)[] = []; 63 | res.on('data', buffer.push.bind(buffer)); 64 | res.on('end', () => { 65 | resolve(buffer.join('').replace(/\0/g, '')); 66 | }); 67 | } 68 | }); 69 | 70 | if (timeout) { 71 | req.setTimeout(timeout, () => { 72 | reject({code: 'TIMEOUT', message: 'Request timed out'}); 73 | }); 74 | } 75 | req.on('error', reject); 76 | req.on('end', unexpectedEnd); 77 | 78 | if (hasContent) { 79 | req.write(data as string); 80 | } 81 | 82 | req.end(); 83 | }); 84 | } 85 | 86 | export function adb( 87 | sdkPath: string, port: number, command: string, timeout: number, 88 | args?: string[]): Promise { 89 | return new Promise((resolve, reject) => { 90 | let child = spawn( 91 | path.resolve(sdkPath, 'platform-tools', 'adb'), 92 | ['-s', 'emulator-' + port, command].concat(args || []), 'pipe'); 93 | let done = false; 94 | let buffer: (string|Buffer)[] = []; 95 | child.stdout.on('data', buffer.push.bind(buffer)); 96 | child.on('error', (err: Error) => { 97 | if (!done) { 98 | done = true; 99 | reject(err); 100 | } 101 | }); 102 | child.on('exit', (code: number, signal: string) => { 103 | if (!done) { 104 | done = true; 105 | if (code === 0) { 106 | resolve(buffer.join('')); 107 | } else { 108 | reject({ 109 | code: code, 110 | message: 'abd command "' + command + '" ' + 111 | (signal ? 'received signal ' + signal : 'returned with a non-zero exit code') + 112 | 'for emulator-' + port 113 | }); 114 | } 115 | } 116 | }); 117 | if (timeout) { 118 | setTimeout(() => { 119 | if (!done) { 120 | done = true; 121 | child.kill(); 122 | reject({ 123 | code: 'TIMEOUT', 124 | message: 'adb command "' + command + '" timed out for emulator-' + port 125 | }); 126 | } 127 | }, timeout); 128 | } 129 | }); 130 | } 131 | -------------------------------------------------------------------------------- /lib/webdriver.ts: -------------------------------------------------------------------------------- 1 | import * as minimist from 'minimist'; 2 | 3 | import {cli as commandline} from './cli_instance'; 4 | 5 | let minimistOptions = commandline.getMinimistOptions(); 6 | let argv = minimist(process.argv.slice(2), minimistOptions); 7 | let cmd = argv._; 8 | if (commandline.programs[cmd[0]]) { 9 | if (cmd[0] === 'help') { 10 | commandline.printHelp(); 11 | } else if (cmd[1] === 'help' || argv['help'] || argv['h']) { 12 | commandline.programs[cmd[0]].printHelp(); 13 | } else { 14 | commandline.programs[cmd[0]].run(JSON.parse(JSON.stringify(argv))); 15 | } 16 | } else { 17 | commandline.printHelp(); 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webdriver-manager", 3 | "version": "12.1.9", 4 | "description": "A selenium server and browser driver manager for your end to end tests.", 5 | "scripts": { 6 | "format": "gulp format", 7 | "format-enforce": "gulp format:enforce", 8 | "gulp": "gulp", 9 | "jasmine": "jasmine", 10 | "prepublish": "npm run format-enforce && tsc && gulp copy", 11 | "tsc": "tsc", 12 | "pretest": "tsc && gulp copy", 13 | "test": "npm run test-unit && npm run test-e2e", 14 | "test-unit": "jasmine", 15 | "pretest-e2e:update": "node ./bin/webdriver-manager update", 16 | "pretest-e2e:start": "node ./bin/webdriver-manager start --detach --seleniumPort 4444 --quiet", 17 | "pretest-e2e": "npm run pretest && npm run pretest-e2e:update && npm run pretest-e2e:start", 18 | "test-e2e": "jasmine JASMINE_CONFIG_PATH=e2e_spec/support/headless.json", 19 | "posttest-e2e": "node ./bin/webdriver-manager shutdown" 20 | }, 21 | "keywords": [ 22 | "angular", 23 | "test", 24 | "testing", 25 | "protractor", 26 | "webdriver", 27 | "webdriverjs", 28 | "selenium", 29 | "selenium-webdriver" 30 | ], 31 | "repository": { 32 | "type": "git", 33 | "url": "git://github.com/angular/webdriver-manager.git" 34 | }, 35 | "bin": { 36 | "webdriver-manager": "bin/webdriver-manager" 37 | }, 38 | "main": "built/lib/webdriver.js", 39 | "author": "Craig Nishina ", 40 | "license": "MIT", 41 | "dependencies": { 42 | "adm-zip": "^0.5.2", 43 | "chalk": "^1.1.1", 44 | "del": "^2.2.0", 45 | "glob": "^7.0.3", 46 | "ini": "^1.3.4", 47 | "minimist": "^1.2.0", 48 | "q": "^1.4.1", 49 | "request": "^2.87.0", 50 | "rimraf": "^2.5.2", 51 | "semver": "^5.3.0", 52 | "xml2js": "^0.4.17" 53 | }, 54 | "devDependencies": { 55 | "@types/adm-zip": "^0.5.0", 56 | "@types/chalk": "^0.4.28", 57 | "@types/form-data": "^0.0.33", 58 | "@types/glob": "^5.0.29", 59 | "@types/ini": "^1.3.28", 60 | "@types/jasmine": "^2.5.43", 61 | "@types/minimatch": "^2.0.28", 62 | "@types/minimist": "^1.1.28", 63 | "@types/node": "^7.0.4", 64 | "@types/q": "^0.0.32", 65 | "@types/request": "^0.0.39", 66 | "@types/rimraf": "^0.0.28", 67 | "@types/selenium-webdriver": "^2.53.35", 68 | "@types/semver": "^5.3.30", 69 | "@types/xml2js": "0.0.32", 70 | "clang-format": "^1.0.35", 71 | "gulp": "^4.0.0", 72 | "gulp-clang-format": "^1.0.23", 73 | "jasmine": "^2.4.1", 74 | "run-sequence": "^1.1.5", 75 | "selenium-webdriver": "~3.0.1", 76 | "typescript": "~2.3.0" 77 | }, 78 | "engines": { 79 | "node": ">=6.9.x" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /release.md: -------------------------------------------------------------------------------- 1 | Webdriver Manager Release Checklist 2 | ----------------------------------- 3 | Say the previous release was 0.0.J, the current release is 0.0.K, and the next release will be 0.0.L. 4 | 5 | - Make sure [Travis](https://travis-ci.org/angular/webdriver-manager/builds) is passing. 6 | 7 | - Make sure .gitignore and .npmignore are updated with any new files that need to be ignored. 8 | 9 | - Update package.json with a version bump. If the changes are only bug fixes, increment the patch (e.g. 0.0.5 -> 0.0.6), otherwise increment the minor version. 10 | 11 | - Update CHANGELOG.md 12 | 13 | - You can get a list of changes in the correct format by running 14 | 15 | ``` 16 | git log 0.0.J..HEAD --format="- ([%h](https://github.com/angular/webdriver-manager/commit/%H)) %n%w(100,2,2)%B" > /tmp/changes.txt 17 | ``` 18 | - Create a new section in CHANGELOG.md and copy in features (`feat`), big dependency version updates (`deps`), bug fixes (`fix`), and breaking changes. No need to note chores or stylistic changes - the changelog should be primarily useful to someone using Protractor, not developing on it. 19 | 20 | - Breaking changes should be in their own section and include before/after examples of how to fix code that needs to change. 21 | 22 | - Make a commit with the API and package.json changes titled chore(release): version bump and changelog for 0.0.K. 23 | 24 | - Tag the release with `git tag 0.0.K` 25 | 26 | - Push to github 27 | 28 | - Push tags to github (`git push --tags`) 29 | 30 | - Verify that the changelog and tags look sane on github 31 | 32 | - NPM publish 33 | 34 | - Let people know 35 | - Have @ProtractorTest tweet about it 36 | 37 | - Close the 0.0.K milestone 38 | -------------------------------------------------------------------------------- /spec/binaries/chrome_driver_spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as rimraf from 'rimraf'; 3 | import {ChromeDriver} from '../../lib/binaries/chrome_driver'; 4 | 5 | describe('chrome driver', () => { 6 | let out_dir = path.resolve('selenium_test'); 7 | 8 | afterAll(() => { 9 | rimraf.sync(out_dir); 10 | }); 11 | 12 | it('should get the id', () => { 13 | expect(new ChromeDriver().id()).toEqual('chrome'); 14 | }); 15 | 16 | it('should get the url', (done) => { 17 | let chromeDriver = new ChromeDriver(); 18 | chromeDriver.configSource.out_dir = out_dir; 19 | chromeDriver.configSource.osarch = 'x64'; 20 | chromeDriver.configSource.ostype = 'Darwin'; 21 | chromeDriver.getUrl('2.20').then(binaryUrl => { 22 | expect(binaryUrl.url).toContain('2.20/chromedriver_mac32.zip'); 23 | done(); 24 | }); 25 | }); 26 | 27 | it('should get the lists', (done) => { 28 | let chromeDriver = new ChromeDriver(); 29 | chromeDriver.configSource.out_dir = out_dir; 30 | chromeDriver.configSource.osarch = 'x64'; 31 | chromeDriver.configSource.ostype = 'Darwin'; 32 | chromeDriver.getVersionList().then(list => { 33 | for (let item of list) { 34 | expect(item).toContain('chromedriver_mac'); 35 | } 36 | done(); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /spec/binaries/chrome_xml_spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as rimraf from 'rimraf'; 3 | import {ChromeXml} from '../../lib/binaries/chrome_xml'; 4 | 5 | describe('chrome xml reader', () => { 6 | let out_dir = path.resolve('selenium_test'); 7 | 8 | afterAll(() => { 9 | rimraf.sync(out_dir); 10 | }); 11 | 12 | it('should get a list', (done) => { 13 | let chromeXml = new ChromeXml(); 14 | chromeXml.out_dir = out_dir; 15 | chromeXml.ostype = 'Darwin'; 16 | chromeXml.osarch = 'x64'; 17 | chromeXml.getVersionList().then(list => { 18 | for (let item of list) { 19 | expect(item).toContain('/chromedriver_mac'); 20 | expect(item).not.toContain('m1'); 21 | } 22 | done(); 23 | }); 24 | }); 25 | 26 | it('should get the 2.27, 64-bit version (arch = x64)', (done) => { 27 | let chromeXml = new ChromeXml(); 28 | chromeXml.out_dir = out_dir; 29 | chromeXml.ostype = 'Darwin'; 30 | chromeXml.osarch = 'x64'; 31 | chromeXml.getUrl('2.27').then(binaryUrl => { 32 | expect(binaryUrl.url).toContain('2.27/chromedriver_mac64.zip'); 33 | done(); 34 | }); 35 | }); 36 | 37 | it('should get the 2.27, 64-bit version (arch = x86)', (done) => { 38 | let chromeXml = new ChromeXml(); 39 | chromeXml.out_dir = out_dir; 40 | chromeXml.ostype = 'Darwin'; 41 | chromeXml.osarch = 'x86'; 42 | chromeXml.getUrl('2.27').then(binaryUrl => { 43 | expect(binaryUrl.url).toEqual(''); 44 | done(); 45 | }); 46 | }); 47 | 48 | it('should get the 2.20, 32-bit version (arch = x64)', (done) => { 49 | let chromeXml = new ChromeXml(); 50 | chromeXml.out_dir = out_dir; 51 | chromeXml.ostype = 'Darwin'; 52 | chromeXml.osarch = 'x64'; 53 | chromeXml.getUrl('2.20').then(binaryUrl => { 54 | expect(binaryUrl.url).toContain('2.20/chromedriver_mac32.zip'); 55 | done(); 56 | }); 57 | }); 58 | 59 | it('should get the 2.20, 32-bit version (arch = x86)', (done) => { 60 | let chromeXml = new ChromeXml(); 61 | chromeXml.out_dir = out_dir; 62 | chromeXml.ostype = 'Darwin'; 63 | chromeXml.osarch = 'x86'; 64 | chromeXml.getUrl('2.20').then((binaryUrl) => { 65 | expect(binaryUrl.url).toContain('2.20/chromedriver_mac32.zip'); 66 | done(); 67 | }); 68 | }); 69 | 70 | // This test case covers a bug when all the following conditions were true. 71 | // arch was 64 with multiple major versions available. 72 | it('should not get the 85.0.4183.38, 32-bit version (arch = x64)', (done) => { 73 | let chromeXml = new ChromeXml(); 74 | chromeXml.out_dir = out_dir; 75 | chromeXml.ostype = 'Windows_NT'; 76 | chromeXml.osarch = 'x64'; 77 | chromeXml.getUrl('85.0.4183.87').then((binaryUrl) => { 78 | expect(binaryUrl.url).toContain('85.0.4183.87/chromedriver_win32.zip'); 79 | done(); 80 | }); 81 | }); 82 | 83 | it('should get the 87.0.4280.88, 64-bit, m1 version (arch = arm64)', (done) => { 84 | let chromeXml = new ChromeXml(); 85 | chromeXml.out_dir = out_dir; 86 | chromeXml.ostype = 'Darwin'; 87 | chromeXml.osarch = 'arm64'; 88 | chromeXml.getUrl('87.0.4280.88').then((binaryUrl) => { 89 | expect(binaryUrl.url).toContain('87.0.4280.88/chromedriver_mac64_m1.zip'); 90 | done(); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /spec/binaries/config_source_spec.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import {GithubApiConfigSource, XmlConfigSource} from '../../lib/binaries/config_source'; 3 | import {Config} from '../../lib/config'; 4 | 5 | export class XMLConfig extends XmlConfigSource { 6 | constructor(public name: string, public xmlUrl: string) { 7 | super(name, xmlUrl); 8 | } 9 | getUrl(version: string): Promise<{url: string, version: string}> { 10 | return null; 11 | } 12 | getVersionList(): Promise { 13 | return null; 14 | } 15 | testGetXml(): Promise { 16 | return this.getXml(); 17 | } 18 | } 19 | 20 | export class JSONConfig extends GithubApiConfigSource { 21 | constructor(name: string, url: string) { 22 | super(name, url); 23 | } 24 | getUrl(version: string): Promise<{url: string, version: string}> { 25 | return null; 26 | } 27 | getVersionList(): Promise { 28 | return null; 29 | } 30 | testGetJson(): Promise { 31 | return this.getJson(); 32 | } 33 | } 34 | 35 | describe('config', () => { 36 | describe('xml config source', () => { 37 | it('on start: should read the xml file and not check the timestamp', done => { 38 | spyOn(fs, 'readFileSync').and.callFake(() => { 39 | return ` 40 | 41 | 42 | foobar-release 43 | 44 | 0.01/foobar.zip 45 | 46 | 47 | `; 48 | }); 49 | spyOn(fs, 'statSync').and.callFake(() => { 50 | return { 51 | mtime: 0 52 | } 53 | }); 54 | Config.runCommand = 'start'; 55 | let xmlConfig = new XMLConfig('xml', 'url'); 56 | xmlConfig.testGetXml() 57 | .then(xml => { 58 | expect(xml.ListBucketResult.Name).toEqual(['foobar-release']); 59 | done(); 60 | }) 61 | .catch(err => { 62 | done.fail(err); 63 | }); 64 | }); 65 | 66 | it('on udpate: should check the timestamp, invaidate cache, and try to make a web request', 67 | done => { 68 | spyOn(fs, 'readFileSync').and.callFake(() => { 69 | return 'foobar'; 70 | }); 71 | spyOn(fs, 'statSync').and.callFake(() => { 72 | return { 73 | mtime: 0 74 | } 75 | }); 76 | Config.runCommand = 'update'; 77 | let xmlConfig = new XMLConfig('xml', 'url'); 78 | xmlConfig.testGetXml() 79 | .then(xml => { 80 | // should do nothing 81 | done.fail('this should not work'); 82 | }) 83 | .catch(err => { 84 | expect(err.toString()).toContain('Invalid URI "url"'); 85 | done(); 86 | }); 87 | }); 88 | 89 | it('on update: if the size of the file is zero, invalidate the cache', done => { 90 | spyOn(fs, 'statSync').and.callFake(() => { 91 | return {size: 0}; 92 | }); 93 | Config.runCommand = 'update'; 94 | let xmlConfig = new XMLConfig('json', 'url'); 95 | xmlConfig.testGetXml() 96 | .then(xml => { 97 | // should do nothing 98 | done.fail('this should not work'); 99 | }) 100 | .catch(err => { 101 | expect(err.toString()).toContain('Invalid URI "url"'); 102 | done(); 103 | }); 104 | }); 105 | }); 106 | 107 | describe('github json', () => { 108 | it('on start: should read the json file and not check the timestamp', done => { 109 | spyOn(fs, 'readFileSync').and.callFake(() => { 110 | return '{ "foo": "bar" }'; 111 | }); 112 | spyOn(fs, 'statSync').and.callFake(() => { 113 | return { 114 | mtime: 0 115 | } 116 | }); 117 | Config.runCommand = 'start'; 118 | let jsonConfig = new JSONConfig('json', 'url'); 119 | jsonConfig.testGetJson() 120 | .then(json => { 121 | expect(json.foo).toEqual('bar'); 122 | done(); 123 | }) 124 | .catch(err => { 125 | done.fail(err); 126 | }); 127 | }); 128 | 129 | it('on udpate: should check the timestamp, invaidate cache, and try to make a web request', 130 | done => { 131 | spyOn(fs, 'readFileSync').and.callFake(() => { 132 | return 'foobar'; 133 | }); 134 | spyOn(fs, 'statSync').and.callFake(() => { 135 | return { 136 | mtime: 0 137 | } 138 | }); 139 | Config.runCommand = 'update'; 140 | let jsonConfig = new JSONConfig('json', 'url'); 141 | jsonConfig.testGetJson() 142 | .then(json => { 143 | // should do nothing 144 | done.fail('this should not work'); 145 | }) 146 | .catch(err => { 147 | expect(err.toString()).toContain('Invalid URI "url"'); 148 | done(); 149 | }); 150 | }); 151 | 152 | it('on update: if the size of the file is zero, invalidate the cache', done => { 153 | spyOn(fs, 'statSync').and.callFake(() => { 154 | return {size: 0}; 155 | }); 156 | Config.runCommand = 'update'; 157 | let jsonConfig = new JSONConfig('json', 'url'); 158 | jsonConfig.testGetJson() 159 | .then(json => { 160 | // should do nothing 161 | done.fail('this should not work'); 162 | }) 163 | .catch(err => { 164 | expect(err.toString()).toContain('Invalid URI "url"'); 165 | done(); 166 | }); 167 | }); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /spec/binaries/gecko_driver_github_spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as rimraf from 'rimraf'; 3 | import {GeckoDriverGithub} from '../../lib/binaries/gecko_driver_github'; 4 | 5 | describe('gecko driver github', () => { 6 | let out_dir = path.resolve('selenium_test'); 7 | 8 | afterAll(() => { 9 | rimraf.sync(out_dir); 10 | }); 11 | 12 | it('should get version 0.13.0', (done) => { 13 | let geckoDriverGithub = new GeckoDriverGithub(); 14 | geckoDriverGithub.out_dir = out_dir; 15 | geckoDriverGithub.getUrl('v0.13.0').then(binaryUrl => { 16 | expect(binaryUrl.url) 17 | .toContain( 18 | 'https://github.com/mozilla/geckodriver/releases/download/v0.13.0/geckodriver-v'); 19 | done(); 20 | }); 21 | }); 22 | 23 | it('should get a version list', (done) => { 24 | let geckoDriverGithub = new GeckoDriverGithub(); 25 | geckoDriverGithub.out_dir = out_dir; 26 | geckoDriverGithub.getVersionList().then(list => { 27 | expect(list.length).toBeGreaterThan(0); 28 | done(); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /spec/binaries/gecko_driver_spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as rimraf from 'rimraf'; 3 | import {GeckoDriver} from '../../lib/binaries/gecko_driver'; 4 | 5 | describe('gecko driver', () => { 6 | let out_dir = path.resolve('selenium_test'); 7 | 8 | afterAll(() => { 9 | rimraf.sync(out_dir); 10 | }); 11 | 12 | it('should get id', () => { 13 | expect(new GeckoDriver().id()).toEqual('gecko'); 14 | }); 15 | 16 | it('should get url for 0.13.0', (done) => { 17 | let geckoDriver = new GeckoDriver(); 18 | geckoDriver.configSource.out_dir = out_dir; 19 | geckoDriver.getUrl('v0.13.0').then(binaryUrl => { 20 | expect(binaryUrl.url) 21 | .toContain( 22 | 'https://github.com/mozilla/geckodriver/releases/download/v0.13.0/geckodriver-v'); 23 | done(); 24 | }); 25 | }); 26 | 27 | it('should get the version list', (done) => { 28 | let geckoDriver = new GeckoDriver(); 29 | geckoDriver.configSource.out_dir = out_dir; 30 | geckoDriver.getVersionList().then(list => { 31 | expect(list.length).toBeGreaterThan(0); 32 | done(); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /spec/binaries/iedriver_spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as rimraf from 'rimraf'; 3 | import {IEDriver} from '../../lib/binaries/iedriver'; 4 | 5 | describe('iedriver', () => { 6 | let out_dir = path.resolve('selenium_test'); 7 | 8 | afterAll(() => { 9 | rimraf.sync(out_dir); 10 | }); 11 | 12 | it('should get the id', () => { 13 | expect(new IEDriver().id()).toEqual('ie'); 14 | }); 15 | 16 | it('should get version 2.53.1', (done) => { 17 | let iedriver = new IEDriver(); 18 | iedriver.configSource.out_dir = out_dir; 19 | iedriver.getUrl('2.53.1').then(binaryUrl => { 20 | expect(binaryUrl.url) 21 | .toEqual( 22 | 'https://selenium-release.storage.googleapis.com/2.53/IEDriverServer_Win32_2.53.1.zip'); 23 | done(); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /spec/binaries/iedriver_xml_spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as rimraf from 'rimraf'; 3 | import {IEDriverXml} from '../../lib/binaries/iedriver_xml'; 4 | 5 | describe('iedriver xml', () => { 6 | let out_dir = path.resolve('selenium_test'); 7 | 8 | afterAll(() => { 9 | rimraf.sync(out_dir); 10 | }); 11 | 12 | it('should get version 2.53.1', (done) => { 13 | let iedriverXml = new IEDriverXml(); 14 | iedriverXml.out_dir = out_dir; 15 | iedriverXml.getUrl('2.53.1').then(binaryUrl => { 16 | expect(binaryUrl.url) 17 | .toEqual( 18 | 'https://selenium-release.storage.googleapis.com/2.53/IEDriverServer_Win32_2.53.1.zip'); 19 | done(); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /spec/binaries/standalone_spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as rimraf from 'rimraf'; 3 | import {Standalone} from '../../lib/binaries/standalone'; 4 | 5 | describe('standalone', () => { 6 | let out_dir = path.resolve('selenium_test'); 7 | 8 | afterAll(() => { 9 | rimraf.sync(out_dir); 10 | }); 11 | 12 | it('should get the id', () => { 13 | expect(new Standalone().id()).toEqual('standalone'); 14 | }); 15 | 16 | it('should get the url', (done) => { 17 | let standalone = new Standalone(); 18 | standalone.configSource.out_dir = out_dir; 19 | standalone.getUrl('2.53.1').then(binaryUrl => { 20 | expect(binaryUrl.url).toContain('2.53/selenium-server-standalone-2.53.1.jar'); 21 | done(); 22 | }); 23 | }); 24 | 25 | it('should get the lists', (done) => { 26 | let standalone = new Standalone(); 27 | standalone.configSource.out_dir = out_dir; 28 | standalone.configSource.osarch = 'x64'; 29 | standalone.configSource.ostype = 'Darwin'; 30 | standalone.getVersionList().then(list => { 31 | for (let item of list) { 32 | expect(item).toContain('selenium-server-standalone-'); 33 | } 34 | done(); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /spec/binaries/standalone_xml_spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as rimraf from 'rimraf'; 3 | import {StandaloneXml} from '../../lib/binaries/standalone_xml'; 4 | 5 | describe('standalone xml reader', () => { 6 | let out_dir = path.resolve('selenium_test'); 7 | 8 | afterAll(() => { 9 | rimraf.sync(out_dir); 10 | }); 11 | 12 | it('should get a list', (done) => { 13 | let standaloneXml = new StandaloneXml(); 14 | standaloneXml.out_dir = out_dir; 15 | standaloneXml.getVersionList().then(list => { 16 | for (let item of list) { 17 | expect(item).toContain('/selenium-server-standalone'); 18 | } 19 | done(); 20 | }); 21 | }); 22 | 23 | it('should get version 2.53.1', (done) => { 24 | let standaloneXml = new StandaloneXml(); 25 | standaloneXml.out_dir = out_dir; 26 | standaloneXml.getUrl('2.53.1').then(binaryUrl => { 27 | expect(binaryUrl.url) 28 | .toBe( 29 | 'https://selenium-release.storage.googleapis.com/2.53/selenium-server-standalone-2.53.1.jar'); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('should get version 3.0.0-beta3', (done) => { 35 | let standaloneXml = new StandaloneXml(); 36 | standaloneXml.out_dir = out_dir; 37 | standaloneXml.getUrl('3.0.0-beta3').then(binaryUrl => { 38 | expect(binaryUrl.url) 39 | .toBe( 40 | 'https://selenium-release.storage.googleapis.com/3.0-beta3/selenium-server-standalone-3.0.0-beta3.jar'); 41 | done(); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /spec/cli/options_spec.ts: -------------------------------------------------------------------------------- 1 | import {Option} from '../../lib/cli/options'; 2 | 3 | 4 | describe('options', () => { 5 | let option: Option; 6 | 7 | describe('get number', () => { 8 | describe('for this.value not set', () => { 9 | it('should return the default value', () => { 10 | option = new Option('fake opt', 'fake description', 'number', 10); 11 | expect(option.getNumber()).toEqual(10); 12 | 13 | option = new Option('fake opt', 'fake description', 'number', 0); 14 | expect(option.getNumber()).toEqual(0); 15 | 16 | option = new Option('fake opt', 'fake description', 'number', -5); 17 | expect(option.getNumber()).toEqual(-5); 18 | }); 19 | 20 | it('should return null if the default value is not set', () => { 21 | option = new Option('fake opt', 'fake description', 'number'); 22 | expect(option.getNumber()).toBeNull(); 23 | }); 24 | }); 25 | 26 | describe('for this.value set', () => { 27 | beforeEach(() => { 28 | option = new Option('fake opt', 'fake description', 'number', -10); 29 | }); 30 | 31 | it('should return the this.value when this.value is a number', () => { 32 | option.value = 20; 33 | expect(option.getNumber()).toEqual(20); 34 | }); 35 | 36 | it('should return a number of this.value when it is a string of a number', () => { 37 | option.value = '10'; 38 | expect(option.getNumber()).toEqual(10); 39 | option.value = '0'; 40 | expect(option.getNumber()).toEqual(0); 41 | option.value = '-5'; 42 | expect(option.getNumber()).toEqual(-5); 43 | }); 44 | 45 | it('should return null if this.value is not a string or a number', () => { 46 | option.value = true; 47 | expect(option.getNumber()).toBeNull(); 48 | option.value = false; 49 | expect(option.getNumber()).toBeNull(); 50 | }); 51 | 52 | it('should return NaN if this.value is a string but is not a number', () => { 53 | option.value = 'foobar'; 54 | expect(option.getNumber()).toEqual(NaN); 55 | }); 56 | }); 57 | }); 58 | 59 | describe('get boolean', () => { 60 | describe('for this.value not set', () => { 61 | it('should return the default value', () => { 62 | option = new Option('fake opt', 'fake description', 'boolean', true); 63 | expect(option.getBoolean()).toBeTruthy(); 64 | option = new Option('fake opt', 'fake description', 'boolean', false); 65 | expect(option.getBoolean()).not.toBeTruthy(); 66 | }); 67 | 68 | it('should return false if the default value is not defined', () => { 69 | option = new Option('fake opt', 'fake description', 'boolean'); 70 | expect(option.getBoolean()).not.toBeTruthy(); 71 | }); 72 | }); 73 | 74 | describe('for this.value set', () => { 75 | beforeEach(() => { 76 | option = new Option('fake opt', 'fake description', 'boolean'); 77 | }); 78 | 79 | it('should return a boolean when this.value is a string', () => { 80 | option.value = 'true'; 81 | expect(option.getBoolean()).toBeTruthy(); 82 | option.value = 'false'; 83 | expect(option.getBoolean()).not.toBeTruthy(); 84 | }); 85 | 86 | it('should return a boolean of this.value when this.value is a number', () => { 87 | option.value = 1; 88 | expect(option.getNumber()).toBeTruthy(); 89 | option.value = 0; 90 | expect(option.getNumber()).not.toBeTruthy(); 91 | }); 92 | 93 | it('should return the boolean of this.value when this.value is a boolean', () => { 94 | option.value = true; 95 | expect(option.getNumber()).toBeNull(); 96 | option.value = false; 97 | expect(option.getNumber()).toBeNull(); 98 | }); 99 | }); 100 | }); 101 | 102 | describe('get string', () => { 103 | describe('for this.value not set', () => { 104 | it('should return the default value', () => { 105 | option = new Option('fake opt', 'fake description', 'string', 'foobar'); 106 | expect(option.getString()).toBe('foobar'); 107 | option = new Option('fake opt', 'fake description', 'string', ''); 108 | expect(option.getString()).toBe(''); 109 | }); 110 | 111 | it('should return an empty string if the default value is not defined', () => { 112 | option = new Option('fake opt', 'fake description', 'string'); 113 | expect(option.getString()).toBe(''); 114 | }); 115 | }); 116 | 117 | describe('for this.value set', () => { 118 | beforeEach(() => { 119 | option = new Option('fake opt', 'fake description', 'string', 'foo'); 120 | }); 121 | 122 | it('should return this.value when this.value is a string', () => { 123 | option.value = 'bar'; 124 | expect(option.getString()).toEqual('bar'); 125 | option.value = ''; 126 | expect(option.getString()).toEqual(''); 127 | }); 128 | 129 | it('should return the string of this.value when this.value is a number', () => { 130 | option.value = 0; 131 | expect(option.getString()).toEqual('0'); 132 | option.value = 1; 133 | expect(option.getString()).toEqual('1'); 134 | }); 135 | 136 | it('should return the string of this.value when this.value is a boolean', () => { 137 | option.value = false; 138 | expect(option.getString()).toEqual('false'); 139 | option.value = true; 140 | expect(option.getString()).toEqual('true'); 141 | }); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /spec/cli/programs_spec.ts: -------------------------------------------------------------------------------- 1 | import {Option, Options, Program} from '../../lib/cli'; 2 | 3 | 4 | describe('program', () => { 5 | let program: Program; 6 | 7 | beforeEach( 8 | () => {program = new Program() 9 | .command('fooCmd', 'fooDescription') 10 | .addOption(new Option('fooString1', 'fooDescription', 'string', 'foo')) 11 | .addOption(new Option('fooString1', 'fooDescription', 'string', 'foo')) 12 | .addOption(new Option('fooBoolean1', 'fooDescription', 'boolean', false)) 13 | .addOption(new Option('fooBoolean2', 'fooDescription', 'boolean', true)) 14 | .addOption(new Option('fooNumber1', 'fooDescription', 'number', 1)) 15 | .addOption(new Option('fooNumber2', 'fooDescription', 'number', 2)) 16 | .addOption(new Option('fooNumber3', 'fooDescription', 'number', 3))}); 17 | 18 | it('should get minimist options', () => { 19 | let json = JSON.parse(JSON.stringify(program.getMinimistOptions())); 20 | expect(json.string.length).toEqual(1); 21 | expect(json.boolean.length).toEqual(2); 22 | expect(json.number.length).toEqual(3); 23 | let length = 0; 24 | for (let item in json.default) { 25 | length++; 26 | } 27 | expect(length).toEqual(6); 28 | expect(json.string[0]).toBe('fooString1'); 29 | expect(json.boolean[0]).toBe('fooBoolean1'); 30 | expect(json.boolean[1]).toBe('fooBoolean2'); 31 | expect(json.number[0]).toBe('fooNumber1'); 32 | expect(json.number[1]).toBe('fooNumber2'); 33 | expect(json.number[2]).toBe('fooNumber3'); 34 | }); 35 | 36 | it('should be able to extract the correct type and value', () => { 37 | let testString: string; 38 | let json = JSON.parse(JSON.stringify({ 39 | '_': ['fooCmd'], 40 | 'fooString1': 'bar', 41 | 'fooBoolean1': true, 42 | 'fooBoolean2': false, 43 | 'fooNumber1': 10, 44 | 'fooNumber2': 20, 45 | 'fooNumber3': 30 46 | })); 47 | let callbackTest = (options: Options) => { 48 | expect(options['fooString1'].getString()).toEqual('bar'); 49 | expect(options['fooBoolean1'].getBoolean()).toEqual(true); 50 | expect(options['fooBoolean2'].getBoolean()).toEqual(false); 51 | expect(options['fooNumber1'].getNumber()).toEqual(10); 52 | expect(options['fooNumber2'].getNumber()).toEqual(20); 53 | expect(options['fooNumber3'].getNumber()).toEqual(30); 54 | }; 55 | program.action(callbackTest); 56 | program.run(json); 57 | }); 58 | 59 | it('should be able to extract the mixed type and get the right type', () => { 60 | let testString: string; 61 | let json = JSON.parse(JSON.stringify({ 62 | '_': ['fooCmd'], 63 | 'fooString1': 1, 64 | 'fooBoolean1': 'true', 65 | 'fooBoolean2': 0, 66 | 'fooNumber1': '100', 67 | 'fooNumber2': 'foo', 68 | 'fooNumber3': true 69 | })); 70 | let callbackTest = (options: Options) => { 71 | expect(options['fooString1'].getString()).toEqual('1'); 72 | expect(options['fooBoolean1'].getBoolean()).toEqual(true); 73 | expect(options['fooBoolean2'].getBoolean()).toEqual(false); 74 | expect(options['fooNumber1'].getNumber()).toEqual(100); 75 | expect(options['fooNumber2'].getNumber()).toEqual(NaN); 76 | expect(options['fooNumber3'].getNumber()).toEqual(null); 77 | }; 78 | program.action(callbackTest); 79 | program.run(json); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /spec/cmds/status_spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import {Logger, WriteTo} from '../../lib/cli/logger'; 4 | import {program} from '../../lib/cmds/update'; 5 | import {spawnSync} from '../../lib/utils'; 6 | 7 | function getVersions(line: string): string[] { 8 | return line.split(':')[3].split(','); 9 | } 10 | 11 | describe('status', () => { 12 | Logger.writeTo = WriteTo.NONE; 13 | let argv: any; 14 | let tmpDir = path.resolve('selenium_test'); 15 | 16 | // chrome 2.20[last], 2.24 17 | // geckodriver {{config version}} [last] 18 | // standalone 2.24 [last], {{config version}} 19 | beforeAll((done) => { 20 | argv = { 21 | '_': ['update'], 22 | 'gecko': 'false', 23 | 'versions': {'chrome': '2.24', 'standalone': '2.44.0'}, 24 | 'out_dir': tmpDir 25 | }; 26 | program.run(JSON.parse(JSON.stringify(argv))) 27 | .then(() => { 28 | argv['versions']['chrome'] = '2.20'; 29 | program.run(JSON.parse(JSON.stringify(argv))).then(() => { 30 | done(); 31 | }); 32 | }) 33 | .catch(err => { 34 | done.fail(); 35 | }); 36 | }); 37 | 38 | xit('should show the version number of the default and latest versions', () => { 39 | let lines = 40 | spawnSync( 41 | process.execPath, 42 | ['built/lib/webdriver.js', 'status', '--out_dir', 'selenium_test', '--gecko', 'false'], 43 | 'pipe') 44 | .output[1] 45 | .toString() 46 | .split('\n'); 47 | let seleniumLine: string = null; 48 | let chromeLine: string = null; 49 | // let geckodriverLine: string = null; 50 | let androidSdkLine: string = null; 51 | let appiumLine: string = null; 52 | 53 | for (let line of lines) { 54 | if (line.indexOf('selenium') >= 0) { 55 | seleniumLine = line; 56 | } else if (line.indexOf('chrome') >= 0) { 57 | chromeLine = line; 58 | // } else if (line.indexOf('geckodriver') >= 0) { 59 | // geckodriverLine = line; 60 | } else if (line.indexOf('android-sdk') >= 0) { 61 | androidSdkLine = line; 62 | } else if (line.indexOf('appium') >= 0) { 63 | appiumLine = line; 64 | } 65 | } 66 | expect(seleniumLine).not.toBeNull(); 67 | expect(getVersions(seleniumLine).length).toEqual(1); 68 | expect(getVersions(seleniumLine)[0]).toContain('2.44.0 [last]'); 69 | 70 | expect(chromeLine).not.toBeNull(); 71 | expect(getVersions(chromeLine).length).toEqual(2); 72 | expect(getVersions(chromeLine)[0]).toContain('2.20 [last]'); 73 | expect(getVersions(chromeLine)[1]).toContain('2.24'); 74 | 75 | // expect(geckodriverLine).not.toBeNull(); 76 | // expect(geckodriverLine).toContain('[last]'); 77 | // expect(getVersions(geckodriverLine).length).toEqual(1); 78 | 79 | expect(androidSdkLine).not.toBeNull(); 80 | expect(androidSdkLine).toContain('not present'); 81 | expect(appiumLine).not.toBeNull(); 82 | expect(appiumLine).toContain('not present'); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /spec/cmds/update_spec.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as rimraf from 'rimraf'; 4 | 5 | import {Logger, WriteTo} from '../../lib/cli/logger'; 6 | import {clearBrowserFile, program} from '../../lib/cmds/update'; 7 | import {Config} from '../../lib/config'; 8 | 9 | interface Argv { 10 | [key: string]: any; 11 | } 12 | let argv: Argv = {}; 13 | 14 | describe('update', () => { 15 | describe('for update-config.json', () => { 16 | let tmpDir = ''; 17 | beforeEach(() => { 18 | Logger.writeTo = WriteTo.NONE; 19 | tmpDir = path.resolve('selenium_test'); 20 | try { 21 | // if the folder does not exist, it will throw an error on statSync 22 | if (fs.statSync(tmpDir).isDirectory()) { 23 | rimraf.sync(tmpDir); 24 | } 25 | } catch (err) { 26 | // do nothing, the directory does not exist 27 | } 28 | fs.mkdirSync(tmpDir); 29 | }); 30 | 31 | afterEach(() => { 32 | rimraf.sync(tmpDir); 33 | clearBrowserFile(); 34 | }); 35 | 36 | it('should create a file for chrome', (done) => { 37 | Config.osType_ = 'Linux'; 38 | Config.osArch_ = 'x64'; 39 | argv = { 40 | '_': ['update'], 41 | 'versions': {'chrome': '2.20'}, 42 | 'standalone': false, 43 | 'gecko': false, 44 | 'out_dir': tmpDir 45 | }; 46 | program.run(JSON.parse(JSON.stringify(argv))) 47 | .then(() => { 48 | let updateConfig = 49 | fs.readFileSync(path.resolve(tmpDir, 'update-config.json')).toString(); 50 | let updateObj = JSON.parse(updateConfig); 51 | expect(updateObj['chrome']['last']).toContain('chromedriver_2.20'); 52 | expect(updateObj['chrome']['all'].length).toEqual(1); 53 | expect(updateObj['chrome']['last']).toEqual(updateObj['chrome']['all'][0]); 54 | expect(updateObj['standalone']).toBeUndefined(); 55 | expect(updateObj['ie']).toBeUndefined(); 56 | done(); 57 | }) 58 | .catch((err: Error) => {done.fail()}); 59 | }); 60 | 61 | xit('should create a file for standalone', (done) => { 62 | Config.osType_ = 'Linux'; 63 | Config.osArch_ = 'x64'; 64 | argv = { 65 | '_': ['update'], 66 | 'versions': {'standalone': '2.53.1'}, 67 | 'chrome': false, 68 | 'gecko': false, 69 | 'out_dir': tmpDir 70 | }; 71 | program.run(JSON.parse(JSON.stringify(argv))) 72 | .then(() => { 73 | let updateConfig = 74 | fs.readFileSync(path.resolve(tmpDir, 'update-config.json')).toString(); 75 | let updateObj = JSON.parse(updateConfig); 76 | expect(updateObj['standalone']['last']).toContain('standalone-2.53.1.jar'); 77 | expect(updateObj['standalone']['all'].length).toEqual(1); 78 | expect(updateObj['standalone']['last']).toEqual(updateObj['standalone']['all'][0]); 79 | expect(updateObj['chrome']).toBeUndefined(); 80 | expect(updateObj['ie']).toBeUndefined(); 81 | done(); 82 | }) 83 | .catch((err: Error) => {done.fail()}); 84 | }); 85 | 86 | // TODO(cnishina): Create a test for Windows for IE driver. This will require rewriting 87 | // how programs get configurations. 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /spec/files/downloader_spec.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as rimraf from 'rimraf'; 4 | 5 | import {Downloader} from '../../lib/files'; 6 | 7 | describe('downloader', () => { 8 | describe('get file', () => { 9 | let fileUrl = 10 | 'https://selenium-release.storage.googleapis.com/3.0/selenium-server-standalone-3.0.0.jar'; 11 | let fileName = 'foobar.jar'; 12 | let outputDir = path.resolve('selenium_test'); 13 | let actualContentLength = 22138949; 14 | let contentLength: number; 15 | 16 | beforeEach(() => { 17 | try { 18 | // if the folder does not exist, it will throw an error on statSync 19 | if (fs.statSync(outputDir).isDirectory()) { 20 | rimraf.sync(outputDir); 21 | } 22 | } catch (err) { 23 | // do nothing, the directory does not exist 24 | } 25 | fs.mkdirSync(outputDir); 26 | }); 27 | 28 | xit('should download a file with mismatch content length', (done) => { 29 | contentLength = 0; 30 | Downloader.getFile(null, fileUrl, fileName, outputDir, contentLength) 31 | .then(result => { 32 | expect(result).toBeTruthy(); 33 | let file = path.resolve(outputDir, fileName); 34 | let stat = fs.statSync(file); 35 | expect(stat.size).toEqual(actualContentLength); 36 | rimraf.sync(file); 37 | done(); 38 | }) 39 | .catch(error => { 40 | console.log(error); 41 | done.fail(); 42 | }); 43 | }); 44 | 45 | it('should not download a file if the content lengths match', (done) => { 46 | contentLength = actualContentLength; 47 | Downloader.getFile(null, fileUrl, fileName, outputDir, contentLength) 48 | .then(result => { 49 | expect(result).not.toBeTruthy(); 50 | let file = path.resolve(outputDir, fileName); 51 | try { 52 | let access = fs.accessSync(file); 53 | } catch (err) { 54 | (err as any).code === 'ENOENT' 55 | } 56 | done(); 57 | }) 58 | .catch(error => { 59 | console.log(error); 60 | done.fail(); 61 | }); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /spec/files/file_manager_spec.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | import {AndroidSDK, Appium, Binary, BinaryMap, ChromeDriver, GeckoDriver, IEDriver, Standalone} from '../../lib/binaries'; 5 | import {Config} from '../../lib/config'; 6 | import {DownloadedBinary, FileManager} from '../../lib/files'; 7 | 8 | 9 | describe('file manager', () => { 10 | describe('setting up for windows', () => { 11 | let osType = 'Windows_NT'; 12 | 13 | it('should find correct binaries', () => { 14 | expect(FileManager.checkOS_(osType, ChromeDriver)).toBe(true); 15 | expect(FileManager.checkOS_(osType, IEDriver)).toBe(true); 16 | expect(FileManager.checkOS_(osType, Standalone)).toBe(true); 17 | expect(FileManager.checkOS_(osType, AndroidSDK)).toBe(true); 18 | expect(FileManager.checkOS_(osType, Appium)).toBe(true); 19 | }); 20 | 21 | it('should return the binary array', () => { 22 | let binaries = FileManager.compileBinaries_(osType); 23 | expect(binaries[Standalone.id].name).toBe((new Standalone()).name); 24 | expect(binaries[ChromeDriver.id].name).toBe((new ChromeDriver()).name); 25 | expect(binaries[IEDriver.id].name).toBe((new IEDriver()).name); 26 | expect(binaries[AndroidSDK.id].name).toBe((new AndroidSDK()).name); 27 | expect(binaries[Appium.id].name).toBe((new Appium()).name); 28 | }); 29 | }); 30 | 31 | describe('setting up for linux', () => { 32 | let osType = 'Linux'; 33 | 34 | it('should find correct binaries', () => { 35 | expect(FileManager.checkOS_(osType, ChromeDriver)).toBe(true); 36 | expect(FileManager.checkOS_(osType, IEDriver)).toBe(false); 37 | expect(FileManager.checkOS_(osType, Standalone)).toBe(true); 38 | expect(FileManager.checkOS_(osType, AndroidSDK)).toBe(true); 39 | expect(FileManager.checkOS_(osType, Appium)).toBe(true); 40 | }); 41 | 42 | it('should return the binary array', () => { 43 | let binaries = FileManager.compileBinaries_(osType); 44 | expect(binaries[Standalone.id].name).toBe((new Standalone()).name); 45 | expect(binaries[ChromeDriver.id].name).toBe((new ChromeDriver()).name); 46 | expect(binaries[AndroidSDK.id].name).toBe((new AndroidSDK()).name); 47 | expect(binaries[Appium.id].name).toBe((new Appium()).name); 48 | expect(binaries[IEDriver.id]).toBeUndefined(); 49 | }); 50 | }); 51 | 52 | describe('setting up for mac', () => { 53 | let osType = 'Darwin'; 54 | 55 | it('should find correct binaries', () => { 56 | expect(FileManager.checkOS_(osType, ChromeDriver)).toBe(true); 57 | expect(FileManager.checkOS_(osType, IEDriver)).toBe(false); 58 | expect(FileManager.checkOS_(osType, Standalone)).toBe(true); 59 | expect(FileManager.checkOS_(osType, AndroidSDK)).toBe(true); 60 | expect(FileManager.checkOS_(osType, Appium)).toBe(true); 61 | }); 62 | 63 | it('should return the binary array', () => { 64 | let binaries = FileManager.compileBinaries_(osType); 65 | expect(binaries[Standalone.id].name).toBe((new Standalone()).name); 66 | expect(binaries[ChromeDriver.id].name).toBe((new ChromeDriver()).name); 67 | expect(binaries[IEDriver.id]).toBeUndefined(); 68 | expect(binaries[AndroidSDK.id].name).toBe((new AndroidSDK()).name); 69 | expect(binaries[Appium.id].name).toBe((new Appium()).name); 70 | }); 71 | }); 72 | 73 | describe('downloaded version checks', () => { 74 | let existingFiles: string[]; 75 | let selenium = new Standalone(); 76 | let chrome = new ChromeDriver(); 77 | let android = new AndroidSDK(); 78 | let appium = new Appium(); 79 | let ie = new IEDriver(); 80 | let ostype: string; 81 | let arch: string; 82 | 83 | function setup(osType: string): void { 84 | ostype = osType; 85 | arch = 'x64'; 86 | existingFiles = [ 87 | selenium.prefix() + '2.51.0' + selenium.executableSuffix(), 88 | selenium.prefix() + '2.52.0' + selenium.executableSuffix() 89 | ]; 90 | chrome.ostype = ostype; 91 | chrome.osarch = arch; 92 | existingFiles.push(chrome.prefix() + '2.20' + chrome.suffix()); 93 | existingFiles.push(chrome.prefix() + '2.20' + chrome.executableSuffix()); 94 | existingFiles.push(chrome.prefix() + '2.21' + chrome.suffix()); 95 | existingFiles.push(chrome.prefix() + '2.21' + chrome.executableSuffix()); 96 | existingFiles.push(android.prefix() + '24.1.0' + android.suffix()); 97 | existingFiles.push(android.prefix() + '24.1.0' + android.executableSuffix()); 98 | existingFiles.push(android.prefix() + '24.1.1' + android.suffix()); 99 | existingFiles.push(android.prefix() + '24.1.1' + android.executableSuffix()); 100 | existingFiles.push(appium.prefix() + '1.6.0' + appium.suffix()); 101 | if (ostype == 'Windows_NT') { 102 | ie.ostype = ostype; 103 | ie.osarch = arch; 104 | existingFiles.push(ie.prefix() + '_Win32_2.51.0' + ie.suffix()); 105 | existingFiles.push(ie.prefix() + '_Win32_2.51.0' + ie.executableSuffix()); 106 | existingFiles.push(ie.prefix() + '_x64_2.51.0' + ie.suffix()); 107 | existingFiles.push(ie.prefix() + '_x64_2.51.0' + ie.executableSuffix()); 108 | existingFiles.push(ie.prefix() + '_Win32_2.52.0' + ie.suffix()); 109 | existingFiles.push(ie.prefix() + '_Win32_2.52.0' + ie.executableSuffix()); 110 | existingFiles.push(ie.prefix() + '_x64_2.52.0' + ie.suffix()); 111 | existingFiles.push(ie.prefix() + '_x64_2.52.0' + ie.executableSuffix()); 112 | } 113 | } 114 | 115 | describe('versions for selenium', () => { 116 | it('should find the correct version for windows', () => { 117 | setup('Windows_NT'); 118 | let downloaded = FileManager.downloadedVersions_(selenium, ostype, arch, existingFiles); 119 | expect(downloaded.versions.length).toBe(2); 120 | expect(downloaded.versions[0]).toBe('2.51.0'); 121 | expect(downloaded.versions[1]).toBe('2.52.0'); 122 | }); 123 | it('should find the correct version for mac', () => { 124 | setup('Darwin'); 125 | let downloaded = FileManager.downloadedVersions_(selenium, ostype, arch, existingFiles); 126 | expect(downloaded.versions.length).toBe(2); 127 | expect(downloaded.versions[0]).toBe('2.51.0'); 128 | expect(downloaded.versions[1]).toBe('2.52.0'); 129 | }); 130 | it('should find the correct version for mac', () => { 131 | setup('Linux'); 132 | let downloaded = FileManager.downloadedVersions_(selenium, ostype, arch, existingFiles); 133 | expect(downloaded.versions.length).toBe(2); 134 | expect(downloaded.versions[0]).toBe('2.51.0'); 135 | expect(downloaded.versions[1]).toBe('2.52.0'); 136 | }); 137 | }); 138 | 139 | describe('versions for chrome', () => { 140 | it('should find the correct version for windows', () => { 141 | setup('Windows_NT'); 142 | let downloaded = FileManager.downloadedVersions_(chrome, ostype, arch, existingFiles); 143 | expect(downloaded.versions.length).toBe(2); 144 | expect(downloaded.versions[0]).toBe('2.20'); 145 | expect(downloaded.versions[1]).toBe('2.21'); 146 | }); 147 | it('should find the correct version for mac', () => { 148 | setup('Darwin'); 149 | let downloaded = FileManager.downloadedVersions_(chrome, ostype, arch, existingFiles); 150 | expect(downloaded.versions.length).toBe(2); 151 | expect(downloaded.versions[0]).toBe('2.20'); 152 | expect(downloaded.versions[1]).toBe('2.21'); 153 | }); 154 | it('should find the correct version for linux', () => { 155 | setup('Linux'); 156 | let downloaded = FileManager.downloadedVersions_(chrome, ostype, arch, existingFiles); 157 | expect(downloaded.versions.length).toBe(2); 158 | expect(downloaded.versions[0]).toBe('2.20'); 159 | expect(downloaded.versions[1]).toBe('2.21'); 160 | }); 161 | }); 162 | 163 | describe('versions for android', () => { 164 | it('should find the correct version for windows', () => { 165 | setup('Windows_NT'); 166 | let downloaded = FileManager.downloadedVersions_(android, ostype, arch, existingFiles); 167 | expect(downloaded.versions.length).toBe(2); 168 | expect(downloaded.versions[0]).toBe('24.1.0'); 169 | expect(downloaded.versions[1]).toBe('24.1.1'); 170 | }); 171 | it('should find the correct version for mac', () => { 172 | setup('Darwin'); 173 | let downloaded = FileManager.downloadedVersions_(android, ostype, arch, existingFiles); 174 | expect(downloaded.versions.length).toBe(2); 175 | expect(downloaded.versions[0]).toBe('24.1.0'); 176 | expect(downloaded.versions[1]).toBe('24.1.1'); 177 | }); 178 | it('should find the correct version for linux', () => { 179 | setup('Linux'); 180 | let downloaded = FileManager.downloadedVersions_(android, ostype, arch, existingFiles); 181 | expect(downloaded.versions.length).toBe(2); 182 | expect(downloaded.versions[0]).toBe('24.1.0'); 183 | expect(downloaded.versions[1]).toBe('24.1.1'); 184 | }); 185 | }); 186 | 187 | describe('versions for appium', () => { 188 | it('should find the correct version for windows', () => { 189 | setup('Windows_NT'); 190 | let downloaded = FileManager.downloadedVersions_(appium, ostype, arch, existingFiles); 191 | expect(downloaded.versions.length).toBe(1); 192 | expect(downloaded.versions[0]).toBe('1.6.0'); 193 | }); 194 | it('should find the correct version for mac', () => { 195 | setup('Darwin'); 196 | let downloaded = FileManager.downloadedVersions_(appium, ostype, arch, existingFiles); 197 | expect(downloaded.versions.length).toBe(1); 198 | expect(downloaded.versions[0]).toBe('1.6.0'); 199 | }); 200 | it('should find the correct version for linux', () => { 201 | setup('Linux'); 202 | let downloaded = FileManager.downloadedVersions_(appium, ostype, arch, existingFiles); 203 | expect(downloaded.versions.length).toBe(1); 204 | expect(downloaded.versions[0]).toBe('1.6.0'); 205 | }); 206 | }); 207 | 208 | describe('versions for ie on windows', () => { 209 | it('should find the correct version for windows', () => { 210 | setup('Windows_NT'); 211 | let downloaded = FileManager.downloadedVersions_(ie, ostype, arch, existingFiles); 212 | expect(downloaded.versions.length).toBe(4); 213 | expect(downloaded.versions[0]).toBe('Win32_2.51.0'); 214 | expect(downloaded.versions[1]).toBe('x64_2.51.0'); 215 | expect(downloaded.versions[2]).toBe('Win32_2.52.0'); 216 | expect(downloaded.versions[3]).toBe('x64_2.52.0'); 217 | }); 218 | }); 219 | }); 220 | 221 | describe('configuring the CDN location', () => { 222 | describe('when no custom CDN is specified', () => { 223 | let defaults = Config.cdnUrls(); 224 | let binaries = FileManager.compileBinaries_('Windows_NT'); 225 | 226 | it('should use the default configuration for Android SDK', () => { 227 | expect(binaries[AndroidSDK.id].cdn).toEqual(defaults[AndroidSDK.id]); 228 | }); 229 | 230 | it('should use the default configuration for Appium', () => { 231 | expect(binaries[Appium.id].cdn).toEqual(defaults[Appium.id]); 232 | }); 233 | 234 | it('should use the default configuration for Chrome Driver', () => { 235 | expect(binaries[ChromeDriver.id].cdn).toEqual(defaults[ChromeDriver.id]); 236 | }); 237 | 238 | it('should use the default configuration for Gecko Driver', () => { 239 | expect(binaries[GeckoDriver.id].cdn).toEqual(defaults[GeckoDriver.id]); 240 | }); 241 | 242 | it('should use the default configuration for IE Driver', () => { 243 | expect(binaries[IEDriver.id].cdn).toEqual(defaults[IEDriver.id]); 244 | }); 245 | 246 | it('should use the default configuration for Selenium Standalone', () => { 247 | expect(binaries[Standalone.id].cdn).toEqual(defaults['selenium']); 248 | }); 249 | }); 250 | 251 | describe('when custom CDN is specified', () => { 252 | it('should configure the CDN for each binary', () => { 253 | let customCDN = 'https://my.corporate.cdn/'; 254 | let binaries = FileManager.compileBinaries_('Windows_NT', customCDN); 255 | 256 | forEachOf(binaries, binary => expect(binary.cdn).toEqual(customCDN, binary.name)); 257 | }); 258 | }); 259 | 260 | function forEachOf(binaries: BinaryMap, fn: (binary: T) => void) { 261 | for (var key in binaries) { 262 | fn(binaries[key]); 263 | } 264 | } 265 | }); 266 | 267 | // TODO(cnishina): download binaries for each os type / arch combination 268 | }); 269 | -------------------------------------------------------------------------------- /spec/http_utils_spec.ts: -------------------------------------------------------------------------------- 1 | import {Config} from '../lib/config'; 2 | import {HttpUtils} from '../lib/http_utils'; 3 | 4 | describe('http utils', () => { 5 | let fileUrlHttp = 'http://foobar.com'; 6 | let fileUrlHttps = 'https://foobar.com'; 7 | let argProxy = 'http://foobar.arg'; 8 | let envNoProxy = 'http://foobar.com'; 9 | let envHttpProxy = 'http://foobar.env'; 10 | let envHttpsProxy = 'https://foobar.env'; 11 | 12 | it('should return undefined when proxy arg is not used', () => { 13 | let proxy = HttpUtils.resolveProxy(fileUrlHttp); 14 | expect(proxy).toBeUndefined(); 15 | }); 16 | 17 | describe('proxy arg', () => { 18 | let opt_proxy = 'http://bar.foo'; 19 | it('should return the proxy arg', () => { 20 | let proxy = HttpUtils.resolveProxy(fileUrlHttp, opt_proxy); 21 | expect(proxy).toBe(opt_proxy); 22 | }); 23 | 24 | it('should always return the proxy arg with env var set', () => { 25 | Config.httpProxy_ = envHttpProxy; 26 | Config.httpsProxy_ = envHttpsProxy; 27 | Config.noProxy_ = envNoProxy; 28 | let proxy = HttpUtils.resolveProxy(fileUrlHttp, opt_proxy); 29 | expect(proxy).toBe(opt_proxy); 30 | }); 31 | }); 32 | 33 | describe('environment variables', () => { 34 | beforeEach(() => { 35 | Config.httpProxy_ = undefined; 36 | Config.httpsProxy_ = undefined; 37 | Config.noProxy_ = undefined; 38 | }); 39 | 40 | it('should return the HTTP env variable', () => { 41 | Config.httpProxy_ = envHttpProxy; 42 | let proxy = HttpUtils.resolveProxy(fileUrlHttp); 43 | expect(proxy).toBe(envHttpProxy); 44 | }); 45 | 46 | it('should return the HTTPS env variable for https protocol', () => { 47 | Config.httpProxy_ = envHttpsProxy; 48 | let proxy = HttpUtils.resolveProxy(fileUrlHttps); 49 | expect(proxy).toBe(envHttpsProxy); 50 | }); 51 | 52 | it('should return the HTTP env variable for https protocol', () => { 53 | Config.httpProxy_ = envHttpProxy; 54 | let proxy = HttpUtils.resolveProxy(fileUrlHttps); 55 | expect(proxy).toBe(envHttpProxy); 56 | }); 57 | 58 | describe('NO_PROXY environment variable', () => { 59 | beforeEach(() => { 60 | Config.noProxy_ = undefined; 61 | }); 62 | 63 | it('should return undefined when the NO_PROXY matches the fileUrl', () => { 64 | Config.noProxy_ = envNoProxy; 65 | let proxy = HttpUtils.resolveProxy(fileUrlHttp); 66 | expect(proxy).toBeUndefined(); 67 | }); 68 | 69 | it('should return undefined when the no_proxy matches the fileUrl', () => { 70 | Config.noProxy_ = envNoProxy; 71 | let proxy = HttpUtils.resolveProxy(fileUrlHttp); 72 | expect(proxy).toBeUndefined(); 73 | }); 74 | }); 75 | }); 76 | }) 77 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "built/spec", 3 | "spec_files": [ 4 | "**/*_spec.js" 5 | ], 6 | "stopSpecOnExpectationFailure": false, 7 | "random": false 8 | } 9 | -------------------------------------------------------------------------------- /spec/webdriver_spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import {cli} from '../lib/cli_instance'; 3 | import {spawnSync} from '../lib/utils'; 4 | 5 | describe('cli', () => { 6 | describe('help', () => { 7 | it('should have usage and commands', () => { 8 | let lines = spawnSync(process.execPath, ['built/lib/webdriver.js', 'help'], 'pipe') 9 | .output[1] 10 | .toString() 11 | .split('\n'); 12 | 13 | // very specific to make sure the 14 | let index = 0; 15 | expect(lines[index++].indexOf('Usage:')).toBe(0); 16 | index++; 17 | expect(lines[index++].indexOf('Commands:')).toBe(0); 18 | for (let cmd in cli.programs) { 19 | expect(lines[index++].indexOf(cmd)).toBe(2); 20 | } 21 | index++; 22 | expect(lines[index++].indexOf('Options:')).toBe(0); 23 | let options = cli.getOptions(); 24 | for (let opt in options) { 25 | expect(lines[index++].indexOf('--' + opt)).toBe(2); 26 | } 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "declaration": true, 8 | "removeComments": false, 9 | "noImplicitAny": true, 10 | "outDir": "built/", 11 | "types": [ 12 | "adm-zip", "chalk", "glob", "jasmine", "minimist", 13 | "node", "q", "request", "rimraf", "semver" 14 | ] 15 | }, 16 | "exclude": [ 17 | "built", 18 | "node_modules" 19 | ] 20 | } 21 | --------------------------------------------------------------------------------