├── .gitignore ├── .travis.yml ├── README.md ├── examples ├── example.travis.yml ├── example_config.json ├── example_datasource.js ├── example_datasource_script.json ├── example_factory.js ├── example_listener.js ├── example_multi_config.json ├── example_proxy.json ├── example_suite.json ├── example_suite_config.json ├── example_travis_config.json └── tests │ ├── a_directory │ ├── assertTitle.json │ └── get.json │ ├── acceptAlert.json │ ├── answerAlert.json │ ├── assertAlertPresent.json │ ├── assertAlertPresent_fail.json │ ├── assertTitle.json │ ├── clickAndVerify.json │ ├── dismissAlert.json │ ├── full_example.json │ ├── get.json │ ├── meow.json │ ├── missingParam.json │ ├── not_assertAlertPresent.json │ ├── not_assertAlertPresent_fail.json │ ├── printTitle.json │ ├── switchToFrame.json │ ├── switchToFrameByIndex.json │ ├── switchToWindow.json │ ├── verifyNotTitle.json │ └── waitForElement_timeout.json ├── interpreter.js ├── package.json ├── step_types ├── AlertPresent.js ├── AlertText.js ├── BodyText.js ├── CookieByName.js ├── CookiePresent.js ├── CurrentUrl.js ├── ElementAttribute.js ├── ElementPresent.js ├── ElementSelected.js ├── ElementStyle.js ├── ElementValue.js ├── Eval.js ├── PageSource.js ├── Text.js ├── TextPresent.js ├── Title.js ├── acceptAlert.js ├── addCookie.js ├── answerAlert.js ├── clickAndHoldElement.js ├── clickElement.js ├── close.js ├── deleteCookie.js ├── dismissAlert.js ├── doubleClickElement.js ├── get.js ├── goBack.js ├── goForward.js ├── mouseOverElement.js ├── pause.js ├── print.js ├── refresh.js ├── releaseElement.js ├── saveScreenshot.js ├── sendKeysToElement.js ├── setElementNotSelected.js ├── setElementSelected.js ├── setElementText.js ├── setWindowSize.js ├── store.js ├── submitElement.js ├── switchToDefaultContent.js ├── switchToFrame.js ├── switchToFrameByIndex.js ├── switchToWindow.js ├── switchToWindowByIndex.js └── switchToWindowByTitle.js └── utils └── sauce_listener.js /.gitignore: -------------------------------------------------------------------------------- 1 | # NPM can pull in the required dependencies on its own. 2 | node_modules/ 3 | 4 | # OS garbage 5 | .DS_Store 6 | .DS_Store? 7 | ._* 8 | .Spotlight-V100 9 | .Trashes 10 | Icon? 11 | ehthumbs.db 12 | Thumbs.db 13 | 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | script: 3 | - "./interpreter.js examples/example_travis_config.json" 4 | env: 5 | global: 6 | - SAUCE_USERNAME=zarkonnen 7 | - secure: "CqbgxeI6t40oWTuitRaYKZHAxJpY9CjTHZSkLeoA4HtRqRIbBQQsMpB3NvfUV/qukaJ8zgYsswvU4c+V1Dz26NC9tdCZV25nDLSHF1mEQ4GAkY1ALus4FGnkgcL+9UwRNX1hA4JtcYpQ+Rz8m5uPH/vBdGMyuNlFTkaV0/YTOdc=" 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | se-interpreter [![Build Status](https://api.travis-ci.org/Zarkonnen/se-interpreter.png)](http://travis-ci.org/Zarkonnen/se-interpreter) 2 | ============== 3 | 4 | This is a command-line tool for interpreting [Selenium Builder](http://www.sebuilder.com) JSON script files, based on [node](http://nodejs.org/) and the [wd](https://github.com/admc/wd) Javascript client driver for [Selenium 2](http://seleniumhq.org/). There is also a [Java-based counterpart](https://github.com/sebuilder/se-builder/wiki/Se-Interpreter). 5 | 6 | Using Selenium Builder, [GitHub for Selenium Builder](http://zarkonnen.github.com/sb-github-integration/), se-interpreter, [Travis](http://travis-ci.org/), and [Sauce OnDemand](http://saucelabs.com/), you can set up a completely, er, cloud-based continuous integration UI testing system for your website. 7 | 8 | se-interpreter is developed by [David Stark](mailto:david.stark@zarkonnen.com) at the behest of [Sauce Labs](http://saucelabs.com/), and licensed under the Apache License, Version 2.0: 9 | 10 | Copyright 2013 Sauce Labs 11 | 12 | Licensed under the Apache License, Version 2.0 (the "License"); 13 | you may not use this file except in compliance with the License. 14 | You may obtain a copy of the License at 15 | 16 | http://www.apache.org/licenses/LICENSE-2.0 17 | 18 | Unless required by applicable law or agreed to in writing, software 19 | distributed under the License is distributed on an "AS IS" BASIS, 20 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | See the License for the specific language governing permissions and 22 | limitations under the License. 23 | 24 | # Documentation 25 | 26 | ## Installation 27 | Install se-interpreter through [npm](https://npmjs.org/) by invoking 28 | 29 | sudo npm install -g se-interpreter 30 | 31 | ## Basic usage 32 | First, make sure you have a local [Selenium Server](http://seleniumhq.org/download/) running. Then, invoke se-interpreter: 33 | 34 | se-interpreter examples/tests/get.json 35 | 36 | This should start up an instance of Firefox, navigate to [the Selenium Builder site](http://sebuilder.github.com/se-builder/), and then exit successfully. 37 | 38 | You can specify multiple commands: 39 | 40 | se-interpreter examples/tests/get.json examples/tests/assertTitle.json 41 | 42 | The second one of these tests is intended to fail. 43 | 44 | And you can use glob syntax to specify whole directories: 45 | 46 | se-interpreter examples/tests/a_directory/* 47 | 48 | Again, the second test is intended to fail. 49 | 50 | ## Suites 51 | 52 | You can also specify paths to suites, which will execute all scripts in them. 53 | 54 | ## Command-line options 55 | * `--quiet` Disables printing of information about each step and script. Only print step outputs and the final result are reported. 56 | * `--noPrint` Disables print step output. 57 | * `--silent` Disables all non-fatal-error text output. 58 | * `--parallel=`_n_ Runs _n_ tests in parallel. The default is no parallel execution. 59 | * `--driver-`_x_`=`_y_ Passes _y_ as the webdriver parameter _x_. Example: `--driver-host=mywebdriver.mycompany.com`. 60 | * `--browser-`_x_`=`_y_ Passes _y_ as the browser parameter _x_. Example: `--browser-browserName=chrome`. 61 | 62 | You can specify multiple `--browser-browserName` arguments, and the tests will be played back on each browser specified. 63 | 64 | ## Playing back tests on Sauce OnDemand 65 | To run your tests on Sauce OnDemand, use the following parameters, with your Sauce username and access key: 66 | 67 | --driver-host=ondemand.saucelabs.com --driver-port=80 --browser-username=[?] --browser-accessKey=[?] 68 | 69 | You can run multiple tests in parallel on OnDemand, but note that if `--parallel` is set to more than the maximum number of parallel tests for your account, __the interpreter will hang indefinitely__. For a free account, this maximum number is two. 70 | 71 | ## Configuration files 72 | Instead of specifying the scripts/suites and configuration on the command-line, you can use JSON-based configuration files. An example: 73 | 74 | { 75 | "type": "interpreter-config", 76 | "configurations": [ 77 | { 78 | "settings": [ 79 | { 80 | "browserOptions": { 81 | "browserName": "firefox" 82 | } 83 | }, 84 | { 85 | "browserOptions": { 86 | "browserName": "chrome" 87 | } 88 | } 89 | ], 90 | "scripts": [ 91 | "examples/tests/printTitle.json", 92 | "examples/tests/a_directory/*" 93 | ] 94 | } 95 | ] 96 | } 97 | 98 | This configuration file runs `printTitle.json` and the two tests in `a_directory/`, on both Firefox and Chrome. The format works as follows: 99 | 100 | * The root object contains two properties: `"type": "interpreter-config"`, and `"configurations"`, which is a list of configurations. 101 | * Each configuration is an independent set of tests to run. It also contains two properties: `"settings"`, which is a list of settings and `"scripts"`, which is a list of paths for the scripts to execute. 102 | * Each settings object may contain `"browserOptions"`, which are treated like `--browser` command line arguments, and `"driverOptions"`, which are treated like `--driver` command line arguments. 103 | * All scripts within a configuration are run with all settings in that configuration. 104 | * `${ENV_VAR_NAME}` expressions are substituted for the value of the specified environment variable. 105 | 106 | The following configuration file runs the same three tests on Sauce OnDemand, assuming you have set the `SAUCE_USERNAME` and `SAUCE_ACCESS_KEY` environment variables. 107 | 108 | { 109 | "type": "interpreter-config", 110 | "configurations": [ 111 | { 112 | "settings": [ 113 | { 114 | "driverOptions": { 115 | "host": "ondemand.saucelabs.com", 116 | "port": 80 117 | }, 118 | "browserOptions": { 119 | "browserName": "firefox", 120 | "username": "${SAUCE_USERNAME}", 121 | "accessKey": "${SAUCE_ACCESS_KEY}" 122 | } 123 | } 124 | ], 125 | "scripts": [ 126 | "examples/tests/printTitle.json", 127 | "examples/tests/a_directory/*" 128 | ] 129 | } 130 | ] 131 | } 132 | 133 | ## Travis integration 134 | se-interpreter integrates with [Travis](https://travis-ci.org/) really easily. The [interpreter-travis-example](https://github.com/Zarkonnen/interpreter-travis-example) repo is an example setup you can fork. The details: 135 | 136 | To set up a repository to run its Builder tests on Travis, add a `.travis.yml` file to the repository root that looks something like this: 137 | 138 | language: node_js 139 | before_script: 140 | - "npm install -g se-interpreter" 141 | script: 142 | - "se-interpreter my_interpreter_config.json" 143 | env: 144 | global: 145 | - SAUCE_USERNAME= 146 | - secure: 147 | 148 | For `my_travis_config.json` use a config file like the second example above. If you are setting up a web server on Travis and using Sauce Connect from Travis to Sauce Labs, then your driverOptions host must be 'localhost' and your port must be '4445'. Note that this configuration uses [Travis' support for encrypting environment variables](http://about.travis-ci.org/docs/user/encryption-keys/) to prevent having to put your access key into a publicly visible place. 149 | 150 | ## Listeners 151 | Using `--listener=`_path-to-listener_, you can specify a module that provides listeners that se-interpreter will attach to each script being run. An example listener module implementation is provided in `examples/example_listener.js`. A listener module should export a function called `getInterpreterListener` that returns an object that may define any of the following functions: 152 | 153 | * `startTestRun(testRun, info)` Called when a test run has started. 154 | * `endTestRun(testRun, info)` Called when a test has completed. 155 | * `startStep(testRun, step)` Called when a step is about to start. 156 | * `endStep(testRun, step, info)` Called when a step has completed. 157 | * `endAllRuns(num_runs, successes)` Called when all tests have completed. 158 | 159 | The `info` objects have two keys: `success`, which is `true` or `false`, and `error`, which may contain an exception if `success` is false. The `interpreter` module itself contains a listener implementation which is used as the default listener if `--quiet` or `--silent` is not specified. 160 | 161 | ## Data sources 162 | You can specify additional data source modules to support custom data-driven testing sources using `--dataSource=`_path-to-datasource_. An example data source module implementation is provided in `examples/example_datasource.js`. 163 | 164 | ## Adding extra step types 165 | There are two ways of adding support for extra step types to se-interpreter. 166 | 167 | First, you can add extra files into the `step_types` directory in the module. See the contents of this directory for examples. The directory contains both files like `get.json`, which implements the `get` step, and `Title.json`, which implements a way to get at the current title, and is used by generic assert/verify/store/waitFor implementations. 168 | 169 | Second, you can specify a `--executorFactory=`_path-to-factory_ command line argument. The executor factory is a module that should export a function called `get(stepType)`, returning either an step type implementation/getter, or null if the module can't supply an implementation for playing back a step called `stepType`. See `examples/example_factory.js` for a simple example. 170 | 171 | ## Using a proxy 172 | You can specify a proxy for the browser to use by putting a [proxy object](https://code.google.com/p/selenium/wiki/DesiredCapabilities) into the browser settings. Example using a proxy at localhost: 173 | 174 | { 175 | "type": "interpreter-config", 176 | "configurations": [ 177 | { 178 | "settings": [ 179 | { 180 | "browserOptions": { 181 | "browserName": "firefox", 182 | "proxy": { 183 | "proxyType": "manual", 184 | "httpProxy": "localhost:8085" 185 | } 186 | } 187 | } 188 | ], 189 | "scripts": [ 190 | "examples/tests/get.json" 191 | ] 192 | } 193 | ] 194 | } 195 | 196 | ## Using se-interpreter as a module 197 | It's also possible to use se-interpreter as a module in other node code, using `require('se-interpreter')`. To try this out, install se-interpreter locally as a node module: 198 | 199 | npm install se-interpreter 200 | 201 | Then, start up a Selenium Server, enter `node` and drive a simple interpreter session from the command line: 202 | 203 | var si = require('se-interpreter'); 204 | var tr = new si.TestRun({"steps": [{"type":"get", "url":"http://www.google.com"}]}, "Go to Google"); 205 | tr.listener = si.getInterpreterListener(tr); 206 | tr.start(); 207 | tr.next(); 208 | tr.end(); 209 | 210 | ## Getting help 211 | Feel free to mail me at david.stark@zarkonnen.com with questions (including "How do I get this to work?), suggestions, and feedback. You can also [report issues on GitHub](https://github.com/Zarkonnen/se-interpreter/issues). For issues with Sauce OnDemand, [contact the Sauce help desk](http://support.saucelabs.com/home). 212 | -------------------------------------------------------------------------------- /examples/example.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | script: 3 | - "./interpreter.js examples/example_travis_config.json" 4 | env: 5 | global: 6 | - SAUCE_USERNAME= 7 | - secure: -------------------------------------------------------------------------------- /examples/example_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "interpreter-config", 3 | "configurations": [ 4 | { 5 | "settings": [ 6 | { 7 | "browserOptions": { 8 | "browserName": "firefox" 9 | } 10 | } 11 | ], 12 | "scripts": [ 13 | "examples/tests/a_directory/*" 14 | ] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /examples/example_datasource.js: -------------------------------------------------------------------------------- 1 | exports.name = 'example'; 2 | exports.load = function(cfg, scriptPath) { 3 | return [{'examplekey': 'examplevalue'}]; // Return a single row with a single column, "examplekey", containing "examplevalue". 4 | }; -------------------------------------------------------------------------------- /examples/example_datasource_script.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 2, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://sebuilder.github.io/se-builder/" 9 | }, 10 | { 11 | "type": "print", 12 | "text": "${examplekey}" 13 | } 14 | ], 15 | "data": { 16 | "configs": { 17 | "example": {} 18 | }, 19 | "source": "example" 20 | }, 21 | "inputs": [] 22 | } -------------------------------------------------------------------------------- /examples/example_factory.js: -------------------------------------------------------------------------------- 1 | /** An example step executor factory that adds a single step type: "meow". */ 2 | exports.get = function(stepType) { 3 | if (stepType == "meow") { 4 | return { 5 | 'run': function(tr, cb) { 6 | console.log("meow!"); 7 | cb({'success': true}); 8 | } 9 | }; 10 | } 11 | 12 | return null; 13 | }; -------------------------------------------------------------------------------- /examples/example_listener.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | /** An example interpreter listener factory with all listener functions implemented. */ 4 | exports.getInterpreterListener = function(testRun) { 5 | return { 6 | 'startTestRun': function(testRun, info) { 7 | console.log("Listener: test run starting!"); 8 | console.log("Listener: success: " + info.success); 9 | console.log("Listener: error: " + util.inspect(info.error)); 10 | }, 11 | 'endTestRun': function(testRun, info) { 12 | console.log("Listener: test run ending!"); 13 | console.log("Listener: success: " + info.success); 14 | console.log("Listener: error: " + util.inspect(info.error)); 15 | }, 16 | 'startStep': function(testRun, step) { 17 | console.log("Listener: step starting!"); 18 | console.log("Listener: " + JSON.stringify(step)); 19 | }, 20 | 'endStep': function(testRun, step, info) { 21 | console.log("Listener: step ending!"); 22 | console.log("Listener: " + JSON.stringify(step)); 23 | console.log("Listener: success: " + info.success); 24 | console.log("Listener: error: " + util.inspect(info.error)); 25 | }, 26 | 'endAllRuns': function(num_runs, successes) { 27 | console.log("Listener: all runs ended!"); 28 | console.log("Listener: number of runs was " + num_runs); 29 | console.log("Listener: number of successful runs was " + successes); 30 | } 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /examples/example_multi_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "interpreter-config", 3 | "configurations": [ 4 | { 5 | "settings": [ 6 | { 7 | "driverOptions": { 8 | "host": "ondemand.saucelabs.com", 9 | "port": 80 10 | }, 11 | "browserOptions": { 12 | "browserName": "firefox", 13 | "username": "${SAUCE_USERNAME}", 14 | "accessKey": "${SAUCE_ACCESS_KEY}" 15 | } 16 | }, 17 | { 18 | "browserOptions": { 19 | "browserName": "firefox" 20 | } 21 | } 22 | ], 23 | "scripts": [ 24 | "examples/tests/*" 25 | ] 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /examples/example_proxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "interpreter-config", 3 | "configurations": [ 4 | { 5 | "settings": [ 6 | { 7 | "browserOptions": { 8 | "browserName": "firefox", 9 | "proxy": { 10 | "proxyType": "manual", 11 | "httpProxy": "localhost:8085" 12 | } 13 | } 14 | } 15 | ], 16 | "scripts": [ 17 | "examples/tests/get.json" 18 | ] 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /examples/example_suite.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "suite", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "scripts": [ 6 | { 7 | "where": "local", 8 | "path": "tests/printTitle.json" 9 | }, 10 | { 11 | "where": "local", 12 | "path": "tests/get.json" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /examples/example_suite_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "interpreter-config", 3 | "configurations": [ 4 | { 5 | "settings": [ 6 | { 7 | "browserOptions": { 8 | "browserName": "firefox" 9 | } 10 | } 11 | ], 12 | "scripts": [ 13 | "examples/example_suite.json" 14 | ] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /examples/example_travis_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "interpreter-config", 3 | "configurations": [ 4 | { 5 | "settings": [ 6 | { 7 | "driverOptions": { 8 | "host": "ondemand.saucelabs.com", 9 | "port": 80 10 | }, 11 | "browserOptions": { 12 | "browserName": "firefox", 13 | "username": "${SAUCE_USERNAME}", 14 | "accessKey": "${SAUCE_ACCESS_KEY}" 15 | } 16 | } 17 | ], 18 | "scripts": [ 19 | "examples/tests/full_example.json", 20 | "examples/example_suite.json" 21 | ] 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /examples/tests/a_directory/assertTitle.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://sebuilder.github.com/se-builder/" 9 | }, 10 | { 11 | "type": "assertTitle", 12 | "title": "Kumquat" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /examples/tests/a_directory/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://sebuilder.github.com/se-builder/" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /examples/tests/acceptAlert.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://sebuilder.github.com/se-builder/test/confirm.html" 9 | }, 10 | { 11 | "type": "acceptAlert" 12 | }, 13 | { 14 | "type": "waitForAlertText", 15 | "text": "yes" 16 | }, 17 | { 18 | "type": "assertAlertText", 19 | "text": "yes" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /examples/tests/answerAlert.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://sebuilder.github.com/se-builder/test/prompt.html" 9 | }, 10 | { 11 | "type": "answerAlert", 12 | "text": "yes" 13 | }, 14 | { 15 | "type": "waitForAlertText", 16 | "text": "yes" 17 | }, 18 | { 19 | "type": "assertAlertText", 20 | "text": "yes" 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /examples/tests/assertAlertPresent.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://sebuilder.github.com/se-builder/test/alert.html" 9 | }, 10 | { 11 | "type": "assertAlertPresent" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /examples/tests/assertAlertPresent_fail.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://sebuilder.github.com/se-builder/test/empty.html" 9 | }, 10 | { 11 | "type": "assertAlertPresent" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /examples/tests/assertTitle.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://sebuilder.github.com/se-builder/" 9 | }, 10 | { 11 | "type": "assertTitle", 12 | "title": "Kumquat" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /examples/tests/clickAndVerify.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://sebuilder.github.com/se-builder/" 9 | }, 10 | { 11 | "type": "clickElement", 12 | "locator": { 13 | "type": "link text", 14 | "value": "Plugins" 15 | } 16 | }, 17 | { 18 | "type": "verifyTextPresent", 19 | "text": "Plugins" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /examples/tests/dismissAlert.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://sebuilder.github.com/se-builder/test/confirm.html" 9 | }, 10 | { 11 | "type": "dismissAlert" 12 | }, 13 | { 14 | "type": "waitForAlertText", 15 | "text": "no" 16 | }, 17 | { 18 | "type": "assertAlertText", 19 | "negated": true, 20 | "text": "yes" 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /examples/tests/full_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://saucelabs.com/test/guinea-pig/" 9 | }, 10 | { 11 | "type": "clickElement", 12 | "locator": { 13 | "type": "link text", 14 | "value": "i am a link" 15 | } 16 | }, 17 | { 18 | "type": "storeTitle", 19 | "variable": "title" 20 | }, 21 | { 22 | "type": "waitForTitle", 23 | "title": "${title}" 24 | }, 25 | { 26 | "type": "verifyTitle", 27 | "title": "${title}" 28 | }, 29 | { 30 | "type": "verifyTitle", 31 | "negated": true, 32 | "title": "asdf" 33 | }, 34 | { 35 | "type": "assertTitle", 36 | "title": "${title}" 37 | }, 38 | { 39 | "type": "assertTitle", 40 | "negated": true, 41 | "title": "asdf" 42 | }, 43 | { 44 | "type": "storeCurrentUrl", 45 | "variable": "url" 46 | }, 47 | { 48 | "type": "waitForCurrentUrl", 49 | "url": "${url}" 50 | }, 51 | { 52 | "type": "verifyCurrentUrl", 53 | "url": "${url}" 54 | }, 55 | { 56 | "type": "verifyCurrentUrl", 57 | "negated": true, 58 | "url": "http://google.com" 59 | }, 60 | { 61 | "type": "waitForCurrentUrl", 62 | "negated": true, 63 | "url": "http://google.com" 64 | }, 65 | { 66 | "type": "assertCurrentUrl", 67 | "url": "${url}" 68 | }, 69 | { 70 | "type": "assertCurrentUrl", 71 | "negated": true, 72 | "url": "http://google.com" 73 | }, 74 | { 75 | "type": "storeText", 76 | "variable": "text", 77 | "locator": { 78 | "type": "id", 79 | "value": "i_am_an_id" 80 | } 81 | }, 82 | { 83 | "type": "waitForText", 84 | "text": "${text}", 85 | "locator": { 86 | "type": "id", 87 | "value": "i_am_an_id" 88 | } 89 | }, 90 | { 91 | "type": "verifyText", 92 | "text": "${text}", 93 | "locator": { 94 | "type": "id", 95 | "value": "i_am_an_id" 96 | } 97 | }, 98 | { 99 | "type": "waitForText", 100 | "text": "not ${text}", 101 | "locator": { 102 | "type": "id", 103 | "value": "i_am_an_id" 104 | }, 105 | "negated": true 106 | }, 107 | { 108 | "type": "verifyText", 109 | "text": "not ${text}", 110 | "locator": { 111 | "type": "id", 112 | "value": "i_am_an_id" 113 | }, 114 | "negated": true 115 | }, 116 | { 117 | "type": "assertText", 118 | "text": "${text}", 119 | "locator": { 120 | "type": "id", 121 | "value": "i_am_an_id" 122 | } 123 | }, 124 | { 125 | "type": "assertText", 126 | "text": "not ${text}", 127 | "locator": { 128 | "type": "id", 129 | "value": "i_am_an_id" 130 | }, 131 | "negated": true 132 | }, 133 | { 134 | "type": "storeTextPresent", 135 | "text": "I am another div", 136 | "variable": "text_is_present" 137 | }, 138 | { 139 | "type": "store", 140 | "variable": "text_present", 141 | "text": "I am another div" 142 | }, 143 | { 144 | "type": "waitForTextPresent", 145 | "text": "${text_present}" 146 | }, 147 | { 148 | "type": "verifyTextPresent", 149 | "text": "${text_present}" 150 | }, 151 | { 152 | "type": "waitForTextPresent", 153 | "text": "not ${text_present}", 154 | "negated": true 155 | }, 156 | { 157 | "type": "verifyTextPresent", 158 | "text": "not ${text_present}", 159 | "negated": true 160 | }, 161 | { 162 | "type": "assertTextPresent", 163 | "text": "${text_present}" 164 | }, 165 | { 166 | "type": "assertTextPresent", 167 | "text": "not ${text_present}", 168 | "negated": true 169 | }, 170 | { 171 | "type": "storeBodyText", 172 | "variable": "body_text" 173 | }, 174 | { 175 | "type": "waitForBodyText", 176 | "text": "${body_text}" 177 | }, 178 | { 179 | "type": "verifyBodyText", 180 | "text": "${body_text}" 181 | }, 182 | { 183 | "type": "waitForBodyText", 184 | "text": "not ${body_text}", 185 | "negated": true 186 | }, 187 | { 188 | "type": "verifyBodyText", 189 | "text": "not ${body_text}", 190 | "negated": true 191 | }, 192 | { 193 | "type": "assertBodyText", 194 | "text": "${body_text}" 195 | }, 196 | { 197 | "type": "assertBodyText", 198 | "text": "not ${body_text}", 199 | "negated": true 200 | }, 201 | { 202 | "type": "storePageSource", 203 | "variable": "page_source" 204 | }, 205 | { 206 | "type": "waitForPageSource", 207 | "source": "${page_source}" 208 | }, 209 | { 210 | "type": "verifyPageSource", 211 | "source": "${page_source}" 212 | }, 213 | { 214 | "type": "waitForPageSource", 215 | "source": " ${page_source}", 216 | "negated": true 217 | }, 218 | { 219 | "type": "verifyPageSource", 220 | "source": " ${page_source}", 221 | "negated": true 222 | }, 223 | { 224 | "type": "assertPageSource", 225 | "source": "${page_source}" 226 | }, 227 | { 228 | "type": "assertPageSource", 229 | "source": " ${page_source}", 230 | "negated": true 231 | }, 232 | { 233 | "type": "addCookie", 234 | "name": "test_cookie", 235 | "value": "this-is-a-cookie", 236 | "options": "path=/,max_age=100000000" 237 | }, 238 | { 239 | "type": "storeCookiePresent", 240 | "variable": "cookie_is_present", 241 | "name": "test_cookie" 242 | }, 243 | { 244 | "type": "storeCookieByName", 245 | "variable": "cookie", 246 | "name": "test_cookie" 247 | }, 248 | { 249 | "type": "print", 250 | "text": "${cookie};" 251 | }, 252 | { 253 | "type": "waitForCookiePresent", 254 | "name": "test_cookie" 255 | }, 256 | { 257 | "type": "verifyCookiePresent", 258 | "name": "test_cookie" 259 | }, 260 | { 261 | "type": "assertCookiePresent", 262 | "name": "test_cookie" 263 | }, 264 | { 265 | "type": "waitForCookieByName", 266 | "name": "test_cookie", 267 | "value": "${cookie}" 268 | }, 269 | { 270 | "type": "verifyCookieByName", 271 | "name": "test_cookie", 272 | "value": "${cookie}" 273 | }, 274 | { 275 | "type": "waitForCookieByName", 276 | "name": "test_cookie", 277 | "value": "not ${cookie}", 278 | "negated": true 279 | }, 280 | { 281 | "type": "verifyCookieByName", 282 | "name": "test_cookie", 283 | "value": "not ${cookie}", 284 | "negated": true 285 | }, 286 | { 287 | "type": "assertCookieByName", 288 | "name": "test_cookie", 289 | "value": "${cookie}" 290 | }, 291 | { 292 | "type": "assertCookieByName", 293 | "name": "test_cookie", 294 | "value": "not ${cookie}", 295 | "negated": true 296 | }, 297 | { 298 | "type": "deleteCookie", 299 | "name": "test_cookie" 300 | }, 301 | { 302 | "type": "waitForCookiePresent", 303 | "name": "test_cookie", 304 | "negated": true 305 | }, 306 | { 307 | "type": "verifyCookiePresent", 308 | "name": "test_cookie", 309 | "negated": true 310 | }, 311 | { 312 | "type": "assertCookiePresent", 313 | "name": "test_cookie", 314 | "negated": true 315 | }, 316 | { 317 | "type": "refresh" 318 | }, 319 | { 320 | "type": "goBack" 321 | }, 322 | { 323 | "type": "goForward" 324 | }, 325 | { 326 | "type": "goBack" 327 | }, 328 | { 329 | "type": "waitForTextPresent", 330 | "text": "comments" 331 | }, 332 | { 333 | "type": "saveScreenshot", 334 | "file": "/tmp/screen.png" 335 | }, 336 | { 337 | "type": "print", 338 | "text": "this is some debug text" 339 | }, 340 | { 341 | "type": "storeElementSelected", 342 | "variable": "element_is_selected", 343 | "locator": { 344 | "type": "id", 345 | "value": "unchecked_checkbox" 346 | } 347 | }, 348 | { 349 | "type": "setElementSelected", 350 | "locator": { 351 | "type": "id", 352 | "value": "unchecked_checkbox" 353 | } 354 | }, 355 | { 356 | "type": "waitForElementSelected", 357 | "locator": { 358 | "type": "id", 359 | "value": "unchecked_checkbox" 360 | } 361 | }, 362 | { 363 | "type": "verifyElementSelected", 364 | "locator": { 365 | "type": "id", 366 | "value": "unchecked_checkbox" 367 | } 368 | }, 369 | { 370 | "type": "assertElementSelected", 371 | "locator": { 372 | "type": "id", 373 | "value": "unchecked_checkbox" 374 | } 375 | }, 376 | { 377 | "type": "setElementNotSelected", 378 | "locator": { 379 | "type": "id", 380 | "value": "unchecked_checkbox" 381 | } 382 | }, 383 | { 384 | "type": "verifyElementSelected", 385 | "locator": { 386 | "type": "id", 387 | "value": "unchecked_checkbox" 388 | }, 389 | "negated": true 390 | }, 391 | { 392 | "type": "assertElementSelected", 393 | "locator": { 394 | "type": "id", 395 | "value": "unchecked_checkbox" 396 | }, 397 | "negated": true 398 | }, 399 | { 400 | "type": "storeElementAttribute", 401 | "variable": "link_href", 402 | "attributeName": "href", 403 | "locator": { 404 | "type": "link text", 405 | "value": "i am a link" 406 | } 407 | }, 408 | { 409 | "type": "waitForElementAttribute", 410 | "locator": { 411 | "type": "link text", 412 | "value": "i am a link" 413 | }, 414 | "attributeName": "href", 415 | "value": "${link_href}" 416 | }, 417 | { 418 | "type": "verifyElementAttribute", 419 | "locator": { 420 | "type": "link text", 421 | "value": "i am a link" 422 | }, 423 | "attributeName": "href", 424 | "value": "${link_href}" 425 | }, 426 | { 427 | "type": "assertElementAttribute", 428 | "locator": { 429 | "type": "link text", 430 | "value": "i am a link" 431 | }, 432 | "attributeName": "href", 433 | "value": "${link_href}" 434 | }, 435 | { 436 | "type": "sendKeysToElement", 437 | "locator": { 438 | "type": "id", 439 | "value": "comments" 440 | }, 441 | "text": "w00t" 442 | }, 443 | { 444 | "type": "waitForElementAttribute", 445 | "locator": { 446 | "type": "link text", 447 | "value": "i am a link" 448 | }, 449 | "attributeName": "href", 450 | "value": "not ${link_href}", 451 | "negated": true 452 | }, 453 | { 454 | "type": "verifyElementAttribute", 455 | "locator": { 456 | "type": "link text", 457 | "value": "i am a link" 458 | }, 459 | "attributeName": "href", 460 | "value": "not ${link_href}", 461 | "negated": true 462 | }, 463 | { 464 | "type": "assertElementAttribute", 465 | "locator": { 466 | "type": "link text", 467 | "value": "i am a link" 468 | }, 469 | "attributeName": "href", 470 | "value": "not ${link_href}", 471 | "negated": true 472 | }, 473 | { 474 | "type": "storeElementValue", 475 | "variable": "comments", 476 | "locator": { 477 | "type": "id", 478 | "value": "comments" 479 | } 480 | }, 481 | { 482 | "type": "waitForElementValue", 483 | "locator": { 484 | "type": "id", 485 | "value": "comments" 486 | }, 487 | "value": "w00t" 488 | }, 489 | { 490 | "type": "verifyElementValue", 491 | "locator": { 492 | "type": "id", 493 | "value": "comments" 494 | }, 495 | "value": "w00t" 496 | }, 497 | { 498 | "type": "assertElementValue", 499 | "locator": { 500 | "type": "id", 501 | "value": "comments" 502 | }, 503 | "value": "w00t" 504 | }, 505 | { 506 | "type": "waitForElementValue", 507 | "locator": { 508 | "type": "id", 509 | "value": "comments" 510 | }, 511 | "value": "not w00t", 512 | "negated": true 513 | }, 514 | { 515 | "type": "verifyElementValue", 516 | "locator": { 517 | "type": "id", 518 | "value": "comments" 519 | }, 520 | "value": "not w00t", 521 | "negated": true 522 | }, 523 | { 524 | "type": "assertElementValue", 525 | "locator": { 526 | "type": "id", 527 | "value": "comments" 528 | }, 529 | "value": "not w00t", 530 | "negated": true 531 | }, 532 | { 533 | "type": "submitElement", 534 | "locator": { 535 | "type": "id", 536 | "value": "comments" 537 | } 538 | }, 539 | { 540 | "type": "verifyTextPresent", 541 | "text": "w00t" 542 | } 543 | ] 544 | } 545 | -------------------------------------------------------------------------------- /examples/tests/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://sebuilder.github.com/se-builder/" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /examples/tests/meow.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "meow" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /examples/tests/missingParam.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "notTheUrl": "http://sebuilder.github.com/se-builder/" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /examples/tests/not_assertAlertPresent.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://sebuilder.github.com/se-builder/test/empty.html" 9 | }, 10 | { 11 | "type": "assertAlertPresent", 12 | "negated": true 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /examples/tests/not_assertAlertPresent_fail.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://sebuilder.github.com/se-builder/test/alert.html" 9 | }, 10 | { 11 | "type": "assertAlertPresent", 12 | "negated": true 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /examples/tests/printTitle.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://sebuilder.github.com/se-builder/" 9 | }, 10 | { 11 | "type": "storeTitle", 12 | "variable": "t" 13 | }, 14 | { 15 | "type": "print", 16 | "text": "Title: ${t}. That's what it is." 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /examples/tests/switchToFrame.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://sebuilder.github.com/se-builder/test/frames.html" 9 | }, 10 | { 11 | "type": "switchToFrame", 12 | "identifier": "content" 13 | }, 14 | { 15 | "type": "clickElement", 16 | "locator": { 17 | "type": "link text", 18 | "value": "Next" 19 | } 20 | }, 21 | { 22 | "type": "assertTextPresent", 23 | "text": "Content Two" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /examples/tests/switchToFrameByIndex.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://sebuilder.github.com/se-builder/test/frames.html" 9 | }, 10 | { 11 | "type": "switchToFrameByIndex", 12 | "index": "1" 13 | }, 14 | { 15 | "type": "clickElement", 16 | "locator": { 17 | "type": "link text", 18 | "value": "Next" 19 | } 20 | }, 21 | { 22 | "type": "assertTextPresent", 23 | "text": "Content Two" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /examples/tests/switchToWindow.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://sebuilder.github.com/se-builder/test/window.html" 9 | }, 10 | { 11 | "type": "pause", 12 | "waitTime": "1000" 13 | }, 14 | { 15 | "type": "switchToWindow", 16 | "name": "win2" 17 | }, 18 | { 19 | "type": "assertTitle", 20 | "title": "Spawned window" 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /examples/tests/verifyNotTitle.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://sebuilder.github.com/se-builder/" 9 | }, 10 | { 11 | "type": "verifyTitle", 12 | "negated": true, 13 | "title": "Kumquat" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /examples/tests/waitForElement_timeout.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "script", 3 | "seleniumVersion": "2", 4 | "formatVersion": 1, 5 | "steps": [ 6 | { 7 | "type": "get", 8 | "url": "http://saucelabs.com/test/guinea-pig/" 9 | }, 10 | { 11 | "type": "waitForElementPresent", 12 | "locator": { 13 | "type": "id", 14 | "value": "nonexistent-element-3998ac91-3b12-453d-a3a7-6a5f66ff5aed" 15 | } 16 | } 17 | ], 18 | "timeoutSeconds": 10 19 | } 20 | -------------------------------------------------------------------------------- /interpreter.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * Copyright 2013 Sauce Labs 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | var interpreter_version = "1.0.10"; 20 | var webdriver = require('wd'); 21 | var S = require('string'); 22 | var glob = require('glob'); 23 | var util = require('util'); 24 | var pathLib = require('path'); 25 | var fs = require('fs'); 26 | var colors = require('colors'); 27 | var sax = require('sax'); 28 | var CSVConverter = require('csvtojson').core.Converter; 29 | 30 | // Common functionality for assert/verify/waitFor/store step types. Only the code for actually 31 | // getting the value has to be implemented individually. 32 | var prefixes = { 33 | 'assert': function(getter, testRun, callback) { 34 | getter.run(testRun, function(info) { 35 | if (info.error) { callback(info); return; } 36 | var match = getter.cmp ? ("" + info.value) == testRun.p(getter.cmp) : info.value; 37 | 38 | if (testRun.currentStep().negated) { 39 | if (match) { 40 | callback({ 'success': false, 'error': new Error(getter.cmp ? getter.cmp + ' matches' : getter.name + ' is true') }); 41 | } else { 42 | callback({ 'success': true }); 43 | } 44 | } else { 45 | if (match) { 46 | callback({ 'success': true }); 47 | } else { 48 | callback({ 'success': false, 'error': new Error(getter.cmp ? getter.cmp + ' does not match' : getter.name + ' is false') }); 49 | } 50 | } 51 | }); 52 | }, 53 | 'verify': function(getter, testRun, callback) { 54 | getter.run(testRun, function(info) { 55 | if (info.error) { callback(info); return; } 56 | callback({ 'success': !!((getter.cmp ? ("" + info.value) == testRun.p(getter.cmp) : info.value) ^ testRun.currentStep().negated) }); 57 | }); 58 | }, 59 | 'store': function(getter, testRun, callback) { 60 | getter.run(testRun, function(info) { 61 | if (info.error) { callback(info); return; } 62 | testRun.setVar(testRun.p('variable'), "" + info.value); 63 | callback({ 'success': true }); 64 | }); 65 | }, 66 | 'waitFor': function(getter, testRun, callback) { 67 | var start = process.hrtime(); 68 | function test() { 69 | getter.run(testRun, function(info) { 70 | if (!info.error && !!((getter.cmp ? ("" + info.value) == testRun.p(getter.cmp) : info.value) ^ testRun.currentStep().negated)) { 71 | callback({ 'success': true }); 72 | } else { 73 | var hr = process.hrtime(start); 74 | if (hr[0] + hr[1]*1e-9 < testRun.script.timeoutSeconds) { 75 | setTimeout(test, 500); 76 | } else { 77 | callback({ 'success': false, 'error': info.error || new Error('Wait timed out.') }); 78 | } 79 | } 80 | }); 81 | } 82 | 83 | setTimeout(test, 500); 84 | } 85 | }; 86 | 87 | /** Creates step executors by loading them as modules from step_types. */ 88 | var DefaultExecutorFactory = function() { 89 | this.executors = {}; 90 | }; 91 | DefaultExecutorFactory.prototype.get = function(stepType) { 92 | if (!this.executors[stepType]) { 93 | try { 94 | this.executors[stepType] = require('./step_types/' + stepType + '.js'); 95 | } catch (e) { 96 | return null; 97 | } 98 | } 99 | return this.executors[stepType]; 100 | }; 101 | 102 | /** Encapsulates a single test run. */ 103 | var TestRun = function(script, name, initialVars) { 104 | this.initialVars = initialVars || {}; 105 | this.vars = {}; 106 | for (var k in this.initialVars) { 107 | this.vars[k] = this.initialVars[k]; 108 | } 109 | this.script = script; 110 | this.stepIndex = -1; 111 | this.wd = null; 112 | this.silencePrints = false; 113 | this.name = name || 'Untitled'; 114 | this.browserOptions = { 'browserName': 'firefox' }; 115 | this.driverOptions = {}; 116 | this.listener = null; 117 | this.success = true; 118 | this.lastError = null; 119 | this.executorFactories = [new DefaultExecutorFactory()]; 120 | this.quitDriverAfterUse = true; 121 | this.shareStateFromPrevTestRun = false; 122 | }; 123 | 124 | TestRun.prototype.start = function(callback, webDriverToUse) { 125 | callback = callback || function() {}; 126 | this.browserOptions.name = this.browserOptions.name || this.name; 127 | if (webDriverToUse) { 128 | this.wd = webDriverToUse; 129 | var info = { 'success': true, 'error': null }; 130 | if (this.listener && this.listener.startTestRun) { 131 | this.listener.startTestRun(this, info); 132 | } 133 | callback(info); 134 | } else { 135 | this.wd = webdriver.remote(this.driverOptions); 136 | var testRun = this; 137 | this.wd.init(this.browserOptions, function(err) { 138 | var info = { 'success': !err, 'error': err }; 139 | if (err) { 140 | if (testRun.listener && testRun.listener.startTestRun) { 141 | testRun.listener.startTestRun(testRun, info); 142 | } 143 | callback(info); 144 | } else { 145 | testRun.wd.setImplicitWaitTimeout((testRun.script.timeoutSeconds || 60) * 1000, function(err) { 146 | var info2 = { 'success': !err, 'error': err }; 147 | if (testRun.listener && testRun.listener.startTestRun) { 148 | testRun.listener.startTestRun(testRun, info2); 149 | } 150 | callback(info2); 151 | }); 152 | } 153 | }); 154 | } 155 | }; 156 | 157 | TestRun.prototype.currentStep = function() { 158 | return this.script.steps[this.stepIndex]; 159 | }; 160 | 161 | TestRun.prototype.hasNext = function() { 162 | return this.stepIndex + 1 < this.script.steps.length; 163 | }; 164 | 165 | TestRun.prototype.next = function(callback) { 166 | callback = callback || function() {}; 167 | this.stepIndex++; 168 | if (this.listener && this.listener.startStep) { 169 | this.listener.startStep(this, this.currentStep()); 170 | } 171 | var stepType = this.currentStep().type; 172 | var prefix = null; 173 | for (var p in prefixes) { 174 | if (S(stepType).startsWith(p) && stepType != p) { 175 | prefix = prefixes[p]; 176 | stepType = stepType.substring(p.length); 177 | break; 178 | } 179 | } 180 | var executor = null; 181 | var i = 0; 182 | while (!executor && i < this.executorFactories.length) { 183 | executor = this.executorFactories[i++].get(stepType); 184 | } 185 | if (!executor) { 186 | var info = { 'success': false, 'error': new Error('Unable to load step type ' + stepType + '.') }; 187 | this.lastError = info.error; 188 | this.success = false; 189 | if (this.listener && this.listener.endStep) { 190 | this.listener.endStep(this, this.currentStep(), info); 191 | } 192 | callback(info); 193 | return; 194 | } 195 | var testRun = this; 196 | var wrappedCallback = callback; 197 | wrappedCallback = function(info) { 198 | testRun.success = testRun.success && info.success; 199 | testRun.lastError = info.error || testRun.lastError; 200 | if (testRun.listener && testRun.listener.endStep) { 201 | testRun.listener.endStep(testRun, testRun.currentStep(), info); 202 | } 203 | callback(info); 204 | }; 205 | try { 206 | if (prefix) { 207 | prefix(executor, this, wrappedCallback); 208 | } else { 209 | executor.run(this, wrappedCallback); 210 | } 211 | } catch (e) { 212 | wrappedCallback({ 'success': false, 'error': e }); 213 | } 214 | }; 215 | 216 | TestRun.prototype.end = function(callback) { 217 | callback = callback || function() {}; 218 | var testRun = this; 219 | if (this.wd) { 220 | if (this.quitDriverAfterUse) { 221 | var wd = this.wd; 222 | this.wd = null; 223 | wd.quit(function(error) { 224 | var info = { 'success': testRun.success && !error, 'error': testRun.lastError || error }; 225 | if (testRun.listener && testRun.listener.endTestRun) { 226 | testRun.listener.endTestRun(testRun, info); 227 | } 228 | callback(info); 229 | }); 230 | } else { 231 | var info = { 'success': testRun.success, 'error': testRun.lastError }; 232 | if (testRun.listener && testRun.listener.endTestRun) { 233 | testRun.listener.endTestRun(testRun, info); 234 | } 235 | callback(info); 236 | } 237 | } else { 238 | var info = { 'success': false, 'error': new Error('No driver running.') }; 239 | if (this.listener && this.listener.endTestRun) { 240 | this.listener.endTestRun(testRun, info); 241 | } 242 | callback(info); 243 | } 244 | }; 245 | 246 | TestRun.prototype.run = function(runCallback, stepCallback, webDriverToUse, defaultVars) { 247 | var testRun = this; 248 | runCallback = runCallback || function() {}; 249 | stepCallback = stepCallback || function() {}; 250 | if (defaultVars) { 251 | for (var k in defaultVars) { 252 | if (!this.vars[k]) { 253 | this.vars[k] = defaultVars[k]; 254 | } 255 | } 256 | } 257 | try { 258 | this.start(function(info) { 259 | if (!info.success) { 260 | var err = new Error('Unable to start playback session.'); 261 | err.reason = info.error; 262 | runCallback({ 'success': false, 'error': err }); 263 | return; 264 | } 265 | function runStep() { 266 | testRun.next(function(info) { 267 | stepCallback(info); 268 | if (info.error) { 269 | testRun.end(function(endInfo) { 270 | if (endInfo.error) { 271 | info.additionalError = endInfo.error; 272 | } 273 | runCallback({'success': false, 'error': info.error}); 274 | }); 275 | return; 276 | } 277 | if (testRun.hasNext()) { 278 | runStep(); 279 | } else { 280 | testRun.end(function(endInfo) { runCallback({ 'success': testRun.success && endInfo.success, 'error': testRun.lastError || endInfo.error }); }); 281 | } 282 | }); 283 | } 284 | runStep(); 285 | }, 286 | webDriverToUse 287 | ); 288 | } catch (e) { 289 | testRun.end(function(endInfo) { 290 | var err = new Error('Unable to start playback session.'); 291 | err.reason = e; 292 | if (endInfo.error) { 293 | info.additionalError = endInfo.error; 294 | } 295 | runCallback({'success': false, 'error': err }); 296 | }); 297 | } 298 | }; 299 | 300 | TestRun.prototype.reset = function() { 301 | this.end(); 302 | this.vars = {}; 303 | for (var k in this.initialVars) { 304 | this.vars[k] = this.initialVars[k]; 305 | } 306 | this.stepIndex = -1; 307 | this.success = true; 308 | this.lastError = null; 309 | }; 310 | 311 | TestRun.prototype.setVar = function(k, v) { 312 | this.vars[k] = v; 313 | }; 314 | 315 | TestRun.prototype.p = function(name) { 316 | var s = this.currentStep(); 317 | if (!(name in s)) { 318 | throw new Error('Missing parameter "' + name + '" in step #' + (this.stepIndex + 1) + '.'); 319 | } 320 | var v = s[name]; 321 | return this.sub(v); 322 | }; 323 | 324 | var keysMap = { 325 | "NULL": "\uE000", 326 | "CANCEL": "\uE001", 327 | "HELP": "\uE002", 328 | "BACK_SPACE": "\uE003", 329 | "TAB": "\uE004", 330 | "CLEAR": "\uE005", 331 | "RETURN": "\uE006", 332 | "ENTER": "\uE007", 333 | "SHIFT": "\uE008", 334 | "LEFT_SHIFT": "\uE008", 335 | "CONTROL": "\uE009", 336 | "LEFT_CONTROL": "\uE009", 337 | "ALT": "\uE00A", 338 | "LEFT_ALT": "\uE00A", 339 | "PAUSE": "\uE00B", 340 | "ESCAPE": "\uE00C", 341 | "SPACE": "\uE00D", 342 | "PAGE_UP": "\uE00E", 343 | "PAGE_DOWN": "\uE00F", 344 | "END": "\uE010", 345 | "HOME": "\uE011", 346 | "LEFT": "\uE012", 347 | "ARROW_LEFT": "\uE012", 348 | "UP": "\uE013", 349 | "ARROW_UP": "\uE013", 350 | "RIGHT": "\uE014", 351 | "ARROW_RIGHT": "\uE014", 352 | "DOWN": "\uE015", 353 | "ARROW_DOWN": "\uE015", 354 | "INSERT": "\uE016", 355 | "DELETE": "\uE017", 356 | "SEMICOLON": "\uE018", 357 | "EQUALS": "\uE019", 358 | "NUMPAD0": "\uE01A", 359 | "NUMPAD1": "\uE01B", 360 | "NUMPAD2": "\uE01C", 361 | "NUMPAD3": "\uE01D", 362 | "NUMPAD4": "\uE01E", 363 | "NUMPAD5": "\uE01F", 364 | "NUMPAD6": "\uE020", 365 | "NUMPAD7": "\uE021", 366 | "NUMPAD8": "\uE022", 367 | "NUMPAD9": "\uE023", 368 | "MULTIPLY": "\uE024", 369 | "ADD": "\uE025", 370 | "SEPARATOR": "\uE026", 371 | "SUBTRACT": "\uE027", 372 | "DECIMAL": "\uE028", 373 | "DIVIDE": "\uE029", 374 | "F1": "\uE031", 375 | "F2": "\uE032", 376 | "F3": "\uE033", 377 | "F4": "\uE034", 378 | "F5": "\uE035", 379 | "F6": "\uE036", 380 | "F7": "\uE037", 381 | "F8": "\uE038", 382 | "F9": "\uE039", 383 | "F10": "\uE03A", 384 | "F11": "\uE03B", 385 | "F12": "\uE03C", 386 | "META": "\uE03D", 387 | "COMMAND": "\uE03D", 388 | "ZENKAKU_HANKAKU": "\uE040" 389 | }; 390 | 391 | TestRun.prototype.sub = function(value) { 392 | for (var k in this.vars) { 393 | value = value.replace(new RegExp("\\$\\{" + k + "\\}", "g"), this.vars[k]); 394 | } 395 | for (var k in keysMap) { 396 | value = value.replace(new RegExp("\\!\\{" + k + "\\}", "g"), keysMap[k]); 397 | } 398 | return value; 399 | }; 400 | 401 | /** 402 | * Calls a function on the webdriver, and defaults to calling the callback with success/failure. 403 | * @param fName The function to call. 404 | * @param args List of arguments. 405 | * @param callback stepCallback that's invoked by default. 406 | * @param successCallback If specified, called on success instead of calling callback. 407 | * @param failureCallback If specified, called on error instead of calling callback. 408 | */ 409 | TestRun.prototype.do = function(fName, args, callback, successCallback, failureCallback) { 410 | if (!this.wd[fName]) { 411 | if (failureCallback) { 412 | failureCallback(new Error('Webdriver has no function "' + fName + '".')); 413 | } else { 414 | callback({'success': false, 'error': new Error('Webdriver has no function "' + fName + '".') }); 415 | } 416 | return; 417 | } 418 | this.wd[fName].apply(this.wd, args.concat([function(err) { 419 | if (err) { 420 | if (failureCallback) { 421 | failureCallback.apply(failureCallback, arguments); 422 | } else { 423 | callback({'success': false, 'error': err}); 424 | } 425 | } else { 426 | if (successCallback) { 427 | successCallback.apply(successCallback, arguments); 428 | } else { 429 | callback({'success': true}); 430 | } 431 | } 432 | }])); 433 | }; 434 | 435 | /** 436 | * Locates an element specified by a locator in the current step. 437 | * @param locatorName Name of the locator step parameter, usually "locator". 438 | * @param callback stepCallback that's invoked by default. 439 | * @param successCallback If specified, called on success instead of calling callback. 440 | * @param failureCallback If specified, called on error instead of calling callback. 441 | */ 442 | TestRun.prototype.locate = function(locatorName, callback, successCallback, failureCallback) { 443 | var locator = this.currentStep()[locatorName]; 444 | if (!locator) { 445 | callback('Missing parameter "' + locatorName + '" in step #' + (this.stepIndex + 1) + '.'); 446 | return; 447 | } 448 | this.wd[{ 449 | 'id': 'elementById', 450 | 'name': 'elementByName', 451 | 'link text': 'elementByLinkText', 452 | 'css selector': 'elementByCss', 453 | 'xpath': 'elementByXPath' 454 | }[locator.type]](this.sub(locator.value), function(err) { 455 | if (err) { 456 | if (failureCallback) { 457 | failureCallback.apply(failureCallback, arguments); 458 | } else { 459 | callback({'success': false, 'error': err}); 460 | } 461 | } else { 462 | if (successCallback) { 463 | successCallback.apply(successCallback, arguments); 464 | } else { 465 | callback({'success': true}); 466 | } 467 | } 468 | }); 469 | }; 470 | 471 | function getInterpreterListener(testRun) { 472 | return { 473 | 'startTestRun': function(testRun, info) { 474 | if (info.success) { 475 | console.log(testRun.name + ": " + "Starting test ".green +("("+ testRun.browserOptions.browserName +") ").yellow + testRun.name ); 476 | } else { 477 | console.log(testRun.name + ": " + "Unable to start test ".red + testRun.name + ": " + util.inspect(info.error)); 478 | } 479 | }, 480 | 'endTestRun': function(testRun, info) { 481 | if (info.success) { 482 | console.log(testRun.name + ": " + "Test passed".green +("("+ testRun.browserOptions.browserName +") ").yellow); 483 | } else { 484 | if (info.error) { 485 | console.log(testRun.name + ": " + "Test failed: ".red +("("+ testRun.browserOptions.browserName +") ").yellow + util.inspect(info.error)); 486 | } else { 487 | console.log(testRun.name + ": " + "Test failed ".red +("("+ testRun.browserOptions.browserName +") ").yellow); 488 | } 489 | } 490 | }, 491 | 'startStep': function(testRun, step) { 492 | }, 493 | 'endStep': function(testRun, step, info) { 494 | name = step.step_name ? step.step_name + " " : ""; 495 | if (info.success) { 496 | console.log(testRun.name + ": " + "Success ".green + name + JSON.stringify(step).grey); 497 | } else { 498 | if (info.error) { 499 | console.log(testRun.name + ": " + "Failed ".red + name + util.inspect(info.error)); 500 | } else { 501 | console.log(testRun.name + ": " + "Failed ".red + name); 502 | } 503 | } 504 | }, 505 | 'endAllRuns': function(num_runs, successes) { 506 | var message = successes + '/' + num_runs + ' tests ran successfully. Exiting'; 507 | if (num_runs == 0) { 508 | message = 'No tests found. Exiting.'.yellow; 509 | } else if (successes == num_runs) { 510 | message = message.green; 511 | } else { 512 | message = message.red; 513 | } 514 | 515 | console.log(message); 516 | } 517 | }; 518 | } 519 | 520 | function parseJSONFile(path, testRuns, silencePrints, listenerFactory, exeFactory, browserOptions, driverOptions, listenerOptions, dataSources) { 521 | var rawData = fs.readFileSync(path, "UTF-8"); 522 | var data = JSON.parse(subEnvVars(rawData)); 523 | if (data.type == 'script') { 524 | parseScriptFile(path, data, testRuns, silencePrints, listenerFactory, exeFactory, browserOptions, driverOptions, listenerOptions, dataSources); 525 | } else if (data.type == 'interpreter-config') { 526 | parseConfigFile(data, testRuns, silencePrints, listenerFactory, exeFactory, listenerOptions, dataSources); 527 | } else if (data.type == 'suite') { 528 | parseSuiteFile(path, data, testRuns, silencePrints, listenerFactory, exeFactory, browserOptions, driverOptions, listenerOptions, dataSources); 529 | } else { 530 | throw new Error("No type property set in JSON file \"" + path + "\".") 531 | } 532 | } 533 | 534 | /** Parses a config JSON file and adds the resulting TestRuns to testRuns. */ 535 | function parseConfigFile(fileContents, testRuns, silencePrints, listenerFactory, exeFactory, listenerOptions, dataSources) { 536 | fileContents.configurations.forEach(function(config) { 537 | var settingsList = config.settings; 538 | if (!settingsList || settingsList.length === 0) { 539 | settingsList = [{ 540 | 'browserOptions': browserOptions, 541 | 'driverOptions': driverOptions 542 | }]; 543 | } 544 | settingsList.forEach(function(settings) { 545 | config.scripts.forEach(function(pathToGlob) { 546 | glob.sync(pathToGlob).forEach(function(path) { 547 | if (S(path).endsWith('.json')) { 548 | parseJSONFile(path, testRuns, silencePrints, listenerFactory, exeFactory, settings.browserOptions, settings.driverOptions, listenerOptions, dataSources); 549 | } 550 | }); 551 | }); 552 | }); 553 | }); 554 | } 555 | 556 | /** Parses a suite JSON file and adds the resulting TestRuns to testRuns. */ 557 | function parseSuiteFile(path, fileContents, testRuns, silencePrints, listenerFactory, exeFactory, browserOptions, driverOptions, listenerOptions, dataSources) { 558 | var shareState = !!fileContents.shareState; 559 | var prevTestRunsLength = testRuns.length; 560 | fileContents.scripts.forEach(function(scriptLocation) { 561 | if (scriptLocation.where != "local") { 562 | console.error('Suite members stored using ' + scriptLocation.where + ' are not supported.'); 563 | return null; 564 | } 565 | var relPath = pathLib.join(path, '..', scriptLocation.path); 566 | var tr = null; 567 | if (fs.existsSync(relPath)) { 568 | parseScriptFile(relPath, null, testRuns, silencePrints, listenerFactory, exeFactory, browserOptions, driverOptions, listenerOptions, dataSources); 569 | } else { 570 | parseScriptFile(scriptLocation.path, null, testRuns, silencePrints, listenerFactory, exeFactory, browserOptions, driverOptions, listenerOptions, dataSources); 571 | } 572 | }); 573 | 574 | if (shareState && testRuns.length > prevTestRunsLength + 1) { 575 | for (var i = prevTestRunsLength; i < testRuns.length - 1; i++) { 576 | testRuns[i].quitDriverAfterUse = false; 577 | } 578 | for (var i = prevTestRunsLength + 1; i < testRuns.length; i++) { 579 | testRuns[i].shareStateFromPrevTestRun = true; 580 | } 581 | } 582 | } 583 | 584 | /** Parses script JSON and adds the resulting runs to the testRuns list. */ 585 | function parseScriptFile(path, data, testRuns, silencePrints, listenerFactory, exeFactory, browserOptions, driverOptions, listenerOptions, dataSources) { 586 | if (!data) { 587 | var rawData = fs.readFileSync(path, "UTF-8"); 588 | data = JSON.parse(subEnvVars(rawData)); 589 | } 590 | var script = data; 591 | var name = path.replace(/.*\/|\\/, "").replace(/\.json$/, ''); 592 | var dataRows = [{}]; 593 | if (script.data) { 594 | dataRows = loadData(script.data, dataSources, path); 595 | } 596 | var rowName = 1; 597 | dataRows.forEach(function(row) { 598 | var runName = name; 599 | if (dataRows.length > 1) { 600 | runName += ", row " + rowName; 601 | rowName++; 602 | } 603 | var tr = new TestRun(script, runName, row); 604 | tr.browserOptions = browserOptions || tr.browserOptions; 605 | tr.driverOptions = driverOptions || tr.driverOptions; 606 | tr.silencePrints = silencePrints; 607 | tr.listener = listenerFactory(tr, listenerOptions); 608 | if (exeFactory) { 609 | tr.executorFactories.splice(0, 0, exeFactory); 610 | } 611 | testRuns.push(tr); 612 | }); 613 | } 614 | 615 | /** Loads a script JSON file and turns it into a test run. Retained for backwards compatibility. */ 616 | function createTestRun(path, silencePrints, listenerFactory, exeFactory, browserOptions, driverOptions, listenerOptions) { 617 | var script = null; 618 | try { 619 | script = JSON.parse(subEnvVars(fs.readFileSync(path, "UTF-8"))); 620 | } catch (e) { 621 | console.error('Unable to load ' + path + ': ' + e); 622 | return null; 623 | } 624 | var name = path.replace(/.*\/|\\/, "").replace(/\.json$/, ''); 625 | tr = new TestRun(script, name); 626 | tr.browserOptions = browserOptions || tr.browserOptions; 627 | tr.driverOptions = driverOptions || tr.driverOptions; 628 | tr.silencePrints = silencePrints; 629 | tr.listener = listenerFactory(tr, listenerOptions); 630 | if (exeFactory) { 631 | tr.executorFactories.splice(0, 0, exeFactory); 632 | } 633 | return tr; 634 | } 635 | 636 | /** Substitutes expressions of the form ${FOO} for environment variables. */ 637 | function subEnvVars(t) { 638 | return t.replace(/\${([^}]+)}/g, function(match, varName) { 639 | return process.env[varName] === undefined ? "${" + varName + "}" : process.env[varName]; 640 | }); 641 | } 642 | 643 | var noneSource = { 644 | name: 'none', 645 | load: function() { 646 | return [{}]; // Return a single empty row. 647 | } 648 | }; 649 | 650 | var manualSource = { 651 | name: 'manual', 652 | load: function(cfg) { 653 | return [cfg]; // Return config as a row. 654 | } 655 | }; 656 | 657 | var jsonSource = { 658 | name: 'json', 659 | load: function(cfg, scriptPath) { 660 | var path = pathLib.resolve(cfg.path); 661 | if (scriptPath) { 662 | var relPath = pathLib.join(scriptPath, '..', cfg.path); 663 | if (fs.existsSync(relPath)) { 664 | path = relPath; 665 | } 666 | } 667 | var rawData = fs.readFileSync(path, "UTF-8"); 668 | var data = JSON.parse(subEnvVars(rawData)); 669 | return data; 670 | } 671 | }; 672 | 673 | var xmlSource = { 674 | name: 'xml', 675 | load: function(cfg, scriptPath) { 676 | var path = pathLib.resolve(cfg.path); 677 | if (scriptPath) { 678 | var relPath = pathLib.join(scriptPath, '..', cfg.path); 679 | if (fs.existsSync(relPath)) { 680 | path = relPath; 681 | } 682 | } 683 | var rawData = fs.readFileSync(path, "UTF-8"); 684 | var rows = []; 685 | var parser = sax.parser(/*strict*/true); 686 | parser.onopentag = function(node) { 687 | if (node.name == "test") { 688 | rows.push(node.attributes); 689 | } 690 | }; 691 | parser.write(rawData).close(); 692 | return rows; 693 | } 694 | }; 695 | 696 | var csvSource = { 697 | name: 'csv', 698 | load: function(cfg, scriptPath) { 699 | var path = pathLib.resolve(cfg.path); 700 | if (scriptPath) { 701 | var relPath = pathLib.join(scriptPath, '..', cfg.path); 702 | if (fs.existsSync(relPath)) { 703 | path = relPath; 704 | } 705 | } 706 | 707 | var rawData = fs.readFileSync(path, "UTF-8").toString(); 708 | var csvConverter = new CSVConverter(); 709 | 710 | var parseComplete = false; 711 | var rowData = []; 712 | csvConverter.on("end_parsed", function(jsonObj) { 713 | parseComplete = true; 714 | rowData = jsonObj; 715 | }); 716 | csvConverter.fromString(rawData, function(err, jsonObj) { 717 | if (err) { 718 | console.log("Error processing CSV:" + err); 719 | } 720 | }); 721 | require('deasync').loopWhile(function(){return !parseComplete;}); 722 | return rowData; 723 | } 724 | }; 725 | 726 | var defaultDataSources = [noneSource, manualSource, jsonSource, xmlSource, csvSource]; 727 | 728 | /** 729 | * Given a data config and a list of data sources, loads the data rows. 730 | * @param dataConfig A config of the form {"source": "sourcename", "configs": {"sourcename": {cfg-data}}} 731 | * @param dataSources An optional list of additional data sources. 732 | * @param scriptPath Optionally, the path of the script we're loading data for, for use in relative paths. 733 | */ 734 | function loadData(dataConfig, dataSources, scriptPath) { 735 | var configSource = 'none'; 736 | if (dataConfig.source && dataConfig.source != 'none') { 737 | configSource = dataConfig.source; 738 | } else if (defaultDataConfig) { 739 | configSource = defaultDataConfig; 740 | } 741 | 742 | if (dataSources) { 743 | var sources = dataSources.filter(function(ds) { return ds.name == configSource; }); 744 | if (sources.length > 0) { 745 | console.log('Using Data: ' + configSource); 746 | return sources[0].load(dataConfig.configs[configSource], scriptPath); 747 | } 748 | } 749 | var sources = defaultDataSources.filter(function(ds) { return ds.name == configSource; }); 750 | if (sources.length == 0) { 751 | throw new Error("No data source of name \"" + dataConfig.source + "\" available."); 752 | } 753 | return sources[0].load(dataConfig.configs[configSource], scriptPath); 754 | } 755 | 756 | exports.TestRun = TestRun; 757 | exports.getInterpreterListener = getInterpreterListener; 758 | exports.parseJSONFile = parseJSONFile; 759 | exports.parseConfigFile = parseConfigFile; 760 | exports.parseSuiteFile = parseSuiteFile; 761 | exports.createTestRun = createTestRun; 762 | exports.parseScriptFile = parseScriptFile; 763 | exports.subEnvVars = subEnvVars; 764 | exports.loadData = loadData; 765 | 766 | // Command-line usage. 767 | if (require.main !== module) { 768 | return; 769 | } 770 | 771 | var opt = require('optimist') 772 | .default('quiet', false).describe('quiet', 'no per-step output') 773 | .default('noPrint', false).describe('noPrint', 'no print step output') 774 | .default('silent', false).describe('silent', 'no non-error output') 775 | .default('parallel', 1).describe('parallel', 'number of tests to run in parallel') 776 | .describe('dataConfig', 'the default dataConfig') 777 | .describe('dataSource', 'path to data source module') 778 | .describe('listener', 'path to listener module') 779 | .describe('executorFactory', 'path to factory for extra type executors') 780 | .demand(1) // At least 1 script to execute. 781 | .usage('Usage: $0 [--option value...] [script-path...]\n\nPrefix browser options like browserName with "browser-", e.g. "--browser-browserName=firefox".\nPrefix driver options like host with "driver-", eg --driver-host=webdriver.foo.com.\nPrefix listener module options with "listener-".'); 782 | 783 | // Process arguments. 784 | var argv = opt.argv; 785 | 786 | var numParallelRunners = parseInt(argv.parallel, 10); 787 | 788 | var browserOptions = { 'browserName': 'firefox' }; 789 | for (var k in argv) { 790 | if (S(k).startsWith('browser-')) { 791 | browserOptions[k.substring('browser-'.length)] = argv[k]; 792 | } 793 | } 794 | 795 | var browserOptionsList = [browserOptions]; 796 | if (typeof browserOptions.browserName == 'object') { 797 | browserOptionsList = browserOptions.browserName.map(function(bname) { 798 | var bo = {}; 799 | for (var k in browserOptions) { 800 | bo[k] = browserOptions[k]; 801 | } 802 | bo.browserName = bname; 803 | return bo; 804 | }); 805 | } 806 | 807 | var driverOptions = {}; 808 | for (var k in argv) { 809 | if (S(k).startsWith('driver-')) { 810 | driverOptions[k.substring('driver-'.length)] = argv[k]; 811 | } 812 | } 813 | 814 | var listenerOptions = {}; 815 | for (var k in argv) { 816 | if (S(k).startsWith('listener-')) { 817 | listenerOptions[k.substring('listener-'.length)] = argv[k]; 818 | } 819 | } 820 | 821 | var dataSources = []; 822 | if (argv.dataSource) { 823 | var ds = []; 824 | if (typeof argv.dataSource == 'string') { 825 | ds.push(argv.dataSource); 826 | } else { 827 | ds = argv.dataSource; 828 | } 829 | ds.forEach(function(sourceName) { 830 | try { 831 | var resolved_path = pathLib.resolve(sourceName); 832 | dataSources.push(require(resolved_path)); 833 | } catch (e) { 834 | console.error('Unable to load data source module from: "' + resolved_path + '": ' + e); 835 | process.exit(78); 836 | } 837 | }); 838 | } 839 | 840 | if (argv.dataConfig) { 841 | var defaultDataConfig = argv.dataConfig; 842 | } 843 | 844 | var listener = null; 845 | if (argv.listener) { 846 | try { 847 | var resolved_path = pathLib.resolve(argv.listener); 848 | listener = require(resolved_path); 849 | } catch (e) { 850 | console.error('Unable to load listener module from: "' + resolved_path + '": ' + e); 851 | process.exit(78); 852 | } 853 | } 854 | 855 | var listenerFactory = function() { return null; }; 856 | if (listener) { 857 | listenerFactory = function(tr, listenerOptions) { return listener.getInterpreterListener(tr, listenerOptions, exports); }; 858 | } else { 859 | if (!argv.silent && !argv.quiet) { 860 | listenerFactory = getInterpreterListener; 861 | } 862 | } 863 | 864 | var exeFactory = null; 865 | if (argv.executorFactory) { 866 | var resolved_path = null; 867 | try { 868 | resolved_path = pathLib.resolve(argv.executorFactory); 869 | exeFactory = require(resolved_path); 870 | } catch (e) { 871 | console.error('Unable to load executor factory module from: "' + resolved_path + '": ' + e); 872 | resolved_path = null; 873 | process.exit(78); 874 | } 875 | } 876 | 877 | var testRuns = []; 878 | 879 | console.log(("SE-Interpreter " + interpreter_version)); 880 | 881 | browserOptionsList.forEach(function(browserOptions) { 882 | argv._.forEach(function(pathToGlob) { 883 | glob.sync(pathToGlob).forEach(function(path) { 884 | if (S(path).endsWith('.json')) { 885 | var name = path.replace(/.*\/|\\/, "").replace(/\.json$/, ""); 886 | var silencePrints = argv.noPrint || argv.silent; 887 | try { 888 | parseJSONFile(path, testRuns, silencePrints, listenerFactory, exeFactory, browserOptions, driverOptions, listenerOptions, dataSources); 889 | } catch (e) { 890 | console.error('Unable to load ' + path + ': ' + e); 891 | process.exit(65); 892 | } 893 | } 894 | }); 895 | }); 896 | }); 897 | 898 | if (numParallelRunners > 1 && !testRuns.every(function(tr) { return tr.quitDriverAfterUse; })) { 899 | console.log("Warning: Parallel test runs are not supported when sharing state within suites.".yellow); 900 | numParallelRunners = 1; 901 | } 902 | 903 | var index = -1; 904 | var successes = 0; 905 | var lastRunFinishedIndex = testRuns.length + numParallelRunners - 1; 906 | function runNext() { 907 | index++; 908 | if (index < testRuns.length) { 909 | testRuns[index].run(function(info) { 910 | if (info.success) { successes++; } 911 | runNext(); 912 | }, 913 | null, 914 | testRuns[index].shareStateFromPrevTestRun ? testRuns[index - 1].wd : null, 915 | testRuns[index].shareStateFromPrevTestRun ? testRuns[index - 1].vars : null); 916 | } else { 917 | if (index == lastRunFinishedIndex) { // We're the last runner to complete. 918 | var listener = listenerFactory(testRuns[index-1], listenerOptions); 919 | if (listener) { 920 | listener.endAllRuns(testRuns.length, successes); 921 | } 922 | process.on('exit', function() { process.exit(successes == testRuns.length ? 0 : 1); }); 923 | } 924 | } 925 | } 926 | 927 | // Spawn as many parallel runners as desired. 928 | for (var i = 0; i < numParallelRunners; i++) { 929 | runNext(); 930 | } 931 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "David Stark ", 3 | "name": "se-interpreter", 4 | "description": "Command-line interpreter for Selenium Builder test scripts.", 5 | "tags": ["selenium", "webdriver", "testing"], 6 | "version": "1.0.10", 7 | "homepage": "https://github.com/Zarkonnen/se-interpreter", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/Zarkonnen/se-interpreter.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/Zarkonnen/se-interpreter/issues", 14 | "email": "david.stark@zarkonnen.com" 15 | }, 16 | "main": "interpreter", 17 | "bin": "./interpreter.js", 18 | "engines": { 19 | "node": ">= 0.4.11" 20 | }, 21 | "dependencies": { 22 | "string": "1.2.0", 23 | "wd": ">= 0.0.28", 24 | "optimist": "0.3.5", 25 | "glob": "3.1.16", 26 | "colors": "~0.6", 27 | "csvtojson": "^0.3.21", 28 | "deasync": "^0.1.0", 29 | "sax": ">= 0.6.1" 30 | }, 31 | "devDependencies": {} 32 | } 33 | -------------------------------------------------------------------------------- /step_types/AlertPresent.js: -------------------------------------------------------------------------------- 1 | exports.name = "AlertPresent"; 2 | exports.run = function(tr, cb) { 3 | tr.do('alertText', [], cb, function() { 4 | cb({'value': true}); 5 | }, function() { 6 | cb({'value': false}); 7 | }); 8 | }; -------------------------------------------------------------------------------- /step_types/AlertText.js: -------------------------------------------------------------------------------- 1 | exports.cmp = 'text'; 2 | exports.run = function(tr, cb) { 3 | tr.do('alertText', [], cb, function(err, text) { 4 | cb({'value': text}); 5 | }); 6 | }; -------------------------------------------------------------------------------- /step_types/BodyText.js: -------------------------------------------------------------------------------- 1 | exports.cmp = "text"; 2 | exports.run = function(tr, cb) { 3 | tr.do('elementByTagName', ['html'], cb, function(err, element) { 4 | tr.do('text', [element], cb, function(err, text) { 5 | cb({'value': text}); 6 | }); 7 | }); 8 | }; -------------------------------------------------------------------------------- /step_types/CookieByName.js: -------------------------------------------------------------------------------- 1 | exports.cmp = 'value'; 2 | exports.run = function(tr, cb) { 3 | tr.do('allCookies', [], cb, function(err, cookies) { 4 | var cs = cookies.filter(function(c) { return c.name == tr.p('name'); }); 5 | if (cs.length == 0) { 6 | cb({'error': new Error('No cookie with name ' + tr.p('name') + ' found.')}); 7 | } else { 8 | cb({'value': cs[0].value}); 9 | } 10 | }); 11 | }; -------------------------------------------------------------------------------- /step_types/CookiePresent.js: -------------------------------------------------------------------------------- 1 | exports.name = "CookiePresent"; 2 | exports.run = function(tr, cb) { 3 | tr.do('allCookies', [], cb, function(err, cookies) { 4 | cb({'value': cookies.some(function(c) { return c.name == tr.p('name'); })}); 5 | }); 6 | }; -------------------------------------------------------------------------------- /step_types/CurrentUrl.js: -------------------------------------------------------------------------------- 1 | exports.cmp = "url"; 2 | exports.run = function(tr, cb) { 3 | tr.do('url', [], cb, function(err, url) { 4 | cb({'value': url}); 5 | }); 6 | }; -------------------------------------------------------------------------------- /step_types/ElementAttribute.js: -------------------------------------------------------------------------------- 1 | exports.cmp = 'value'; 2 | exports.run = function(tr, cb) { 3 | tr.locate('locator', cb, function(err, element) { 4 | tr.do('getAttribute', [element, tr.p('attributeName')], cb, function(err, value) { 5 | cb({'value': value}); 6 | }); 7 | }); 8 | }; -------------------------------------------------------------------------------- /step_types/ElementPresent.js: -------------------------------------------------------------------------------- 1 | exports.name = "ElementPresent"; 2 | exports.run = function(tr, cb) { 3 | tr.locate('locator', cb, function() { 4 | cb({'value': true}); 5 | }, function() { 6 | cb({'value': false}); 7 | }); 8 | }; -------------------------------------------------------------------------------- /step_types/ElementSelected.js: -------------------------------------------------------------------------------- 1 | exports.name = "ElementSelected"; 2 | exports.run = function(tr, cb) { 3 | tr.locate('locator', cb, function(err, element) { 4 | tr.do('isSelected', [element], cb, function(err, isSelected) { 5 | cb({'value': isSelected}); 6 | }); 7 | }); 8 | }; -------------------------------------------------------------------------------- /step_types/ElementStyle.js: -------------------------------------------------------------------------------- 1 | exports.cmp = 'value'; 2 | exports.run = function(tr, cb) { 3 | tr.locate('locator', cb, function(err, element) { 4 | tr.do('getComputedCSS', [element, tr.p('propertyName')], cb, function(err, value) { 5 | cb({'value': value}); 6 | }); 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /step_types/ElementValue.js: -------------------------------------------------------------------------------- 1 | exports.cmp = 'value'; 2 | exports.run = function(tr, cb) { 3 | tr.locate('locator', cb, function(err, element) { 4 | tr.do('getAttribute', [element, 'value'], cb, function(err, value) { 5 | cb({'value': value}); 6 | }); 7 | }); 8 | }; -------------------------------------------------------------------------------- /step_types/Eval.js: -------------------------------------------------------------------------------- 1 | exports.cmp = 'value'; 2 | exports.run = function(tr, cb) { 3 | tr.do('execute', [tr.p('script'), []], cb, function(err, value) { 4 | cb({'value': value}); 5 | }); 6 | }; -------------------------------------------------------------------------------- /step_types/PageSource.js: -------------------------------------------------------------------------------- 1 | exports.cmp = "source"; 2 | exports.run = function(tr, cb) { 3 | tr.do('source', [], cb, function(err, source) { 4 | cb({'value': source}); 5 | }); 6 | }; -------------------------------------------------------------------------------- /step_types/Text.js: -------------------------------------------------------------------------------- 1 | exports.cmp = 'text'; 2 | exports.run = function(tr, cb) { 3 | tr.locate('locator', cb, function(err, element) { 4 | tr.do('text', [element], cb, function(err, text) { 5 | cb({'value': text}); 6 | }); 7 | }); 8 | }; -------------------------------------------------------------------------------- /step_types/TextPresent.js: -------------------------------------------------------------------------------- 1 | exports.name = "TextPresent"; 2 | exports.run = function(tr, cb) { 3 | tr.do('elementByTagName', ['html'], cb, function(err, element) { 4 | tr.do('text', [element], cb, function(err, text) { 5 | cb({'value': text.indexOf(tr.p('text')) != -1}); 6 | }); 7 | }); 8 | }; -------------------------------------------------------------------------------- /step_types/Title.js: -------------------------------------------------------------------------------- 1 | exports.cmp = "title"; 2 | exports.run = function(tr, cb) { 3 | tr.do('title', [], cb, function(err, title) { 4 | cb({'value': title}); 5 | }); 6 | }; -------------------------------------------------------------------------------- /step_types/acceptAlert.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.do('acceptAlert', [], cb); 3 | }; -------------------------------------------------------------------------------- /step_types/addCookie.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | var data = {value: tr.p('value'), name: tr.p('name')}; 3 | tr.p('options').split('/').forEach(function(entry) { 4 | var entryArr = entry.split('='); 5 | data[entryArr[0]] = data[entryArr[1]]; 6 | }); 7 | tr.do('setCookie', [data], cb); 8 | }; -------------------------------------------------------------------------------- /step_types/answerAlert.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.do('alertKeys', [tr.p('text')], cb, function() { 3 | tr.do('acceptAlert', [], cb); 4 | }); 5 | }; -------------------------------------------------------------------------------- /step_types/clickAndHoldElement.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.locate('locator', cb, function(err, element) { 3 | tr.do('moveTo', [element, 0, 0], cb, function() { 4 | tr.do('buttonDown', [], cb); 5 | }); 6 | }); 7 | }; -------------------------------------------------------------------------------- /step_types/clickElement.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.locate('locator', cb, function(err, el) { 3 | tr.do('clickElement', [el], cb); 4 | }); 5 | }; -------------------------------------------------------------------------------- /step_types/close.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.do('close', [], cb); 3 | }; -------------------------------------------------------------------------------- /step_types/deleteCookie.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.do('deleteCookie', [tr.p('name')], cb); 3 | }; -------------------------------------------------------------------------------- /step_types/dismissAlert.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.do('dismissAlert', [], cb); 3 | }; -------------------------------------------------------------------------------- /step_types/doubleClickElement.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.locate('locator', cb, function(err, element) { 3 | tr.do('moveTo', [element, 0, 0], cb, function() { 4 | tr.do('doubleclick', [], cb); 5 | }); 6 | }); 7 | }; -------------------------------------------------------------------------------- /step_types/get.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.do('get', [tr.p('url')], cb); 3 | }; -------------------------------------------------------------------------------- /step_types/goBack.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.do('back', [], cb); 3 | }; -------------------------------------------------------------------------------- /step_types/goForward.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.do('forward', [], cb); 3 | }; -------------------------------------------------------------------------------- /step_types/mouseOverElement.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.locate('locator', cb, function(err, el) { 3 | tr.do('moveTo', [el, null, null], cb); 4 | }); 5 | }; -------------------------------------------------------------------------------- /step_types/pause.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | setTimeout(function() { 3 | cb({'success': true}); 4 | }, tr.p('waitTime')); 5 | }; -------------------------------------------------------------------------------- /step_types/print.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | if (!tr.silencePrints) { 3 | console.log(tr.p('text')); 4 | } 5 | cb({'success': true}); 6 | }; -------------------------------------------------------------------------------- /step_types/refresh.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.do('refresh', [], cb); 3 | }; -------------------------------------------------------------------------------- /step_types/releaseElement.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.locate('locator', cb, function(err, element) { 3 | tr.do('moveTo', [element, 0, 0], cb, function() { 4 | tr.do('buttonUp', [], cb); 5 | }); 6 | }); 7 | }; -------------------------------------------------------------------------------- /step_types/saveScreenshot.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | exports.run = function(tr, cb) { 4 | tr.do('takeScreenshot', [], cb, function(err, base64Image) { 5 | var decodedImage = new Buffer(base64Image, 'base64'); 6 | fs.writeFile(tr.name + '-' + Date.now() + '.png', decodedImage, function(err) { 7 | cb({'success': !err, 'error': err}); 8 | }); 9 | }); 10 | }; -------------------------------------------------------------------------------- /step_types/sendKeysToElement.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.locate('locator', cb, function(err, element) { 3 | tr.do('type', [element, tr.p('text')], cb); 4 | }); 5 | }; -------------------------------------------------------------------------------- /step_types/setElementNotSelected.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.locate('locator', cb, function(err, element) { 3 | tr.do('isSelected', [element], cb, function(err, isSelected) { 4 | if (!isSelected) { 5 | cb({'success': true}); 6 | } else { 7 | tr.do('clickElement', [element], cb); 8 | } 9 | }); 10 | }); 11 | }; -------------------------------------------------------------------------------- /step_types/setElementSelected.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.locate('locator', cb, function(err, element) { 3 | tr.do('isSelected', [element], cb, function(err, isSelected) { 4 | if (isSelected) { 5 | cb({'success': true}); 6 | } else { 7 | tr.do('clickElement', [element], cb); 8 | } 9 | }); 10 | }); 11 | }; -------------------------------------------------------------------------------- /step_types/setElementText.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.locate('locator', cb, function(err, element) { 3 | tr.do('clear', [element], cb, function() { 4 | tr.do('type', [element, tr.p('text')], cb); 5 | }); 6 | }); 7 | }; -------------------------------------------------------------------------------- /step_types/setWindowSize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Step to change the size of the current window 3 | * Implementaion of JSONWire Proctocol: 4 | * POST /session/:sessionId/window/:windowHandle/size 5 | * 6 | * author: david linse 7 | * version: 0.0.1 8 | * 9 | * usage: { "type": "windowSize", "width": 800, "height": 600 } 10 | */ 11 | 12 | exports.run = function(tr, cb) { 13 | tr.do('windowHandle', [], cb, function (err, handle) { 14 | var w = parseInt(tr.p('width'), 10), 15 | h = parseInt(tr.p('height'), 10); 16 | tr.do('windowSize', [handle, w, h], cb, function(err) { 17 | cb({'success': !err, 'error': err}); 18 | }); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /step_types/store.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.setVar(tr.p('variable'), tr.p('text')); 3 | cb({'success': true}); 4 | }; -------------------------------------------------------------------------------- /step_types/submitElement.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.locate('locator', cb, function(err, el) { 3 | tr.do('submit', [el], cb); 4 | }); 5 | }; -------------------------------------------------------------------------------- /step_types/switchToDefaultContent.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.do('frame', [null], cb); 3 | }; -------------------------------------------------------------------------------- /step_types/switchToFrame.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.do('frame', [tr.p('identifier')], cb); 3 | }; -------------------------------------------------------------------------------- /step_types/switchToFrameByIndex.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.do('frame', [parseInt(tr.p('index'))], cb); 3 | }; -------------------------------------------------------------------------------- /step_types/switchToWindow.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.do('window', [tr.p('name')], cb); 3 | }; -------------------------------------------------------------------------------- /step_types/switchToWindowByIndex.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.do('windowHandles', [], cb, function(err, handles) { 3 | tr.do('window', [handles[parseInt(tr.p('index'))]], cb); 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /step_types/switchToWindowByTitle.js: -------------------------------------------------------------------------------- 1 | exports.run = function(tr, cb) { 2 | tr.do('windowHandles', [], cb, function(err, handles) { 3 | var requiredTitle = tr.p('title'); 4 | function tryHandle(handleIndex) { 5 | if (handleIndex >= handles.length) { 6 | cb({'success': false}); 7 | return; 8 | } 9 | tr.do('window', [handles[handleIndex]], cb, function(err) { 10 | tr.do('title', [], cb, function(err, title) { 11 | if (requiredTitle == title) { 12 | cb({'success': true}); 13 | } else { 14 | tryHandle(handleIndex + 1); 15 | } 16 | }); 17 | }); 18 | } 19 | tryHandle(0); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /utils/sauce_listener.js: -------------------------------------------------------------------------------- 1 | /** 2 | A listener that reports test success/failure to Sauce OnDemand. 3 | 4 | Example usage: 5 | node interpreter.js --browser-browserName=firefox --driver-host=ondemand.saucelabs.com --driver-port=80 --browser-username=$SAUCE_USERNAME --browser-accessKey=$SAUCE_ACCESSKEY --listener=./utils/sauce_listener.js examples/tests/get.json 6 | 7 | You can also use --listener-silent=true to prevent the default listener output from happening, just like the --silent command. 8 | */ 9 | var https = require('https'); 10 | var util = require('util'); 11 | 12 | function Listener(testRun, params, interpreter_module) { 13 | this.testRun = testRun; 14 | this.originalListener = params.silent ? null : interpreter_module.getInterpreterListener(testRun, params, interpreter_module); 15 | }; 16 | 17 | Listener.prototype.startTestRun = function(testRun, info) { 18 | this.sessionID = testRun.wd.sessionID; 19 | this.username = testRun.browserOptions.username; 20 | this.accessKey = testRun.browserOptions.accessKey; 21 | if (this.originalListener) { this.originalListener.startTestRun(testRun, info); } 22 | }; 23 | 24 | Listener.prototype.endTestRun = function(testRun, info) { 25 | var data = null; 26 | if (info.error) { 27 | data = JSON.stringify({'passed': info.success, 'name': testRun.name, 'custom-data': {'interpreter-error': util.inspect(info.error)}}); 28 | } else { 29 | data = JSON.stringify({'passed': info.success, 'name': testRun.name}); 30 | } 31 | 32 | var options = { 33 | 'hostname': 'saucelabs.com', 34 | 'port': 443, 35 | 'path': '/rest/v1/' + this.username + '/jobs/' + this.sessionID, 36 | 'method': 'PUT', 37 | 'auth': this.username + ':' + this.accessKey, 38 | 'headers': { 'Content-Type': 'application/json', 'Content-Length': data.length } 39 | }; 40 | 41 | var req = https.request(options); 42 | 43 | req.on('error', function(e) { 44 | console.error(e); 45 | }); 46 | 47 | req.write(data); 48 | req.end(); 49 | if (this.originalListener) { this.originalListener.endTestRun(testRun, info); } 50 | }; 51 | 52 | Listener.prototype.startStep = function(testRun, step) { 53 | if (this.originalListener) { this.originalListener.startStep(testRun, step); } 54 | }; 55 | 56 | Listener.prototype.endStep = function(testRun, step, info) { 57 | if (this.originalListener) { this.originalListener.endStep(testRun, step, info); } 58 | }; 59 | 60 | Listener.prototype.endAllRuns = function(num_runs, successes) { 61 | if (this.originalListener) { this.originalListener.endAllRuns(num_runs, successes); } 62 | }; 63 | 64 | exports.getInterpreterListener = function(testRun, options, interpreter_module) { 65 | return new Listener(testRun, options, interpreter_module); 66 | }; 67 | --------------------------------------------------------------------------------