| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * | |
| 3 | * Copyright (c) 2013 Sebastian Golasch | |
| 4 | * | |
| 5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
| 6 | * copy of this software and associated documentation files (the "Software"), | |
| 7 | * to deal in the Software without restriction, including without limitation | |
| 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
| 9 | * and/or sell copies of the Software, and to permit persons to whom the | |
| 10 | * Software is furnished to do so, subject to the following conditions: | |
| 11 | * | |
| 12 | * The above copyright notice and this permission notice shall be included | |
| 13 | * in all copies or substantial portions of the Software. | |
| 14 | * | |
| 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
| 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
| 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
| 21 | * DEALINGS IN THE SOFTWARE. | |
| 22 | * | |
| 23 | */ | |
| 24 | ||
| 25 | 1 | 'use strict'; |
| 26 | ||
| 27 | // ext. libs | |
| 28 | 1 | var async = require('async'); |
| 29 | 1 | var EventEmitter2 = require('eventemitter2').EventEmitter2; |
| 30 | ||
| 31 | // int. libs | |
| 32 | 1 | var Driver = require('./dalek/driver'); |
| 33 | 1 | var Reporter = require('./dalek/reporter'); |
| 34 | 1 | var Timer = require('./dalek/timer'); |
| 35 | 1 | var Config = require('./dalek/config'); |
| 36 | 1 | var Host = require('./dalek/host'); |
| 37 | ||
| 38 | /** | |
| 39 | * Default options | |
| 40 | * @type {Object} | |
| 41 | */ | |
| 42 | ||
| 43 | 1 | var defaults = { |
| 44 | reporter: ['console'], | |
| 45 | driver: ['native'], | |
| 46 | browser: ['phantomjs'], | |
| 47 | viewport: {width: 1280, height: 1024}, | |
| 48 | logLevel: 3 | |
| 49 | }; | |
| 50 | ||
| 51 | /** | |
| 52 | * Setup all the options needed to configure dalek | |
| 53 | * | |
| 54 | * @param {options} opts Configuration options | |
| 55 | * @constructor | |
| 56 | */ | |
| 57 | ||
| 58 | 1 | var Dalek = function (opts) { |
| 59 | // setup instance | |
| 60 | 7 | this._initialize(); |
| 61 | ||
| 62 | // register exception handler | |
| 63 | 7 | this._registerExceptionHandler(); |
| 64 | ||
| 65 | // normalize options | |
| 66 | 7 | this.options = this.normalizeOptions(opts); |
| 67 | ||
| 68 | // getting advanced options | |
| 69 | 7 | if (opts && opts.advanced) { |
| 70 | 2 | this.advancedOptions = opts.advanced; |
| 71 | } | |
| 72 | ||
| 73 | // initiate config | |
| 74 | 7 | this.config = new Config(defaults, this.options, this.advancedOptions); |
| 75 | ||
| 76 | // override tests if provided on the commandline | |
| 77 | 7 | if (this.options.tests) { |
| 78 | 6 | this.config.config.tests = this.options.tests; |
| 79 | } | |
| 80 | ||
| 81 | // prepare and load reporter(s) | |
| 82 | 7 | this._setupReporters(); |
| 83 | ||
| 84 | // count all passed & failed assertions | |
| 85 | 7 | this.reporterEvents.on('report:assertion', this._onReportAssertion.bind(this)); |
| 86 | ||
| 87 | // init the timer instance | |
| 88 | 7 | this.timer = new Timer(); |
| 89 | ||
| 90 | // prepare driver event emitter instance | |
| 91 | 7 | this._setupDriverEmitter(); |
| 92 | ||
| 93 | // check for file option, throw error if none is given | |
| 94 | // only bypasses if dalek runs in the remote mode | |
| 95 | 7 | if (!Array.isArray(this.config.get('tests')) && !this.options.remote) { |
| 96 | 1 | this.reporterEvents.emit('error', 'No test files given!'); |
| 97 | 1 | this.driverEmitter.emit('killAll'); |
| 98 | 1 | process.exit(127); |
| 99 | } | |
| 100 | ||
| 101 | // init the driver instance | |
| 102 | 7 | this._initDriver(); |
| 103 | ||
| 104 | // check if dalek runs as a remote browser launcher | |
| 105 | 7 | if (this.options.remote) { |
| 106 | 0 | var host = new Host({reporterEvents: this.reporterEvents, config: this.config}); |
| 107 | 0 | host.run({ |
| 108 | port: !isNaN(parseFloat(this.options.remote)) && isFinite(this.options.remote) ? this.options.remote : false | |
| 109 | }); | |
| 110 | } | |
| 111 | }; | |
| 112 | ||
| 113 | /** | |
| 114 | * Daleks base module | |
| 115 | * Used to configure all the things | |
| 116 | * and to start off the tests | |
| 117 | * | |
| 118 | * @module DalekJS | |
| 119 | * @class Dalek | |
| 120 | */ | |
| 121 | ||
| 122 | 1 | Dalek.prototype = { |
| 123 | ||
| 124 | /** | |
| 125 | * Runs the configured testsuites | |
| 126 | * | |
| 127 | * @method run | |
| 128 | * @chainable | |
| 129 | */ | |
| 130 | ||
| 131 | run: function () { | |
| 132 | // early return; in case of remote | |
| 133 | 0 | if (this.options.remote) { |
| 134 | 0 | return this; |
| 135 | } | |
| 136 | ||
| 137 | // start the timer to measure the execution time | |
| 138 | 0 | this.timer.start(); |
| 139 | ||
| 140 | // emit the runner started event | |
| 141 | 0 | this.reporterEvents.emit('report:runner:started'); |
| 142 | ||
| 143 | // execute all given drivers sequentially | |
| 144 | 0 | var drivers = this.driver.getDrivers(); |
| 145 | 0 | async.series(drivers, this.testsuitesFinished.bind(this)); |
| 146 | 0 | return this; |
| 147 | }, | |
| 148 | ||
| 149 | /** | |
| 150 | * Reports the all testsuites executed event | |
| 151 | * | |
| 152 | * @method testsuitesFinished | |
| 153 | * @chainable | |
| 154 | */ | |
| 155 | ||
| 156 | testsuitesFinished: function () { | |
| 157 | 1 | this.driverEmitter.emit('tests:complete'); |
| 158 | 1 | setTimeout(this.reportRunFinished.bind(this), 0); |
| 159 | 1 | return this; |
| 160 | }, | |
| 161 | ||
| 162 | /** | |
| 163 | * Reports the all testsuites executed event | |
| 164 | * | |
| 165 | * @method reportRunFinished | |
| 166 | * @chainable | |
| 167 | */ | |
| 168 | ||
| 169 | reportRunFinished: function () { | |
| 170 | 2 | this.reporterEvents.emit('report:runner:finished', { |
| 171 | elapsedTime: this.timer.stop().getElapsedTimeFormatted(), | |
| 172 | assertions: this.assertionsFailed + this.assertionsPassed, | |
| 173 | assertionsFailed: this.assertionsFailed, | |
| 174 | assertionsPassed: this.assertionsPassed, | |
| 175 | status: this.runnerStatus | |
| 176 | }); | |
| 177 | ||
| 178 | //we want to exit process with code 1 to single that test did not pass | |
| 179 | 2 | if(this.runnerStatus !== true) { |
| 180 | 0 | var processExitCaptured = false; |
| 181 | ||
| 182 | 0 | process.on('exit', function() { |
| 183 | 0 | if(processExitCaptured === false) { |
| 184 | 0 | processExitCaptured = true; |
| 185 | 0 | process.exit(1); |
| 186 | } | |
| 187 | }); | |
| 188 | } | |
| 189 | ||
| 190 | 2 | return this; |
| 191 | }, | |
| 192 | ||
| 193 | /** | |
| 194 | * Normalizes options | |
| 195 | * | |
| 196 | * @method normalizeOptions | |
| 197 | * @param {object} options Raw options | |
| 198 | * @return {object} Normalized options | |
| 199 | */ | |
| 200 | ||
| 201 | normalizeOptions: function (options) { | |
| 202 | 7 | Object.keys(options).forEach(function (key) { |
| 203 | 51 | if ({reporter: 1, driver: 1}[key]) { |
| 204 | 14 | options[key] = options[key].map(function (input) { return input.trim(); }); |
| 205 | } | |
| 206 | }); | |
| 207 | ||
| 208 | 7 | return options; |
| 209 | }, | |
| 210 | ||
| 211 | /** | |
| 212 | * Sets up system env properties | |
| 213 | * | |
| 214 | * @method _initialize | |
| 215 | * @chainable | |
| 216 | * @private | |
| 217 | */ | |
| 218 | ||
| 219 | _initialize: function () { | |
| 220 | // prepare error data | |
| 221 | 7 | this.warnings = []; |
| 222 | 7 | this.errors = []; |
| 223 | ||
| 224 | // prepare state data for the complete test run | |
| 225 | 7 | this.runnerStatus = true; |
| 226 | 7 | this.assertionsFailed = 0; |
| 227 | 7 | this.assertionsPassed = 0; |
| 228 | ||
| 229 | 7 | return this; |
| 230 | }, | |
| 231 | ||
| 232 | /** | |
| 233 | * Sets up all the reporters | |
| 234 | * | |
| 235 | * @method _setupReporters | |
| 236 | * @chainable | |
| 237 | * @private | |
| 238 | */ | |
| 239 | ||
| 240 | _setupReporters: function () { | |
| 241 | 7 | this.reporters = []; |
| 242 | 7 | this.reporterEvents = new EventEmitter2(); |
| 243 | 7 | this.reporterEvents.setMaxListeners(Infinity); |
| 244 | 7 | this.options.reporter = this.config.verifyReporters(this.config.get('reporter'), Reporter); |
| 245 | 7 | this.options.reporter.forEach(this._addReporter, this); |
| 246 | 7 | return this; |
| 247 | }, | |
| 248 | ||
| 249 | /** | |
| 250 | * Adds a reporter | |
| 251 | * | |
| 252 | * @method _addReporter | |
| 253 | * @param {string} reporter Name of the reporter to add | |
| 254 | * @chainable | |
| 255 | * @private | |
| 256 | */ | |
| 257 | ||
| 258 | _addReporter: function (reporter) { | |
| 259 | 7 | this.reporters.push(Reporter.loadReporter(reporter, {events: this.reporterEvents, config: this.config, logLevel: this.config.get('logLevel')})); |
| 260 | 7 | return this; |
| 261 | }, | |
| 262 | ||
| 263 | /** | |
| 264 | * Updates the assertion state | |
| 265 | * | |
| 266 | * @method _onReportAssertion | |
| 267 | * @param {object} assertion Informations aout the runned assertions | |
| 268 | * @chainable | |
| 269 | * @private | |
| 270 | */ | |
| 271 | ||
| 272 | _onReportAssertion: function (assertion) { | |
| 273 | 2 | if (assertion.success) { |
| 274 | 1 | this.assertionsPassed++; |
| 275 | } else { | |
| 276 | 1 | this.runnerStatus = false; |
| 277 | 1 | this.assertionsFailed++; |
| 278 | } | |
| 279 | 2 | return this; |
| 280 | }, | |
| 281 | ||
| 282 | /** | |
| 283 | * Initizializes the driver instances | |
| 284 | * | |
| 285 | * @method _initDriver | |
| 286 | * @chainable | |
| 287 | * @private | |
| 288 | */ | |
| 289 | ||
| 290 | _initDriver: function () { | |
| 291 | 7 | this.driver = new Driver({ |
| 292 | config: this.config, | |
| 293 | driverEmitter: this.driverEmitter, | |
| 294 | reporterEvents: this.reporterEvents | |
| 295 | }); | |
| 296 | ||
| 297 | 7 | this.options.driver = this.config.verifyDrivers(this.config.get('driver'), this.driver); |
| 298 | 7 | return this; |
| 299 | }, | |
| 300 | ||
| 301 | /** | |
| 302 | * Sets up the event dispatcher for driver events | |
| 303 | * | |
| 304 | * @method _setupDriverEmitter | |
| 305 | * @chainable | |
| 306 | * @private | |
| 307 | */ | |
| 308 | ||
| 309 | _setupDriverEmitter: function () { | |
| 310 | 7 | var driverEmitter = new EventEmitter2(); |
| 311 | 7 | driverEmitter.setMaxListeners(Infinity); |
| 312 | 7 | this.driverEmitter = driverEmitter; |
| 313 | 7 | return this; |
| 314 | }, | |
| 315 | ||
| 316 | /** | |
| 317 | * Make sure to shutdown dalek & its spawned | |
| 318 | * components, webservers gracefully if a | |
| 319 | * runtime error pops up | |
| 320 | * | |
| 321 | * @method _registerExceptionHandler | |
| 322 | * @private | |
| 323 | * @chainable | |
| 324 | */ | |
| 325 | ||
| 326 | _registerExceptionHandler: function () { | |
| 327 | 7 | process.setMaxListeners(Infinity); |
| 328 | 7 | process.on('uncaughtException', this._shutdown.bind(this)); |
| 329 | 7 | return this; |
| 330 | }, | |
| 331 | ||
| 332 | /** | |
| 333 | * Shutdown on uncaught exception | |
| 334 | * | |
| 335 | * @method _shutdown | |
| 336 | * @param {object} exception Runtime exception | |
| 337 | * @private | |
| 338 | */ | |
| 339 | ||
| 340 | _shutdown: function (exception) { | |
| 341 | // ios emulator hack, needs to go in the future | |
| 342 | 0 | if (exception.message && exception.message.search('This socket has been ended by the other party') !== -1) { |
| 343 | 0 | return false; |
| 344 | } | |
| 345 | ||
| 346 | 0 | this.driverEmitter.emit('killAll'); |
| 347 | 0 | this.reporterEvents.emit('error', exception); |
| 348 | } | |
| 349 | ||
| 350 | }; | |
| 351 | ||
| 352 | // export dalek as a module | |
| 353 | 1 | module.exports = Dalek; |
| 354 |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * | |
| 3 | * Copyright (c) 2013 Sebastian Golasch | |
| 4 | * | |
| 5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
| 6 | * copy of this software and associated documentation files (the "Software"), | |
| 7 | * to deal in the Software without restriction, including without limitation | |
| 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
| 9 | * and/or sell copies of the Software, and to permit persons to whom the | |
| 10 | * Software is furnished to do so, subject to the following conditions: | |
| 11 | * | |
| 12 | * The above copyright notice and this permission notice shall be included | |
| 13 | * in all copies or substantial portions of the Software. | |
| 14 | * | |
| 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
| 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
| 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
| 21 | * DEALINGS IN THE SOFTWARE. | |
| 22 | */ | |
| 23 | ||
| 24 | 1 | 'use strict'; |
| 25 | ||
| 26 | // ext. libs | |
| 27 | 1 | var Q = require('q'); |
| 28 | 1 | var uuid = require('./uuid'); |
| 29 | 1 | var cheerio = require('cheerio'); |
| 30 | ||
| 31 | // int. global | |
| 32 | 1 | var reporter = null; |
| 33 | ||
| 34 | /** | |
| 35 | * Actions are a way to control your browsers, e.g. simulate user interactions | |
| 36 | * like clicking elements, open urls, filling out input fields, etc. | |
| 37 | * | |
| 38 | * @class Actions | |
| 39 | * @constructor | |
| 40 | * @part Actions | |
| 41 | * @api | |
| 42 | */ | |
| 43 | ||
| 44 | 1 | var Actions = function () { |
| 45 | 5 | this.uuids = {}; |
| 46 | }; | |
| 47 | ||
| 48 | /** | |
| 49 | * It can be really cumbersome to repeat selectors all over when performing | |
| 50 | * multiple actions or assertions on the same element(s). | |
| 51 | * When you use the query method (or its alias $), you're able to specify a | |
| 52 | * selector once instead of repeating it all over the place. | |
| 53 | * | |
| 54 | * So, instead of writing this: | |
| 55 | * | |
| 56 | * ```javascript | |
| 57 | * test.open('http://doctorwhotv.co.uk/') | |
| 58 | * .assert.text('#nav').is('Navigation') | |
| 59 | * .assert.visible('#nav') | |
| 60 | * .assert.attr('#nav', 'data-nav', 'true') | |
| 61 | * .click('#nav') | |
| 62 | * .done(); | |
| 63 | * ``` | |
| 64 | * | |
| 65 | * you can write this: | |
| 66 | * | |
| 67 | * ```javascript | |
| 68 | * test.open('http://doctorwhotv.co.uk/') | |
| 69 | * .query('#nav') | |
| 70 | * .assert.text().is('Navigation') | |
| 71 | * .assert.visible() | |
| 72 | * .assert.attr('data-nav', 'true') | |
| 73 | * .click() | |
| 74 | * .end() | |
| 75 | * .done(); | |
| 76 | * ``` | |
| 77 | * | |
| 78 | * Always make sure to terminate it with the [end](assertions.html#meth-end) method! | |
| 79 | * | |
| 80 | * @api | |
| 81 | * @method query | |
| 82 | * @param {string} selector Selector of the element to query | |
| 83 | * @chainable | |
| 84 | */ | |
| 85 | ||
| 86 | 1 | Actions.prototype.query = function (selector) { |
| 87 | 0 | var that = !this.test ? this : this.test; |
| 88 | 0 | that.lastChain.push('querying'); |
| 89 | 0 | that.selector = selector; |
| 90 | 0 | that.querying = true; |
| 91 | 0 | return this.test ? this : that; |
| 92 | }; | |
| 93 | ||
| 94 | /** | |
| 95 | * Alias of query | |
| 96 | * | |
| 97 | * @api | |
| 98 | * @method $ | |
| 99 | * @param {string} selector Selector of the element to query | |
| 100 | * @chainable | |
| 101 | */ | |
| 102 | ||
| 103 | 1 | Actions.prototype.$ = Actions.prototype.query; |
| 104 | ||
| 105 | /** | |
| 106 | * Triggers a mouse event on the first element found matching the provided selector. | |
| 107 | * Supported events are mouseup, mousedown, click, mousemove, mouseover and mouseout. | |
| 108 | * TODO: IMPLEMENT | |
| 109 | * | |
| 110 | * @method mouseEvent | |
| 111 | * @param {string} type | |
| 112 | * @param {string} selector | |
| 113 | * @chainable | |
| 114 | */ | |
| 115 | ||
| 116 | 1 | Actions.prototype.mouseEvent = function (type, selector) { |
| 117 | 0 | var hash = uuid(); |
| 118 | 0 | var cb = this._generateCallbackAssertion('mouseEvent', 'mouseEvent', type, selector, hash); |
| 119 | 0 | this._addToActionQueue([type, selector, hash], 'mouseEvent', cb); |
| 120 | 0 | return this; |
| 121 | }; | |
| 122 | ||
| 123 | /** | |
| 124 | * Sets HTTP_AUTH_USER and HTTP_AUTH_PW values for HTTP based authentication systems. | |
| 125 | * | |
| 126 | * If your site is behind a HTTP basic auth, you're able to set the username and the password | |
| 127 | * | |
| 128 | * ```javascript | |
| 129 | * test.setHttpAuth('OSWIN', 'rycbrar') | |
| 130 | * .open('http://admin.therift.com'); | |
| 131 | * ``` | |
| 132 | * | |
| 133 | * Most of the time, you`re not storing your passwords within files that will be checked | |
| 134 | * in your vcs, for this scenario, you have two options: | |
| 135 | * | |
| 136 | * The first option is, to use daleks cli capabilities to generate config variables | |
| 137 | * from the command line, like this | |
| 138 | * | |
| 139 | * ```batch | |
| 140 | * $ dalek --vars USER=OSWIN,PASS=rycbrar | |
| 141 | * ``` | |
| 142 | * | |
| 143 | * ```javascript | |
| 144 | * test.setHttpAuth(test.config.get('USER'), test.config.get('PASS')) | |
| 145 | * .open('http://admin.therift.com'); | |
| 146 | * ``` | |
| 147 | * | |
| 148 | * The second option is, to use env variables to generate config variables | |
| 149 | * from the command line, like this | |
| 150 | * | |
| 151 | * ```batch | |
| 152 | * $ SET USER=OSWIN | |
| 153 | * $ SET PASS=rycbrar | |
| 154 | * $ dalek | |
| 155 | * ``` | |
| 156 | * | |
| 157 | * ```javascript | |
| 158 | * test.setHttpAuth(test.config.get('USER'), test.config.get('PASS')) | |
| 159 | * .open('http://admin.therift.com'); | |
| 160 | * ``` | |
| 161 | * | |
| 162 | * If both, dalek variables & env variables are set, the dalek variables win. | |
| 163 | * For more information about this, I recommend to check out the [configuration docs](/docs/config.html) | |
| 164 | * | |
| 165 | * TODO: IMPLEMENT | |
| 166 | * | |
| 167 | * @method setHttpAuth | |
| 168 | * @param {string} username | |
| 169 | * @param {string} password | |
| 170 | * @return {Actions} | |
| 171 | */ | |
| 172 | ||
| 173 | 1 | Actions.prototype.setHttpAuth = function (username, password) { |
| 174 | 0 | var hash = uuid(); |
| 175 | 0 | var cb = this._generateCallbackAssertion('setHttpAuth', 'setHttpAuth', username, password, hash); |
| 176 | 0 | this._addToActionQueue([username, password, hash], 'setHttpAuth', cb); |
| 177 | 0 | return this; |
| 178 | }; | |
| 179 | ||
| 180 | /** | |
| 181 | * Switches to an iFrame context | |
| 182 | * | |
| 183 | * Sometimes you encounter situations, where you need to drive/access an iFrame sitting in your page. | |
| 184 | * You can access such frames with this method, but be aware of the fact, that the complete test context | |
| 185 | * than switches to the iframe context, every action and assertion will be executed within the iFrame context. | |
| 186 | * Btw.: The domain of the IFrame can be whatever you want, this method has no same origin policy restrictions. | |
| 187 | * | |
| 188 | * If you wan't to get back to the parents context, you have to use the [toParent](#meth-toParent) method. | |
| 189 | * | |
| 190 | * ```html | |
| 191 | * <div> | |
| 192 | * <iframe id="login" src="/login.html"/> | |
| 193 | * </div> | |
| 194 | * ``` | |
| 195 | * | |
| 196 | * ```javascript | |
| 197 | * test.open('http://adomain.withiframe.com') | |
| 198 | * .assert.title().is('Title of a page that embeds an iframe') | |
| 199 | * .toFrame('#login') | |
| 200 | * .assert.title().is('Title of a page that can be embedded as an iframe') | |
| 201 | * .toParent() | |
| 202 | * .done(); | |
| 203 | * ``` | |
| 204 | * | |
| 205 | * > NOTE: Buggy in Firefox | |
| 206 | * | |
| 207 | * @api | |
| 208 | * @method toFrame | |
| 209 | * @param {string} selector Selector of the frame to switch to | |
| 210 | * @chainable | |
| 211 | */ | |
| 212 | ||
| 213 | 1 | Actions.prototype.toFrame = function (selector) { |
| 214 | 0 | var hash = uuid(); |
| 215 | ||
| 216 | 0 | if (this.querying === true) { |
| 217 | 0 | selector = this.selector; |
| 218 | } | |
| 219 | ||
| 220 | 0 | var cb = this._generateCallbackAssertion('toFrame', 'toFrame', selector, hash); |
| 221 | 0 | this._addToActionQueue([selector, hash], 'toFrame', cb); |
| 222 | 0 | return this; |
| 223 | }; | |
| 224 | ||
| 225 | /** | |
| 226 | * Switches back to the parent page context when the test context has been | |
| 227 | * switched to an iFrame context | |
| 228 | * | |
| 229 | * ```html | |
| 230 | * <div> | |
| 231 | * <iframe id="login" src="/login.html"/> | |
| 232 | * </div> | |
| 233 | * ``` | |
| 234 | * | |
| 235 | * ```javascript | |
| 236 | * test.open('http://adomain.withiframe.com') | |
| 237 | * .assert.title().is('Title of a page that embeds an iframe') | |
| 238 | * .toFrame('#login') | |
| 239 | * .assert.title().is('Title of a page that can be embedded as an iframe') | |
| 240 | * .toParent() | |
| 241 | * .assert.title().is('Title of a page that embeds an iframe') | |
| 242 | * .done(); | |
| 243 | * ``` | |
| 244 | * | |
| 245 | * > NOTE: Buggy in Firefox | |
| 246 | * | |
| 247 | * @api | |
| 248 | * @method toParent | |
| 249 | * @chainable | |
| 250 | */ | |
| 251 | ||
| 252 | 1 | Actions.prototype.toParent = function () { |
| 253 | 0 | var hash = uuid(); |
| 254 | 0 | var cb = this._generateCallbackAssertion('toFrame', 'toFrame', null, hash); |
| 255 | 0 | this._addToActionQueue([null, hash], 'toFrame', cb); |
| 256 | 0 | return this; |
| 257 | }; | |
| 258 | ||
| 259 | /** | |
| 260 | * Switches to a different window context | |
| 261 | * | |
| 262 | * Sometimes you encounter situations, where you need to access a different window, like popup windows. | |
| 263 | * You can access such windows with this method, but be aware of the fact, that the complete test context | |
| 264 | * than switches to the window context, every action and assertion will be executed within the chosen window context. | |
| 265 | * Btw.: The domain of the window can be whatever you want, this method has no same origin policy restrictions. | |
| 266 | * | |
| 267 | * If you want to get back to the parents context, you have to use the [toParentWindow](#meth-toParentWindow) method. | |
| 268 | * | |
| 269 | * ```html | |
| 270 | * <div> | |
| 271 | * <a onclick="window.open('http://google.com','goog','width=480, height=300')">Open Google</a> | |
| 272 | * </div> | |
| 273 | * ``` | |
| 274 | * | |
| 275 | * ```javascript | |
| 276 | * test.open('http://adomain.com') | |
| 277 | * .assert.title().is('Title of a page that can open a popup window') | |
| 278 | * .toWindow('goog') | |
| 279 | * .assert.title().is('Google') | |
| 280 | * .toParentWindow() | |
| 281 | * .done(); | |
| 282 | * ``` | |
| 283 | * | |
| 284 | * > NOTE: Buggy in Firefox | |
| 285 | * | |
| 286 | * @api | |
| 287 | * @method toWindow | |
| 288 | * @param {string} name Name of the window to switch to | |
| 289 | * @chainable | |
| 290 | */ | |
| 291 | ||
| 292 | 1 | Actions.prototype.toWindow = function (name) { |
| 293 | 0 | var hash = uuid(); |
| 294 | 0 | var cb = this._generateCallbackAssertion('toWindow', 'toWindow', name, hash); |
| 295 | 0 | this._addToActionQueue([name, hash], 'toWindow', cb); |
| 296 | 0 | return this; |
| 297 | }; | |
| 298 | ||
| 299 | /** | |
| 300 | * Switches back to the parent window context when the test context has been | |
| 301 | * switched to a different window context | |
| 302 | * | |
| 303 | * ```html | |
| 304 | * <div> | |
| 305 | * <a onclick="window.open('http://google.com','goog','width=480, height=300')">Open Google</a> | |
| 306 | * </div> | |
| 307 | * ``` | |
| 308 | * | |
| 309 | * ```javascript | |
| 310 | * test.open('http://adomain.com') | |
| 311 | * .assert.title().is('Title of a page that can open a popup window') | |
| 312 | * .toWindow('goog') | |
| 313 | * .assert.title().is('Google') | |
| 314 | * .toParentWindow() | |
| 315 | * .assert.title().is('Title of a page that can open a popup window') | |
| 316 | * .done(); | |
| 317 | * ``` | |
| 318 | * | |
| 319 | * > NOTE: Buggy in Firefox | |
| 320 | * | |
| 321 | * @api | |
| 322 | * @method toParentWindow | |
| 323 | * @chainable | |
| 324 | */ | |
| 325 | ||
| 326 | 1 | Actions.prototype.toParentWindow = function () { |
| 327 | 0 | var hash = uuid(); |
| 328 | 0 | var cb = this._generateCallbackAssertion('toWindow', 'toWindow', null, hash); |
| 329 | 0 | this._addToActionQueue([null, hash], 'toWindow', cb); |
| 330 | 0 | return this; |
| 331 | }; | |
| 332 | ||
| 333 | /** | |
| 334 | * Wait until a resource that matches the given testFx is loaded to process a next step. | |
| 335 | * | |
| 336 | * TODO: IMPLEMENT | |
| 337 | * | |
| 338 | * @method waitForResource | |
| 339 | * @param {string} ressource URL of the ressource that should be waited for | |
| 340 | * @param {number} timeout Timeout in miliseconds | |
| 341 | * @chainable | |
| 342 | */ | |
| 343 | ||
| 344 | 1 | Actions.prototype.waitForResource = function (ressource, timeout) { |
| 345 | 0 | var hash = uuid(); |
| 346 | 0 | var cb = this._generateCallbackAssertion('waitForResource', 'waitForResource', ressource, timeout, hash); |
| 347 | 0 | this._addToActionQueue([ressource, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitForResource', cb); |
| 348 | 0 | return this; |
| 349 | }; | |
| 350 | ||
| 351 | /** | |
| 352 | * Waits until the passed text is present in the page contents before processing the immediate next step. | |
| 353 | * | |
| 354 | * TODO: IMPLEMENT | |
| 355 | * | |
| 356 | * @method waitForText | |
| 357 | * @param {string} text Text to be waited for | |
| 358 | * @param {number} timeout Timeout in miliseconds | |
| 359 | * @chainable | |
| 360 | */ | |
| 361 | ||
| 362 | 1 | Actions.prototype.waitForText = function (text, timeout) { |
| 363 | 0 | var hash = uuid(); |
| 364 | 0 | var cb = this._generateCallbackAssertion('waitForText', 'waitForText', text, timeout, hash); |
| 365 | 0 | this._addToActionQueue([text, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitForText', cb); |
| 366 | 0 | return this; |
| 367 | }; | |
| 368 | ||
| 369 | /** | |
| 370 | * Waits until an element matching the provided selector expression is visible in the remote DOM to process a next step. | |
| 371 | * | |
| 372 | * TODO: IMPLEMENT | |
| 373 | * | |
| 374 | * @method waitUntilVisible | |
| 375 | * @param {string} selector Selector of the element that should be waited to become invisible | |
| 376 | * @param {number} timeout Timeout in miliseconds | |
| 377 | * @chainable | |
| 378 | */ | |
| 379 | ||
| 380 | 1 | Actions.prototype.waitUntilVisible = function (selector, timeout) { |
| 381 | 0 | var hash = uuid(); |
| 382 | ||
| 383 | 0 | if (this.querying === true) { |
| 384 | 0 | timeout = selector; |
| 385 | 0 | selector = this.selector; |
| 386 | } | |
| 387 | ||
| 388 | 0 | var cb = this._generateCallbackAssertion('waitUntilVisible', 'waitUntilVisible', selector, timeout, hash); |
| 389 | 0 | this._addToActionQueue([selector, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitUntilVisible', cb); |
| 390 | 0 | return this; |
| 391 | }; | |
| 392 | ||
| 393 | /** | |
| 394 | * Waits until an element matching the provided selector expression is no longer visible in remote DOM to process a next step. | |
| 395 | * | |
| 396 | * TODO: IMPLEMENT | |
| 397 | * | |
| 398 | * @method waitWhileVisible | |
| 399 | * @param {string} selector Selector of the element that should be waited to become visible | |
| 400 | * @param {number} timeout Timeout in miliseconds | |
| 401 | * @chainable | |
| 402 | */ | |
| 403 | ||
| 404 | 1 | Actions.prototype.waitWhileVisible = function (selector, timeout) { |
| 405 | 0 | var hash = uuid(); |
| 406 | ||
| 407 | 0 | if (this.querying === true) { |
| 408 | 0 | timeout = selector; |
| 409 | 0 | selector = this.selector; |
| 410 | } | |
| 411 | ||
| 412 | 0 | var cb = this._generateCallbackAssertion('waitWhileVisible', 'waitWhileVisible', selector, timeout, hash); |
| 413 | 0 | this._addToActionQueue([selector, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitWhileVisible', cb); |
| 414 | 0 | return this; |
| 415 | }; | |
| 416 | ||
| 417 | /** | |
| 418 | * Take a screenshot of the current page or css element. | |
| 419 | * | |
| 420 | * The pathname argument takes some placeholders that will be replaced | |
| 421 | * Placeholder: | |
| 422 | * | |
| 423 | * - `:browser` - The browser name (e.g. 'Chrome', 'Safari', 'Firefox', etc.) | |
| 424 | * - `:version` - The browser version (e.g. '10_0', '23_11_5', etc.) | |
| 425 | * - `:os` - The operating system (e.g. `OSX`, `Windows`, `Linux`) | |
| 426 | * - `:osVersion` - The operating system version (e.g `XP`, `7`, `10_8`, etc.) | |
| 427 | * - `:viewport` - The current viewport in pixels (e.g. `w1024_h768`) | |
| 428 | * - `:timestamp` - UNIX like timestapm (e.g. `637657345`) | |
| 429 | * - `:date` - Current date in format MM_DD_YYYY (e.g. `12_24_2013`) | |
| 430 | * - `:datetime` - Current datetime in format MM_DD_YYYY_HH_mm_ss (e.g. `12_24_2013_14_55_23`) | |
| 431 | * | |
| 432 | * ```javascript | |
| 433 | * // creates 'my/folder/my_file.png' | |
| 434 | * test.screenshot('my/folder/my_file.png'); | |
| 435 | * // creates 'my/page/in/safari/homepage.png' | |
| 436 | * test.screenshot('my/page/in/:browser/homepage.png'); | |
| 437 | * // creates 'my/page/in/safari_6_0_1/homepage.png' | |
| 438 | * test.screenshot('my/page/in/:browser_:version/homepage.png'); | |
| 439 | * // creates 'my/page/in/safari_6_0_1/on/osx/homepage.png' | |
| 440 | * test.screenshot('my/page/in/:browser_:version/on/:os/homepage.png'); | |
| 441 | * // creates 'my/page/in/safari_6_0_1/on/osx_10_8/homepage.png' | |
| 442 | * test.screenshot('my/page/in/:browser_:version/on/:os_:osVersion/homepage.png'); | |
| 443 | * // creates 'my/page/at/w1024_h768/homepage.png' | |
| 444 | * test.screenshot('my/page/at/:viewport/homepage.png'); | |
| 445 | * // creates 'my/page/at/637657345/homepage.png' | |
| 446 | * test.screenshot('my/page/in_time/:timestamp/homepage.png'); | |
| 447 | * // creates 'my/page/at/12_24_2013/homepage.png' | |
| 448 | * test.screenshot('my/page/in_time/:date/homepage.png'); | |
| 449 | * // creates 'my/page/at/12_24_2013_14_55_23/homepage.png' | |
| 450 | * test.screenshot('my/page/in_time/:datetime/homepage.png'); | |
| 451 | * ``` | |
| 452 | * | |
| 453 | * @api | |
| 454 | * @method screenshot | |
| 455 | * @param {string} pathname Name of the folder and file the screenshot should be saved to | |
| 456 | * @param {string} css selector of element should be screeshoted | |
| 457 | * @return chainable | |
| 458 | */ | |
| 459 | ||
| 460 | 1 | Actions.prototype.screenshot = function (pathname, selector) { |
| 461 | 3 | var hash = uuid(); |
| 462 | ||
| 463 | 3 | if (this.querying === true) { |
| 464 | 0 | selector = this.selector; |
| 465 | } | |
| 466 | ||
| 467 | 3 | var opts = { |
| 468 | realpath : undefined, | |
| 469 | selector : selector | |
| 470 | }; | |
| 471 | ||
| 472 | 3 | this.screenshotParams = opts; |
| 473 | ||
| 474 | 3 | var screenshotcb = this._generatePlainCallback('screenshot', hash, opts, 'realpath', typeof selector === 'undefined'); |
| 475 | 3 | this._addToActionQueue(['', pathname, hash], 'screenshot', screenshotcb.bind(this)); |
| 476 | ||
| 477 | 3 | if (selector) { |
| 478 | 1 | var imagecutcb = this._generateCallbackAssertion('imagecut', 'screenshot element', opts, hash); |
| 479 | 1 | this._addToActionQueue([opts, hash], 'imagecut', imagecutcb); |
| 480 | } | |
| 481 | ||
| 482 | 3 | this.reporter.emit('report:screenshot', { |
| 483 | 'pathname' : pathname, | |
| 484 | 'uuid' : hash | |
| 485 | }); | |
| 486 | ||
| 487 | 3 | return this; |
| 488 | }; | |
| 489 | ||
| 490 | /** | |
| 491 | * Generates a callback that will be fired when the action has been completed. | |
| 492 | * The callback will then store value into opts variable. | |
| 493 | * | |
| 494 | * @method _generateCallbackAssertion | |
| 495 | * @param {string} type Type of the action (normalle the actions name) | |
| 496 | * @param {string} hash Unique id of the action | |
| 497 | * @param {string} opts Variable where will be stored result of execution of the action | |
| 498 | * @param {string} key Name of the property where will be stored result of execution of the action | |
| 499 | * @return {function} The generated callback function | |
| 500 | * @private | |
| 501 | */ | |
| 502 | 1 | Actions.prototype._generatePlainCallback = function (type, hash, opts, property, last) { |
| 503 | 3 | var cb = function (data) { |
| 504 | 0 | if (data.hash === hash && data.key === type && !this.uuids[data.uuid]) { |
| 505 | 0 | if (typeof opts === 'object' && typeof property === 'string') { |
| 506 | 0 | opts[property] = data.value; |
| 507 | } | |
| 508 | 0 | if (data.key === 'screenshot') { |
| 509 | 0 | this.reporter.emit('report:action', { |
| 510 | value: data.value, | |
| 511 | type: type, | |
| 512 | uuid: data.uuid | |
| 513 | }); | |
| 514 | } | |
| 515 | ||
| 516 | 0 | if (last) { |
| 517 | 0 | this.uuids[data.uuid] = true; |
| 518 | } | |
| 519 | } | |
| 520 | }; | |
| 521 | 3 | return cb; |
| 522 | }; | |
| 523 | ||
| 524 | /** | |
| 525 | * Pause steps suite execution for a given amount of time, and optionally execute a step on done. | |
| 526 | * | |
| 527 | * This makes sense, if you have a ticker for example, tht scrolls like every ten seconds | |
| 528 | * & you want to assure that the visible content changes every ten seconds | |
| 529 | * | |
| 530 | * ```javascript | |
| 531 | * test.open('http://myticker.org') | |
| 532 | * .assert.visible('.ticker-element:first-child', 'First ticker element is visible') | |
| 533 | * .wait(10000) | |
| 534 | * .assert.visible('.ticker-element:nth-child(2)', 'Snd. ticker element is visible') | |
| 535 | * .wait(10000) | |
| 536 | * .assert.visible('.ticker-element:last-child', 'Third ticker element is visible') | |
| 537 | * .done(); | |
| 538 | * ``` | |
| 539 | * If no timeout argument is given, a default timeout of 5 seconds will be used | |
| 540 | * | |
| 541 | * ```javascript | |
| 542 | * test.open('http://myticker.org') | |
| 543 | * .assert.visible('.ticker-element:first-child', 'First ticker element is visible') | |
| 544 | * .wait() | |
| 545 | * .assert.visible('.ticker-element:nth-child(2)', 'Snd. ticker element is visible') | |
| 546 | * .wait() | |
| 547 | * .assert.visible('.ticker-element:last-child', 'Third ticker element is visible') | |
| 548 | * .done(); | |
| 549 | * ``` | |
| 550 | * | |
| 551 | * @api | |
| 552 | * @method wait | |
| 553 | * @param {number} timeout in milliseconds | |
| 554 | * @chainable | |
| 555 | */ | |
| 556 | ||
| 557 | 1 | Actions.prototype.wait = function (timeout) { |
| 558 | 0 | var hash = uuid(); |
| 559 | 0 | var cb = this._generateCallbackAssertion('wait', 'wait', timeout, hash); |
| 560 | 0 | this._addToActionQueue([(timeout ? parseInt(timeout, 10) : 5000), hash], 'wait', cb); |
| 561 | 0 | return this; |
| 562 | }; | |
| 563 | ||
| 564 | /** | |
| 565 | * Reloads current page location. | |
| 566 | * | |
| 567 | * This is basically the same as hitting F5/refresh in your browser | |
| 568 | * | |
| 569 | * ```javascript | |
| 570 | * test.open('http://google.com') | |
| 571 | * .reload() | |
| 572 | * .done(); | |
| 573 | * ``` | |
| 574 | * | |
| 575 | * @api | |
| 576 | * @method reload | |
| 577 | * @chainable | |
| 578 | */ | |
| 579 | ||
| 580 | 1 | Actions.prototype.reload = function () { |
| 581 | 0 | var hash = uuid(); |
| 582 | 0 | var cb = this._generateCallbackAssertion('refresh', 'refresh', '', hash); |
| 583 | 0 | this._addToActionQueue([hash], 'refresh', cb); |
| 584 | 0 | return this; |
| 585 | }; | |
| 586 | ||
| 587 | /** | |
| 588 | * Moves a step forward in browser's history. | |
| 589 | * | |
| 590 | * This is basically the same as hitting the forward button in your browser | |
| 591 | * | |
| 592 | * ```javascript | |
| 593 | * test.open('http://google.com') | |
| 594 | * .open('https://github.com') | |
| 595 | * .assert.url.is('https://github.com/', 'We are at GitHub') | |
| 596 | * .back() | |
| 597 | * .assert.url.is('http://google.com', 'We are at Google!') | |
| 598 | * .forward() | |
| 599 | * .assert.url.is('https://github.com/', 'Back at GitHub! Timetravel FTW') | |
| 600 | * .done(); | |
| 601 | * ``` | |
| 602 | * | |
| 603 | * @api | |
| 604 | * @method forward | |
| 605 | * @chainable | |
| 606 | */ | |
| 607 | ||
| 608 | 1 | Actions.prototype.forward = function () { |
| 609 | 0 | var hash = uuid(); |
| 610 | 0 | var cb = this._generateCallbackAssertion('forward', 'forward', '', hash); |
| 611 | 0 | this._addToActionQueue([hash], 'forward', cb); |
| 612 | 0 | return this; |
| 613 | }; | |
| 614 | ||
| 615 | /** | |
| 616 | * Moves back a step in browser's history. | |
| 617 | * | |
| 618 | * This is basically the same as hitting the back button in your browser | |
| 619 | * | |
| 620 | * ```javascript | |
| 621 | * test.open('http://google.com') | |
| 622 | * .open('https://github.com') | |
| 623 | * .assert.url.is('https://github.com/', 'We are at GitHub') | |
| 624 | * .back() | |
| 625 | * .assert.url.is('http://google.com', 'We are at Google!') | |
| 626 | * .forward() | |
| 627 | * .assert.url.is('https://github.com/', 'Back at GitHub! Timetravel FTW'); | |
| 628 | * .done(); | |
| 629 | * ``` | |
| 630 | * | |
| 631 | * @api | |
| 632 | * @method back | |
| 633 | * @chainable | |
| 634 | */ | |
| 635 | ||
| 636 | 1 | Actions.prototype.back = function () { |
| 637 | 0 | var hash = uuid(); |
| 638 | 0 | var cb = this._generateCallbackAssertion('back', 'back', '', hash); |
| 639 | 0 | this._addToActionQueue([hash], 'back', cb); |
| 640 | 0 | return this; |
| 641 | }; | |
| 642 | ||
| 643 | /** | |
| 644 | * Performs a click on the element matching the provided selector expression. | |
| 645 | * | |
| 646 | * If we take Daleks homepage (the one you're probably visiting right now), | |
| 647 | * the HTML looks something like this (it does not really, but hey, lets assume this for a second) | |
| 648 | * | |
| 649 | * ```html | |
| 650 | * <nav> | |
| 651 | * <ul> | |
| 652 | * <li><a id="homeapge" href="/index.html">DalekJS</a></li> | |
| 653 | * <li><a id="docs" href="/docs.html">Documentation</a></li> | |
| 654 | * <li><a id="faq" href="/faq.html">F.A.Q</a></li> | |
| 655 | * </ul> | |
| 656 | * </nav> | |
| 657 | * ``` | |
| 658 | * | |
| 659 | * ```javascript | |
| 660 | * test.open('http://dalekjs.com') | |
| 661 | * .click('#faq') | |
| 662 | * .assert.title().is('DalekJS - Frequently asked questions', 'What the F.A.Q.') | |
| 663 | * .done(); | |
| 664 | * ``` | |
| 665 | * | |
| 666 | * By default, this performs a left click. | |
| 667 | * In the future it might become the ability to also execute a "right button" click. | |
| 668 | * | |
| 669 | * > Note: Does not work correctly in Firefox when used on `<select>` & `<option>` elements | |
| 670 | * | |
| 671 | * @api | |
| 672 | * @method click | |
| 673 | * @param {string} selector Selector of the element to be clicked | |
| 674 | * @chainable | |
| 675 | */ | |
| 676 | ||
| 677 | 1 | Actions.prototype.click = function (selector) { |
| 678 | 0 | var hash = uuid(); |
| 679 | ||
| 680 | 0 | if (this.querying === true) { |
| 681 | 0 | selector = this.selector; |
| 682 | } | |
| 683 | ||
| 684 | 0 | var cb = this._generateCallbackAssertion('click', 'click', selector, hash); |
| 685 | 0 | this._addToActionQueue([selector, hash], 'click', cb); |
| 686 | 0 | return this; |
| 687 | }; | |
| 688 | ||
| 689 | /** | |
| 690 | * Submits a form. | |
| 691 | * | |
| 692 | * ```html | |
| 693 | * <form id="skaaro" action="skaaro.php" method="GET"> | |
| 694 | * <input type="hidden" name="intheshadows" value="itis"/> | |
| 695 | * <input type="text" name="truth" id="truth" value=""/> | |
| 696 | * </form> | |
| 697 | * ``` | |
| 698 | * | |
| 699 | * ```javascript | |
| 700 | * test.open('http://home.dalek.com') | |
| 701 | * .type('#truth', 'out there is') | |
| 702 | * .submit('#skaaro') | |
| 703 | * .done(); | |
| 704 | * ``` | |
| 705 | * | |
| 706 | * > Note: Does not work in Firefox yet | |
| 707 | * | |
| 708 | * @api | |
| 709 | * @method submit | |
| 710 | * @param {string} selector Selector of the form to be submitted | |
| 711 | * @chainable | |
| 712 | */ | |
| 713 | ||
| 714 | 1 | Actions.prototype.submit = function (selector) { |
| 715 | 0 | var hash = uuid(); |
| 716 | ||
| 717 | 0 | if (this.querying === true) { |
| 718 | 0 | selector = this.selector; |
| 719 | } | |
| 720 | ||
| 721 | 0 | var cb = this._generateCallbackAssertion('submit', 'submit', selector, hash); |
| 722 | 0 | this._addToActionQueue([selector, hash], 'submit', cb); |
| 723 | 0 | return this; |
| 724 | }; | |
| 725 | ||
| 726 | /** | |
| 727 | * Performs an HTTP request for opening a given location. | |
| 728 | * You can forge GET, POST, PUT, DELETE and HEAD requests. | |
| 729 | * | |
| 730 | * Basically the same as typing a location into your browsers URL bar and | |
| 731 | * hitting return. | |
| 732 | * | |
| 733 | * ```javascript | |
| 734 | * test.open('http://dalekjs.com') | |
| 735 | * .assert.url().is('http://dalekjs.com', 'DalekJS I\'m in you') | |
| 736 | * .done(); | |
| 737 | * ``` | |
| 738 | * | |
| 739 | * @api | |
| 740 | * @method open | |
| 741 | * @param {string} location URL of the page to open | |
| 742 | * @chainable | |
| 743 | */ | |
| 744 | ||
| 745 | 1 | Actions.prototype.open = function (location) { |
| 746 | //see if we should prepend the location with the configured base url is available and needed | |
| 747 | 0 | if(location.substr(0, 1) === '/' && this.driver.config.config.baseUrl) { |
| 748 | 0 | location = this.driver.config.config.baseUrl + location; |
| 749 | } | |
| 750 | ||
| 751 | 0 | var hash = uuid(); |
| 752 | 0 | var cb = this._generateCallbackAssertion('open', 'open', location, hash); |
| 753 | 0 | this._addToActionQueue([location, hash], 'open', cb); |
| 754 | 0 | return this; |
| 755 | }; | |
| 756 | ||
| 757 | /** | |
| 758 | * Types a text into an input field or text area. | |
| 759 | * And yes, it really types, character for character, like you would | |
| 760 | * do when using your keyboard. | |
| 761 | * | |
| 762 | * | |
| 763 | * ```html | |
| 764 | * <form id="skaaro" action="skaaro.php" method="GET"> | |
| 765 | * <input type="hidden" name="intheshadows" value="itis"/> | |
| 766 | * <input type="text" name="truth" id="truth" value=""/> | |
| 767 | * </form> | |
| 768 | * ``` | |
| 769 | * | |
| 770 | * ```javascript | |
| 771 | * test.open('http://home.dalek.com') | |
| 772 | * .type('#truth', 'out there is') | |
| 773 | * .assert.val('#truth', 'out there is', 'Text has been set') | |
| 774 | * .done(); | |
| 775 | * ``` | |
| 776 | * | |
| 777 | * You can also send special keys using unicode. | |
| 778 | * | |
| 779 | * * ```javascript | |
| 780 | * test.open('http://home.dalek.com') | |
| 781 | * .type('#truth', 'out \uE008there\uE008 is') | |
| 782 | * .assert.val('#truth', 'out THERE is', 'Text has been set') | |
| 783 | * .done(); | |
| 784 | * ``` | |
| 785 | * You can go [here](https://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/element/:id/value) to read up on special keys and unicodes for them (note that a code of U+EXXX is actually written in code as \uEXXX). | |
| 786 | * | |
| 787 | * > Note: Does not work correctly in Firefox with special keys | |
| 788 | * | |
| 789 | * @api | |
| 790 | * @method type | |
| 791 | * @param {string} selector Selector of the form field to be filled | |
| 792 | * @param {string} keystrokes Text to be applied to the element | |
| 793 | * @chainable | |
| 794 | */ | |
| 795 | ||
| 796 | 1 | Actions.prototype.type = function (selector, keystrokes) { |
| 797 | 0 | var hash = uuid(); |
| 798 | ||
| 799 | 0 | if (this.querying === true) { |
| 800 | 0 | keystrokes = selector; |
| 801 | 0 | selector = this.selector; |
| 802 | } | |
| 803 | ||
| 804 | 0 | var cb = this._generateCallbackAssertion('type', 'type', selector, keystrokes, hash); |
| 805 | 0 | this._addToActionQueue([selector, keystrokes], 'type', cb); |
| 806 | 0 | return this; |
| 807 | }; | |
| 808 | ||
| 809 | /** | |
| 810 | * This acts just like .type() with a key difference. | |
| 811 | * This action can be used on non-input elements (useful for test site wide keyboard shortcuts and the like). | |
| 812 | * So assumeing we have a keyboard shortcut that display an alert box, we could test that with something like this: | |
| 813 | * | |
| 814 | * ```javascript | |
| 815 | * test.open('http://home.dalek.com') | |
| 816 | * .sendKeys('body', '\uE00C') | |
| 817 | * .assert.dialogText('press the escape key give this alert text') | |
| 818 | * .done(); | |
| 819 | * ``` | |
| 820 | * | |
| 821 | * | |
| 822 | * > Note: Does not work correctly in Firefox with special keys | |
| 823 | * | |
| 824 | * @api | |
| 825 | * @method sendKeys | |
| 826 | * @param {string} selector Selector of the form field to be filled | |
| 827 | * @param {string} keystrokes Text to be applied to the element | |
| 828 | * @chainable | |
| 829 | */ | |
| 830 | ||
| 831 | 1 | Actions.prototype.sendKeys = function (selector, keystrokes) { |
| 832 | 0 | var hash = uuid(); |
| 833 | ||
| 834 | 0 | if (this.querying === true) { |
| 835 | 0 | keystrokes = selector; |
| 836 | 0 | selector = this.selector; |
| 837 | } | |
| 838 | ||
| 839 | 0 | var cb = this._generateCallbackAssertion('sendKeys', 'sendKeys', selector, keystrokes, hash); |
| 840 | 0 | this._addToActionQueue([selector, keystrokes], 'sendKeys', cb); |
| 841 | 0 | return this; |
| 842 | }; | |
| 843 | ||
| 844 | /** | |
| 845 | * Types a text into the text input field of a prompt dialog. | |
| 846 | * Like you would do when using your keyboard. | |
| 847 | * | |
| 848 | * ```html | |
| 849 | * <div> | |
| 850 | * <a id="aquestion" onclick="this.innerText = window.prompt('Your favourite companion:')">????</a> | |
| 851 | * </div> | |
| 852 | * ``` | |
| 853 | * | |
| 854 | * ```javascript | |
| 855 | * test.open('http://adomain.com') | |
| 856 | * .click('#aquestion') | |
| 857 | * .answer('Rose') | |
| 858 | * .assert.text('#aquestion').is('Rose', 'Awesome she was!') | |
| 859 | * .done(); | |
| 860 | * ``` | |
| 861 | * | |
| 862 | * | |
| 863 | * > Note: Does not work in Firefox & PhantomJS | |
| 864 | * | |
| 865 | * @api | |
| 866 | * @method answer | |
| 867 | * @param {string} keystrokes Text to be applied to the element | |
| 868 | * @return chainable | |
| 869 | */ | |
| 870 | ||
| 871 | 1 | Actions.prototype.answer = function (keystrokes) { |
| 872 | 0 | var hash = uuid(); |
| 873 | 0 | var cb = this._generateCallbackAssertion('promptText', 'promptText', keystrokes, hash); |
| 874 | 0 | this._addToActionQueue([keystrokes, hash], 'promptText', cb); |
| 875 | 0 | return this; |
| 876 | }; | |
| 877 | ||
| 878 | /** | |
| 879 | * Executes a JavaScript function within the browser context | |
| 880 | * | |
| 881 | * ```javascript | |
| 882 | * test.open('http://adomain.com') | |
| 883 | * .execute(function () { | |
| 884 | * window.myFramework.addRow('foo'); | |
| 885 | * window.myFramework.addRow('bar'); | |
| 886 | * }) | |
| 887 | * .done(); | |
| 888 | * ``` | |
| 889 | * | |
| 890 | * You can also apply arguments to the function | |
| 891 | * | |
| 892 | * ```javascript | |
| 893 | * test.open('http://adomain.com') | |
| 894 | * .execute(function (paramFoo, aBar) { | |
| 895 | * window.myFramework.addRow(paramFoo); | |
| 896 | * window.myFramework.addRow(aBar); | |
| 897 | * }, 'foo', 'bar') | |
| 898 | * .done(); | |
| 899 | * ``` | |
| 900 | * | |
| 901 | * > Note: Buggy in Firefox | |
| 902 | * | |
| 903 | * @api | |
| 904 | * @method execute | |
| 905 | * @param {function} script JavaScript function that should be executed | |
| 906 | * @return chainable | |
| 907 | */ | |
| 908 | ||
| 909 | 1 | Actions.prototype.execute = function (script) { |
| 910 | 0 | var hash = uuid(); |
| 911 | 0 | var args = [this.contextVars].concat(Array.prototype.slice.call(arguments, 1) || []); |
| 912 | 0 | var cb = this._generateCallbackAssertion('execute', 'execute', script, args, hash); |
| 913 | 0 | this._addToActionQueue([script, args, hash], 'execute', cb); |
| 914 | 0 | return this; |
| 915 | }; | |
| 916 | ||
| 917 | /** | |
| 918 | * Waits until a function returns true to process any next step. | |
| 919 | * | |
| 920 | * You can also set a callback on timeout using the onTimeout argument, | |
| 921 | * and set the timeout using the timeout one, in milliseconds. The default timeout is set to 5000ms. | |
| 922 | * | |
| 923 | * ```javascript | |
| 924 | * test.open('http://adomain.com') | |
| 925 | * .waitFor(function () { | |
| 926 | * return window.myCheck === true; | |
| 927 | * }) | |
| 928 | * .done(); | |
| 929 | * ``` | |
| 930 | * | |
| 931 | * You can also apply arguments to the function, as well as a timeout | |
| 932 | * | |
| 933 | * ```javascript | |
| 934 | * test.open('http://adomain.com') | |
| 935 | * .waitFor(function (aCheck) { | |
| 936 | * return window.myThing === aCheck; | |
| 937 | * }, ['arg1', 'arg2'], 10000) | |
| 938 | * .done(); | |
| 939 | * ``` | |
| 940 | * | |
| 941 | * > Note: Buggy in Firefox | |
| 942 | * | |
| 943 | * @method waitFor | |
| 944 | * @param {function} fn Async function that resolves an promise when ready | |
| 945 | * @param {array} args Additional arguments | |
| 946 | * @param {number} timeout Timeout in miliseconds | |
| 947 | * @chainable | |
| 948 | * @api | |
| 949 | */ | |
| 950 | ||
| 951 | 1 | Actions.prototype.waitFor = function (script, args, timeout) { |
| 952 | 0 | var hash = uuid(); |
| 953 | 0 | timeout = timeout || 5000; |
| 954 | 0 | args = [this.contextVars].concat(Array.prototype.slice.call(arguments, 1) || []); |
| 955 | 0 | var cb = this._generateCallbackAssertion('waitFor', 'waitFor', script, args, timeout, hash); |
| 956 | 0 | this._addToActionQueue([script, args, timeout, hash], 'waitFor', cb); |
| 957 | 0 | return this; |
| 958 | }; | |
| 959 | ||
| 960 | /** | |
| 961 | * Accepts an alert/prompt/confirm dialog. This is basically the same actions as when | |
| 962 | * you are clicking okay or hitting return in one of that dialogs. | |
| 963 | * | |
| 964 | * ```html | |
| 965 | * <div> | |
| 966 | * <a id="attentione" onclick="window.alert('Alonsy!')">ALERT!ALERT!</a> | |
| 967 | * </div> | |
| 968 | * ``` | |
| 969 | * | |
| 970 | * ```javascript | |
| 971 | * test.open('http://adomain.com') | |
| 972 | * // alert appears | |
| 973 | * .click('#attentione') | |
| 974 | * // alert is gone | |
| 975 | * .accept() | |
| 976 | * .done(); | |
| 977 | * ``` | |
| 978 | * | |
| 979 | * > Note: Does not work in Firefox & PhantomJS | |
| 980 | * | |
| 981 | * @api | |
| 982 | * @method accept | |
| 983 | * @return chainable | |
| 984 | */ | |
| 985 | ||
| 986 | 1 | Actions.prototype.accept = function () { |
| 987 | 0 | var hash = uuid(); |
| 988 | 0 | var cb = this._generateCallbackAssertion('acceptAlert', 'acceptAlert', hash); |
| 989 | 0 | this._addToActionQueue([hash], 'acceptAlert', cb); |
| 990 | 0 | return this; |
| 991 | }; | |
| 992 | ||
| 993 | /** | |
| 994 | * Dismisses an prompt/confirm dialog. This is basically the same actions as when | |
| 995 | * you are clicking cancel in one of that dialogs. | |
| 996 | * | |
| 997 | * ```html | |
| 998 | * <div> | |
| 999 | * <a id="nonono" onclick="(this.innerText = window.confirm('No classic doctors in the 50th?') ? 'Buh!' : ':(') ">What!</a> | |
| 1000 | * </div> | |
| 1001 | * ``` | |
| 1002 | * | |
| 1003 | * ```javascript | |
| 1004 | * test.open('http://adomain.com') | |
| 1005 | * // prompt appears | |
| 1006 | * .click('#nonono') | |
| 1007 | * // prompt is gone | |
| 1008 | * .dismiss() | |
| 1009 | * .assert.text('#nonono').is(':(', 'So sad') | |
| 1010 | * .done(); | |
| 1011 | * ``` | |
| 1012 | * | |
| 1013 | * > Note: Does not work in Firefox & PhantomJS | |
| 1014 | * | |
| 1015 | * @api | |
| 1016 | * @method dismiss | |
| 1017 | * @return chainable | |
| 1018 | */ | |
| 1019 | ||
| 1020 | 1 | Actions.prototype.dismiss = function () { |
| 1021 | 0 | var hash = uuid(); |
| 1022 | 0 | var cb = this._generateCallbackAssertion('dismissAlert', 'dismissAlert', hash); |
| 1023 | 0 | this._addToActionQueue([hash], 'dismissAlert', cb); |
| 1024 | 0 | return this; |
| 1025 | }; | |
| 1026 | ||
| 1027 | /** | |
| 1028 | * Resizes the browser window to a set of given dimensions (in px). | |
| 1029 | * The default configuration of dalek opening pages is a width of 1280px | |
| 1030 | * and a height of 1024px. You can specify your own default in the configuration. | |
| 1031 | * | |
| 1032 | * ```html | |
| 1033 | * <div> | |
| 1034 | * <span id="magicspan">The span in the fireplace</span> | |
| 1035 | * </div> | |
| 1036 | * ``` | |
| 1037 | * | |
| 1038 | * ```css | |
| 1039 | * #magicspan { | |
| 1040 | * display: inline; | |
| 1041 | * } | |
| 1042 | * | |
| 1043 | * // @media all and (max-width: 500px) and (min-width: 300px) | |
| 1044 | * #magicspan { | |
| 1045 | * display: none; | |
| 1046 | * } | |
| 1047 | * ``` | |
| 1048 | * | |
| 1049 | * ```javascript | |
| 1050 | * test.open('http://adomain.com') | |
| 1051 | * .assert.visible('#magicspan', 'Big screen, visible span') | |
| 1052 | * .resize({width: 400, height: 500}) | |
| 1053 | * .assert.notVisible('#magicspan', 'Small screen, no visible span magic!') | |
| 1054 | * .done(); | |
| 1055 | * ``` | |
| 1056 | * | |
| 1057 | * | |
| 1058 | * > Note: Does not work in Firefox | |
| 1059 | * | |
| 1060 | * @api | |
| 1061 | * @method resize | |
| 1062 | * @param {object} dimensions Width and height as properties to apply | |
| 1063 | * @chainable | |
| 1064 | */ | |
| 1065 | ||
| 1066 | 1 | Actions.prototype.resize = function (dimensions) { |
| 1067 | 0 | var hash = uuid(); |
| 1068 | 0 | var cb = this._generateCallbackAssertion('resize', 'resize', dimensions, hash); |
| 1069 | 0 | this._addToActionQueue([dimensions, hash], 'resize', cb); |
| 1070 | 0 | return this; |
| 1071 | }; | |
| 1072 | ||
| 1073 | /** | |
| 1074 | * Maximizes the browser window. | |
| 1075 | * | |
| 1076 | * ```html | |
| 1077 | * <div> | |
| 1078 | * <span id="magicspan">The span in the fireplace</span> | |
| 1079 | * </div> | |
| 1080 | * ``` | |
| 1081 | * | |
| 1082 | * ```css | |
| 1083 | * #magicspan { | |
| 1084 | * display: inline; | |
| 1085 | * } | |
| 1086 | * | |
| 1087 | * @media all and (max-width: 500px) and (min-width: 300px) { | |
| 1088 | * #magicspan { | |
| 1089 | * display: none; | |
| 1090 | * } | |
| 1091 | * } | |
| 1092 | * ``` | |
| 1093 | * | |
| 1094 | * ```javascript | |
| 1095 | * test.open('http://adomain.com') | |
| 1096 | * .resize({width: 400, height: 500}) | |
| 1097 | * .assert.notVisible('#magicspan', 'Small screen, no visible span magic!') | |
| 1098 | * .maximize() | |
| 1099 | * .assert.visible('#magicspan', 'Big screen, visible span') | |
| 1100 | * .done(); | |
| 1101 | * ``` | |
| 1102 | * | |
| 1103 | * > Note: Does not work in Firefox and PhantomJS | |
| 1104 | * | |
| 1105 | * @api | |
| 1106 | * @method maximize | |
| 1107 | * @chainable | |
| 1108 | */ | |
| 1109 | ||
| 1110 | 1 | Actions.prototype.maximize = function () { |
| 1111 | 0 | var hash = uuid(); |
| 1112 | 0 | var cb = this._generateCallbackAssertion('maximize', 'maximize', hash); |
| 1113 | 0 | this._addToActionQueue([hash], 'maximize', cb); |
| 1114 | 0 | return this; |
| 1115 | }; | |
| 1116 | ||
| 1117 | /** | |
| 1118 | * Sets a cookie. | |
| 1119 | * More configuration options will be implemented in the future, | |
| 1120 | * by now, you can only set a cookie with a specific name and contents. | |
| 1121 | * This will be a domain wide set cookie. | |
| 1122 | * | |
| 1123 | * ```javascript | |
| 1124 | * test.open('http://adomain.com') | |
| 1125 | * .setCookie('my_cookie_name', 'my=content') | |
| 1126 | * .done(); | |
| 1127 | * ``` | |
| 1128 | * | |
| 1129 | * @api | |
| 1130 | * @method setCookie | |
| 1131 | * @chainable | |
| 1132 | */ | |
| 1133 | ||
| 1134 | 1 | Actions.prototype.setCookie = function (name, contents) { |
| 1135 | 0 | var hash = uuid(); |
| 1136 | 0 | var cb = this._generateCallbackAssertion('setCookie', 'setCookie', name, contents, hash); |
| 1137 | 0 | this._addToActionQueue([name, contents, hash], 'setCookie', cb); |
| 1138 | 0 | return this; |
| 1139 | }; | |
| 1140 | ||
| 1141 | /** | |
| 1142 | * Waits until an element matching the provided | |
| 1143 | * selector expression exists in remote DOM to process any next step. | |
| 1144 | * | |
| 1145 | * Lets assume we have a ticker that loads its contents via AJAX, | |
| 1146 | * and appends new elements, when the call has been successfully answered: | |
| 1147 | * | |
| 1148 | * ```javascript | |
| 1149 | * test.open('http://myticker.org') | |
| 1150 | * .assert.text('.ticker-element:first-child', 'First!', 'First ticker element is visible') | |
| 1151 | * // now we load the next ticker element, defsult timeout is 5 seconds | |
| 1152 | * .waitForElement('.ticker-element:nth-child(2)') | |
| 1153 | * .assert.text('.ticker-element:nth-child(2)', 'Me snd. one', 'Snd. ticker element is visible') | |
| 1154 | * // Lets assume that this AJAX call can take longer, so we raise the default timeout to 10 seconds | |
| 1155 | * .waitForElement('.ticker-element:last-child', 10000) | |
| 1156 | * .assert.text('.ticker-element:last-child', 'Me, third one!', 'Third ticker element is visible') | |
| 1157 | * .done(); | |
| 1158 | * ``` | |
| 1159 | * | |
| 1160 | * Note that the function exits succesfully when the first element is found, matching the given selector | |
| 1161 | * | |
| 1162 | * @api | |
| 1163 | * @method waitForElement | |
| 1164 | * @param {string} selector Selector that matches the element to wait for | |
| 1165 | * @param {number} timeout Timeout in milliseconds | |
| 1166 | * @chainable | |
| 1167 | */ | |
| 1168 | ||
| 1169 | 1 | Actions.prototype.waitForElement = function (selector, timeout) { |
| 1170 | 0 | var hash = uuid(); |
| 1171 | ||
| 1172 | 0 | if (this.querying === true) { |
| 1173 | 0 | timeout = selector; |
| 1174 | 0 | selector = this.selector; |
| 1175 | } | |
| 1176 | ||
| 1177 | 0 | var cb = this._generateCallbackAssertion('waitForElement', 'waitForElement', selector + ' : ' + timeout, hash); |
| 1178 | 0 | this._addToActionQueue([selector, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitForElement', cb); |
| 1179 | 0 | return this; |
| 1180 | }; | |
| 1181 | ||
| 1182 | /** | |
| 1183 | * Fills the fields of a form with given values. | |
| 1184 | * | |
| 1185 | * ```html | |
| 1186 | * <input type="text" value="not really a value" id="ijustwannahaveavalue"/> | |
| 1187 | * ``` | |
| 1188 | * | |
| 1189 | * ```javascript | |
| 1190 | * test.open('http://dalekjs.com') | |
| 1191 | * .setValue('#ijustwannahaveavalue', 'a value') | |
| 1192 | * .assert.val('#ijustwannahaveavalue', 'a value', 'Value is changed'); | |
| 1193 | * ``` | |
| 1194 | * | |
| 1195 | * @api | |
| 1196 | * @method setValue | |
| 1197 | * @param {string} selector | |
| 1198 | * @param {string} value | |
| 1199 | * @return {Actions} | |
| 1200 | */ | |
| 1201 | ||
| 1202 | 1 | Actions.prototype.setValue = function (selector, value) { |
| 1203 | 0 | var hash = uuid(); |
| 1204 | ||
| 1205 | 0 | if (this.querying === true) { |
| 1206 | 0 | value = selector; |
| 1207 | 0 | selector = this.selector; |
| 1208 | } | |
| 1209 | ||
| 1210 | 0 | var cb = this._generateCallbackAssertion('setValue', 'setValue', selector + ' : ' + value, hash); |
| 1211 | 0 | this._addToActionQueue([selector, value, hash], 'setValue', cb); |
| 1212 | 0 | return this; |
| 1213 | }; | |
| 1214 | ||
| 1215 | // LOG (May should live in its own module) | |
| 1216 | // --------------------------------------- | |
| 1217 | ||
| 1218 | 1 | Actions.prototype.logger = {}; |
| 1219 | ||
| 1220 | /** | |
| 1221 | * Logs a part of the remote dom | |
| 1222 | * | |
| 1223 | * ```html | |
| 1224 | * <body> | |
| 1225 | * <div id="smth"> | |
| 1226 | * <input type="hidden" value="not really a value" id="ijustwannahaveavalue"/> | |
| 1227 | * </div> | |
| 1228 | * </body> | |
| 1229 | * ``` | |
| 1230 | * | |
| 1231 | * ```javascript | |
| 1232 | * test.open('http://dalekjs.com/guineapig') | |
| 1233 | * .log.dom('#smth') | |
| 1234 | * .done(); | |
| 1235 | * ``` | |
| 1236 | * | |
| 1237 | * Will output this: | |
| 1238 | * | |
| 1239 | * ```html | |
| 1240 | * DOM: #smth <input type="hidden" value="not really a value" id="ijustwannahaveavalue"/> | |
| 1241 | * ``` | |
| 1242 | ||
| 1243 | * | |
| 1244 | * @api | |
| 1245 | * @method log.dom | |
| 1246 | * @param {string} selector CSS selector | |
| 1247 | * @chainable | |
| 1248 | */ | |
| 1249 | ||
| 1250 | 1 | Actions.prototype.logger.dom = function (selector) { |
| 1251 | 0 | var hash = uuid(); |
| 1252 | ||
| 1253 | 0 | var cb = function logDomCb (data) { |
| 1254 | 0 | if (data && data.key === 'source' && !this.uuids[data.uuid]) { |
| 1255 | 0 | this.uuids[data.uuid] = true; |
| 1256 | 0 | var $ = cheerio.load(data.value); |
| 1257 | 0 | var result = selector ? $(selector).html() : $.html(); |
| 1258 | 0 | selector = selector ? selector : ' '; |
| 1259 | 0 | result = !result ? ' Not found' : result; |
| 1260 | 0 | this.reporter.emit('report:log:user', 'DOM: ' + selector + ' ' + result); |
| 1261 | } | |
| 1262 | }.bind(this); | |
| 1263 | ||
| 1264 | 0 | this._addToActionQueue([hash], 'source', cb); |
| 1265 | 0 | return this; |
| 1266 | }; | |
| 1267 | ||
| 1268 | /** | |
| 1269 | * Logs a user defined message | |
| 1270 | * | |
| 1271 | * ```javascript | |
| 1272 | * test.open('http://dalekjs.com/guineapig') | |
| 1273 | * .execute(function () { | |
| 1274 | * this.data('aKey', 'aValue'); | |
| 1275 | * }) | |
| 1276 | * .log.message(function () { | |
| 1277 | * return test.data('aKey'); // outputs MESSAGE: 'aValue' | |
| 1278 | * }) | |
| 1279 | * .done(); | |
| 1280 | * ``` | |
| 1281 | * | |
| 1282 | * 'Normal' messages can be logged too: | |
| 1283 | * | |
| 1284 | * ```javascript | |
| 1285 | * test.open('http://dalekjs.com/guineapig') | |
| 1286 | * .log.message('FooBar') // outputs MESSAGE: FooBar | |
| 1287 | * .done(); | |
| 1288 | * ``` | |
| 1289 | * | |
| 1290 | * @api | |
| 1291 | * @method log.message | |
| 1292 | * @param {function|string} message | |
| 1293 | * @chainable | |
| 1294 | */ | |
| 1295 | ||
| 1296 | 1 | Actions.prototype.logger.message = function (message) { |
| 1297 | 0 | var hash = uuid(); |
| 1298 | ||
| 1299 | 0 | var cb = function logMessageCb (data) { |
| 1300 | 0 | if (data && data.key === 'noop' && !this.uuids[data.hash]) { |
| 1301 | 0 | this.uuids[data.hash] = true; |
| 1302 | 0 | var result = (typeof(data.value) === 'function') ? data.value.bind(this)() : data.value; |
| 1303 | 0 | this.reporter.emit('report:log:user', 'MESSAGE: ' + result); |
| 1304 | } | |
| 1305 | }.bind(this); | |
| 1306 | ||
| 1307 | 0 | this._addToActionQueue([message, hash], 'noop', cb); |
| 1308 | 0 | return this; |
| 1309 | }; | |
| 1310 | ||
| 1311 | /** | |
| 1312 | * Generates a callback that will be fired when the action has been completed. | |
| 1313 | * The callback itself will then validate the answer and will also emit an event | |
| 1314 | * that the action has been successfully executed. | |
| 1315 | * | |
| 1316 | * @method _generateCallbackAssertion | |
| 1317 | * @param {string} key Unique key of the action | |
| 1318 | * @param {string} type Type of the action (normalle the actions name) | |
| 1319 | * @return {function} The generated callback function | |
| 1320 | * @private | |
| 1321 | */ | |
| 1322 | ||
| 1323 | 1 | Actions.prototype._generateCallbackAssertion = function (key, type) { |
| 1324 | 1 | var cb = function (data) { |
| 1325 | 0 | if (data && data.key === key && !this.uuids[data.uuid]) { |
| 1326 | 0 | if (!data || (data.value && data.value === null)) { |
| 1327 | 0 | data.value = ''; |
| 1328 | } | |
| 1329 | ||
| 1330 | 0 | if (key === 'execute') { |
| 1331 | 0 | Object.keys(data.value.dalek).forEach(function (key) { |
| 1332 | 0 | this.contextVars[key] = data.value.dalek[key]; |
| 1333 | }.bind(this)); | |
| 1334 | ||
| 1335 | 0 | data.value.test.forEach(function (test) { |
| 1336 | 0 | this.reporter.emit('report:assertion', { |
| 1337 | success: test.ok, | |
| 1338 | expected: true, | |
| 1339 | value: test.ok, | |
| 1340 | message: test.message, | |
| 1341 | type: 'OK' | |
| 1342 | }); | |
| 1343 | ||
| 1344 | 0 | this.incrementExpectations(); |
| 1345 | ||
| 1346 | 0 | if (!test.ok) { |
| 1347 | 0 | this.incrementFailedAssertions(); |
| 1348 | } | |
| 1349 | }.bind(this)); | |
| 1350 | ||
| 1351 | 0 | data.value = ''; |
| 1352 | } | |
| 1353 | ||
| 1354 | 0 | this.uuids[data.uuid] = true; |
| 1355 | 0 | reporter.emit('report:action', { |
| 1356 | value: data.value, | |
| 1357 | type: type, | |
| 1358 | uuid: data.uuid | |
| 1359 | }); | |
| 1360 | } | |
| 1361 | }.bind(this); | |
| 1362 | 1 | return cb; |
| 1363 | }; | |
| 1364 | ||
| 1365 | /** | |
| 1366 | * Adds a method to the queue of actions/assertions to execute | |
| 1367 | * | |
| 1368 | * @method _addToActionQueue | |
| 1369 | * @param {object} opts Options of the action to invoke | |
| 1370 | * @param {string} driverMethod Name of the method to call on the driver | |
| 1371 | * @param {function} A callback function that will be executed when the action has been executed | |
| 1372 | * @private | |
| 1373 | * @chainable | |
| 1374 | */ | |
| 1375 | ||
| 1376 | 1 | Actions.prototype._addToActionQueue = function (opts, driverMethod, cb) { |
| 1377 | 4 | if (driverMethod !== 'screenshot' && driverMethod !== 'imagecut') { |
| 1378 | 0 | this.screenshotParams = undefined; |
| 1379 | } | |
| 1380 | ||
| 1381 | 4 | this.actionPromiseQueue.push(function () { |
| 1382 | 0 | var deferred = Q.defer(); |
| 1383 | // add a generic identifier as the last argument to any action method call | |
| 1384 | 0 | opts.push(uuid()); |
| 1385 | // check the method on the driver object && the callback function | |
| 1386 | 0 | if (typeof(this.driver[driverMethod]) === 'function' && typeof(cb) === 'function') { |
| 1387 | // call the method on the driver object | |
| 1388 | 0 | this.driver[driverMethod].apply(this.driver, opts); |
| 1389 | 0 | deferred.resolve(); |
| 1390 | } else { | |
| 1391 | 0 | deferred.reject(); |
| 1392 | } | |
| 1393 | ||
| 1394 | // listen to driver message events & apply the callback argument | |
| 1395 | 0 | this.driver.events.on('driver:message', cb); |
| 1396 | 0 | return deferred.promise; |
| 1397 | }.bind(this)); | |
| 1398 | 4 | return this; |
| 1399 | }; | |
| 1400 | ||
| 1401 | 1 | Actions.prototype._button = function(button) { |
| 1402 | 0 | var buttons = {LEFT: 0, MIDDLE: 1, RIGHT: 2}; |
| 1403 | ||
| 1404 | 0 | if (button === undefined) { |
| 1405 | 0 | button = 0; |
| 1406 | 0 | } else if (typeof button !== 'number') { |
| 1407 | 0 | button = buttons[button.toUpperCase()] || 0; |
| 1408 | } | |
| 1409 | ||
| 1410 | 0 | return button; |
| 1411 | }; | |
| 1412 | ||
| 1413 | // http://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/click | |
| 1414 | 1 | Actions.prototype.buttonClick = function (button) { |
| 1415 | 0 | var hash = uuid(); |
| 1416 | 0 | button = this._button(button); |
| 1417 | ||
| 1418 | 0 | var cb = this._generateCallbackAssertion('buttonClick', 'buttonClick'); |
| 1419 | 0 | this._addToActionQueue([button, hash], 'buttonClick', cb); |
| 1420 | ||
| 1421 | 0 | return this; |
| 1422 | }; | |
| 1423 | ||
| 1424 | // http://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/moveto | |
| 1425 | 1 | Actions.prototype.moveTo = function (selector, x, y) { |
| 1426 | 0 | var hash = uuid(); |
| 1427 | ||
| 1428 | 0 | if (this.querying === true) { |
| 1429 | 0 | selector = this.selector; |
| 1430 | } | |
| 1431 | ||
| 1432 | 0 | if (x === undefined) { |
| 1433 | 0 | x = null; |
| 1434 | } | |
| 1435 | ||
| 1436 | 0 | if (y === undefined) { |
| 1437 | 0 | y = null; |
| 1438 | } | |
| 1439 | ||
| 1440 | // move to coordinate | |
| 1441 | 0 | var cb = this._generateCallbackAssertion('moveto', 'moveto'); |
| 1442 | 0 | this._addToActionQueue([selector, x, y, hash], 'moveto', cb); |
| 1443 | ||
| 1444 | 0 | return this; |
| 1445 | }; | |
| 1446 | ||
| 1447 | /** | |
| 1448 | * Close the active window and automatically selects the parent window. | |
| 1449 | * | |
| 1450 | * ```javascript | |
| 1451 | * this.test.toWindow('test'); | |
| 1452 | * this.test.close(); | |
| 1453 | * | |
| 1454 | * //you can now write your code as if the original parent window was selected because .close() | |
| 1455 | * //selects that automatically for you so you don't have to call .toParentWindow() everytime | |
| 1456 | * ``` | |
| 1457 | * | |
| 1458 | * @api | |
| 1459 | * @method close | |
| 1460 | * @chainable | |
| 1461 | */ | |
| 1462 | 1 | Actions.prototype.close = function () { |
| 1463 | 0 | var hash = uuid(); |
| 1464 | 0 | var cb = this._generateCallbackAssertion('close', 'close', hash); |
| 1465 | 0 | this._addToActionQueue([hash], 'close', cb); |
| 1466 | ||
| 1467 | //since the current window is now closed, make sense to automatically select the parent window since you would have to do this anyway | |
| 1468 | 0 | this.toParentWindow(); |
| 1469 | ||
| 1470 | 0 | return this; |
| 1471 | }; | |
| 1472 | ||
| 1473 | /** | |
| 1474 | * @module DalekJS | |
| 1475 | */ | |
| 1476 | ||
| 1477 | 1 | module.exports = function (opts) { |
| 1478 | 1 | reporter = opts.reporter; |
| 1479 | 1 | return Actions; |
| 1480 | }; | |
| 1481 |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * | |
| 3 | * Copyright (c) 2013 Sebastian Golasch | |
| 4 | * | |
| 5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
| 6 | * copy of this software and associated documentation files (the "Software"), | |
| 7 | * to deal in the Software without restriction, including without limitation | |
| 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
| 9 | * and/or sell copies of the Software, and to permit persons to whom the | |
| 10 | * Software is furnished to do so, subject to the following conditions: | |
| 11 | * | |
| 12 | * The above copyright notice and this permission notice shall be included | |
| 13 | * in all copies or substantial portions of the Software. | |
| 14 | * | |
| 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
| 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
| 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
| 21 | * DEALINGS IN THE SOFTWARE. | |
| 22 | */ | |
| 23 | ||
| 24 | 1 | 'use strict'; |
| 25 | ||
| 26 | // ext. libs | |
| 27 | 1 | var Q = require('q'); |
| 28 | 1 | var uuid = require('./uuid'); |
| 29 | 1 | var chai = require('chai'); |
| 30 | ||
| 31 | // Module variable | |
| 32 | 1 | var Assertions; |
| 33 | ||
| 34 | /** | |
| 35 | * @module Assertions | |
| 36 | * @namespace Dalek.Internal | |
| 37 | */ | |
| 38 | ||
| 39 | 1 | module.exports = function () { |
| 40 | 1 | return Assertions; |
| 41 | }; | |
| 42 | ||
| 43 | /** | |
| 44 | * Assertions check if the assumptions you made about a website are correct. | |
| 45 | * For example they might check if the title of a page or the content text of | |
| 46 | * an element is as expected, or if your mobile website version only displays | |
| 47 | * a certain amount of elements. | |
| 48 | * | |
| 49 | * @class Assertions | |
| 50 | * @constructor | |
| 51 | * @part Assertions | |
| 52 | * @api | |
| 53 | */ | |
| 54 | ||
| 55 | 1 | Assertions = function (opts) { |
| 56 | 0 | this.test = opts.test; |
| 57 | 0 | this.proceeded = []; |
| 58 | 0 | this.chaining = false; |
| 59 | }; | |
| 60 | ||
| 61 | /** | |
| 62 | * It can be really cumbersome to always write assert, assert & assert | |
| 63 | * all over the place when you're doing multiple assertions. | |
| 64 | * To avoid this, open an assertion context in your test which allows | |
| 65 | * you to write (n) assertions without having to write 'assert' before each. | |
| 66 | * | |
| 67 | * So, instead of writing this: | |
| 68 | * | |
| 69 | * ```javascript | |
| 70 | * test.open('http://doctorwhotv.co.uk/') | |
| 71 | * .assert.text('#nav').is('Navigation') | |
| 72 | * .assert.visible('#nav') | |
| 73 | * .assert.attr('#nav', 'data-nav', 'true') | |
| 74 | * .done(); | |
| 75 | * ``` | |
| 76 | * | |
| 77 | * you can write this: | |
| 78 | * | |
| 79 | * ```javascript | |
| 80 | * test.open('http://doctorwhotv.co.uk/') | |
| 81 | * .assert.chain() | |
| 82 | * .text('#nav').is('Navigation') | |
| 83 | * .visible('#nav') | |
| 84 | * .attr('#nav', 'data-nav', 'true') | |
| 85 | * .end() | |
| 86 | * .done(); | |
| 87 | * ``` | |
| 88 | * | |
| 89 | * to make it even more concise, you can combine this with the [query](actions.html#meth-query) method: | |
| 90 | * | |
| 91 | * ```javascript | |
| 92 | * test.open('http://doctorwhotv.co.uk/') | |
| 93 | * .assert.chain() | |
| 94 | * .query('#nav') | |
| 95 | * .text().is('Navigation') | |
| 96 | * .visible() | |
| 97 | * .attr('data-nav', 'true') | |
| 98 | * .end() | |
| 99 | * .end() | |
| 100 | * .done(); | |
| 101 | * ``` | |
| 102 | * | |
| 103 | * Always make sure to terminate it with the [end](#meth-end) method! | |
| 104 | * | |
| 105 | * @api | |
| 106 | * @method chain | |
| 107 | * @chainable | |
| 108 | */ | |
| 109 | ||
| 110 | 1 | Assertions.prototype.chain = function () { |
| 111 | 0 | this.test.lastChain.push('chaining'); |
| 112 | 0 | this.chaining = true; |
| 113 | 0 | return this; |
| 114 | }; | |
| 115 | ||
| 116 | /** | |
| 117 | * Terminates an assertion chain or a query | |
| 118 | * | |
| 119 | * ```javascript | |
| 120 | * test.open('http://doctorwhotv.co.uk/') | |
| 121 | * .assert.chain() | |
| 122 | * .query('#nav') | |
| 123 | * .text().is('Navigation') | |
| 124 | * .visible() | |
| 125 | * .attr('data-nav', 'true') | |
| 126 | * .end() | |
| 127 | * .end() | |
| 128 | * .done(); | |
| 129 | * ``` | |
| 130 | * | |
| 131 | * @api | |
| 132 | * @method end | |
| 133 | * @chainable | |
| 134 | */ | |
| 135 | ||
| 136 | 1 | Assertions.prototype.end = function () { |
| 137 | 0 | var lastAction = this.test.lastChain.pop(); |
| 138 | 0 | if (lastAction === 'chaining') { |
| 139 | 0 | this.chaining = false; |
| 140 | } | |
| 141 | ||
| 142 | 0 | if (lastAction === 'querying') { |
| 143 | 0 | this.test.querying = false; |
| 144 | } | |
| 145 | 0 | return this.test; |
| 146 | }; | |
| 147 | ||
| 148 | /** | |
| 149 | * Asserts that a given resource does exist in the environment. | |
| 150 | * | |
| 151 | * @method resourceExists | |
| 152 | * @param {string} url URL of the resource to check | |
| 153 | * @param {string} message Message for the test reporter | |
| 154 | * @chainable | |
| 155 | */ | |
| 156 | ||
| 157 | 1 | Assertions.prototype.resourceExists = function (url, message) { |
| 158 | 0 | var hash = uuid(); |
| 159 | 0 | var cb = this._generateCallbackAssertion('resourceExists', 'resourceExists', this._testTruthy, hash, {url: url, message: message}).bind(this.test); |
| 160 | 0 | this._addToActionQueue([url, hash], 'resourceExists', cb); |
| 161 | 0 | return this.chaining ? this : this.test; |
| 162 | }; | |
| 163 | ||
| 164 | /** | |
| 165 | * Asserts that a given element appears n times on the page. | |
| 166 | * | |
| 167 | * Given this portion of HTML, you would like to assure that all of these elements | |
| 168 | * are ending up in your rendered markup on your page. | |
| 169 | * | |
| 170 | * ```html | |
| 171 | * <section id="blog-overview"> | |
| 172 | * <article class="teaser"></article> | |
| 173 | * <article class="teaser"></article> | |
| 174 | * <article class="teaser"></article> | |
| 175 | * <article class="teaser"></article> | |
| 176 | * </section> | |
| 177 | * ``` | |
| 178 | * | |
| 179 | * The simple solution is to check if all these elements are present | |
| 180 | * | |
| 181 | * ```javascript | |
| 182 | * test.assert.numberOfElements('#blog-overview .teaser', 4, '4 blog teasers are present') | |
| 183 | * ``` | |
| 184 | * The alternate syntax for this is: | |
| 185 | * | |
| 186 | * ```javascript | |
| 187 | * test.assert.numberOfElements('#blog-overview .teaser') | |
| 188 | * .is(4, '4 blog teasers are present') | |
| 189 | * ``` | |
| 190 | * | |
| 191 | * If you are not sure how many elements will exactly end up in your markup, | |
| 192 | * you could use the between assertion handler | |
| 193 | * | |
| 194 | * ```javascript | |
| 195 | * test.assert.numberOfElements('#blog-overview .teaser') | |
| 196 | * .is.between([2, 6], 'Between 2 and 6 blog teasers are present') | |
| 197 | * ``` | |
| 198 | * | |
| 199 | * If you dealing with the situation that you have a minimum of elements, | |
| 200 | * you expect, you can use this helper: | |
| 201 | * | |
| 202 | * ```javascript | |
| 203 | * test.assert.numberOfElements('#blog-overview .teaser') | |
| 204 | * .is.gt(2, 'At least 3 blog teasers are present') | |
| 205 | * ``` | |
| 206 | * If you want to know if its 'greater than equal', use this one | |
| 207 | * | |
| 208 | * ```javascript | |
| 209 | * test.assert.numberOfElements('#blog-overview .teaser') | |
| 210 | * .is.gte(2, 'At least 2 blog teasers are present') | |
| 211 | * ``` | |
| 212 | * as well as their 'lower than' and 'lower than equal' equivalents. | |
| 213 | * | |
| 214 | * ```javascript | |
| 215 | * test.assert.numberOfElements('#blog-overview .teaser') | |
| 216 | * .is.lt(5, 'Less than 5 blog teasers are present') | |
| 217 | * ``` | |
| 218 | * | |
| 219 | * ```javascript | |
| 220 | * test.assert.numberOfElements('#blog-overview .teaser') | |
| 221 | * .is.lte(5, 'Less than, or 5 blog teasers are present') | |
| 222 | * ``` | |
| 223 | * And if you just want to know, if a certain amount of teasers isnʼt present, | |
| 224 | * you can still use the not() assertion helper | |
| 225 | * | |
| 226 | * ```javascript | |
| 227 | * test.assert.numberOfElements('#blog-overview .teaser') | |
| 228 | * .is.not(5, 'There are more or less than 5 teasers present') | |
| 229 | * ``` | |
| 230 | * | |
| 231 | * @api | |
| 232 | * @method numberOfElements | |
| 233 | * @param {string} selector Selector that matches the elements to test | |
| 234 | * @param {string} expected Expected test result | |
| 235 | * @param {string} message Message for the test reporter | |
| 236 | * @chainable | |
| 237 | */ | |
| 238 | ||
| 239 | 1 | Assertions.prototype.numberOfElements = function (selector, expected, message) { |
| 240 | 0 | var hash = uuid(); |
| 241 | ||
| 242 | 0 | if (this.test.querying === true) { |
| 243 | 0 | message = expected; |
| 244 | 0 | expected = selector; |
| 245 | 0 | selector = this.test.selector; |
| 246 | } | |
| 247 | ||
| 248 | 0 | var cb = this._generateCallbackAssertion('numberOfElements', 'numberOfElements', this._testShallowEquals, hash, {expected: expected, selector: selector, message: message}).bind(this.test); |
| 249 | 0 | this._addToActionQueue([selector, expected, hash], 'getNumberOfElements', cb); |
| 250 | 0 | return this.chaining ? this : this.test; |
| 251 | }; | |
| 252 | ||
| 253 | /** | |
| 254 | * | |
| 255 | * Asserts that a given element is visible n times in the current viewport. | |
| 256 | * | |
| 257 | * | |
| 258 | * Given this portion of HTML, you would like to assure that all of these elements | |
| 259 | * are ending up in your rendered markup on your page. | |
| 260 | * | |
| 261 | * ```html | |
| 262 | * <section id="blog-overview"> | |
| 263 | * <article class="teaser"></article> | |
| 264 | * <article class="teaser"></article> | |
| 265 | * <article class="teaser"></article> | |
| 266 | * <article class="teaser"></article> | |
| 267 | * </section> | |
| 268 | * ``` | |
| 269 | * | |
| 270 | * The simple solution is to check if all these elements are visible | |
| 271 | * | |
| 272 | * ```javascript | |
| 273 | * test.assert.numberOfVisibleElements('#blog-overview .teaser', 4, '4 blog teasers are visible') | |
| 274 | * ``` | |
| 275 | * The alternate syntax for this is: | |
| 276 | * | |
| 277 | * ```javascript | |
| 278 | * test.assert.numberOfVisibleElements('#blog-overview .teaser') | |
| 279 | * .is(4, '4 blog teasers are visible') | |
| 280 | * ``` | |
| 281 | * | |
| 282 | * If you are not sure how many elements will exactly be shown in the current viewport, | |
| 283 | * you could use the between assertion handler | |
| 284 | * | |
| 285 | * ```javascript | |
| 286 | * test.assert.numberOfVisibleElements('#blog-overview .teaser') | |
| 287 | * .is.between(2, 6, 'Between 2 and 6 blog teasers are visible') | |
| 288 | * ``` | |
| 289 | * | |
| 290 | * If you dealing with the situation that you have a minimum of elements, | |
| 291 | * you expect, use this helper: | |
| 292 | * | |
| 293 | * ```javascript | |
| 294 | * test.assert.numberOfVisibleElements('#blog-overview .teaser') | |
| 295 | * .is.gt(2, 'At least 3 blog teasers are visible') | |
| 296 | * ``` | |
| 297 | * If you want to know if its 'greater than equal', you can use this one | |
| 298 | * | |
| 299 | * ```javascript | |
| 300 | * test.assert.numberOfVisibleElements('#blog-overview .teaser') | |
| 301 | * .is.gte(2, 'At least 2 blog teasers are visible') | |
| 302 | * ``` | |
| 303 | * as well as their 'lower than' and 'lower than equal' equivalents. | |
| 304 | * | |
| 305 | * ```javascript | |
| 306 | * test.assert.numberOfVisibleElements('#blog-overview .teaser') | |
| 307 | * .is.lt(5, 'Less than 5 blog teasers are visible') | |
| 308 | * ``` | |
| 309 | * | |
| 310 | * ```javascript | |
| 311 | * test.assert.numberOfVisibleElements('#blog-overview .teaser') | |
| 312 | * .is.lte(5, 'Less than, or 5 blog teasers are visible') | |
| 313 | * ``` | |
| 314 | * And if you just want to know, if a certain amount of teasers isnʼt visible, | |
| 315 | * you can still use the ':not(): assertion helper | |
| 316 | * | |
| 317 | * ```javascript | |
| 318 | * test.assert.numberOfVisibleElements('#blog-overview .teaser') | |
| 319 | * .is.not(5, 'There are more or less than 5 teasers visible') | |
| 320 | * ``` | |
| 321 | * | |
| 322 | * > NOTE: Buggy on all browsers | |
| 323 | * | |
| 324 | * @api | |
| 325 | * @method numberOfVisibleElements | |
| 326 | * @param {string} selector Selector that matches the elements to test | |
| 327 | * @param {string} expected Expected test result | |
| 328 | * @param {string} message Message for the test reporter | |
| 329 | * @chainable | |
| 330 | */ | |
| 331 | ||
| 332 | 1 | Assertions.prototype.numberOfVisibleElements = function (selector, expected, message) { |
| 333 | 0 | var hash = uuid(); |
| 334 | ||
| 335 | 0 | if (this.test.querying === true) { |
| 336 | 0 | message = expected; |
| 337 | 0 | expected = selector; |
| 338 | 0 | selector = this.test.selector; |
| 339 | } | |
| 340 | ||
| 341 | 0 | var cb = this._generateCallbackAssertion('numberOfVisibleElements', 'numberOfVisibleElements', this._testShallowEquals, hash, {expected: expected, selector: selector, message: message}).bind(this.test); |
| 342 | 0 | this._addToActionQueue([selector, expected, hash], 'getNumberOfVisibleElements', cb); |
| 343 | 0 | return this.chaining ? this : this.test; |
| 344 | }; | |
| 345 | ||
| 346 | /** | |
| 347 | * Asserts that a given form field has the provided value. | |
| 348 | * | |
| 349 | * Given this portion of HTML, we would like to get the information which option element | |
| 350 | * is currently selected. | |
| 351 | * | |
| 352 | * ```html | |
| 353 | * <form name="fav-doctor" id="fav-doctor"> | |
| 354 | * <select id="the-doctors"> | |
| 355 | * <option value="9">Eccleston</option> | |
| 356 | * <option selected value="10">Tennant</option> | |
| 357 | * <option value="11">Smith</option> | |
| 358 | * </select> | |
| 359 | * </form> | |
| 360 | * ``` | |
| 361 | * | |
| 362 | * ```javascript | |
| 363 | * test | |
| 364 | * .assert.val('#the-doctors', 10, 'David is the favourite') | |
| 365 | * // lets change the favourite by selection the last option | |
| 366 | * .click('#the-doctors option:last') | |
| 367 | * .assert.val('#the-doctors', 11, 'Matt is now my favourite, bow ties are cool') | |
| 368 | * ``` | |
| 369 | * | |
| 370 | * This assertion is capable of getting the values from every form element | |
| 371 | * that holds a value attribute | |
| 372 | * | |
| 373 | * Getting texts out of normal input fields is pretty straight forward | |
| 374 | * | |
| 375 | * ```html | |
| 376 | * <label for="fav-enemy">Tell my your favourity Who enemy:</label> | |
| 377 | * <input id="fav-enemy" name="fav-enemy" type="text" value="Daleks" /> | |
| 378 | * ``` | |
| 379 | * | |
| 380 | * ```javascript | |
| 381 | * test | |
| 382 | * .assert.val('#fav-enemy', 'Daleks', 'Daleks are so cute') | |
| 383 | * // lets change the favourite by typing smth. new | |
| 384 | * .type('#fav-enemy', 'Cyberman') | |
| 385 | * .assert.val('#fav-enemy', 'Cyberman', 'Cyberman are so cyber') | |
| 386 | * ``` | |
| 387 | * | |
| 388 | * @api | |
| 389 | * @method val | |
| 390 | * @param {string} selector Selector that matches the elements to test | |
| 391 | * @param {string} expected Expected test result | |
| 392 | * @param {string} message Message for the test reporter | |
| 393 | * @chainable | |
| 394 | */ | |
| 395 | ||
| 396 | 1 | Assertions.prototype.val = function (selector, expected, message) { |
| 397 | 0 | var hash = uuid(); |
| 398 | ||
| 399 | 0 | if (this.test.querying === true) { |
| 400 | 0 | message = expected; |
| 401 | 0 | expected = selector; |
| 402 | 0 | selector = this.test.selector; |
| 403 | } | |
| 404 | ||
| 405 | 0 | var cb = this._generateCallbackAssertion('val', 'val', this._testShallowEquals, hash, {expected: expected, selector: selector, message: message}).bind(this.test); |
| 406 | 0 | this._addToActionQueue([selector, expected, hash], 'val', cb); |
| 407 | 0 | return this.chaining ? this : this.test; |
| 408 | }; | |
| 409 | ||
| 410 | /** | |
| 411 | * Checks the computed style. | |
| 412 | * Note: When selecting values like em or percent, they will be converted to px. So it's currently not possible | |
| 413 | * to check em, %, ... values, only px. | |
| 414 | * | |
| 415 | * ```html | |
| 416 | * <div id="superColoredElement">Rose</div> | |
| 417 | * ``` | |
| 418 | * | |
| 419 | * ```css | |
| 420 | * #superColoredElement { | |
| 421 | * background-color: rgba(255, 0, 0, 1); | |
| 422 | * color: rgba(0, 128, 0, 1); | |
| 423 | * } | |
| 424 | * ``` | |
| 425 | * | |
| 426 | * ```javascript | |
| 427 | * test | |
| 428 | * .open('http://unicorns.rainbows.io') | |
| 429 | * .assert.css('#superColoredElement', 'background-color', 'rgba(255, 0, 0, 1)') | |
| 430 | * .assert.css('#superColoredElement', 'color', 'rgba(0, 128, 0, 1)') | |
| 431 | * .done(); | |
| 432 | * ``` | |
| 433 | * | |
| 434 | * Can also check if a computed style is greater or lower than the expected value. | |
| 435 | * TODO: We might extract the part that determines the comparison operator to reuse it in other test. We might also | |
| 436 | * add >= and <=. | |
| 437 | * | |
| 438 | * ```html | |
| 439 | * <div id="fancyPlacedElement">Tulip</div> | |
| 440 | * ``` | |
| 441 | * | |
| 442 | * ```css | |
| 443 | * #fancyPlacedElement { | |
| 444 | * top: 100px; | |
| 445 | * } | |
| 446 | * ``` | |
| 447 | * | |
| 448 | * ```javascript | |
| 449 | * test | |
| 450 | * .open('http://unicorns.rainbows.io') | |
| 451 | * .assert.css('#fancyPlacedElement', 'top', '>50px') // 100 is greater than 50, success | |
| 452 | * .assert.css('#fancyPlacedElement', 'top', '<150px') // 100 is lower than 150, success | |
| 453 | * .assert.css('#fancyPlacedElement', 'top', '>150px') // 100 is lower than 150, fail | |
| 454 | * .assert.css('#fancyPlacedElement', 'top', '<50px') // 100 is greater than 50, fail | |
| 455 | * .done(); | |
| 456 | * ``` | |
| 457 | * | |
| 458 | * @api | |
| 459 | * @method css | |
| 460 | * @param {string} selector Selector that matches the elements to test | |
| 461 | * @param {string} property CSS property to check | |
| 462 | * @param {string} expected Expected test result | |
| 463 | * @param {string} message Message for the test reporter | |
| 464 | * @chainable | |
| 465 | */ | |
| 466 | ||
| 467 | 1 | Assertions.prototype.css = function (selector, property, expected, message) { |
| 468 | 0 | var hash = uuid(); |
| 469 | 0 | var assertionMethod; |
| 470 | 0 | var comparisonOperator; |
| 471 | ||
| 472 | 0 | if (this.test.querying === true) { |
| 473 | 0 | message = expected; |
| 474 | 0 | expected = property; |
| 475 | 0 | property = selector; |
| 476 | 0 | selector = this.test.selector; |
| 477 | } | |
| 478 | ||
| 479 | 0 | switch (expected.substr(0, 1)) { |
| 480 | case '>': | |
| 481 | 0 | assertionMethod = this._testLowerThan; |
| 482 | 0 | expected = expected.substr(1); |
| 483 | 0 | comparisonOperator = '>'; |
| 484 | 0 | break; |
| 485 | case '<': | |
| 486 | 0 | assertionMethod = this._testGreaterThan; |
| 487 | 0 | expected = expected.substr(1); |
| 488 | 0 | comparisonOperator = '<'; |
| 489 | 0 | break; |
| 490 | default: | |
| 491 | 0 | assertionMethod = this._testShallowEquals; |
| 492 | 0 | comparisonOperator = ''; |
| 493 | 0 | break; |
| 494 | } | |
| 495 | ||
| 496 | 0 | var cb = this._generateCallbackAssertion('css', 'css', assertionMethod, hash, {comparisonOperator: comparisonOperator, expected: expected, selector: selector, porperty: property, message: message, parseFloatOnValues: true}).bind(this.test); |
| 497 | 0 | this._addToActionQueue([selector, property, expected, hash], 'css', cb); |
| 498 | 0 | return this.chaining ? this : this.test; |
| 499 | }; | |
| 500 | ||
| 501 | /** | |
| 502 | * Checks the actual width of an element. | |
| 503 | * | |
| 504 | * ```html | |
| 505 | * <div id="fixed-dimensions" style="width: 100px"></div> | |
| 506 | * ``` | |
| 507 | * | |
| 508 | * ```javascript | |
| 509 | * test | |
| 510 | * .open('http://localhost:5000/index.html') | |
| 511 | * // all true, all pixel | |
| 512 | * .assert.width('#fixed-dimensions', 100) | |
| 513 | * .assert.width('#fixed-dimensions').is(100) | |
| 514 | * .assert.width('#fixed-dimensions').is.not(100) | |
| 515 | * .assert.width('#fixed-dimensions').is.gt(90) | |
| 516 | * .assert.width('#fixed-dimensions').is.gte(97) | |
| 517 | * .assert.width('#fixed-dimensions').is.lt(120) | |
| 518 | * .assert.width('#fixed-dimensions').is.lte(110) | |
| 519 | * .assert.width('#fixed-dimensions').is.between([90, 110]) | |
| 520 | * .done(); | |
| 521 | * ``` | |
| 522 | * | |
| 523 | * @api | |
| 524 | * @method width | |
| 525 | * @param {string} selector Selector that matches the elements to test | |
| 526 | * @param {string} expected Expected test result | |
| 527 | * @param {string} message Message for the test reporter | |
| 528 | * @chainable | |
| 529 | */ | |
| 530 | ||
| 531 | 1 | Assertions.prototype.width = function (selector, expected, message) { |
| 532 | 0 | var hash = uuid(); |
| 533 | ||
| 534 | 0 | if (this.test.querying === true) { |
| 535 | 0 | message = expected; |
| 536 | 0 | expected = selector; |
| 537 | 0 | selector = this.test.selector; |
| 538 | } | |
| 539 | ||
| 540 | 0 | var cb = this._generateCallbackAssertion('width', 'width', this._testShallowEquals, hash, {expected: expected, selector: selector, message: message}).bind(this.test); |
| 541 | 0 | this._addToActionQueue([selector, expected, hash], 'width', cb); |
| 542 | 0 | return this.chaining ? this : this.test; |
| 543 | }; | |
| 544 | ||
| 545 | /** | |
| 546 | * Checks the actual height of an element. | |
| 547 | * | |
| 548 | * ```html | |
| 549 | * <div id="fixed-dimensions" style="height: 100px"></div> | |
| 550 | * ``` | |
| 551 | * | |
| 552 | * ```javascript | |
| 553 | * test | |
| 554 | * .open('http://localhost:5000/index.html') | |
| 555 | * // all true, all pixel | |
| 556 | * .assert.height('#fixed-dimensions', 100) | |
| 557 | * .assert.height('#fixed-dimensions').is(100) | |
| 558 | * .assert.height('#fixed-dimensions').is.not(100) | |
| 559 | * .assert.height('#fixed-dimensions').is.gt(90) | |
| 560 | * .assert.height('#fixed-dimensions').is.gte(97) | |
| 561 | * .assert.height('#fixed-dimensions').is.lt(120) | |
| 562 | * .assert.height('#fixed-dimensions').is.lte(110) | |
| 563 | * .assert.height('#fixed-dimensions').is.between([90, 110]) | |
| 564 | * .done(); | |
| 565 | * ``` | |
| 566 | * | |
| 567 | * @api | |
| 568 | * @method height | |
| 569 | * @param {string} selector Selector that matches the elements to test | |
| 570 | * @param {string} expected Expected test result | |
| 571 | * @param {string} message Message for the test reporter | |
| 572 | * @chainable | |
| 573 | */ | |
| 574 | ||
| 575 | 1 | Assertions.prototype.height = function (selector, expected, message) { |
| 576 | 0 | var hash = uuid(); |
| 577 | ||
| 578 | 0 | if (this.test.querying === true) { |
| 579 | 0 | message = expected; |
| 580 | 0 | expected = selector; |
| 581 | 0 | selector = this.test.selector; |
| 582 | } | |
| 583 | ||
| 584 | 0 | var cb = this._generateCallbackAssertion('height', 'height', this._testShallowEquals, hash, {expected: expected, selector: selector, message: message}).bind(this.test); |
| 585 | 0 | this._addToActionQueue([selector, expected, hash], 'height', cb); |
| 586 | 0 | return this.chaining ? this : this.test; |
| 587 | }; | |
| 588 | ||
| 589 | /** | |
| 590 | * Determine if an <option> element, or an <input> element of type checkbox or radio is currently selected. | |
| 591 | * | |
| 592 | * ```html | |
| 593 | * <input type="checkbox" id="unchecked_checkbox" name="unchecked_checkbox"/> | |
| 594 | * <input type="checkbox" id="checked_checkbox" name="checked_checkbox" checked="checked"/> | |
| 595 | * <select id="select_elm" name="select_elm"> | |
| 596 | * <option value="9">Eccleston</option> | |
| 597 | * <option selected value="10">Tennant</option> | |
| 598 | * <option value="11">Smith</option> | |
| 599 | * </select> | |
| 600 | * ``` | |
| 601 | * | |
| 602 | * Checking radio and checkboxes: | |
| 603 | * | |
| 604 | * ```javascript | |
| 605 | * test | |
| 606 | * .open('http://selectables.org') | |
| 607 | * .assert.selected('#checked_checkbox') | |
| 608 | * .done(); | |
| 609 | * ``` | |
| 610 | * | |
| 611 | * Checking option elements: | |
| 612 | * | |
| 613 | * ```javascript | |
| 614 | * test | |
| 615 | * .open('http://selectables.org') | |
| 616 | * .assert.selected('#select_elm option:nth-child(2)') | |
| 617 | * .done(); | |
| 618 | * ``` | |
| 619 | * | |
| 620 | * @api | |
| 621 | * @method selected | |
| 622 | * @param {string} selector Selector that matches the elements to test | |
| 623 | * @param {string} message Message for the test reporter | |
| 624 | * @chainable | |
| 625 | */ | |
| 626 | ||
| 627 | 1 | Assertions.prototype.selected = function (selector, message) { |
| 628 | 0 | var hash = uuid(); |
| 629 | ||
| 630 | 0 | if (this.test.querying === true) { |
| 631 | 0 | message = selector; |
| 632 | 0 | selector = this.test.selector; |
| 633 | } | |
| 634 | ||
| 635 | 0 | var cb = this._generateCallbackAssertion('selected', 'selected', this._testShallowEquals, hash, {expected: true, selector: selector, message: message}).bind(this.test); |
| 636 | 0 | this._addToActionQueue([selector, true, hash], 'selected', cb); |
| 637 | 0 | return this.chaining ? this : this.test; |
| 638 | }; | |
| 639 | ||
| 640 | /** | |
| 641 | * Determine if an <option> element, or an <input> element of type | |
| 642 | * checkbox or radio is currently not selected. | |
| 643 | * | |
| 644 | * ```html | |
| 645 | * <input type="checkbox" id="unchecked_checkbox" name="unchecked_checkbox"/> | |
| 646 | * <input type="checkbox" id="checked_checkbox" name="checked_checkbox" checked="checked"/> | |
| 647 | * <select id="select_elm" name="select_elm"> | |
| 648 | * <option value="9">Eccleston</option> | |
| 649 | * <option selected value="10">Tennant</option> | |
| 650 | * <option value="11">Smith</option> | |
| 651 | * </select> | |
| 652 | * ``` | |
| 653 | * | |
| 654 | * Checking radio and checkboxes: | |
| 655 | * | |
| 656 | * ```javascript | |
| 657 | * test | |
| 658 | * .open('http://selectables.org') | |
| 659 | * .assert.notSelected('#unchecked_checkbox') | |
| 660 | * .done(); | |
| 661 | * ``` | |
| 662 | * | |
| 663 | * Checking option elements: | |
| 664 | * | |
| 665 | * ```javascript | |
| 666 | * test | |
| 667 | * .open('http://selectables.org') | |
| 668 | * .assert.notSelected('#select_elm option:last-child') | |
| 669 | * .done(); | |
| 670 | * ``` | |
| 671 | * | |
| 672 | * @api | |
| 673 | * @method notSelected | |
| 674 | * @param {string} selector Selector that matches the elements to test | |
| 675 | * @param {string} message Message for the test reporter | |
| 676 | * @chainable | |
| 677 | */ | |
| 678 | ||
| 679 | 1 | Assertions.prototype.notSelected = function (selector, message) { |
| 680 | 0 | var hash = uuid(); |
| 681 | ||
| 682 | 0 | if (this.test.querying === true) { |
| 683 | 0 | message = selector; |
| 684 | 0 | selector = this.test.selector; |
| 685 | } | |
| 686 | ||
| 687 | 0 | var cb = this._generateCallbackAssertion('selected', 'selected', this._testShallowEquals, hash, {expected: false, selector: selector, message: message}).bind(this.test); |
| 688 | 0 | this._addToActionQueue([selector, false, hash], 'selected', cb); |
| 689 | 0 | return this.chaining ? this : this.test; |
| 690 | }; | |
| 691 | ||
| 692 | /** | |
| 693 | * Determine if an element is currently enabled. | |
| 694 | * | |
| 695 | * ```html | |
| 696 | * <input id="onmars" type="text" size="50" name="onmars" placeholder="State your name, rank and intention!"></input> | |
| 697 | * ``` | |
| 698 | * | |
| 699 | * ```javascript | |
| 700 | * test | |
| 701 | * .open('http://doctor.thedoctor.com/doctor') | |
| 702 | * .assert.enabled('#onmars', 'Is enabled!') | |
| 703 | * .done(); | |
| 704 | * ``` | |
| 705 | * | |
| 706 | * @api | |
| 707 | * @method enabled | |
| 708 | * @param {string} selector Selector that matches the elements to test | |
| 709 | * @param {string} message Message for the test reporter | |
| 710 | * @chainable | |
| 711 | */ | |
| 712 | ||
| 713 | 1 | Assertions.prototype.enabled = function (selector, message) { |
| 714 | 0 | var hash = uuid(); |
| 715 | ||
| 716 | 0 | if (this.test.querying === true) { |
| 717 | 0 | message = selector; |
| 718 | 0 | selector = this.test.selector; |
| 719 | } | |
| 720 | ||
| 721 | 0 | var cb = this._generateCallbackAssertion('enabled', 'enabled', this._testShallowEquals, hash, {expected: true, selector: selector, message: message}).bind(this.test); |
| 722 | 0 | this._addToActionQueue([selector, true, hash], 'enabled', cb); |
| 723 | 0 | return this.chaining ? this : this.test; |
| 724 | }; | |
| 725 | ||
| 726 | /** | |
| 727 | * Determine if an element is currently disabled. | |
| 728 | * | |
| 729 | * ```html | |
| 730 | * <input disabled id="onearth" type="text" size="50" name="onearth" placeholder="State your name, rank and intention!"></input> | |
| 731 | * ``` | |
| 732 | * | |
| 733 | * ```javascript | |
| 734 | * test | |
| 735 | * .open('http://doctor.thedoctor.com/doctor') | |
| 736 | * .assert.disabled('#onearth', 'Is disabled!') | |
| 737 | * .done(); | |
| 738 | * ``` | |
| 739 | * | |
| 740 | * @api | |
| 741 | * @method disabled | |
| 742 | * @param {string} selector Selector that matches the elements to test | |
| 743 | * @param {string} message Message for the test reporter | |
| 744 | * @chainable | |
| 745 | */ | |
| 746 | ||
| 747 | 1 | Assertions.prototype.disabled = function (selector, message) { |
| 748 | 0 | var hash = uuid(); |
| 749 | ||
| 750 | 0 | if (this.test.querying === true) { |
| 751 | 0 | message = selector; |
| 752 | 0 | selector = this.test.selector; |
| 753 | } | |
| 754 | ||
| 755 | 0 | var cb = this._generateCallbackAssertion('enabled', 'enabled', this._testShallowEquals, hash, {expected: false, selector: selector, message: message}).bind(this.test); |
| 756 | 0 | this._addToActionQueue([selector, false, hash], 'enabled', cb); |
| 757 | 0 | return this.chaining ? this : this.test; |
| 758 | }; | |
| 759 | ||
| 760 | /** | |
| 761 | * Checks the contents of a cookie. | |
| 762 | * | |
| 763 | * ```javascript | |
| 764 | * test | |
| 765 | * .open('http://cookiejar.io/not_your_mothers_javascript.html') | |
| 766 | * .setCookie('atestcookie', 'foobar=baz') | |
| 767 | * .assert.cookie('atestcookie', 'foobar=baz') | |
| 768 | * .done(); | |
| 769 | * ``` | |
| 770 | * | |
| 771 | * @api | |
| 772 | * @method cookie | |
| 773 | * @param {string} name Name of the cookie | |
| 774 | * @param {string} expect Expected testresult | |
| 775 | * @param {string} message Message for the test reporter | |
| 776 | * @chainable | |
| 777 | */ | |
| 778 | ||
| 779 | 1 | Assertions.prototype.cookie = function (name, expected, message) { |
| 780 | 0 | var hash = uuid(); |
| 781 | 0 | var cb = this._generateCallbackAssertion('cookie', 'cookie', this._testShallowEquals, hash, {expected: expected, name: name, message: message}).bind(this.test); |
| 782 | 0 | this._addToActionQueue([name, expected, hash], 'cookie', cb); |
| 783 | 0 | return this.chaining ? this : this.test; |
| 784 | }; | |
| 785 | ||
| 786 | /** | |
| 787 | * Asserts that current HTTP status code is the same as the one passed as argument. | |
| 788 | * TODO: Needs some work to be implement (maybe JavaScript, Webdriver ha no method for this) | |
| 789 | * | |
| 790 | * @method httpStatus | |
| 791 | * @param {integer} status HTTP status code | |
| 792 | * @param {string} message Message for the test reporter | |
| 793 | * @chainable | |
| 794 | */ | |
| 795 | ||
| 796 | 1 | Assertions.prototype.httpStatus = function (status, message) { |
| 797 | 0 | var hash = uuid(); |
| 798 | 0 | var cb = this._generateCallbackAssertion('httpStatus', 'httpStatus', this._testShallowEquals, hash, {expected: status, message: message}).bind(this.test); |
| 799 | 0 | this._addToActionQueue([status, hash], 'httpStatus', cb); |
| 800 | 0 | return this.chaining ? this : this.test; |
| 801 | }; | |
| 802 | ||
| 803 | /** | |
| 804 | * Asserts that an element matching the provided selector expression exists in remote DOM environment. | |
| 805 | * | |
| 806 | * ```html | |
| 807 | * <body> | |
| 808 | * <p id="so-lonely">Last of the timelords</p> | |
| 809 | * </body> | |
| 810 | * ``` | |
| 811 | * | |
| 812 | * ```javascript | |
| 813 | * test | |
| 814 | * .open('http://doctor.thedoctor.com/doctor') | |
| 815 | * .assert.exists('#so-lonely', 'The loneliest element in the universe exists') | |
| 816 | * .done() | |
| 817 | * ``` | |
| 818 | * | |
| 819 | * @api | |
| 820 | * @method exists | |
| 821 | * @param {string} selector Selector that matches the elements to test | |
| 822 | * @param {string} message Message for the test reporter | |
| 823 | * @chainable | |
| 824 | */ | |
| 825 | ||
| 826 | 1 | Assertions.prototype.exists = function (selector, message) { |
| 827 | 0 | var hash = uuid(); |
| 828 | ||
| 829 | 0 | if (this.test.querying === true) { |
| 830 | 0 | message = selector; |
| 831 | 0 | selector = this.test.selector; |
| 832 | } | |
| 833 | ||
| 834 | 0 | var cb = this._generateCallbackAssertion('exists', 'exists', this._testTruthy, hash, {selector: selector, message: message}).bind(this.test); |
| 835 | 0 | this._addToActionQueue([selector, hash], 'exists', cb); |
| 836 | 0 | return this.chaining ? this : this.test; |
| 837 | }; | |
| 838 | ||
| 839 | /** | |
| 840 | * Asserts that an element matching the provided selector expression doesnʼt | |
| 841 | * exists within the remote DOM environment. | |
| 842 | * | |
| 843 | * ```html | |
| 844 | * <body> | |
| 845 | * <p id="so-lonely">Last of the time lords</p> | |
| 846 | * </body> | |
| 847 | * ``` | |
| 848 | * | |
| 849 | * ```javascript | |
| 850 | * test | |
| 851 | * .open('http://doctor.thedoctor.com/doctor') | |
| 852 | * .assert.doesntExist('#the-master', 'The master element has not been seen') | |
| 853 | * .done(); | |
| 854 | * ``` | |
| 855 | * | |
| 856 | * @api | |
| 857 | * @method doesntExist | |
| 858 | * @param {string} selector Selector that matches the elements to test | |
| 859 | * @param {string} message Message for the test reporter | |
| 860 | * @chainable | |
| 861 | */ | |
| 862 | ||
| 863 | 1 | Assertions.prototype.doesntExist = function (selector, message) { |
| 864 | 0 | var hash = uuid(); |
| 865 | ||
| 866 | 0 | if (this.test.querying === true) { |
| 867 | 0 | message = selector; |
| 868 | 0 | selector = this.test.selector; |
| 869 | } | |
| 870 | ||
| 871 | 0 | var cb = this._generateCallbackAssertion('exists', '!exists', this._testFalsy, hash, {selector: selector, message: message}).bind(this.test); |
| 872 | 0 | this._addToActionQueue([selector, hash], 'exists', cb); |
| 873 | 0 | return this.chaining ? this : this.test; |
| 874 | }; | |
| 875 | ||
| 876 | /** | |
| 877 | * Asserts that the element matching the provided selector expression is not visible. | |
| 878 | * | |
| 879 | * ```html | |
| 880 | * <body> | |
| 881 | * <h1 style="display: none">Me? So hidden …</h1> | |
| 882 | * <h2>Me? So in viewport...</h2> | |
| 883 | * </body> | |
| 884 | * ``` | |
| 885 | * | |
| 886 | * ```javascript | |
| 887 | * test | |
| 888 | * .open('http://allyourviewportsbelongto.us') | |
| 889 | * .assert.notVisible('h1', 'Element is not visible') | |
| 890 | * .done(); | |
| 891 | * ``` | |
| 892 | * | |
| 893 | * | |
| 894 | * > NOTE: Buggy on all browsers | |
| 895 | * | |
| 896 | * @api | |
| 897 | * @method notVisible | |
| 898 | * @param {string} selector Selector that matches the elements to test | |
| 899 | * @param {string} message Message for the test reporter | |
| 900 | * @chainable | |
| 901 | */ | |
| 902 | ||
| 903 | 1 | Assertions.prototype.notVisible = function (selector, message) { |
| 904 | 0 | var hash = uuid(); |
| 905 | ||
| 906 | 0 | if (this.test.querying === true) { |
| 907 | 0 | message = selector; |
| 908 | 0 | selector = this.test.selector; |
| 909 | } | |
| 910 | ||
| 911 | 0 | var cb = this._generateCallbackAssertion('visible', '!visible', this._testFalsy, hash, {selector: selector, message: message}).bind(this.test); |
| 912 | 0 | this._addToActionQueue([selector, hash], 'visible', cb); |
| 913 | 0 | return this.chaining ? this : this.test; |
| 914 | }; | |
| 915 | ||
| 916 | /** | |
| 917 | * Asserts that the element matching the provided selector expression is visible. | |
| 918 | * | |
| 919 | * ```html | |
| 920 | * <body> | |
| 921 | * <h1>Me? So in viewport …</h1> | |
| 922 | * </body> | |
| 923 | * ``` | |
| 924 | * | |
| 925 | * ```javascript | |
| 926 | * test | |
| 927 | * .open('http://allyourviewportsbelongto.us') | |
| 928 | * .assert.visible('h1', 'Element is visible') | |
| 929 | * .done(); | |
| 930 | * ``` | |
| 931 | * | |
| 932 | * > NOTE: Buggy on all browsers | |
| 933 | * | |
| 934 | * @api | |
| 935 | * @method visible | |
| 936 | * @param {string} selector Selector that matches the elements to test | |
| 937 | * @param {string} message Message for the test reporter | |
| 938 | * @chainable | |
| 939 | */ | |
| 940 | ||
| 941 | 1 | Assertions.prototype.visible = function (selector, message) { |
| 942 | 0 | var hash = uuid(); |
| 943 | ||
| 944 | 0 | if (this.test.querying === true) { |
| 945 | 0 | message = selector; |
| 946 | 0 | selector = this.test.selector; |
| 947 | } | |
| 948 | ||
| 949 | 0 | var cb = this._generateCallbackAssertion('visible', 'visible', this._testTruthy, hash, {selector: selector, message: message}).bind(this.test); |
| 950 | 0 | this._addToActionQueue([selector, hash], 'visible', cb); |
| 951 | 0 | return this.chaining ? this : this.test; |
| 952 | }; | |
| 953 | ||
| 954 | /** | |
| 955 | * Asserts that given text does not exist in the provided selector. | |
| 956 | * | |
| 957 | * ```html | |
| 958 | * <body> | |
| 959 | * <h1>This is a CasperJS sandbox</h1> | |
| 960 | * </body> | |
| 961 | * ``` | |
| 962 | * | |
| 963 | * ```javascript | |
| 964 | * test | |
| 965 | * .open('http://dalekjs.com/guineapig/') | |
| 966 | * .assert.doesntHaveText('h1', 'This page is a Dalek sandbox', 'It´s a sandbox!') | |
| 967 | * .done(); | |
| 968 | * ``` | |
| 969 | * | |
| 970 | * > NOTE: You cant match for a substring with contain() here. | |
| 971 | * | |
| 972 | * @api | |
| 973 | * @method doesntHaveText | |
| 974 | * @param {string} selector Selector that matches the elements to test | |
| 975 | * @param {string} expected Expected test result | |
| 976 | * @param {string} message Message for the test reporter | |
| 977 | * @chainable | |
| 978 | */ | |
| 979 | ||
| 980 | 1 | Assertions.prototype.doesntHaveText = function (selector, expected, message) { |
| 981 | 0 | var hash = uuid(); |
| 982 | 0 | if (this.test.querying === true) { |
| 983 | 0 | message = expected; |
| 984 | 0 | expected = selector; |
| 985 | 0 | selector = this.test.selector; |
| 986 | } | |
| 987 | ||
| 988 | 0 | var cb = this._generateCallbackAssertion('text', '!text', this._testShallowUnequals, hash, {selector: selector, expected: expected, message: message}).bind(this.test); |
| 989 | 0 | this._addToActionQueue([selector, expected, hash], 'text', cb); |
| 990 | 0 | return this.chaining ? this : this.test; |
| 991 | }; | |
| 992 | ||
| 993 | /** | |
| 994 | * Asserts that given text does not exist in the current alert/prompt/confirm dialog. | |
| 995 | * | |
| 996 | * ```html | |
| 997 | * <a href="#" id="alert_confirm" onclick="confirm('Confirm me!')">I make confirm</a> | |
| 998 | * ``` | |
| 999 | * | |
| 1000 | * ```javascript | |
| 1001 | * test | |
| 1002 | * .open('http://skaaro.com/index.html') | |
| 1003 | * .click('#alert_confirm') | |
| 1004 | * .assert.dialogDoesntHaveText('I am an alert') | |
| 1005 | * .accept() | |
| 1006 | * .done(); | |
| 1007 | * ``` | |
| 1008 | * | |
| 1009 | * > NOTE: You cant match for a substring with contain() here. | |
| 1010 | * > NOTE: Does not work in Firefox & PhantomJS | |
| 1011 | * | |
| 1012 | * @api | |
| 1013 | * @method dialogDoesntHaveText | |
| 1014 | * @param {string} expected Expected test result | |
| 1015 | * @param {string} message Message for the test reporter | |
| 1016 | * @chainable | |
| 1017 | */ | |
| 1018 | ||
| 1019 | 1 | Assertions.prototype.dialogDoesntHaveText = function (expected, message) { |
| 1020 | 0 | var hash = uuid(); |
| 1021 | 0 | var cb = this._generateCallbackAssertion('alertText', '!alertText', this._testShallowUnequals, hash, {expected: expected, message: message}).bind(this.test); |
| 1022 | 0 | this._addToActionQueue([expected, hash], 'alertText', cb); |
| 1023 | 0 | return this.chaining ? this : this.test; |
| 1024 | }; | |
| 1025 | ||
| 1026 | /** | |
| 1027 | * Asserts that given text does exist in the provided selector. | |
| 1028 | * | |
| 1029 | * ```html | |
| 1030 | * <body> | |
| 1031 | * <h1>This is a Dalek sandbox</h1> | |
| 1032 | * </body> | |
| 1033 | * ``` | |
| 1034 | * | |
| 1035 | * ```javascript | |
| 1036 | * test | |
| 1037 | * .open('http://dalekjs.com/guineapig/') | |
| 1038 | * .assert.text('h1', 'This page is a Dalek sandbox', 'Exterminate!') | |
| 1039 | * .done(); | |
| 1040 | * ``` | |
| 1041 | * | |
| 1042 | * of course, text works also with the assertion helpers is() and not() | |
| 1043 | * | |
| 1044 | * ```javascript | |
| 1045 | * test | |
| 1046 | * .open('http://dalekjs.com/guineapig/') | |
| 1047 | * .assert.text('h1').is('This page is a Dalek sandbox', 'Exterminate!') | |
| 1048 | * .done(); | |
| 1049 | * ``` | |
| 1050 | * | |
| 1051 | * ```javascript | |
| 1052 | * test | |
| 1053 | * .open('http://dalekjs.com/guineapig/') | |
| 1054 | * .assert.text('h1').is.not('This page is a CasperJS sandbox', 'Exterminate!') | |
| 1055 | * .done(); | |
| 1056 | * ``` | |
| 1057 | * | |
| 1058 | * and you can also check for the occurrence of a substring with to.contain() (but don't try to chain it with not() as this is not possible) | |
| 1059 | * | |
| 1060 | * ```javascript | |
| 1061 | * test | |
| 1062 | * .open('http://dalekjs.com/guineapig/') | |
| 1063 | * .assert.text('h1').to.contain('CasperJS') | |
| 1064 | * .done(); | |
| 1065 | * ``` | |
| 1066 | * | |
| 1067 | * @api | |
| 1068 | * @method text | |
| 1069 | * @param {string} selector Selector that matches the elements to test | |
| 1070 | * @param {string} expected Expected test result | |
| 1071 | * @param {string} message Message for the test reporter | |
| 1072 | * @chainable | |
| 1073 | */ | |
| 1074 | ||
| 1075 | 1 | Assertions.prototype.text = function (selector, expected, message) { |
| 1076 | 0 | var hash = uuid(); |
| 1077 | 0 | if (this.test.querying === true) { |
| 1078 | 0 | message = expected; |
| 1079 | 0 | expected = selector; |
| 1080 | 0 | selector = this.test.selector; |
| 1081 | } | |
| 1082 | ||
| 1083 | 0 | var cb = this._generateCallbackAssertion('text', 'text', this._testShallowEquals, hash, {selector: selector, expected: expected, message: message}).bind(this.test); |
| 1084 | 0 | this._addToActionQueue([selector, expected, hash], 'text', cb); |
| 1085 | 0 | return (this.chaining || this.test.querying) ? this : this.test; |
| 1086 | }; | |
| 1087 | ||
| 1088 | /** | |
| 1089 | * Asserts that given alertText does exist in the provided alert/confirm or prompt dialog. | |
| 1090 | * | |
| 1091 | * ```html | |
| 1092 | * <a href="#" id="alert" onclick="alert('I am an alert')">I make alerts!</a> | |
| 1093 | * ``` | |
| 1094 | * | |
| 1095 | * ```javascript | |
| 1096 | * test | |
| 1097 | * .open('http://skaaro.com/index.html') | |
| 1098 | * .click('#alert_confirm') | |
| 1099 | * .assert.dialogText('I am an alert') | |
| 1100 | * .accept() | |
| 1101 | * .done(); | |
| 1102 | * ``` | |
| 1103 | * | |
| 1104 | * of course, text works also with the assertion helpers is() and not() | |
| 1105 | * | |
| 1106 | * ```javascript | |
| 1107 | * test | |
| 1108 | * .open('http://dalekjs.com/guineapig/') | |
| 1109 | * .assert.dialogText().is('I am an alert', 'Exterminate!') | |
| 1110 | * .done(); | |
| 1111 | * ``` | |
| 1112 | * | |
| 1113 | * ```javascript | |
| 1114 | * test | |
| 1115 | * .open('http://dalekjs.com/guineapig/') | |
| 1116 | * .assert.dialogText().is.not('I am an prompt', 'Exterminate!') | |
| 1117 | * .done(); | |
| 1118 | * ``` | |
| 1119 | * | |
| 1120 | * | |
| 1121 | * > NOTE: Does not work in Firefox & PhantomJS | |
| 1122 | * | |
| 1123 | * @api | |
| 1124 | * @method dialogText | |
| 1125 | * @param {string} expected Expected testresult | |
| 1126 | * @param {string} message Message for the test reporter | |
| 1127 | * @chainable | |
| 1128 | */ | |
| 1129 | ||
| 1130 | 1 | Assertions.prototype.dialogText = function (expected, message) { |
| 1131 | 0 | var hash = uuid(); |
| 1132 | 0 | var cb = this._generateCallbackAssertion('alertText', 'alertText', this._testShallowEquals, hash, {expected: expected, message: message}).bind(this.test); |
| 1133 | 0 | this._addToActionQueue([expected, hash], 'alertText', cb); |
| 1134 | 0 | return (this.chaining || this.test.querying) ? this : this.test; |
| 1135 | }; | |
| 1136 | ||
| 1137 | /** | |
| 1138 | * Asserts that the page title is as expected. | |
| 1139 | * | |
| 1140 | * ```javascript | |
| 1141 | * test.open('http://doctorwhotv.co.uk/') | |
| 1142 | * .assert.title('Doctor Who TV', 'Not your Daleks TV') | |
| 1143 | * .done(); | |
| 1144 | * ``` | |
| 1145 | * | |
| 1146 | * Yep, using assertion helpers is also possible: | |
| 1147 | * | |
| 1148 | * ```javascript | |
| 1149 | * test.open('http://doctorwhotv.co.uk/') | |
| 1150 | * .assert.title().is('Doctor Who TV', 'Not your Daleks TV') | |
| 1151 | * .done(); | |
| 1152 | * ``` | |
| 1153 | * | |
| 1154 | * the not() helper is available too: | |
| 1155 | * | |
| 1156 | * ```javascript | |
| 1157 | * test.open('http://doctorwhotv.co.uk/') | |
| 1158 | * .assert.title().is.not('Dalek Emperor TV', 'Not your Daleks TV') | |
| 1159 | * .done(); | |
| 1160 | * ``` | |
| 1161 | * | |
| 1162 | * and you can also match for a substring with to.contain() (but don't try to chain it with not() as this is not possible): | |
| 1163 | * | |
| 1164 | * ```javascript | |
| 1165 | * test.open('http://doctorwhotv.co.uk/') | |
| 1166 | * .assert.title().to.contain('Emperor') | |
| 1167 | * .done(); | |
| 1168 | * ``` | |
| 1169 | * | |
| 1170 | * @api | |
| 1171 | * @method title | |
| 1172 | * @param {string} expected Expected test result | |
| 1173 | * @param {string} message Message for the test reporter | |
| 1174 | * @chainable | |
| 1175 | */ | |
| 1176 | ||
| 1177 | 1 | Assertions.prototype.title = function (expected, message) { |
| 1178 | 0 | var hash = uuid(); |
| 1179 | 0 | var cb = this._generateCallbackAssertion('title', 'title', this._testShallowEquals, hash, {expected: expected, message: message}).bind(this.test); |
| 1180 | 0 | this._addToActionQueue([expected, hash], 'title', cb); |
| 1181 | 0 | return this.chaining ? this : this.test; |
| 1182 | }; | |
| 1183 | ||
| 1184 | /** | |
| 1185 | * Asserts that given title does not match the given expectations. | |
| 1186 | * | |
| 1187 | * ```javascript | |
| 1188 | * test.open('http://doctorwhotv.co.uk/') | |
| 1189 | * .assert.doesntHaveTitle('Dalek Emperor TV', 'Not your Daleks TV') | |
| 1190 | * .done(); | |
| 1191 | * ``` | |
| 1192 | * | |
| 1193 | * @api | |
| 1194 | * @method doesntHaveTitle | |
| 1195 | * @param {string} expected Expected test result | |
| 1196 | * @param {string} message Message for the test reporter | |
| 1197 | * @chainable | |
| 1198 | */ | |
| 1199 | ||
| 1200 | 1 | Assertions.prototype.doesntHaveTitle = function (expected, message) { |
| 1201 | 0 | var hash = uuid(); |
| 1202 | 0 | var cb = this._generateCallbackAssertion('title', '!title', this._testShallowUnequals, hash, {expected: expected, message: message}).bind(this.test); |
| 1203 | 0 | this._addToActionQueue([expected, hash], 'title', cb); |
| 1204 | 0 | return this.chaining ? this : this.test; |
| 1205 | }; | |
| 1206 | ||
| 1207 | /** | |
| 1208 | * Asserts that screenshot is equal to already stored image. | |
| 1209 | * Should follow only after screenshot action | |
| 1210 | * | |
| 1211 | * ```javascript | |
| 1212 | * test.open('http://google.com') | |
| 1213 | * .wait(500) | |
| 1214 | * .screenshot('test/screenshots/google.png','#lga') | |
| 1215 | * .assert.screenshotIsEqualTo('test/screenshots/google_etalon.png', true, 'Google Doodles') | |
| 1216 | * .done(); | |
| 1217 | * ``` | |
| 1218 | * | |
| 1219 | * @api | |
| 1220 | * @method doesntHaveTitle | |
| 1221 | * @param {string} expected Path to expected png image | |
| 1222 | * @param {boolean} do make diff image | |
| 1223 | * @param {string} message Message for the test reporter | |
| 1224 | * @chainable | |
| 1225 | */ | |
| 1226 | 1 | Assertions.prototype.screenshotIsEqualTo = function (expected, makediff, message) { |
| 1227 | 0 | if (this.test.screenshotParams) { |
| 1228 | 0 | var hash = uuid(); |
| 1229 | ||
| 1230 | 0 | if (typeof makediff === 'string') { |
| 1231 | 0 | message = makediff; |
| 1232 | 0 | makediff = true; |
| 1233 | 0 | } else if (typeof makediff === 'undefined') { |
| 1234 | 0 | makediff = true; |
| 1235 | } | |
| 1236 | ||
| 1237 | 0 | var cb = this._generateCallbackAssertion('imagecompare', 'compare with etalon', this._testImagecompare, hash, { expected: expected, message: message, comparisonOperator: 'Screenshot is equal to ' }).bind(this.test); |
| 1238 | 0 | this._addToActionQueue([this.test.screenshotParams, expected, makediff, hash], 'imagecompare', cb); |
| 1239 | } else { | |
| 1240 | 0 | this.test.reporter.emit('error', 'Assert screenshotIsEqualTo can follow only after screenshot action!'); |
| 1241 | } | |
| 1242 | ||
| 1243 | 0 | return this.chaining ? this : this.test; |
| 1244 | }; | |
| 1245 | ||
| 1246 | /** | |
| 1247 | * Asserts that the page’s url is as expected. | |
| 1248 | * | |
| 1249 | * ```javascript | |
| 1250 | * test.open('http://doctorwhotv.co.uk/') | |
| 1251 | * .assert.url('http://doctorwhotv.co.uk/', 'URL is as expected') | |
| 1252 | * .done(); | |
| 1253 | * ``` | |
| 1254 | * | |
| 1255 | * You can also check if the protocol changed, | |
| 1256 | * nice to see when you open GitHub with 'http' instead of 'https' | |
| 1257 | * | |
| 1258 | * ```javascript | |
| 1259 | * test.open('http://github.com') | |
| 1260 | * .assert.url('https://github.com/', 'Changed prototcols') | |
| 1261 | * .done(); | |
| 1262 | * ``` | |
| 1263 | * | |
| 1264 | * Yep, using assertion helpers is also possible: | |
| 1265 | * | |
| 1266 | * ```javascript | |
| 1267 | * test.open('http://github.com') | |
| 1268 | * .assert.url().is('http://doctorwhotv.co.uk/', 'URL is as expected') | |
| 1269 | * .done(); | |
| 1270 | * ``` | |
| 1271 | * | |
| 1272 | * the not() helper is available too: | |
| 1273 | * | |
| 1274 | * ```javascript | |
| 1275 | * test.open('http://doctorwhotv.co.uk/') | |
| 1276 | * .assert.url().is.not('http://doctorwhotv.co.uk/', 'URL is as expected') | |
| 1277 | * .done(); | |
| 1278 | * ``` | |
| 1279 | * | |
| 1280 | * and you can also match for a substring with to.contain() (but don't try to chain it with not() as this is not possible): | |
| 1281 | * | |
| 1282 | * ```javascript | |
| 1283 | * test.open('http://doctorwhotv.co.uk/') | |
| 1284 | * .assert.url().to.contain('doctor') | |
| 1285 | * .done(); | |
| 1286 | * ``` | |
| 1287 | * | |
| 1288 | * @api | |
| 1289 | * @method url | |
| 1290 | * @param {string} expected Expected test result | |
| 1291 | * @param {string} message Message for the test reporter | |
| 1292 | * @chainable | |
| 1293 | */ | |
| 1294 | ||
| 1295 | 1 | Assertions.prototype.url = function (expected, message) { |
| 1296 | 0 | var hash = uuid(); |
| 1297 | 0 | var cb = this._generateCallbackAssertion('url', 'url', this._testShallowEquals, hash, {expected: expected, message: message}).bind(this.test); |
| 1298 | 0 | this._addToActionQueue([expected, hash], 'url', cb); |
| 1299 | 0 | return this.chaining ? this : this.test; |
| 1300 | }; | |
| 1301 | ||
| 1302 | /** | |
| 1303 | * Asserts that the pages URL does not match the expectation. | |
| 1304 | * | |
| 1305 | * ```javascript | |
| 1306 | * test.open('http://doctorwhotv.co.uk/') | |
| 1307 | * .assert.doesntHaveUrl('http://doctorwhotv.co.uk/', 'URL is not expected') | |
| 1308 | * .done(); | |
| 1309 | * ``` | |
| 1310 | * | |
| 1311 | * Oh, you might also match for a substring with to.contain(): | |
| 1312 | * | |
| 1313 | * * ```javascript | |
| 1314 | * test.open('http://doctorwhotv.co.uk/') | |
| 1315 | * .assert.doesntHaveUrl().to.contain('doctor') | |
| 1316 | * .done(); | |
| 1317 | * ``` | |
| 1318 | * | |
| 1319 | * @api | |
| 1320 | * @method doesntHaveUrl | |
| 1321 | * @param {string} expected Expected test result | |
| 1322 | * @param {string} message Message for the test reporter | |
| 1323 | * @chainable | |
| 1324 | */ | |
| 1325 | ||
| 1326 | 1 | Assertions.prototype.doesntHaveUrl = function (expected, message) { |
| 1327 | 0 | var hash = uuid(); |
| 1328 | 0 | var cb = this._generateCallbackAssertion('url', '!url', this._testShallowUnequals, hash, {expected: expected, message: message}).bind(this.test); |
| 1329 | 0 | this._addToActionQueue([expected, hash], 'url', cb); |
| 1330 | 0 | return this.chaining ? this : this.test; |
| 1331 | }; | |
| 1332 | ||
| 1333 | /** | |
| 1334 | * Asserts that an elements attribute is as expected. | |
| 1335 | * | |
| 1336 | * ```html | |
| 1337 | * <form> | |
| 1338 | * <button class="jumpButton" type="submit">Fire</button> | |
| 1339 | * </form> | |
| 1340 | * ``` | |
| 1341 | * | |
| 1342 | * ```javascript | |
| 1343 | * test | |
| 1344 | * .open('http://dalekjs.com/guineapig/') | |
| 1345 | * .assert.attr('.jumpButton', 'type', 'submit') | |
| 1346 | * .done(); | |
| 1347 | * ``` | |
| 1348 | * | |
| 1349 | * ```html | |
| 1350 | * <div class="wellImUpperUpperClassHighSociety" id="dataDiv" data-spot="cat"></div> | |
| 1351 | * ``` | |
| 1352 | * | |
| 1353 | * ```javascript | |
| 1354 | * test | |
| 1355 | * .open('http://dalekjs.com/guineapig/') | |
| 1356 | * .assert.attr('#dataDiv').is('data-spot', 'cat', 'We found Dataʼs cat!') | |
| 1357 | * .done(); | |
| 1358 | * ``` | |
| 1359 | * | |
| 1360 | * ```javascript | |
| 1361 | * test | |
| 1362 | * .open('http://dalekjs.com/guineapig/') | |
| 1363 | * .assert.attr('#dataDiv').is.not('data-spot', 'doc', 'Spot is not a dog!') | |
| 1364 | * .done(); | |
| 1365 | * ``` | |
| 1366 | * | |
| 1367 | * You can also use attr() for checking if a class is existent | |
| 1368 | * | |
| 1369 | * ```javascript | |
| 1370 | * test | |
| 1371 | * .open('http://dalekjs.com/guineapig/') | |
| 1372 | * .assert.attr('#dataDiv', 'class', 'wellImUpperUpperClassHighSociety') | |
| 1373 | * .done(); | |
| 1374 | * ``` | |
| 1375 | * | |
| 1376 | * and you can also match a substring (e. g. a single class if more classes are on that elem) with to.contain(): | |
| 1377 | * | |
| 1378 | * ```javascript | |
| 1379 | * test | |
| 1380 | * .open('http://dalekjs.com/guineapig/') | |
| 1381 | * .assert.attr('#dataDiv', 'class').to.contain('upperUpperClass') | |
| 1382 | * .done(); | |
| 1383 | * ``` | |
| 1384 | * | |
| 1385 | * @api | |
| 1386 | * @method attr | |
| 1387 | * @param {string} selector Selector that matches the elements to test | |
| 1388 | * @param {string} attribute The attribute to test | |
| 1389 | * @param {string} expected Expected test result | |
| 1390 | * @param {string} message Message for the test reporter | |
| 1391 | * @chainable | |
| 1392 | */ | |
| 1393 | ||
| 1394 | 1 | Assertions.prototype.attr = function (selector, attribute, expected, message) { |
| 1395 | 0 | var hash = uuid(); |
| 1396 | ||
| 1397 | 0 | if (this.test.querying === true) { |
| 1398 | 0 | message = expected; |
| 1399 | 0 | expected = attribute; |
| 1400 | 0 | attribute = selector; |
| 1401 | 0 | selector = this.test.selector; |
| 1402 | } | |
| 1403 | ||
| 1404 | 0 | var cb = this._generateCallbackAssertion('attribute', 'attribute', this._testShallowEquals, hash, {expected: expected, message: message, selector: selector, attribute: attribute}).bind(this.test); |
| 1405 | 0 | this._addToActionQueue([selector, attribute, expected, hash], 'attribute', cb); |
| 1406 | 0 | return this.chaining ? this : this.test; |
| 1407 | }; | |
| 1408 | ||
| 1409 | ||
| 1410 | /** | |
| 1411 | * Asserts that an element contains class. | |
| 1412 | * | |
| 1413 | * ```html | |
| 1414 | * <form> | |
| 1415 | * <button class="plain-btn action submit-product" type="submit">Fire</button> | |
| 1416 | * </form> | |
| 1417 | * ``` | |
| 1418 | * | |
| 1419 | * ```javascript | |
| 1420 | * test | |
| 1421 | * .open('http://dalekjs.com/index.html') | |
| 1422 | * .assert.hasClass('.grid__item.one-whole.info-box', 'info-box', 'div has info-box class') | |
| 1423 | * .done(); | |
| 1424 | * ``` | |
| 1425 | * | |
| 1426 | * @api | |
| 1427 | * @method attr | |
| 1428 | * @param {string} selector Selector that matches the elements to test | |
| 1429 | * @param {string} expected Expected test result | |
| 1430 | * @param {string} message Message for the test reporter | |
| 1431 | * @chainable | |
| 1432 | */ | |
| 1433 | ||
| 1434 | 1 | Assertions.prototype.hasClass = function (selector, expected, message) { |
| 1435 | 0 | var hash = uuid(); |
| 1436 | 0 | var attribute = 'class'; |
| 1437 | ||
| 1438 | 0 | if (this.test.querying === true) { |
| 1439 | 0 | message = expected; |
| 1440 | 0 | expected = selector; |
| 1441 | 0 | selector = this.test.selector; |
| 1442 | } | |
| 1443 | ||
| 1444 | 0 | var cb = this._generateCallbackAssertion('attribute', 'hasClass', this._containsItem, hash, {expected: expected, message: message, selector: selector, attribute: attribute}).bind(this.test); |
| 1445 | 0 | this._addToActionQueue([selector, attribute, expected, hash], 'attribute', cb); |
| 1446 | 0 | return this.chaining ? this : this.test; |
| 1447 | }; | |
| 1448 | ||
| 1449 | ||
| 1450 | /** | |
| 1451 | * Asserts that element doesn't contain class. | |
| 1452 | * | |
| 1453 | * ```html | |
| 1454 | * <form> | |
| 1455 | * <button class="plain-btn action submit-product" type="submit">Fire</button> | |
| 1456 | * </form> | |
| 1457 | * ``` | |
| 1458 | * | |
| 1459 | * ```javascript | |
| 1460 | * test | |
| 1461 | * .open('http://dalekjs.com/index.html') | |
| 1462 | * .assert.doesntHaveClass('.grid__item.one-whole.info-box', 'info', 'div doesn\'t have info class') | |
| 1463 | * .done(); | |
| 1464 | * ``` | |
| 1465 | * | |
| 1466 | * @api | |
| 1467 | * @method attr | |
| 1468 | * @param {string} selector Selector that matches the elements to test | |
| 1469 | * @param {string} expected Expected test result | |
| 1470 | * @param {string} message Message for the test reporter | |
| 1471 | * @chainable | |
| 1472 | */ | |
| 1473 | ||
| 1474 | 1 | Assertions.prototype.doesntHaveClass = function (selector, expected, message) { |
| 1475 | 0 | var hash = uuid(); |
| 1476 | 0 | var attribute = 'class'; |
| 1477 | ||
| 1478 | 0 | if (this.test.querying === true) { |
| 1479 | 0 | message = expected; |
| 1480 | 0 | expected = selector; |
| 1481 | 0 | selector = this.test.selector; |
| 1482 | } | |
| 1483 | ||
| 1484 | 0 | var cb = this._generateCallbackAssertion('attribute', '!hasClass', this._doesntContainItem, hash, {expected: expected, message: message, selector: selector, attribute: attribute}).bind(this.test); |
| 1485 | 0 | this._addToActionQueue([selector, attribute, expected, hash], 'attribute', cb); |
| 1486 | 0 | return this.chaining ? this : this.test; |
| 1487 | }; | |
| 1488 | ||
| 1489 | /** | |
| 1490 | Assert result of the execution of JavaScript function within the browser context. | |
| 1491 | ||
| 1492 | test | |
| 1493 | .open('http://dalekjs.com/index.html') | |
| 1494 | .assert.evaluate(function () { | |
| 1495 | return document.getElementsByClassName('grid').length; | |
| 1496 | }).is(2, 'Count of grid on page is equal 2') | |
| 1497 | .done(); | |
| 1498 | ||
| 1499 | * > Note: Buggy in Firefox | |
| 1500 | * | |
| 1501 | * @api | |
| 1502 | * @method execute | |
| 1503 | * @param {function} script JavaScript function that should be executed | |
| 1504 | * @return chainable | |
| 1505 | */ | |
| 1506 | ||
| 1507 | 1 | Assertions.prototype.evaluate = function (script) { |
| 1508 | 0 | var hash = uuid(); |
| 1509 | 0 | var args = [this.contextVars].concat(Array.prototype.slice.call(arguments, 1) || []); |
| 1510 | ||
| 1511 | 0 | var cb = this._generateCallbackAssertion('evaluate', 'evaluate', this._testTruthy, hash, {script: script, args: args, has:hash}).bind(this.test); |
| 1512 | 0 | this._addToActionQueue([script, args, hash], 'evaluate', cb); |
| 1513 | 0 | return this.chaining ? this : this.test; |
| 1514 | }; | |
| 1515 | ||
| 1516 | ||
| 1517 | // TEST HELPER | |
| 1518 | // ----------- | |
| 1519 | ||
| 1520 | /** | |
| 1521 | * Is helper | |
| 1522 | * | |
| 1523 | * @method is | |
| 1524 | * @param {mixed} expected Value to check | |
| 1525 | * @param {string} message Test message | |
| 1526 | * @chainable | |
| 1527 | */ | |
| 1528 | ||
| 1529 | 1 | Assertions.prototype.is = function (expected, message) { |
| 1530 | 0 | return this.generateTestHelper('is', '_testShallowEquals', false)(expected, message); |
| 1531 | }; | |
| 1532 | ||
| 1533 | /** | |
| 1534 | * Not helper | |
| 1535 | * | |
| 1536 | * @method not | |
| 1537 | * @param {mixed} expected Value to check | |
| 1538 | * @param {string} message Test message | |
| 1539 | * @chainable | |
| 1540 | */ | |
| 1541 | ||
| 1542 | 1 | Assertions.prototype.not = function (expected, message) { |
| 1543 | 0 | return this.generateTestHelper('not', '_testShallowEquals', true)(expected, message); |
| 1544 | }; | |
| 1545 | ||
| 1546 | /** | |
| 1547 | * Between helper | |
| 1548 | * | |
| 1549 | * @method between | |
| 1550 | * @param {mixed} expected Value to check | |
| 1551 | * @param {string} message Test message | |
| 1552 | * @chainable | |
| 1553 | */ | |
| 1554 | ||
| 1555 | 1 | Assertions.prototype.between = function (expected, message) { |
| 1556 | 0 | return this.generateTestHelper('between', '_testBetween', false)(expected, message); |
| 1557 | }; | |
| 1558 | ||
| 1559 | /** | |
| 1560 | * Gt helper | |
| 1561 | * | |
| 1562 | * @method gt | |
| 1563 | * @param {mixed} expected Value to check | |
| 1564 | * @param {string} message Test message | |
| 1565 | * @chainable | |
| 1566 | */ | |
| 1567 | ||
| 1568 | 1 | Assertions.prototype.gt = function (expected, message) { |
| 1569 | 0 | return this.generateTestHelper('gt', '_testGreaterThan', false)(expected, message); |
| 1570 | }; | |
| 1571 | ||
| 1572 | /** | |
| 1573 | * Gte helper | |
| 1574 | * | |
| 1575 | * @method gte | |
| 1576 | * @param {mixed} expected Value to check | |
| 1577 | * @param {string} message Test message | |
| 1578 | * @chainable | |
| 1579 | */ | |
| 1580 | ||
| 1581 | 1 | Assertions.prototype.gte = function (expected, message) { |
| 1582 | 0 | return this.generateTestHelper('gte', '_testGreaterThanEqual', false)(expected, message); |
| 1583 | }; | |
| 1584 | ||
| 1585 | /** | |
| 1586 | * Lt helper | |
| 1587 | * | |
| 1588 | * @method lt | |
| 1589 | * @param {mixed} expected Value to check | |
| 1590 | * @param {string} message Test message | |
| 1591 | * @chainable | |
| 1592 | */ | |
| 1593 | ||
| 1594 | 1 | Assertions.prototype.lt = function (expected, message) { |
| 1595 | 0 | return this.generateTestHelper('lt', '_testLowerThan', false)(expected, message); |
| 1596 | }; | |
| 1597 | ||
| 1598 | /** | |
| 1599 | * Lte helper | |
| 1600 | * | |
| 1601 | * @method lte | |
| 1602 | * @param {mixed} expected Value to check | |
| 1603 | * @param {string} message Test message | |
| 1604 | * @chainable | |
| 1605 | */ | |
| 1606 | ||
| 1607 | 1 | Assertions.prototype.lte = function (expected, message) { |
| 1608 | 0 | return this.generateTestHelper('lte', '_testLowerThanEqual', false)(expected, message); |
| 1609 | }; | |
| 1610 | ||
| 1611 | /** | |
| 1612 | * Assert if a given value contians a second given value | |
| 1613 | * | |
| 1614 | * @method _containsItem | |
| 1615 | * @param {mixed} a Value to test | |
| 1616 | * @param {mixed} b Value to test | |
| 1617 | * @return {bool} false if values doesn't contain, true if contains | |
| 1618 | * @private | |
| 1619 | */ | |
| 1620 | ||
| 1621 | 1 | Assertions.prototype._containsItem = function (a, b) { |
| 1622 | 0 | a = a || ''; |
| 1623 | 0 | var index = a.split(' ').indexOf(b); |
| 1624 | ||
| 1625 | 0 | try { |
| 1626 | 0 | chai.expect(index).to.be.above(-1); |
| 1627 | } catch (e) { | |
| 1628 | 0 | return false; |
| 1629 | } | |
| 1630 | ||
| 1631 | 0 | return true; |
| 1632 | }; | |
| 1633 | ||
| 1634 | /** | |
| 1635 | * Assert if a given value doesn't contian a second given value | |
| 1636 | * | |
| 1637 | * @method _containsItem | |
| 1638 | * @param {mixed} a Value to test | |
| 1639 | * @param {mixed} b Value to test | |
| 1640 | * @return {bool} false if values contains, true if don't contain | |
| 1641 | * @private | |
| 1642 | */ | |
| 1643 | 1 | Assertions.prototype._doesntContainItem = function (a, b) { |
| 1644 | 0 | a = a || ''; |
| 1645 | 0 | var index = a.split(' ').indexOf(b); |
| 1646 | 0 | try { |
| 1647 | 0 | chai.assert.equal(index, -1); |
| 1648 | } catch (e) { | |
| 1649 | 0 | return false; |
| 1650 | } | |
| 1651 | ||
| 1652 | 0 | return true; |
| 1653 | }; | |
| 1654 | ||
| 1655 | /** | |
| 1656 | * Contain helper | |
| 1657 | * | |
| 1658 | * @method contain | |
| 1659 | * @param {mixed} expected Value to check | |
| 1660 | * @param {string} message Test message | |
| 1661 | * @chainable | |
| 1662 | */ | |
| 1663 | ||
| 1664 | 1 | Assertions.prototype.contain = function (expected, message) { |
| 1665 | 0 | return this.generateTestHelper('contain', '_contain', false)(expected, message); |
| 1666 | }; | |
| 1667 | ||
| 1668 | /** | |
| 1669 | * Not contain helper | |
| 1670 | * | |
| 1671 | * @method notContain | |
| 1672 | * @param {mixed} expected Value to check | |
| 1673 | * @param {string} message Test message | |
| 1674 | * @chainable | |
| 1675 | */ | |
| 1676 | ||
| 1677 | 1 | Assertions.prototype.notContain = function (expected, message) { |
| 1678 | 0 | return this.generateTestHelper('notContain', '_notContain', false)(expected, message); |
| 1679 | }; | |
| 1680 | ||
| 1681 | /** | |
| 1682 | * Match helper | |
| 1683 | * | |
| 1684 | * @method match | |
| 1685 | * @param {string} expected Regex to match on | |
| 1686 | * @param {string} message Test message | |
| 1687 | * @chainable | |
| 1688 | */ | |
| 1689 | ||
| 1690 | 1 | Assertions.prototype.match = function (expected, message) { |
| 1691 | 0 | return this.generateTestHelper('match', '_match', false)(expected, message); |
| 1692 | }; | |
| 1693 | ||
| 1694 | /** | |
| 1695 | * Equals case insensitive helper | |
| 1696 | * | |
| 1697 | * @method equalsCaseInsensitive | |
| 1698 | * @param {mixed} expected Value to check | |
| 1699 | * @param {string} message Test message | |
| 1700 | * @chainable | |
| 1701 | */ | |
| 1702 | ||
| 1703 | 1 | Assertions.prototype.equalsCaseInsensitive = function (expected, message) { |
| 1704 | 0 | return this.generateTestHelper('equalsCaseInsensitive', '_caseInsensitiveMatch', false)(expected, message); |
| 1705 | }; | |
| 1706 | ||
| 1707 | // HELPER METHODS | |
| 1708 | // -------------- | |
| 1709 | ||
| 1710 | /** | |
| 1711 | * Generates a callback that will be fired when the action has been completed. | |
| 1712 | * The callback itself will then validate the answer and will also emit an event | |
| 1713 | * that the action has been successfully executed. | |
| 1714 | * | |
| 1715 | * @method _generateCallbackAssertion | |
| 1716 | * @param {string} key Unique key of the action | |
| 1717 | * @param {string} type Type of the action (usually the actions name) | |
| 1718 | * @param {string} test test method to be used | |
| 1719 | * @param {string} hash the uuid | |
| 1720 | * @param {object} opts the options object | |
| 1721 | * @return {function} The generated callback function | |
| 1722 | * @private | |
| 1723 | */ | |
| 1724 | ||
| 1725 | 1 | Assertions.prototype._generateCallbackAssertion = function (key, type, test, hash, opts) { |
| 1726 | 0 | var cb = function (data) { |
| 1727 | 0 | if (data && data.key === key && data.hash === hash) { |
| 1728 | ||
| 1729 | 0 | this._lastGeneratedAction = {key: key, type: type, test: test, hash: hash, opts: opts, data: data}; |
| 1730 | ||
| 1731 | 0 | var chainingKeys = ['title', 'width', 'height', 'url', 'text' , 'attribute', 'numberOfElements', 'numberOfVisibleElements', 'evaluate']; |
| 1732 | 0 | if (!opts.expected && chainingKeys.indexOf(key) > -1) { |
| 1733 | 0 | return false; |
| 1734 | } | |
| 1735 | ||
| 1736 | 0 | if (typeof opts.expected === 'function') { |
| 1737 | 0 | opts.expected = opts.expected(); |
| 1738 | } | |
| 1739 | ||
| 1740 | 0 | var testResult = test(data.value, opts.expected, opts.parseFloatOnValues); |
| 1741 | 0 | this.reporter.emit('report:assertion', { |
| 1742 | success: testResult, | |
| 1743 | expected: opts.comparisonOperator ? opts.comparisonOperator + opts.expected : opts.expected, | |
| 1744 | value: data.value, | |
| 1745 | message: opts.message, | |
| 1746 | type: type, | |
| 1747 | selector: data.selector | |
| 1748 | }); | |
| 1749 | ||
| 1750 | 0 | this.incrementExpectations(); |
| 1751 | 0 | if (!testResult) { |
| 1752 | 0 | this.incrementFailedAssertions(); |
| 1753 | } | |
| 1754 | } | |
| 1755 | }; | |
| 1756 | 0 | return cb; |
| 1757 | }; | |
| 1758 | ||
| 1759 | /** | |
| 1760 | * Adds a method to the queue of actions/assertions to execute | |
| 1761 | * | |
| 1762 | * @method _addToActionQueue | |
| 1763 | * @param {object} opts Options of the action to invoke | |
| 1764 | * @param {string} driverMethod Name of the method to call on the driver | |
| 1765 | * @param {function} A callback function that will be executed when the action has been executed | |
| 1766 | * @private | |
| 1767 | * @chainable | |
| 1768 | */ | |
| 1769 | ||
| 1770 | 1 | Assertions.prototype._addToActionQueue = function (opts, driverMethod, cb) { |
| 1771 | 0 | this._lastGeneratedShit = {opts: opts, driverMethod: driverMethod}; |
| 1772 | 0 | this.test.actionPromiseQueue.push(function () { |
| 1773 | 0 | var deferredAction = Q.defer(); |
| 1774 | 0 | this.test.driver[driverMethod].apply(this.test.driver, opts); |
| 1775 | 0 | deferredAction.resolve(); |
| 1776 | 0 | this.test.driver.events.on('driver:message', cb); |
| 1777 | 0 | return deferredAction.promise; |
| 1778 | }.bind(this)); | |
| 1779 | 0 | return this; |
| 1780 | }; | |
| 1781 | ||
| 1782 | /** | |
| 1783 | * Generates a function that can be used | |
| 1784 | * | |
| 1785 | * @method generateTestHelper | |
| 1786 | * @param name | |
| 1787 | * @param assertionFn | |
| 1788 | * @param negate | |
| 1789 | * @return | |
| 1790 | * @private | |
| 1791 | */ | |
| 1792 | ||
| 1793 | 1 | Assertions.prototype.generateTestHelper = function (name, assertionFn, negate) { |
| 1794 | 0 | return function (expected, message) { |
| 1795 | 0 | var gen = this._lastGeneratedShit; |
| 1796 | ||
| 1797 | 0 | this.test.actionPromiseQueue.push(function () { |
| 1798 | 0 | var deferredAction = Q.defer(); |
| 1799 | 0 | deferredAction.resolve(); |
| 1800 | 0 | this.test.driver.events.on('driver:message', function () { |
| 1801 | ||
| 1802 | 0 | if (gen.opts && gen.opts[(gen.opts.length - 1)] && this.test._lastGeneratedAction && this.test._lastGeneratedAction.hash) { |
| 1803 | 0 | if (gen.opts[(gen.opts.length - 1)] === this.test._lastGeneratedAction.hash && !this.proceeded[this.test._lastGeneratedAction.hash + name]) { |
| 1804 | 0 | var testResult = this[assertionFn](expected, this.test._lastGeneratedAction.data.value); |
| 1805 | ||
| 1806 | 0 | if (negate) { |
| 1807 | 0 | testResult = !testResult; |
| 1808 | } | |
| 1809 | ||
| 1810 | 0 | this.proceeded[this.test._lastGeneratedAction.hash + name] = true; |
| 1811 | ||
| 1812 | 0 | this.test.reporter.emit('report:assertion', { |
| 1813 | success: testResult, | |
| 1814 | expected: expected, | |
| 1815 | value: this.test._lastGeneratedAction.data.value, | |
| 1816 | message: message, | |
| 1817 | type: this.test._lastGeneratedAction.type | |
| 1818 | }); | |
| 1819 | ||
| 1820 | 0 | this.test.incrementExpectations(); |
| 1821 | ||
| 1822 | 0 | if (!testResult) { |
| 1823 | 0 | this.test.incrementFailedAssertions(); |
| 1824 | } | |
| 1825 | } | |
| 1826 | } | |
| 1827 | }.bind(this)); | |
| 1828 | 0 | return deferredAction.promise; |
| 1829 | }.bind(this)); | |
| 1830 | ||
| 1831 | 0 | return this.chaining ? this : this.test; |
| 1832 | }.bind(this); | |
| 1833 | }; | |
| 1834 | ||
| 1835 | // ASSERT METHODS | |
| 1836 | // -------------- | |
| 1837 | ||
| 1838 | /** | |
| 1839 | * Assert if a given value shallow equals a second given value | |
| 1840 | * | |
| 1841 | * @method _testShallowEquals | |
| 1842 | * @param {mixed} a Value to test | |
| 1843 | * @param {mixed} b Value to test | |
| 1844 | * @return {bool} false if values donʼt match, true if they match | |
| 1845 | * @private | |
| 1846 | */ | |
| 1847 | ||
| 1848 | 1 | Assertions.prototype._testShallowEquals = function (a, b) { |
| 1849 | 0 | try { |
| 1850 | 0 | chai.assert.equal(a, b); |
| 1851 | } catch (e) { | |
| 1852 | 0 | return false; |
| 1853 | } | |
| 1854 | ||
| 1855 | 0 | return true; |
| 1856 | }; | |
| 1857 | ||
| 1858 | /** | |
| 1859 | * Assert if a given value shallow does not equal a second given value | |
| 1860 | * | |
| 1861 | * @method _testShallowUnequals | |
| 1862 | * @param {mixed} a Value to test | |
| 1863 | * @param {mixed} b Value to test | |
| 1864 | * @return {bool} true if values donʼt match, false if they match | |
| 1865 | * @private | |
| 1866 | */ | |
| 1867 | ||
| 1868 | 1 | Assertions.prototype._testShallowUnequals = function (a, b) { |
| 1869 | 0 | try { |
| 1870 | 0 | chai.assert.notEqual(a, b); |
| 1871 | } catch (e) { | |
| 1872 | 0 | return false; |
| 1873 | } | |
| 1874 | ||
| 1875 | 0 | return true; |
| 1876 | }; | |
| 1877 | ||
| 1878 | /** | |
| 1879 | * Assert if a given value matches a range | |
| 1880 | * | |
| 1881 | * @method _testBetween | |
| 1882 | * @param {array} a Range to test | |
| 1883 | * @param {bool} b Value to compare | |
| 1884 | * @return {bool} test result | |
| 1885 | * @private | |
| 1886 | */ | |
| 1887 | ||
| 1888 | 1 | Assertions.prototype._testBetween = function (a, b) { |
| 1889 | 0 | try { |
| 1890 | 0 | chai.expect(b).to.be.within(a[0], a[1]); |
| 1891 | } catch (e) { | |
| 1892 | 0 | return false; |
| 1893 | } | |
| 1894 | ||
| 1895 | 0 | return true; |
| 1896 | }; | |
| 1897 | ||
| 1898 | /** | |
| 1899 | * Assert if a given value is greater than the value to compare | |
| 1900 | * | |
| 1901 | * @method _testGreaterThan | |
| 1902 | * @param {bool} a Value to test | |
| 1903 | * @param {bool} b Value to compare | |
| 1904 | * @param {parseFloatOnValues} b Whether to apply parseFloat to both values prior comparision | |
| 1905 | * @return {bool} test result | |
| 1906 | * @private | |
| 1907 | */ | |
| 1908 | ||
| 1909 | 1 | Assertions.prototype._testGreaterThan = function (a, b, parseFloatOnValues) { |
| 1910 | 0 | if (parseFloatOnValues) { |
| 1911 | 0 | a = parseFloat(a); |
| 1912 | 0 | b = parseFloat(b); |
| 1913 | } | |
| 1914 | ||
| 1915 | 0 | try { |
| 1916 | 0 | chai.expect(b).to.be.above(a); |
| 1917 | } catch (e) { | |
| 1918 | 0 | return false; |
| 1919 | } | |
| 1920 | ||
| 1921 | 0 | return true; |
| 1922 | }; | |
| 1923 | ||
| 1924 | /** | |
| 1925 | * Assert if a given value is greater or equal than the value to compare | |
| 1926 | * | |
| 1927 | * @method _testGreaterThanEqual | |
| 1928 | * @param {bool} a Value to test | |
| 1929 | * @param {bool} b Value to compare | |
| 1930 | * @return {bool} test result | |
| 1931 | * @private | |
| 1932 | */ | |
| 1933 | ||
| 1934 | 1 | Assertions.prototype._testGreaterThanEqual = function (a, b) { |
| 1935 | 0 | return this._testGreaterThan(a - 1, b); |
| 1936 | }; | |
| 1937 | ||
| 1938 | /** | |
| 1939 | * Assert if a given value is lower than the value to compare | |
| 1940 | * | |
| 1941 | * @method _testLowerThan | |
| 1942 | * @param {bool} a Value to test | |
| 1943 | * @param {bool} b Value to compare | |
| 1944 | * @param {parseFloatOnValues} b Whether to apply parseFloatOnValues to both values prior comparision | |
| 1945 | * @return {bool} test result | |
| 1946 | * @private | |
| 1947 | */ | |
| 1948 | ||
| 1949 | 1 | Assertions.prototype._testLowerThan = function (a, b, parseFloatOnValues) { |
| 1950 | 0 | if (parseFloatOnValues) { |
| 1951 | 0 | a = parseFloat(a); |
| 1952 | 0 | b = parseFloat(b); |
| 1953 | } | |
| 1954 | ||
| 1955 | 0 | try { |
| 1956 | 0 | chai.expect(b).to.be.below(a); |
| 1957 | } catch (e) { | |
| 1958 | 0 | return false; |
| 1959 | } | |
| 1960 | ||
| 1961 | 0 | return true; |
| 1962 | }; | |
| 1963 | ||
| 1964 | /** | |
| 1965 | * Asserts that 2 string match regardless of case | |
| 1966 | * | |
| 1967 | * @method _caseInsensitiveMatch | |
| 1968 | * @param {string} a Value to test | |
| 1969 | * @param {string} b Value to compare | |
| 1970 | * @return {bool} testresult | |
| 1971 | * @private | |
| 1972 | */ | |
| 1973 | ||
| 1974 | 1 | Assertions.prototype._caseInsensitiveMatch = function (a, b) { |
| 1975 | 0 | try { |
| 1976 | 0 | if(a.toLowerCase && b.toLowerCase) { |
| 1977 | 0 | chai.expect(b.toLowerCase()).to.eql(a.toLowerCase()); |
| 1978 | } else { | |
| 1979 | 0 | return false; |
| 1980 | } | |
| 1981 | } catch (e) { | |
| 1982 | 0 | return false; |
| 1983 | } | |
| 1984 | ||
| 1985 | 0 | return true; |
| 1986 | }; | |
| 1987 | ||
| 1988 | /** | |
| 1989 | * Assert if a given value contain another value | |
| 1990 | * | |
| 1991 | * @method _contain | |
| 1992 | * @param {bool} a Value to test | |
| 1993 | * @param {bool} b Value to compare | |
| 1994 | * @return {bool} test result | |
| 1995 | * @private | |
| 1996 | */ | |
| 1997 | ||
| 1998 | 1 | Assertions.prototype._contain = function (a, b) { |
| 1999 | 0 | try { |
| 2000 | 0 | chai.expect(b).to.include(a); |
| 2001 | } catch (e) { | |
| 2002 | 0 | return false; |
| 2003 | } | |
| 2004 | ||
| 2005 | 0 | return true; |
| 2006 | }; | |
| 2007 | ||
| 2008 | /** | |
| 2009 | * Assert if a given value doesn't contain another value | |
| 2010 | * | |
| 2011 | * @method _notContain | |
| 2012 | * @param {bool} a Value to test | |
| 2013 | * @param {bool} b Value to compare | |
| 2014 | * @return {bool} test result | |
| 2015 | * @private | |
| 2016 | */ | |
| 2017 | 1 | Assertions.prototype._notContain = function (a, b) { |
| 2018 | 0 | try { |
| 2019 | 0 | chai.expect(b).to.not.include(a); |
| 2020 | } catch (e) { | |
| 2021 | 0 | return false; |
| 2022 | } | |
| 2023 | ||
| 2024 | 0 | return true; |
| 2025 | }; | |
| 2026 | ||
| 2027 | /** | |
| 2028 | * Assert if a given value is lower or equal than the value to compare | |
| 2029 | * | |
| 2030 | * @method _testLowerThanEqual | |
| 2031 | * @param {bool} a Value to test | |
| 2032 | * @param {bool} b Value to compare | |
| 2033 | * @return {bool} test result | |
| 2034 | * @private | |
| 2035 | */ | |
| 2036 | ||
| 2037 | 1 | Assertions.prototype._testLowerThanEqual = function (a, b) { |
| 2038 | 0 | return this._testLowerThan(a + 1, b); |
| 2039 | }; | |
| 2040 | ||
| 2041 | /** | |
| 2042 | * Assert if a given value is boolean 'true' | |
| 2043 | * | |
| 2044 | * @method _testTruthy | |
| 2045 | * @param {bool} a Value to test | |
| 2046 | * @return {bool} false if value is false, true if value is true | |
| 2047 | * @private | |
| 2048 | */ | |
| 2049 | ||
| 2050 | 1 | Assertions.prototype._testTruthy = function (a) { |
| 2051 | 0 | return a === 'true' || a === true; |
| 2052 | }; | |
| 2053 | ||
| 2054 | /** | |
| 2055 | * Assert if a given value is boolean 'false' | |
| 2056 | * | |
| 2057 | * @method _testFalsy | |
| 2058 | * @param {bool} a Value to test | |
| 2059 | * @return {bool} true if value is false, false if value is true | |
| 2060 | * @private | |
| 2061 | */ | |
| 2062 | ||
| 2063 | 1 | Assertions.prototype._testFalsy = function (a) { |
| 2064 | 0 | return a === 'false' || a === false; |
| 2065 | }; | |
| 2066 | ||
| 2067 | /** | |
| 2068 | * Assert a given value matches a RegEx | |
| 2069 | * | |
| 2070 | * @method _contain | |
| 2071 | * @param {mixed} a Value to test | |
| 2072 | * @param {string} b Value to compare | |
| 2073 | * @return {bool} test result | |
| 2074 | * @private | |
| 2075 | */ | |
| 2076 | ||
| 2077 | 1 | Assertions.prototype._match = function (a, b) { |
| 2078 | 0 | try { |
| 2079 | 0 | chai.expect(b).to.match(a); |
| 2080 | } catch (e) { | |
| 2081 | 0 | return false; |
| 2082 | } | |
| 2083 | ||
| 2084 | 0 | return true; |
| 2085 | }; | |
| 2086 | ||
| 2087 | ||
| 2088 | /** | |
| 2089 | * Assert if a image compare result is equal | |
| 2090 | * | |
| 2091 | * @method _testImagecompare | |
| 2092 | * @param {string} a Value to test if it equal to 'equal' | |
| 2093 | * @return {bool} true if value is 'equal', false if value has some other values | |
| 2094 | * @private | |
| 2095 | */ | |
| 2096 | ||
| 2097 | 1 | Assertions.prototype._testImagecompare = function (a) { |
| 2098 | 0 | return a === 'equal'; |
| 2099 | }; | |
| 2100 |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * | |
| 3 | * Copyright (c) 2013 Sebastian Golasch | |
| 4 | * | |
| 5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
| 6 | * copy of this software and associated documentation files (the "Software"), | |
| 7 | * to deal in the Software without restriction, including without limitation | |
| 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
| 9 | * and/or sell copies of the Software, and to permit persons to whom the | |
| 10 | * Software is furnished to do so, subject to the following conditions: | |
| 11 | * | |
| 12 | * The above copyright notice and this permission notice shall be included | |
| 13 | * in all copies or substantial portions of the Software. | |
| 14 | * | |
| 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
| 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
| 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
| 21 | * DEALINGS IN THE SOFTWARE. | |
| 22 | * | |
| 23 | */ | |
| 24 | ||
| 25 | 1 | 'use strict'; |
| 26 | ||
| 27 | // ext. libs | |
| 28 | 1 | var path = require('path'); |
| 29 | 1 | var fs = require('fs'); |
| 30 | 1 | var _ = require('lodash'); |
| 31 | 1 | var yaml = require('js-yaml'); |
| 32 | 1 | var JSON5 = require('json5'); |
| 33 | 1 | var glob = require('glob'); |
| 34 | 1 | require('coffee-script/register'); |
| 35 | ||
| 36 | /** | |
| 37 | * Configures the config instance | |
| 38 | * | |
| 39 | * @param {object} defaults Default parameter options | |
| 40 | * @param {object} opts Command line options | |
| 41 | * @constructor | |
| 42 | */ | |
| 43 | ||
| 44 | 1 | var Config = function (defaults, opts, advOpts) { |
| 45 | 20 | this.customFilename = null; |
| 46 | 20 | this.defaultFilename = 'Dalekfile'; |
| 47 | 20 | this.supportedExtensions = ['yml', 'json5', 'json', 'js', 'coffee']; |
| 48 | 20 | this.advancedOptions = advOpts; |
| 49 | 20 | this.config = this.load(defaults, opts.config, opts); |
| 50 | }; | |
| 51 | ||
| 52 | /** | |
| 53 | * Parses config data & loads config files for [DalekJS](//github.com/dalekjs/dalek) tests. | |
| 54 | * | |
| 55 | * This module is a driver plugin for [DalekJS](//github.com/dalekjs/dalek). | |
| 56 | * It connects Daleks testsuite with the remote testing environment of [Sauce Labs](https://saucelabs.com). | |
| 57 | * | |
| 58 | * The driver can be installed with the following command: | |
| 59 | * | |
| 60 | * ```bash | |
| 61 | * $ npm install dalek-driver-sauce --save-dev | |
| 62 | * ``` | |
| 63 | * | |
| 64 | * You can use the driver by adding a config option to the your [Dalekfile](/docs/config.html) | |
| 65 | * | |
| 66 | * ```javascript | |
| 67 | * "driver": ["sauce"] | |
| 68 | * ``` | |
| 69 | * | |
| 70 | * Or you can tell Dalek that it should run your tests via sauces service via the command line: | |
| 71 | * | |
| 72 | * ```bash | |
| 73 | * $ dalek mytest.js -d sauce | |
| 74 | * ``` | |
| 75 | * | |
| 76 | * In order to run your tests within the Sauce Labs infrastructure, you must add your sauce username & key | |
| 77 | * to your dalek configuration. Those two parameters must be set in order to get this driver up & running. | |
| 78 | * You can specifiy them within your [Dalekfile](/docs/config.html) like so: | |
| 79 | * | |
| 80 | * ```javascript | |
| 81 | * "driver.sauce": { | |
| 82 | * "user": "dalekjs", | |
| 83 | * "key": "aaaaaa-1234-567a-1abc-1br6d9f68689" | |
| 84 | * } | |
| 85 | * ``` | |
| 86 | * | |
| 87 | * It is also possible to specify a set of other extra saucy parameters like `name` & `tags`: | |
| 88 | * | |
| 89 | * ```javascript | |
| 90 | * "driver.sauce": { | |
| 91 | * "user": "dalekjs", | |
| 92 | * "key": "aaaaaa-1234-567a-1abc-1br6d9f68689", | |
| 93 | * "name": "Guineapig", | |
| 94 | * "tags": ["dalek", "testproject"] | |
| 95 | * } | |
| 96 | * ``` | |
| 97 | * | |
| 98 | * If you would like to have a more control over the browser/OS combinations that are available, you are able | |
| 99 | * to configure you custom combinations: | |
| 100 | * | |
| 101 | * ```javascript | |
| 102 | * "browsers": [{ | |
| 103 | * "chrome": { | |
| 104 | * "platform": "OS X 10.6", | |
| 105 | * "actAs": "chrome", | |
| 106 | * "version": 27 | |
| 107 | * }, | |
| 108 | * "chromeWin": { | |
| 109 | * "platform": "Windows 7", | |
| 110 | * "actAs": "chrome", | |
| 111 | * "version": 27 | |
| 112 | * }, | |
| 113 | * "chromeLinux": { | |
| 114 | * "platform": "Linux", | |
| 115 | * "actAs": "chrome", | |
| 116 | * "version": 26 | |
| 117 | * } | |
| 118 | * ``` | |
| 119 | * | |
| 120 | * You can then call your custom browsers like so: | |
| 121 | * | |
| 122 | * ```bash | |
| 123 | * $ dalek mytest.js -d sauce -b chrome,chromeWin,chromeLinux | |
| 124 | * ``` | |
| 125 | * | |
| 126 | * or you can define them in your Dalekfile: | |
| 127 | * | |
| 128 | * ```javascript | |
| 129 | * "browser": ["chrome", "chromeWin", "chromeLinux"] | |
| 130 | * ``` | |
| 131 | * | |
| 132 | * A list of all available browser/OS combinations, can be found [here](https://saucelabs.com/docs/platforms). | |
| 133 | * | |
| 134 | * @module DalekJS | |
| 135 | * @class Config | |
| 136 | * @namespace Dalek | |
| 137 | * @part Config | |
| 138 | * @api | |
| 139 | */ | |
| 140 | ||
| 141 | 1 | Config.prototype = { |
| 142 | ||
| 143 | /** | |
| 144 | * Checks if a config file is available | |
| 145 | * | |
| 146 | * @method checkAvailabilityOfConfigFile | |
| 147 | * @param {String} pathname | |
| 148 | * @return {String} config File path | |
| 149 | */ | |
| 150 | ||
| 151 | checkAvailabilityOfConfigFile: function (pathname) { | |
| 152 | // check if a pathname is given, | |
| 153 | // then check if the file is available | |
| 154 | 21 | if (pathname && fs.existsSync(pathname)) { |
| 155 | 1 | return fs.realpathSync(pathname); |
| 156 | } | |
| 157 | ||
| 158 | // check if any of the default configuration files is available | |
| 159 | 20 | return this.supportedExtensions.reduce(this._checkFile.bind(this)); |
| 160 | }, | |
| 161 | ||
| 162 | /** | |
| 163 | * Iterator function that checks the existance of a given file | |
| 164 | * | |
| 165 | * @method _checkFile | |
| 166 | * @param {String} previousValue Last iterations result | |
| 167 | * @param {String} ext File extension to check | |
| 168 | * @param {integer} idx Iteration index | |
| 169 | * @param {object} data File data | |
| 170 | * @return {String} config File path | |
| 171 | * @private | |
| 172 | */ | |
| 173 | ||
| 174 | _checkFile: function (previousValue, ext, idx, data) { | |
| 175 | 83 | if (previousValue.length > 6) { |
| 176 | 1 | return previousValue; |
| 177 | } | |
| 178 | ||
| 179 | 82 | var fileToCheck = this.defaultFilename + '.' + previousValue; |
| 180 | 82 | if (fs.existsSync(fileToCheck)) { |
| 181 | 1 | return fs.realpathSync(fileToCheck); |
| 182 | } | |
| 183 | ||
| 184 | 81 | return this._checkDefaultFile(ext, data); |
| 185 | }, | |
| 186 | ||
| 187 | /** | |
| 188 | * Iterator function that checks the existance of a the default file | |
| 189 | * | |
| 190 | * @method _checkDefaultFile | |
| 191 | * @param {String} ext File extension to check | |
| 192 | * @param {object} data File data | |
| 193 | * @return {String} config File path | |
| 194 | * @private | |
| 195 | */ | |
| 196 | ||
| 197 | _checkDefaultFile: function (ext, data) { | |
| 198 | 81 | if (ext === data[data.length - 1]) { |
| 199 | 21 | var fileToCheck = this.defaultFilename + '.' + ext; |
| 200 | 21 | if (fs.existsSync(fileToCheck)) { |
| 201 | 1 | return fs.realpathSync(fileToCheck); |
| 202 | } | |
| 203 | } | |
| 204 | ||
| 205 | 80 | return ext; |
| 206 | }, | |
| 207 | ||
| 208 | /** | |
| 209 | * Loads a file & merges the results with the | |
| 210 | * commandline options & the default config | |
| 211 | * | |
| 212 | * @method load | |
| 213 | * @param {object} defaults Default config | |
| 214 | * @param {String} pathname Filename of the config file to load | |
| 215 | * @param {object} opts Command line options | |
| 216 | * @return {object} config Merged config data | |
| 217 | */ | |
| 218 | ||
| 219 | load: function (defaults, pathname, opts) { | |
| 220 | 20 | var file = this.checkAvailabilityOfConfigFile(pathname); |
| 221 | 20 | var data = {}; |
| 222 | ||
| 223 | 20 | if (!this.advancedOptions || this.advancedOptions.dalekfile !== false) { |
| 224 | 20 | data = this.loadFile(file); |
| 225 | } | |
| 226 | ||
| 227 | // remove the tests property if the array length is 0 | |
| 228 | 20 | if (opts.tests.length === 0) { |
| 229 | 14 | delete opts.tests; |
| 230 | } | |
| 231 | ||
| 232 | 20 | if (data.tests && _.isArray(data.tests) && data.tests.length > 0) { |
| 233 | 0 | var tests = []; |
| 234 | ||
| 235 | //get all the files that match | |
| 236 | 0 | _.forEach(data.tests, function(search) { |
| 237 | 0 | tests = tests.concat(glob.sync(search)); |
| 238 | }); | |
| 239 | ||
| 240 | //remove duplicate files | |
| 241 | 0 | tests = tests.filter(function(elem, pos, self) { |
| 242 | 0 | return self.indexOf(elem) === pos; |
| 243 | }); | |
| 244 | ||
| 245 | 0 | data.tests = tests; |
| 246 | } | |
| 247 | ||
| 248 | 20 | return _.merge(defaults, data, opts, (this.advancedOptions || {})); |
| 249 | }, | |
| 250 | ||
| 251 | /** | |
| 252 | * Loads a config file & parses it based on the file extension | |
| 253 | * | |
| 254 | * @method loadFile | |
| 255 | * @param {String} pathname Filename of the config file to load | |
| 256 | * @return {object} data Config data | |
| 257 | */ | |
| 258 | ||
| 259 | loadFile: function (pathname) { | |
| 260 | 20 | var ext = path.extname(pathname).replace('.', ''); |
| 261 | 20 | return this['read' + ext] ? this['read' + ext](pathname) : {}; |
| 262 | }, | |
| 263 | ||
| 264 | /** | |
| 265 | * Fetches & returns a config item | |
| 266 | * | |
| 267 | * @method get | |
| 268 | * @param {String} item Key of the item to load | |
| 269 | * @return {mixed|null} data Requested config data | |
| 270 | */ | |
| 271 | ||
| 272 | get: function (item) { | |
| 273 | 50 | return this.config[item] || null; |
| 274 | }, | |
| 275 | ||
| 276 | /** | |
| 277 | * Loads a json config file | |
| 278 | * | |
| 279 | * @method readjson | |
| 280 | * @return {object} data Parsed config data | |
| 281 | */ | |
| 282 | ||
| 283 | readjson: function (pathname) { | |
| 284 | 1 | var contents = fs.readFileSync((pathname || this.defaultFilename + '.json'), 'utf8'); |
| 285 | 1 | return JSON.parse(contents); |
| 286 | }, | |
| 287 | ||
| 288 | /** | |
| 289 | * Loads a json5 config file | |
| 290 | * | |
| 291 | * @method readJson5 | |
| 292 | * @return {object} data Parsed config data | |
| 293 | */ | |
| 294 | ||
| 295 | readjson5: function (pathname) { | |
| 296 | 1 | var contents = fs.readFileSync((pathname || this.defaultFilename + '.json5'), 'utf8'); |
| 297 | 1 | return JSON5.parse(contents); |
| 298 | }, | |
| 299 | ||
| 300 | /** | |
| 301 | * Loads a yaml config file | |
| 302 | * | |
| 303 | * @method readyaml | |
| 304 | * @return {object} data Parsed config data | |
| 305 | */ | |
| 306 | ||
| 307 | readyml: function (pathname) { | |
| 308 | 1 | var contents = fs.readFileSync((pathname || this.defaultFilename + '.yml'), 'utf8'); |
| 309 | 1 | return yaml.load(contents); |
| 310 | }, | |
| 311 | ||
| 312 | /** | |
| 313 | * Loads a javascript config file | |
| 314 | * | |
| 315 | * @method readjs | |
| 316 | * @return {object} data Parsed config data | |
| 317 | */ | |
| 318 | ||
| 319 | readjs: function (pathname) { | |
| 320 | 1 | return require((pathname || this.defaultFilename)); |
| 321 | }, | |
| 322 | ||
| 323 | /** | |
| 324 | * Loads a coffescript config file | |
| 325 | * | |
| 326 | * @method readcoffee | |
| 327 | * @return {object} data Parsed config data | |
| 328 | */ | |
| 329 | ||
| 330 | readcoffee: function (pathname) { | |
| 331 | 1 | return require((pathname || this.defaultFilename)); |
| 332 | }, | |
| 333 | ||
| 334 | /** | |
| 335 | * Verifies if a reporter is given, exists & is valid | |
| 336 | * | |
| 337 | * @method verifyReporters | |
| 338 | * @return {array} data List of verified reporters | |
| 339 | */ | |
| 340 | ||
| 341 | verifyReporters: function (reporters, reporter) { | |
| 342 | 8 | return _.compact(this._verify(reporters, 'isReporter', reporter)); |
| 343 | }, | |
| 344 | ||
| 345 | /** | |
| 346 | * Verifies if a driver is given, exists & is valid | |
| 347 | * | |
| 348 | * @method verifyDrivers | |
| 349 | * @return {array} data List of verified drivers | |
| 350 | */ | |
| 351 | ||
| 352 | verifyDrivers: function (drivers, driver) { | |
| 353 | 8 | return _.compact(this._verify(drivers, 'isDriver', driver)); |
| 354 | }, | |
| 355 | ||
| 356 | /** | |
| 357 | * Verifies if a driver is given, exists & is valid | |
| 358 | * | |
| 359 | * @method _verify | |
| 360 | * @param {array} check Data that should be mapped | |
| 361 | * @param {string} fn Name of the function that should be invoked on the veryify object | |
| 362 | * @param {object} instance Object instance where the verify function should be invoked | |
| 363 | * @return {array} data List of verified items | |
| 364 | * @private | |
| 365 | */ | |
| 366 | ||
| 367 | _verify: function (check, fn, instance) { | |
| 368 | 16 | return check.map(this._verifyIterator.bind(this, fn, instance)); |
| 369 | }, | |
| 370 | ||
| 371 | /** | |
| 372 | * Verifies if a driver is given, exists & is valid | |
| 373 | * | |
| 374 | * @method _verifyIterator | |
| 375 | * @param {string} fn Name of the function that should be invoked on the veryify object | |
| 376 | * @param {object} instance Object instance where the verify function should be invoked | |
| 377 | * @param {string} elm Name of the element that should be checked | |
| 378 | * @return {string|null} element name of the verified element or false if checked failed | |
| 379 | * @priavte | |
| 380 | */ | |
| 381 | ||
| 382 | _verifyIterator: function (fn, instance, elm) { | |
| 383 | 16 | return instance[fn](elm) ? elm : false; |
| 384 | } | |
| 385 | }; | |
| 386 | ||
| 387 | // export the module | |
| 388 | 1 | module.exports = Config; |
| 389 |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * | |
| 3 | * Copyright (c) 2013 Sebastian Golasch | |
| 4 | * | |
| 5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
| 6 | * copy of this software and associated documentation files (the "Software"), | |
| 7 | * to deal in the Software without restriction, including without limitation | |
| 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
| 9 | * and/or sell copies of the Software, and to permit persons to whom the | |
| 10 | * Software is furnished to do so, subject to the following conditions: | |
| 11 | * | |
| 12 | * The above copyright notice and this permission notice shall be included | |
| 13 | * in all copies or substantial portions of the Software. | |
| 14 | * | |
| 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
| 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
| 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
| 21 | * DEALINGS IN THE SOFTWARE. | |
| 22 | * | |
| 23 | */ | |
| 24 | ||
| 25 | 1 | 'use strict'; |
| 26 | ||
| 27 | // ext. libs | |
| 28 | 1 | var async = require('async'); |
| 29 | ||
| 30 | // int. libs | |
| 31 | 1 | var Suite = require('./suite'); |
| 32 | ||
| 33 | /** | |
| 34 | * Configures the driver instance | |
| 35 | * | |
| 36 | * @constructor | |
| 37 | */ | |
| 38 | ||
| 39 | 1 | var Driver = function (options) { |
| 40 | // add configuration data to the driver instance | |
| 41 | 7 | this.config = options.config; |
| 42 | 7 | this.browser = this.config.get('browser'); |
| 43 | 7 | this.files = this.config.get('tests'); |
| 44 | 7 | this.drivers = this.config.get('driver'); |
| 45 | ||
| 46 | // flag if we use the canary driver builds | |
| 47 | 7 | this.driverIsCanary = false; |
| 48 | ||
| 49 | // link driver events | |
| 50 | 7 | this.driverEmitter = options.driverEmitter; |
| 51 | 7 | this.reporterEvents = options.reporterEvents; |
| 52 | }; | |
| 53 | ||
| 54 | /** | |
| 55 | * Generates & starts drivers & browsers | |
| 56 | * the tests will be run in | |
| 57 | * | |
| 58 | * @module DalekJS | |
| 59 | * @class Driver | |
| 60 | * @namespace Dalek | |
| 61 | * @part Driver | |
| 62 | * @api | |
| 63 | */ | |
| 64 | ||
| 65 | 1 | Driver.prototype = { |
| 66 | ||
| 67 | /** | |
| 68 | * Checks if the requested driver is available | |
| 69 | * | |
| 70 | * @method isDriver | |
| 71 | * @param {string} driver Name of the requested driver | |
| 72 | * @return {bool} isDriver Driver is availavle | |
| 73 | */ | |
| 74 | ||
| 75 | isDriver: function (driver) { | |
| 76 | 7 | try { |
| 77 | 7 | require.resolve('dalek-driver-' + driver); |
| 78 | } catch (e) { | |
| 79 | 0 | try { |
| 80 | 0 | require.resolve('dalek-driver-' + driver + '-canary'); |
| 81 | } catch (e) { | |
| 82 | 0 | return false; |
| 83 | } | |
| 84 | 0 | this.driverIsCanary = true; |
| 85 | 0 | return true; |
| 86 | } | |
| 87 | 7 | return true; |
| 88 | }, | |
| 89 | ||
| 90 | /** | |
| 91 | * Loads the requested driver | |
| 92 | * Emits an event to the reporter | |
| 93 | * | |
| 94 | * @method loadDriver | |
| 95 | * @param {string} driver Name of the requested driver | |
| 96 | * @return {object} driverModule Instance of the driver module | |
| 97 | */ | |
| 98 | ||
| 99 | loadDriver: function (driver) { | |
| 100 | 0 | this.reporterEvents.emit('report:log:system', 'dalek-internal-driver: Loading driver: "' + driver + '"'); |
| 101 | 0 | return require('dalek-driver-' + driver + (this.driverIsCanary ? '-canary' : '')); |
| 102 | }, | |
| 103 | ||
| 104 | /** | |
| 105 | * Returns a list with browser driver instances | |
| 106 | * | |
| 107 | * @method getDrivers | |
| 108 | * @return {array} verifiedDrivers | |
| 109 | */ | |
| 110 | ||
| 111 | getDrivers: function () { | |
| 112 | 0 | return this.drivers.map(this.getVerifiedBrowser, this)[0]; |
| 113 | }, | |
| 114 | ||
| 115 | /** | |
| 116 | * Returns a list with browser driver instances | |
| 117 | * | |
| 118 | * @method getVerifiedBrowser | |
| 119 | * @param {string} driver Name of the requested driver | |
| 120 | * @return {array} verifiedDrivers Array of dribver 'run' functions | |
| 121 | */ | |
| 122 | ||
| 123 | getVerifiedBrowser: function (driver) { | |
| 124 | 0 | return this.browser.map(this.getVerifiedDriver.bind(this, this.loadDriver(driver), driver)); |
| 125 | }, | |
| 126 | ||
| 127 | /** | |
| 128 | * Returns a scoped version of the driver run function | |
| 129 | * | |
| 130 | * @method getVerifiedDriver | |
| 131 | * @param {object} driverModule Instance of the used driver | |
| 132 | * @param {string} driver Name of ther used driver | |
| 133 | * @param {string} browser Name of the used browser | |
| 134 | * @return {function} run Function that kicks off execution of a testsuite chain in a browser | |
| 135 | */ | |
| 136 | ||
| 137 | getVerifiedDriver: function (driverModule, driver, browser) { | |
| 138 | 0 | return this.run.bind(this, driver, driverModule, browser); |
| 139 | }, | |
| 140 | ||
| 141 | /** | |
| 142 | * Loads a browser driver | |
| 143 | * | |
| 144 | * @method loadBrowserConfiguration | |
| 145 | * @param {string} browser Name of the requested browser driver | |
| 146 | * @param {object} browsers Configuration options for the requested browser | |
| 147 | * @return {object} browserConfiguration Browser driver isntance and configuration meta data | |
| 148 | */ | |
| 149 | ||
| 150 | loadBrowserConfiguration: function (browser, browsers, driver) { | |
| 151 | 0 | var browserConfiguration; |
| 152 | ||
| 153 | 0 | if (driver.dummyBrowser && driver.dummyBrowser()) { |
| 154 | 0 | return driver.getBrowser(driver); |
| 155 | } | |
| 156 | ||
| 157 | 0 | try { |
| 158 | 0 | browserConfiguration = this.getDefaultBrowserConfiguration(browser, browsers); |
| 159 | } catch (e) { | |
| 160 | 0 | browserConfiguration = this.getUserBrowserConfiguration(browser, browsers); |
| 161 | } | |
| 162 | ||
| 163 | 0 | return browserConfiguration; |
| 164 | }, | |
| 165 | ||
| 166 | /** | |
| 167 | * Loads the default browser driver | |
| 168 | * | |
| 169 | * @method getDefaultBrowserConfiguration | |
| 170 | * @param {string} browser Name of the requested browser driver | |
| 171 | * @param {object} browsers Configuration options for the requested browser | |
| 172 | * @return {object} browserConfiguration Browser driver isntance and configuration meta data | |
| 173 | */ | |
| 174 | ||
| 175 | getDefaultBrowserConfiguration: function (browser, browsers) { | |
| 176 | 0 | var browserConfiguration = {configuration: null, module: null}; |
| 177 | ||
| 178 | // set browser configuration | |
| 179 | 0 | if (browsers[browser]) { |
| 180 | 0 | browserConfiguration.configuration = browsers[browser]; |
| 181 | } | |
| 182 | ||
| 183 | // try to load `normal` browser modules first, | |
| 184 | // if that doesnt work, try canary builds | |
| 185 | 0 | try { |
| 186 | // check if the browser is a remote instance | |
| 187 | // else, try to load the local browser | |
| 188 | 0 | if (browserConfiguration.configuration && browserConfiguration.configuration.type === 'remote') { |
| 189 | 0 | browserConfiguration.module = require('./remote'); |
| 190 | } else { | |
| 191 | 0 | browserConfiguration.module = require('dalek-browser-' + browser); |
| 192 | } | |
| 193 | } catch (e) { | |
| 194 | 0 | browserConfiguration.module = require('dalek-browser-' + browser + '-canary'); |
| 195 | } | |
| 196 | ||
| 197 | 0 | return browserConfiguration; |
| 198 | }, | |
| 199 | ||
| 200 | /** | |
| 201 | * Loads a user configured browser driver | |
| 202 | * | |
| 203 | * @method getUserBrowserConfiguration | |
| 204 | * @param {string} browser Name of the requested browser driver | |
| 205 | * @param {object} browsers Configuration options for the requested browser | |
| 206 | * @return {object} browserConfiguration Browser driver isntance and configuration meta data | |
| 207 | */ | |
| 208 | ||
| 209 | getUserBrowserConfiguration: function (browser, browsers) { | |
| 210 | 0 | var browserConfiguration = {configuration: null, module: null}; |
| 211 | ||
| 212 | 0 | if (browsers && browsers[browser] && browsers[browser].actAs) { |
| 213 | 0 | browserConfiguration.module = require('dalek-browser-' + browsers[browser].actAs); |
| 214 | 0 | browserConfiguration.configuration = browsers[browser]; |
| 215 | } | |
| 216 | ||
| 217 | 0 | if (!browserConfiguration.module && browser.search(':') !== -1) { |
| 218 | 0 | var args = browser.split(':'); |
| 219 | 0 | var extractedBrowser = args[0].trim(); |
| 220 | 0 | var browserType = args[1].trim().toLowerCase(); |
| 221 | 0 | browserConfiguration.module = require('dalek-browser-' + extractedBrowser); |
| 222 | ||
| 223 | 0 | if (browserConfiguration.module && browserConfiguration.module.browserTypes && browserConfiguration.module.browserTypes[browserType]) { |
| 224 | 0 | var binary = (process.platform === 'win32' ? browserConfiguration.module.browserTypes[browserType].win32 : browserConfiguration.module.browserTypes[browserType].darwin); |
| 225 | 0 | browserConfiguration.configuration = { |
| 226 | binary: binary, | |
| 227 | type: browserType | |
| 228 | }; | |
| 229 | } | |
| 230 | } | |
| 231 | ||
| 232 | 0 | return browserConfiguration; |
| 233 | }, | |
| 234 | ||
| 235 | /** | |
| 236 | * Couple driver & session status events for the reporter | |
| 237 | * | |
| 238 | * @method coupleReporterEvents | |
| 239 | * @param {string} driverName Name of the requested driver | |
| 240 | * @param {string} browser Name of the requested browser | |
| 241 | * @chainable | |
| 242 | */ | |
| 243 | ||
| 244 | coupleReporterEvents: function (driverName, browser) { | |
| 245 | 0 | this.driverEmitter.on('driver:sessionStatus:' + driverName + ':' + browser, this.reporterEvents.emit.bind(this.reporterEvents, 'report:driver:session')); |
| 246 | 0 | this.driverEmitter.on('driver:status:' + driverName + ':' + browser, this.reporterEvents.emit.bind(this.reporterEvents, 'report:driver:status')); |
| 247 | 0 | return this; |
| 248 | }, | |
| 249 | ||
| 250 | /** | |
| 251 | * Returns a list of testsuite runner functions | |
| 252 | * | |
| 253 | * @method getTestsuiteInstances | |
| 254 | * @param {object} driverInstance Instance of the requested driver | |
| 255 | * @return {array} testsuiteRunners List of testsuites that should be run | |
| 256 | */ | |
| 257 | ||
| 258 | getTestsuiteInstances: function (driverInstance) { | |
| 259 | 0 | return this.files.map(this.createTestsuiteInstance.bind(this, driverInstance)); |
| 260 | }, | |
| 261 | ||
| 262 | /** | |
| 263 | * Creates a testsuite runner function | |
| 264 | * | |
| 265 | * @method createTestsuiteInstance | |
| 266 | * @param {object} driverInstance Instance of the requested driver | |
| 267 | * @param {string} file Filename of the testsuite | |
| 268 | * @return {function} testsuiteRunner Runner function from the testsuite | |
| 269 | */ | |
| 270 | ||
| 271 | createTestsuiteInstance: function (driverInstance, file) { | |
| 272 | 0 | var suite = new Suite({numberOfSuites: this.files.length, file: file, driver: driverInstance, driverEmitter: this.driverEmitter, reporterEmitter: this.reporterEvents}); |
| 273 | 0 | return suite.run.bind(suite); |
| 274 | }, | |
| 275 | ||
| 276 | /** | |
| 277 | * Generates a testsuite instance, emits the | |
| 278 | * browser running event & starts a new async() sesries execution | |
| 279 | * Will be called when the driver is ready | |
| 280 | * | |
| 281 | * @method _onDriverReady | |
| 282 | * @param {string} browser Name of the requested browser | |
| 283 | * @param {string} driverName Name of the requested driver | |
| 284 | * @param {function} callback Asyncs next() callback function | |
| 285 | * @param {object} driverInstance Instance of the requested driver | |
| 286 | * @chainable | |
| 287 | * @private | |
| 288 | */ | |
| 289 | ||
| 290 | _onDriverReady: function (browser, driverName, callback, driverInstance) { | |
| 291 | // generate testsuite instance from test files | |
| 292 | 0 | var testsuites = this.getTestsuiteInstances(driverInstance); |
| 293 | 0 | this.reporterEvents.emit('report:run:browser', driverInstance.webdriverClient.opts.longName); |
| 294 | 0 | async.series(testsuites, this._onTestsuiteComplete.bind(this, callback, driverName, browser)); |
| 295 | 0 | return this; |
| 296 | }, | |
| 297 | ||
| 298 | /** | |
| 299 | * Emits a 'tests complete' event & calls async's next() callback | |
| 300 | * | |
| 301 | * @method _onTestsuiteComplete | |
| 302 | * @param {function} callback Async's next() callback function | |
| 303 | * @param {string} driverName Name of the requested driver | |
| 304 | * @param {string} browser Name of the requested browser | |
| 305 | * @chainable | |
| 306 | * @private | |
| 307 | */ | |
| 308 | ||
| 309 | _onTestsuiteComplete: function (callback, driverName, browser) { | |
| 310 | 0 | this.driverEmitter.emit('tests:complete:' + driverName + ':' + browser); |
| 311 | 0 | callback(); |
| 312 | 0 | return this; |
| 313 | }, | |
| 314 | ||
| 315 | /** | |
| 316 | * Driver runner function. | |
| 317 | * Registers event handlers for this run, | |
| 318 | * loads browser & driver configuration & instances, | |
| 319 | * emits the 'driver ready' event for the browser/driver combination | |
| 320 | * | |
| 321 | * @method run | |
| 322 | * @param {string} driverName Name of the requested driver | |
| 323 | * @param {object} driverModule Instance of the used driver module | |
| 324 | * @param {string} browser Name of the requested browser | |
| 325 | * @param {function} callback Asyncs next() callback function | |
| 326 | * @chainable | |
| 327 | */ | |
| 328 | ||
| 329 | run: function (driverName, driverModule, browser, callback) { | |
| 330 | // load browser configuration | |
| 331 | 0 | var browsersRaw = this.config.get('browsers'); |
| 332 | 0 | var browsers = []; |
| 333 | ||
| 334 | // Check if we have a valid browser conf, then get the data out | |
| 335 | 0 | if (browsersRaw !== null) { |
| 336 | 0 | browsers = browsersRaw[0]; |
| 337 | } | |
| 338 | ||
| 339 | // init the browser configuration | |
| 340 | 0 | var browserConfiguration = this.loadBrowserConfiguration(browser, browsers, driverModule); |
| 341 | ||
| 342 | // check if we need to inject the browser alias into the browser module | |
| 343 | 0 | if (browserConfiguration.module.setBrowser) { |
| 344 | 0 | browserConfiguration.module.setBrowser(browser); |
| 345 | } | |
| 346 | ||
| 347 | // init the driver instance | |
| 348 | 0 | var driverInstance = driverModule.create({events: this.driverEmitter, reporter: this.reporterEvents, browser: browser, config: this.config, browserMo: browserConfiguration.module, browserConf: browserConfiguration.configuration}); |
| 349 | // couple driver & session status events for the reporter | |
| 350 | 0 | this.coupleReporterEvents(driverName, browser); |
| 351 | ||
| 352 | // register shutdown handler | |
| 353 | 0 | if (driverInstance.webdriverClient.opts && driverInstance.webdriverClient.opts.kill) { |
| 354 | 0 | this.driverEmitter.on('killAll', driverInstance.webdriverClient.opts.kill.bind(driverInstance.webdriverClient.opts)); |
| 355 | } | |
| 356 | ||
| 357 | 0 | if (driverInstance.webdriverClient.quit) { |
| 358 | 0 | this.driverEmitter.on('killAll', driverInstance.webdriverClient.quit.bind(driverInstance.webdriverClient)); |
| 359 | } | |
| 360 | ||
| 361 | // dispatch some (web)driver events to the reporter | |
| 362 | 0 | this.driverEmitter.on('driver:webdriver:response', function (res) { |
| 363 | 0 | this.reporterEvents.emit('report:log:system:webdriver', 'webdriver: ' + res.statusCode + ' ' + res.method + ' ' + res.path); |
| 364 | 0 | this.reporterEvents.emit('report:log:system:webdriver', 'webdriver: ' + res.data); |
| 365 | }.bind(this)); | |
| 366 | ||
| 367 | // run the tests in the browser, when the driver is ready | |
| 368 | // emit the tests:complete event, when all tests have been run | |
| 369 | 0 | this.driverEmitter.on('driver:ready:' + driverName + ':' + browser, this._onDriverReady.bind(this, browser, driverName, callback, driverInstance)); |
| 370 | 0 | return this; |
| 371 | } | |
| 372 | }; | |
| 373 | ||
| 374 | // export driver module | |
| 375 | 1 | module.exports = Driver; |
| 376 |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * | |
| 3 | * Copyright (c) 2013 Sebastian Golasch | |
| 4 | * | |
| 5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
| 6 | * copy of this software and associated documentation files (the "Software"), | |
| 7 | * to deal in the Software without restriction, including without limitation | |
| 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
| 9 | * and/or sell copies of the Software, and to permit persons to whom the | |
| 10 | * Software is furnished to do so, subject to the following conditions: | |
| 11 | * | |
| 12 | * The above copyright notice and this permission notice shall be included | |
| 13 | * in all copies or substantial portions of the Software. | |
| 14 | * | |
| 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
| 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
| 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
| 21 | * DEALINGS IN THE SOFTWARE. | |
| 22 | * | |
| 23 | */ | |
| 24 | ||
| 25 | 1 | 'use strict'; |
| 26 | ||
| 27 | // ext. libs | |
| 28 | 1 | var http = require('http'); |
| 29 | 1 | var os = require('os'); |
| 30 | 1 | var Q = require('q'); |
| 31 | ||
| 32 | /** | |
| 33 | * Sets the configuration options for the | |
| 34 | * dalek remote browser executor | |
| 35 | * | |
| 36 | * @param {options} opts Configuration options | |
| 37 | * @constructor | |
| 38 | */ | |
| 39 | ||
| 40 | 1 | var Host = function (opts) { |
| 41 | 0 | this.reporterEvents = opts.reporterEvents; |
| 42 | 0 | this.config = opts.config; |
| 43 | }; | |
| 44 | ||
| 45 | /** | |
| 46 | * Remote Dalek host proxy | |
| 47 | * | |
| 48 | * @module Dalek | |
| 49 | * @class Host | |
| 50 | * @part Remote | |
| 51 | * @api | |
| 52 | */ | |
| 53 | ||
| 54 | 1 | Host.prototype = { |
| 55 | ||
| 56 | /** | |
| 57 | * Default port that the Dalek remote server is linking against | |
| 58 | * | |
| 59 | * @property defaultPort | |
| 60 | * @type {integer} | |
| 61 | * @default 9020 | |
| 62 | */ | |
| 63 | ||
| 64 | defaultPort: 9020, | |
| 65 | ||
| 66 | /** | |
| 67 | * Instance of the local browser | |
| 68 | * | |
| 69 | * @property bro | |
| 70 | * @type {object} | |
| 71 | * @default null | |
| 72 | */ | |
| 73 | ||
| 74 | bro: null, | |
| 75 | ||
| 76 | /** | |
| 77 | * Instance of the reporter event emitter | |
| 78 | * | |
| 79 | * @property reporterEvents | |
| 80 | * @type {EventEmitter2} | |
| 81 | * @default null | |
| 82 | */ | |
| 83 | ||
| 84 | reporterEvents: null, | |
| 85 | ||
| 86 | /** | |
| 87 | * Instance of the dalek config | |
| 88 | * | |
| 89 | * @property config | |
| 90 | * @type {Dalek.Config} | |
| 91 | * @default null | |
| 92 | */ | |
| 93 | ||
| 94 | config: null, | |
| 95 | ||
| 96 | /** | |
| 97 | * Local configuration | |
| 98 | * | |
| 99 | * @property configuration | |
| 100 | * @type {object} | |
| 101 | * @default {} | |
| 102 | */ | |
| 103 | ||
| 104 | configuration: {}, | |
| 105 | ||
| 106 | /** | |
| 107 | * Host address of the called webdriver server | |
| 108 | * | |
| 109 | * @property remoteHost | |
| 110 | * @type {string} | |
| 111 | * @default null | |
| 112 | */ | |
| 113 | ||
| 114 | remoteHost: null, | |
| 115 | ||
| 116 | /** | |
| 117 | * Path of the webdriver server endpoint | |
| 118 | * | |
| 119 | * @property remotePath | |
| 120 | * @type {string} | |
| 121 | * @default null | |
| 122 | */ | |
| 123 | ||
| 124 | remotePath: null, | |
| 125 | ||
| 126 | /** | |
| 127 | * Port of the called webdriver server | |
| 128 | * | |
| 129 | * @property remotePort | |
| 130 | * @type {string} | |
| 131 | * @default null | |
| 132 | */ | |
| 133 | ||
| 134 | remotePort: null, | |
| 135 | ||
| 136 | /** | |
| 137 | * Secret that got emitted by the remote instance | |
| 138 | * | |
| 139 | * @property remoteSecret | |
| 140 | * @type {string} | |
| 141 | * @default null | |
| 142 | */ | |
| 143 | ||
| 144 | remoteSecret: null, | |
| 145 | ||
| 146 | /** | |
| 147 | * Identifier of the remote client | |
| 148 | * | |
| 149 | * @property remoteId | |
| 150 | * @type {string} | |
| 151 | * @default null | |
| 152 | */ | |
| 153 | ||
| 154 | remoteId: null, | |
| 155 | ||
| 156 | /** | |
| 157 | * Secret that is stored in the local instance | |
| 158 | * | |
| 159 | * @property secret | |
| 160 | * @type {string} | |
| 161 | * @default null | |
| 162 | */ | |
| 163 | ||
| 164 | secret: null, | |
| 165 | ||
| 166 | /** | |
| 167 | * Incoming message that needs to be proxied | |
| 168 | * to the local webdriver server | |
| 169 | * | |
| 170 | * @property proxyRequest | |
| 171 | * @type {http.IncomingMessage} | |
| 172 | * @default null | |
| 173 | */ | |
| 174 | ||
| 175 | proxyRequest: null, | |
| 176 | ||
| 177 | /** | |
| 178 | * Starts the remote proxy server, | |
| 179 | * prepares the config | |
| 180 | * | |
| 181 | * @method run | |
| 182 | * @param {object} opts Configuration options | |
| 183 | * @chainable | |
| 184 | */ | |
| 185 | ||
| 186 | run: function (opts) { | |
| 187 | // apply configuration | |
| 188 | 0 | this.configuration = this.config.get('host') || {}; |
| 189 | 0 | this.configuration.host = this.configuration.host ? !this.configuration.port : 'localhost'; |
| 190 | 0 | this.secret = this.configuration.secret ? this.configuration.secret : this.secret; |
| 191 | 0 | if (!this.configuration.port || opts.port) { |
| 192 | 0 | this.configuration.port = opts.port ? opts.port : this.defaultPort; |
| 193 | } | |
| 194 | ||
| 195 | // start the proxy server// emit the instance ready event | |
| 196 | 0 | this.server = http.createServer(this._createServer.bind(this)).listen(this.configuration.port, this.reporterEvents.emit.bind(this.reporterEvents, 'report:remote:ready', {ip: this._getLocalIp(), port: this.configuration.port})); |
| 197 | 0 | return this; |
| 198 | }, | |
| 199 | ||
| 200 | /** | |
| 201 | * Shutdown the proxy server | |
| 202 | * | |
| 203 | * @method kill | |
| 204 | * @return {object} Promise | |
| 205 | */ | |
| 206 | ||
| 207 | kill: function () { | |
| 208 | 0 | var deferred = Q.defer(); |
| 209 | 0 | this.server.close(deferred.resolve); |
| 210 | 0 | return deferred.promise; |
| 211 | }, | |
| 212 | ||
| 213 | /** | |
| 214 | * Launches the local browser | |
| 215 | * | |
| 216 | * @method _launcher | |
| 217 | * @param {object} request Request from the dalek remote caller | |
| 218 | * @param {object} response Response to the dalek remote caller | |
| 219 | * @private | |
| 220 | * @chainable | |
| 221 | */ | |
| 222 | ||
| 223 | _launcher: function (request, response) { | |
| 224 | // extract the browser id from the request url | |
| 225 | 0 | var browser = this._extractBrowser(request.url); |
| 226 | ||
| 227 | // load local browser module | |
| 228 | 0 | this.bro = this._loadBrowserModule(browser, response); |
| 229 | ||
| 230 | // launch the local browser | |
| 231 | 0 | if (this.bro) { |
| 232 | 0 | this.bro |
| 233 | .launch({}, this.reporterEvents, this.config) | |
| 234 | .then(this._onBrowserLaunch.bind(this, browser, response)); | |
| 235 | } | |
| 236 | ||
| 237 | 0 | return this; |
| 238 | }, | |
| 239 | ||
| 240 | /** | |
| 241 | * Shuts the local browser down, | |
| 242 | * end the otherwise hanging request | |
| 243 | * | |
| 244 | * @method _launcher | |
| 245 | * @param {object} response Response to the dalek remote caller | |
| 246 | * @private | |
| 247 | * @chainable | |
| 248 | */ | |
| 249 | ||
| 250 | _killer: function (response) { | |
| 251 | 0 | if (this.bro) { |
| 252 | 0 | this.bro.kill(); |
| 253 | } | |
| 254 | 0 | response.setHeader('Connection', 'close'); |
| 255 | 0 | response.end(); |
| 256 | 0 | this.reporterEvents.emit('report:remote:closed', {id: this.remoteId, browser: this.bro.longName}); |
| 257 | 0 | return this; |
| 258 | }, | |
| 259 | ||
| 260 | /** | |
| 261 | * Requires the local browser module & returns it | |
| 262 | * | |
| 263 | * @method _loadBrowserModule | |
| 264 | * @param {string} browser Name of the browser to load | |
| 265 | * @param {object} response Response to the dalek remote caller | |
| 266 | * @return {object} The local browser module | |
| 267 | * @private | |
| 268 | */ | |
| 269 | ||
| 270 | _loadBrowserModule: function (browser, response) { | |
| 271 | 0 | var bro = null; |
| 272 | 0 | try { |
| 273 | 0 | bro = require('dalek-browser-' + browser); |
| 274 | } catch (e) { | |
| 275 | 0 | try { |
| 276 | 0 | bro = require('dalek-browser-' + browser + '-canary'); |
| 277 | } catch (e) { | |
| 278 | 0 | response.setHeader('Connection', 'close'); |
| 279 | 0 | response.end(JSON.stringify({error: 'The requested browser "' + browser + '" could not be loaded'})); |
| 280 | } | |
| 281 | } | |
| 282 | ||
| 283 | 0 | return bro; |
| 284 | }, | |
| 285 | ||
| 286 | /** | |
| 287 | * Stores network data from the local browser instance, | |
| 288 | * sends browser specific data to the client | |
| 289 | * | |
| 290 | * @method _onBrowserLaunch | |
| 291 | * @param {string} browser Name of the browser to load | |
| 292 | * @param {object} response Response to the dalek remote caller | |
| 293 | * @chainable | |
| 294 | * @private | |
| 295 | */ | |
| 296 | ||
| 297 | _onBrowserLaunch: function (browser, response) { | |
| 298 | 0 | this.remoteHost = this.bro.getHost(); |
| 299 | 0 | this.remotePort = this.bro.getPort(); |
| 300 | 0 | this.remotePath = this.bro.path.replace('/', ''); |
| 301 | 0 | this.reporterEvents.emit('report:remote:established', {id: this.remoteId, browser: this.bro.longName}); |
| 302 | 0 | response.setHeader('Connection', 'close'); |
| 303 | 0 | response.end(JSON.stringify({browser: browser, caps: this.bro.desiredCapabilities, defaults: this.bro.driverDefaults, name: this.bro.longName})); |
| 304 | 0 | return this; |
| 305 | }, | |
| 306 | ||
| 307 | /** | |
| 308 | * Dispatches all incoming requests, | |
| 309 | * possible endpoints local webdriver server, | |
| 310 | * browser launcher, browser shutdown handler | |
| 311 | * | |
| 312 | * @method _createServer | |
| 313 | * @param {object} request Request from the dalek remote caller | |
| 314 | * @param {object} response Response to the dalek remote caller | |
| 315 | * @private | |
| 316 | * @chainable | |
| 317 | */ | |
| 318 | ||
| 319 | _createServer: function (request, response) { | |
| 320 | // delegate calls based on url | |
| 321 | 0 | if (request.url.search('/dalek/launch/') !== -1) { |
| 322 | ||
| 323 | // store the remotes ip address | |
| 324 | 0 | this.remoteId = request.connection.remoteAddress; |
| 325 | ||
| 326 | // store the remote secret | |
| 327 | 0 | if (request.headers['secret-token']) { |
| 328 | 0 | this.remoteSecret = request.headers['secret-token']; |
| 329 | } | |
| 330 | ||
| 331 | // check if the secrets match, then launch browser | |
| 332 | // else emit an error | |
| 333 | 0 | if (this.secret === this.remoteSecret) { |
| 334 | 0 | this._launcher(request, response); |
| 335 | } else { | |
| 336 | 0 | response.setHeader('Connection', 'close'); |
| 337 | 0 | response.end(JSON.stringify({error: 'Secrets do not match'})); |
| 338 | } | |
| 339 | ||
| 340 | 0 | } else if (request.url.search('/dalek/kill') !== -1) { |
| 341 | 0 | this._killer(response); |
| 342 | } else { | |
| 343 | 0 | this.proxyRequest = http.request(this._generateProxyRequestOptions(request.headers, request.method, request.url), this._onProxyRequest.bind(this, response, request)); |
| 344 | 0 | request.on('data', this._onRequestDataChunk.bind(this)); |
| 345 | 0 | request.on('end', this.proxyRequest.end.bind(this.proxyRequest)); |
| 346 | } | |
| 347 | ||
| 348 | 0 | return this; |
| 349 | }, | |
| 350 | ||
| 351 | /** | |
| 352 | * Proxies data from the local webdriver server to the client | |
| 353 | * | |
| 354 | * @method _onRequestDataChunk | |
| 355 | * @param {buffer} chunk Chunk of the incoming request data | |
| 356 | * @private | |
| 357 | * @chainable | |
| 358 | */ | |
| 359 | ||
| 360 | _onRequestDataChunk: function (chunk) { | |
| 361 | 0 | this.proxyRequest.write(chunk, 'binary'); |
| 362 | 0 | return this; |
| 363 | }, | |
| 364 | ||
| 365 | /** | |
| 366 | * Proxies remote data to the webdriver server | |
| 367 | * | |
| 368 | * @method _onProxyRequest | |
| 369 | * @param {object} request Request from the dalek remote caller | |
| 370 | * @param {object} response Response to the dalek remote caller | |
| 371 | * @param {object} res Response to the local webdriver server | |
| 372 | * @private | |
| 373 | * @chainable | |
| 374 | */ | |
| 375 | ||
| 376 | _onProxyRequest: function (response, request, res) { | |
| 377 | 0 | var chunks = []; |
| 378 | ||
| 379 | // deny access if the remote ids (onitial request, webdriver request) do not match | |
| 380 | 0 | if (this.remoteId !== request.connection.remoteAddress) { |
| 381 | 0 | response.setHeader('Connection', 'close'); |
| 382 | 0 | response.end(); |
| 383 | 0 | return this; |
| 384 | } | |
| 385 | ||
| 386 | 0 | res.on('data', function (chunk) { |
| 387 | 0 | chunks.push(chunk+''); |
| 388 | }).on('end', this._onProxyRequestEnd.bind(this, res, response, request, chunks)); | |
| 389 | 0 | return this; |
| 390 | }, | |
| 391 | ||
| 392 | /** | |
| 393 | * Handles data exchange between the client and the | |
| 394 | * local webdriver server | |
| 395 | * | |
| 396 | * @method _onProxyRequest | |
| 397 | * @param {object} request Request from the dalek remote caller | |
| 398 | * @param {object} response Response to the dalek remote caller | |
| 399 | * @param {object} res Response to the local webdriver server | |
| 400 | * @param {array} chunks Array of received data pieces that should be forwarded to the local webdriver server | |
| 401 | * @private | |
| 402 | * @chainable | |
| 403 | */ | |
| 404 | ||
| 405 | _onProxyRequestEnd: function (res, response, request, chunks) { | |
| 406 | 0 | var buf = ''; |
| 407 | ||
| 408 | // proxy headers for the session request | |
| 409 | 0 | if (request.url === '/session') { |
| 410 | 0 | response.setHeader('Connection', 'close'); |
| 411 | 0 | Object.keys(res.headers).forEach(function (key) { |
| 412 | 0 | response.setHeader(key, res.headers[key]); |
| 413 | }); | |
| 414 | } | |
| 415 | ||
| 416 | 0 | if (chunks.length) { |
| 417 | 0 | buf = chunks.join(''); |
| 418 | } | |
| 419 | ||
| 420 | 0 | response.write(buf); |
| 421 | 0 | response.end(); |
| 422 | 0 | return this; |
| 423 | }, | |
| 424 | ||
| 425 | /** | |
| 426 | * Extracts the browser that should be launched | |
| 427 | * from the launch url request | |
| 428 | * | |
| 429 | * @method _extractBrowser | |
| 430 | * @param {string} url Url to parse | |
| 431 | * @return {string} Extracted browser | |
| 432 | * @private | |
| 433 | */ | |
| 434 | ||
| 435 | _extractBrowser: function (url) { | |
| 436 | 0 | return url.replace('/dalek/launch/', ''); |
| 437 | }, | |
| 438 | ||
| 439 | /** | |
| 440 | * Generates the request options from the incoming | |
| 441 | * request that should then be forwared to the local | |
| 442 | * webdriver server | |
| 443 | * | |
| 444 | * @method _generateProxyRequestOptions | |
| 445 | * @param {object} header Header meta data | |
| 446 | * @param {string} method HTTP method | |
| 447 | * @param {string} url Webriver server endpoint url | |
| 448 | * @return {object} Request options | |
| 449 | * @private | |
| 450 | */ | |
| 451 | ||
| 452 | _generateProxyRequestOptions: function (headers, method, url) { | |
| 453 | 0 | var options = { |
| 454 | host: this.remoteHost, | |
| 455 | port: this.remotePort, | |
| 456 | path: this.remotePath + url, | |
| 457 | method: method, | |
| 458 | headers: { | |
| 459 | 'Content-Type': headers['content-type'], | |
| 460 | 'Content-Length': headers['content-length'] | |
| 461 | } | |
| 462 | }; | |
| 463 | ||
| 464 | // check if the path is valid, | |
| 465 | // else prepend a `root` slash | |
| 466 | 0 | if (options.path.charAt(0) !== '/') { |
| 467 | 0 | options.path = '/' + options.path; |
| 468 | } | |
| 469 | ||
| 470 | 0 | return options; |
| 471 | }, | |
| 472 | ||
| 473 | /** | |
| 474 | * Gets the local ip address | |
| 475 | * (should be the IPv4 address where the runner is accessible from) | |
| 476 | * | |
| 477 | * @method _getLocalIp | |
| 478 | * @return {string} Local IP address | |
| 479 | * @private | |
| 480 | */ | |
| 481 | ||
| 482 | _getLocalIp: function () { | |
| 483 | 0 | var ifaces = os.networkInterfaces(); |
| 484 | 0 | var address = [null]; |
| 485 | 0 | for (var dev in ifaces) { |
| 486 | 0 | var alias = [0]; |
| 487 | 0 | ifaces[dev].forEach(this._grepIp.bind(this, alias, address)); |
| 488 | } | |
| 489 | ||
| 490 | 0 | return address[0]; |
| 491 | }, | |
| 492 | ||
| 493 | /** | |
| 494 | * Tries to find the local IP address | |
| 495 | * | |
| 496 | * @method _grepIp | |
| 497 | * @param | |
| 498 | * @param | |
| 499 | * @param | |
| 500 | * @chainable | |
| 501 | * @private | |
| 502 | */ | |
| 503 | ||
| 504 | _grepIp: function (alias, address, details) { | |
| 505 | 0 | if (details.family === 'IPv4') { |
| 506 | 0 | if (details.address !== '127.0.0.1') { |
| 507 | 0 | address[0] = details.address; |
| 508 | } | |
| 509 | 0 | ++alias[0]; |
| 510 | } | |
| 511 | ||
| 512 | 0 | return this; |
| 513 | } | |
| 514 | ||
| 515 | }; | |
| 516 | ||
| 517 | 1 | module.exports = Host; |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * | |
| 3 | * Copyright (c) 2013 Sebastian Golasch | |
| 4 | * | |
| 5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
| 6 | * copy of this software and associated documentation files (the "Software"), | |
| 7 | * to deal in the Software without restriction, including without limitation | |
| 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
| 9 | * and/or sell copies of the Software, and to permit persons to whom the | |
| 10 | * Software is furnished to do so, subject to the following conditions: | |
| 11 | * | |
| 12 | * The above copyright notice and this permission notice shall be included | |
| 13 | * in all copies or substantial portions of the Software. | |
| 14 | * | |
| 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
| 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
| 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
| 21 | * DEALINGS IN THE SOFTWARE. | |
| 22 | * | |
| 23 | */ | |
| 24 | ||
| 25 | 1 | 'use strict'; |
| 26 | ||
| 27 | /** | |
| 28 | * Checks & loads reporter modules | |
| 29 | * | |
| 30 | * @module DalekJS | |
| 31 | * @class Reporter | |
| 32 | * @namespace Dalek | |
| 33 | * @part Reporter | |
| 34 | * @api | |
| 35 | */ | |
| 36 | ||
| 37 | 1 | var Reporter = { |
| 38 | ||
| 39 | /** | |
| 40 | * Reporters from the canary channel | |
| 41 | * | |
| 42 | * @param isCanary | |
| 43 | */ | |
| 44 | ||
| 45 | isCanary: {}, | |
| 46 | ||
| 47 | /** | |
| 48 | * Checks if the requested reporter exists | |
| 49 | * | |
| 50 | * @method isReporter | |
| 51 | * @param {string} reporter Name of the reporter | |
| 52 | * @return {bool} isReporter Reporter exists | |
| 53 | */ | |
| 54 | ||
| 55 | isReporter: function (reporter) { | |
| 56 | 7 | try { |
| 57 | 7 | require.resolve('dalek-reporter-' + reporter); |
| 58 | } catch (e) { | |
| 59 | 0 | try { |
| 60 | 0 | require.resolve('dalek-reporter-' + reporter + '-canary'); |
| 61 | } catch (e) { | |
| 62 | 0 | return false; |
| 63 | } | |
| 64 | ||
| 65 | 0 | this.isCanary[reporter] = true; |
| 66 | 0 | return true; |
| 67 | } | |
| 68 | 7 | return true; |
| 69 | }, | |
| 70 | ||
| 71 | /** | |
| 72 | * Loads a requested reporter | |
| 73 | * | |
| 74 | * @method loadReporter | |
| 75 | * @param {string} reporter Name of the reporter | |
| 76 | * @param {object} options Options to pass to the reporter | |
| 77 | * @return {object} reporterInstance Reporter instance | |
| 78 | */ | |
| 79 | ||
| 80 | loadReporter: function (reporter, options) { | |
| 81 | 7 | return require('dalek-reporter-' + reporter + (this.isCanary[reporter] ? '-canary' : ''))(options); |
| 82 | } | |
| 83 | }; | |
| 84 | ||
| 85 | // export the module | |
| 86 | 1 | module.exports = Reporter; |
| 87 |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * | |
| 3 | * Copyright (c) 2013 Sebastian Golasch | |
| 4 | * | |
| 5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
| 6 | * copy of this software and associated documentation files (the "Software"), | |
| 7 | * to deal in the Software without restriction, including without limitation | |
| 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
| 9 | * and/or sell copies of the Software, and to permit persons to whom the | |
| 10 | * Software is furnished to do so, subject to the following conditions: | |
| 11 | * | |
| 12 | * The above copyright notice and this permission notice shall be included | |
| 13 | * in all copies or substantial portions of the Software. | |
| 14 | * | |
| 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
| 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
| 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
| 21 | * DEALINGS IN THE SOFTWARE. | |
| 22 | * | |
| 23 | */ | |
| 24 | ||
| 25 | 1 | 'use strict'; |
| 26 | ||
| 27 | // ext. libs | |
| 28 | 1 | var _ = require('lodash'); |
| 29 | 1 | var fs = require('fs'); |
| 30 | 1 | var EventEmitter2 = require('eventemitter2').EventEmitter2; |
| 31 | ||
| 32 | // int. libs | |
| 33 | 1 | var unit = require('./unit'); |
| 34 | ||
| 35 | /** | |
| 36 | * @constructor | |
| 37 | * @param {object} options | |
| 38 | */ | |
| 39 | ||
| 40 | 1 | var Suite = function (options) { |
| 41 | 0 | this.emitter = new EventEmitter2(); |
| 42 | 0 | this.emitter.setMaxListeners(Infinity); |
| 43 | 0 | this.initialize(options); |
| 44 | 0 | this.suite = this.loadTestsuite(options.file); |
| 45 | }; | |
| 46 | ||
| 47 | /** | |
| 48 | * Suite (Testsuite) | |
| 49 | * | |
| 50 | * @module DalekJS | |
| 51 | * @class Suite | |
| 52 | * @namespace Dalek | |
| 53 | * @part Testsuite | |
| 54 | * @api | |
| 55 | */ | |
| 56 | ||
| 57 | 1 | Suite.prototype = { |
| 58 | ||
| 59 | /** | |
| 60 | * Assigns the initial options | |
| 61 | * driverEmitter -> the drivers event dispatcher | |
| 62 | * reporterEmitter -> the reporters event dispatcher | |
| 63 | * driver -> the driver instance (e.g. native webdriver, selenium, etc.) | |
| 64 | * name -> the suites filename (default suite name) | |
| 65 | * | |
| 66 | * @method initialize | |
| 67 | * @param {object} options | |
| 68 | * @chainable | |
| 69 | */ | |
| 70 | ||
| 71 | initialize: function (options) { | |
| 72 | 0 | this.driverEmitter = options.driverEmitter; |
| 73 | 0 | this.reporterEmitter = options.reporterEmitter; |
| 74 | 0 | this.driver = options.driver; |
| 75 | 0 | this.name = options.file; |
| 76 | 0 | this.numberOfSuites = options.numberOfSuites; |
| 77 | 0 | this.error = null; |
| 78 | 0 | return this; |
| 79 | }, | |
| 80 | ||
| 81 | /** | |
| 82 | * Loads the testsuite that should be executed | |
| 83 | * | |
| 84 | * @method loadTestsuite | |
| 85 | * @param {string} testfile | |
| 86 | * @return {object} testsuite | |
| 87 | */ | |
| 88 | ||
| 89 | loadTestsuite: function (testfile) { | |
| 90 | 0 | var suite = {}; |
| 91 | ||
| 92 | // if the tests were passed in *as* a list of tests, just use them | |
| 93 | 0 | if (testfile && typeof testfile === 'object') { |
| 94 | 0 | var allAreTestFunctions = true; |
| 95 | 0 | for (var testname in testfile) { |
| 96 | 0 | if (typeof testfile[testname] !== 'function') { allAreTestFunctions = false; } |
| 97 | } | |
| 98 | 0 | if (allAreTestFunctions) { |
| 99 | 0 | return testfile; |
| 100 | } | |
| 101 | } | |
| 102 | ||
| 103 | // catch any errors, like falsy requires & stuff | |
| 104 | 0 | try { |
| 105 | ||
| 106 | 0 | if (fs.existsSync(process.cwd() + '/' + testfile)) { |
| 107 | 0 | suite = require(process.cwd() + '/' + testfile.replace('.js', '')); |
| 108 | } else { | |
| 109 | 0 | this.error = 'Suite "' + testfile + '" does not exist. Skipping!'; |
| 110 | 0 | return suite; |
| 111 | } | |
| 112 | } catch (e) { | |
| 113 | 0 | this.error = '\n' + e.name + ': ' + e.message + '\nFailure loading suite "' + testfile + '". Skipping!' + e; |
| 114 | 0 | return suite; |
| 115 | } | |
| 116 | ||
| 117 | 0 | suite._uid = _.uniqueId('Suite'); |
| 118 | 0 | return suite; |
| 119 | }, | |
| 120 | ||
| 121 | /** | |
| 122 | * Checks if all tests from the testsuite are executed. | |
| 123 | * Runs the next test if not. | |
| 124 | * Triggers `asyncs` callback if the suite is finished. | |
| 125 | * Decrements the `testsToBeExecuted` counter | |
| 126 | * | |
| 127 | * @method testFinished | |
| 128 | * @param {function} callback | |
| 129 | * @param {array} tests | |
| 130 | * @param {object} test | |
| 131 | * @param {string} event | |
| 132 | * @chainable | |
| 133 | */ | |
| 134 | ||
| 135 | testFinished: function (callback, tests) { | |
| 136 | 0 | var complete = function() { |
| 137 | // check if there are still tests that should be executed in this suite, | |
| 138 | // if so, run them | |
| 139 | 0 | if (this.decrementTestsToBeExecuted() > 1) { |
| 140 | 0 | this.executeNextTest(tests); |
| 141 | 0 | return this; |
| 142 | } | |
| 143 | ||
| 144 | // run a function after the testsuite, if given | |
| 145 | 0 | if (this.options.teardown) { |
| 146 | 0 | this.options.teardown(); |
| 147 | } | |
| 148 | ||
| 149 | // emit the suite finished event | |
| 150 | 0 | this.reporterEmitter.emit('report:testsuite:finished', this.name); |
| 151 | ||
| 152 | // move on to the next suite | |
| 153 | 0 | callback(); |
| 154 | 0 | return this; |
| 155 | }.bind(this); | |
| 156 | ||
| 157 | // run a function after the test, if given | |
| 158 | ||
| 159 | 0 | if (typeof this.options.afterEach === 'function') { |
| 160 | // If there is an argument, assume async. | |
| 161 | 0 | if (this.options.afterEach.length === 1) { |
| 162 | 0 | this.options.afterEach(function() { |
| 163 | 0 | return complete(); |
| 164 | }.bind(this)); | |
| 165 | } else { | |
| 166 | // no argument, assume sync. | |
| 167 | 0 | this.options.afterEach(); |
| 168 | 0 | return complete(); |
| 169 | } | |
| 170 | } else { | |
| 171 | 0 | return complete(); |
| 172 | } | |
| 173 | ||
| 174 | }, | |
| 175 | ||
| 176 | /** | |
| 177 | * Decrements number of tests that should be executed in this suite | |
| 178 | * | |
| 179 | * @method decrementTestsToBeExecuted | |
| 180 | * @return {integer} numberOfTestsToBeExecuted | |
| 181 | */ | |
| 182 | ||
| 183 | decrementTestsToBeExecuted: function () { | |
| 184 | 0 | return (this.testsToBeExecuted--) -1; |
| 185 | }, | |
| 186 | ||
| 187 | /** | |
| 188 | * Returns the name of the testsuite | |
| 189 | * If the suite has no name, it will return the testsuites filename | |
| 190 | * | |
| 191 | * @method getName | |
| 192 | * @return {string} name | |
| 193 | */ | |
| 194 | ||
| 195 | getName: function () { | |
| 196 | 0 | if (this.suite.name && _.isString(this.suite.name)) { |
| 197 | 0 | var name = this.suite.name; |
| 198 | 0 | delete this.suite.name; |
| 199 | 0 | return name; |
| 200 | } | |
| 201 | ||
| 202 | 0 | return this.name; |
| 203 | }, | |
| 204 | ||
| 205 | /** | |
| 206 | * Returns the options of the testsuite | |
| 207 | * If the suite has no options, it will return an empty object | |
| 208 | * | |
| 209 | * @method getOptions | |
| 210 | * @return {object} options Suite options | |
| 211 | */ | |
| 212 | ||
| 213 | getOptions: function () { | |
| 214 | 0 | if (this.suite.options && _.isObject(this.suite.options)) { |
| 215 | 0 | var options = this.suite.options; |
| 216 | 0 | delete this.suite.options; |
| 217 | 0 | return options; |
| 218 | } | |
| 219 | ||
| 220 | 0 | return {}; |
| 221 | }, | |
| 222 | ||
| 223 | /** | |
| 224 | * Returns all names (aka. object keys) the tests that should be executed | |
| 225 | * | |
| 226 | * @method getTests | |
| 227 | * @return {array} test | |
| 228 | */ | |
| 229 | ||
| 230 | getTests: function () { | |
| 231 | 0 | return Object.keys(this.suite); |
| 232 | }, | |
| 233 | ||
| 234 | /** | |
| 235 | * Returns the number of tests to be executed | |
| 236 | * | |
| 237 | * @method getNumberOfTests | |
| 238 | * @param {array} tests | |
| 239 | * @return {integer} numberOfTests | |
| 240 | */ | |
| 241 | ||
| 242 | getNumberOfTests: function (tests) { | |
| 243 | 0 | return tests.length; |
| 244 | }, | |
| 245 | ||
| 246 | /** | |
| 247 | * Returns the next test, that should be executed | |
| 248 | * | |
| 249 | * @method getNextTest | |
| 250 | * @return {string} testName | |
| 251 | */ | |
| 252 | ||
| 253 | getNextTest: function (tests) { | |
| 254 | 0 | return tests.shift(); |
| 255 | }, | |
| 256 | ||
| 257 | /** | |
| 258 | * Executes the next test in the sequence | |
| 259 | * | |
| 260 | * @method executeNextTest | |
| 261 | * @param {array} tests | |
| 262 | * @return {mixed} testValue | |
| 263 | */ | |
| 264 | ||
| 265 | executeNextTest: function (tests, callback) { | |
| 266 | 0 | var cb = callback || function() {}; |
| 267 | // grab the next test in the queue | |
| 268 | 0 | var testName = this.getNextTest(tests); |
| 269 | // get the next test function | |
| 270 | 0 | var testFunction = this.getTest(testName); |
| 271 | // generate an instance of the test | |
| 272 | 0 | var test = this.getTestInstance(testName); |
| 273 | // run a setup function before the test, if given | |
| 274 | 0 | if (typeof this.options.beforeEach !== 'function') { |
| 275 | 0 | cb(null, null); |
| 276 | 0 | testFunction.apply(test,[test]); |
| 277 | 0 | return this; |
| 278 | } | |
| 279 | 0 | if (this.options.beforeEach.length === 1) { |
| 280 | // if function takes an argument, assume async | |
| 281 | 0 | this.options.beforeEach(function() { |
| 282 | // start it | |
| 283 | 0 | testFunction.apply(test,[test]); |
| 284 | 0 | cb(null, null); |
| 285 | }); | |
| 286 | } else { | |
| 287 | // otherwise, assume sync | |
| 288 | 0 | this.options.beforeEach(); |
| 289 | 0 | testFunction.apply(test,[test]); |
| 290 | 0 | cb(null, null); |
| 291 | } | |
| 292 | 0 | return this; |
| 293 | }, | |
| 294 | ||
| 295 | /** | |
| 296 | * Generates a new test instance | |
| 297 | * | |
| 298 | * @method getTestInstance | |
| 299 | * @param {string} name | |
| 300 | * @return {Dalek.Test} test | |
| 301 | */ | |
| 302 | ||
| 303 | getTestInstance: function (name) { | |
| 304 | 0 | return unit({events: this.emitter, driver: this.driver, reporter: this.reporterEmitter, name: name}); |
| 305 | }, | |
| 306 | ||
| 307 | /** | |
| 308 | * Returns a test function by its name | |
| 309 | * | |
| 310 | * @method getTest | |
| 311 | * @param {string} name | |
| 312 | * @return {function} test | |
| 313 | */ | |
| 314 | ||
| 315 | getTest: function (name) { | |
| 316 | 0 | return this.suite[name] && _.isFunction(this.suite[name]) ? this.suite[name] : this.testDoesNotExist; |
| 317 | }, | |
| 318 | ||
| 319 | /** | |
| 320 | * Will be executed if a test is started, that does not exist | |
| 321 | * | |
| 322 | * @method testDoesNotExist | |
| 323 | * @param {object} options | |
| 324 | */ | |
| 325 | ||
| 326 | testDoesNotExist: function (options) { | |
| 327 | 0 | if (options.name) { |
| 328 | 0 | this.reporterEmitter.emit('warning', 'Test "' + options.name + '" does not exist! Skipping.'); |
| 329 | } | |
| 330 | 0 | return this; |
| 331 | }, | |
| 332 | ||
| 333 | /** | |
| 334 | * Runs any tests from this testsuite in sequence | |
| 335 | * | |
| 336 | * @method run | |
| 337 | * @param {function} callback | |
| 338 | * @chainable | |
| 339 | */ | |
| 340 | ||
| 341 | run: function (callback) { | |
| 342 | 0 | var tests = []; |
| 343 | ||
| 344 | // check if the suite is | |
| 345 | 0 | if (this.error) { |
| 346 | 0 | this.reporterEmitter.emit('report:testsuite:started', null); |
| 347 | // emit a warning notice | |
| 348 | 0 | this.reporterEmitter.emit('warning', this.error); |
| 349 | // emit the suite finished event | |
| 350 | 0 | this.reporterEmitter.emit('report:testsuite:finished', null); |
| 351 | // move on to the next suite | |
| 352 | 0 | callback(); |
| 353 | } | |
| 354 | ||
| 355 | // extract suite name | |
| 356 | 0 | this.name = this.getName(); |
| 357 | // extract suite options | |
| 358 | 0 | this.options = this.getOptions(); |
| 359 | ||
| 360 | // extract tests | |
| 361 | 0 | tests = this.getTests(); |
| 362 | 0 | this.testsToBeExecuted = this.numberOfTests = this.getNumberOfTests(tests); |
| 363 | ||
| 364 | // run a function before the testsuite has been launched, if given | |
| 365 | 0 | if (this.options.setup) { |
| 366 | 0 | this.options.setup(); |
| 367 | } | |
| 368 | ||
| 369 | // kickstart the test execution | |
| 370 | 0 | this.executeNextTest(tests, function() { |
| 371 | // emit the suite started event | |
| 372 | 0 | this.reporterEmitter.emit('report:testsuite:started', this.name); |
| 373 | // listen to the test:finished event & then start the next test | |
| 374 | // if there are no tests in this suite left, | |
| 375 | // run the async callback & mark this suite as finished | |
| 376 | 0 | this.emitter.onAny(this.testFinished.bind(this, callback, tests)); |
| 377 | }.bind(this)); | |
| 378 | ||
| 379 | 0 | return this; |
| 380 | } | |
| 381 | }; | |
| 382 | ||
| 383 | // export the testuite instance | |
| 384 | 1 | module.exports = Suite; |
| 385 |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * | |
| 3 | * Copyright (c) 2013 Sebastian Golasch | |
| 4 | * | |
| 5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
| 6 | * copy of this software and associated documentation files (the "Software"), | |
| 7 | * to deal in the Software without restriction, including without limitation | |
| 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
| 9 | * and/or sell copies of the Software, and to permit persons to whom the | |
| 10 | * Software is furnished to do so, subject to the following conditions: | |
| 11 | * | |
| 12 | * The above copyright notice and this permission notice shall be included | |
| 13 | * in all copies or substantial portions of the Software. | |
| 14 | * | |
| 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
| 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
| 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
| 21 | * DEALINGS IN THE SOFTWARE. | |
| 22 | * | |
| 23 | */ | |
| 24 | ||
| 25 | 1 | 'use strict'; |
| 26 | ||
| 27 | /** | |
| 28 | * Initializes the timers default values | |
| 29 | * | |
| 30 | * @constructor | |
| 31 | * @class Timer | |
| 32 | */ | |
| 33 | ||
| 34 | 1 | var Timer = function () { |
| 35 | 9 | this.timerData = [0, 0]; |
| 36 | 9 | this.interval = [0, 0]; |
| 37 | }; | |
| 38 | ||
| 39 | /** | |
| 40 | * Timing module to measure test run times | |
| 41 | * | |
| 42 | * @module DalekJS | |
| 43 | * @class Timer | |
| 44 | * @namespace Dalek | |
| 45 | * @part Timer | |
| 46 | * @api | |
| 47 | */ | |
| 48 | ||
| 49 | 1 | Timer.prototype = { |
| 50 | ||
| 51 | /** | |
| 52 | * Starts the timer | |
| 53 | * | |
| 54 | * @method start | |
| 55 | * @chainable | |
| 56 | */ | |
| 57 | ||
| 58 | start: function () { | |
| 59 | 1 | this.timerData = process.hrtime(); |
| 60 | 1 | return this; |
| 61 | }, | |
| 62 | ||
| 63 | /** | |
| 64 | * Stops the timer | |
| 65 | * | |
| 66 | * @method stop | |
| 67 | * @chainable | |
| 68 | */ | |
| 69 | ||
| 70 | stop: function () { | |
| 71 | 2 | this.interval = process.hrtime(this.timerData); |
| 72 | 2 | return this; |
| 73 | }, | |
| 74 | ||
| 75 | /** | |
| 76 | * Returns the elapsed time in ms | |
| 77 | * | |
| 78 | * @method getElapsedTime | |
| 79 | * @return {float} | |
| 80 | */ | |
| 81 | ||
| 82 | getElapsedTime: function () { | |
| 83 | 2 | return (this.interval[0]*1e3 + this.interval[1]/1e6) / 1000; |
| 84 | }, | |
| 85 | ||
| 86 | /** | |
| 87 | * Returns an object with test run time information | |
| 88 | * containing hours, minutes & seconds | |
| 89 | * | |
| 90 | * @method getElapsedTimeFormatted | |
| 91 | * @return {Object} | |
| 92 | */ | |
| 93 | ||
| 94 | getElapsedTimeFormatted: function () { | |
| 95 | 2 | var hours, minutes, seconds; |
| 96 | 2 | var elapsedTimeInSeconds = this.getElapsedTime(); |
| 97 | ||
| 98 | // assign elapsed time (in seconds) to the seconds output | |
| 99 | 2 | seconds = elapsedTimeInSeconds; |
| 100 | ||
| 101 | // check if the elapsed time in seconds is more than a minute | |
| 102 | // and convert the raw seconds to minutes & seconds | |
| 103 | 2 | if (elapsedTimeInSeconds > 60) { |
| 104 | 2 | minutes = Math.floor(elapsedTimeInSeconds / 60); |
| 105 | 2 | seconds = elapsedTimeInSeconds - minutes * 60; |
| 106 | } | |
| 107 | ||
| 108 | // check if the elapsed time in minutes is more than an hour | |
| 109 | // and convert the raw seconds to hours, minutes & seconds | |
| 110 | 2 | if (minutes > 60) { |
| 111 | 2 | hours = Math.floor(elapsedTimeInSeconds / 3600); |
| 112 | 2 | minutes = elapsedTimeInSeconds - hours * 60; |
| 113 | 2 | seconds = elapsedTimeInSeconds - minutes * 3600; |
| 114 | } | |
| 115 | ||
| 116 | 2 | return {hours: hours, minutes: minutes, seconds: seconds}; |
| 117 | } | |
| 118 | }; | |
| 119 | ||
| 120 | // export the timer module | |
| 121 | 1 | module.exports = Timer; |
| 122 |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * | |
| 3 | * Copyright (c) 2013 Sebastian Golasch | |
| 4 | * | |
| 5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
| 6 | * copy of this software and associated documentation files (the "Software"), | |
| 7 | * to deal in the Software without restriction, including without limitation | |
| 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
| 9 | * and/or sell copies of the Software, and to permit persons to whom the | |
| 10 | * Software is furnished to do so, subject to the following conditions: | |
| 11 | * | |
| 12 | * The above copyright notice and this permission notice shall be included | |
| 13 | * in all copies or substantial portions of the Software. | |
| 14 | * | |
| 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
| 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
| 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
| 21 | * DEALINGS IN THE SOFTWARE. | |
| 22 | * | |
| 23 | */ | |
| 24 | ||
| 25 | 1 | 'use strict'; |
| 26 | ||
| 27 | // Ext. libs | |
| 28 | 1 | var _ = require('lodash'); |
| 29 | 1 | var Q = require('q'); |
| 30 | ||
| 31 | // Int. libs | |
| 32 | 1 | var actions = require('./actions'); |
| 33 | 1 | var assertions = require('./assertions'); |
| 34 | ||
| 35 | // Default timeout for calling done | |
| 36 | 1 | var DONE_TIMEOUT = 10000; |
| 37 | ||
| 38 | /** | |
| 39 | * Prepares the test instance values | |
| 40 | * | |
| 41 | * @param {object} opts Options like the tests name, etc. | |
| 42 | * @constructor | |
| 43 | */ | |
| 44 | ||
| 45 | 1 | var Unit = function (opts) { |
| 46 | // prepare meta queue data | |
| 47 | 0 | this.actionPromiseQueue = []; |
| 48 | ||
| 49 | // prepare assertion data | |
| 50 | 0 | this.expectation = null; |
| 51 | 0 | this.runnedExpactations = 0; |
| 52 | 0 | this.failedAssertions = 0; |
| 53 | ||
| 54 | // prepare test specific data | |
| 55 | 0 | this.name = opts.name; |
| 56 | 0 | this.lastChain = []; |
| 57 | 0 | this.uuids = {}; |
| 58 | 0 | this.contextVars = {}; |
| 59 | ||
| 60 | 0 | if (this.name) { |
| 61 | 0 | this.timeoutForDone = setTimeout(function () { |
| 62 | 0 | this.done(); |
| 63 | 0 | this.reporter.emit('warning', 'done() not called before timeout!'); |
| 64 | }.bind(this), DONE_TIMEOUT); | |
| 65 | } | |
| 66 | }; | |
| 67 | ||
| 68 | /** | |
| 69 | * Generates an test instance | |
| 70 | * | |
| 71 | * @module DalekJS | |
| 72 | * @class Unit | |
| 73 | * @namespace Dalek | |
| 74 | * @part test | |
| 75 | * @api | |
| 76 | */ | |
| 77 | ||
| 78 | 1 | Unit.prototype = { |
| 79 | ||
| 80 | /** | |
| 81 | * Specify how many assertions are expected to run within a test. | |
| 82 | * Very useful for ensuring that all your callbacks and assertions are run. | |
| 83 | * | |
| 84 | * @method expect | |
| 85 | * @param {Integer} expecatation Number of assertions that should be run | |
| 86 | * @chainable | |
| 87 | */ | |
| 88 | ||
| 89 | expect: function (expectation) { | |
| 90 | 0 | this.expectation = parseInt(expectation, 10); |
| 91 | 0 | return this; |
| 92 | }, | |
| 93 | ||
| 94 | /** | |
| 95 | * Global data store (works between the node & browser envs) | |
| 96 | * | |
| 97 | * @method data | |
| 98 | * @param {string|number} key Key to store or fetch data | |
| 99 | * @param {mixed} value *optional* Data that should be stored | |
| 100 | * @return {mixed} Data that has been stored | |
| 101 | * @chainable | |
| 102 | */ | |
| 103 | ||
| 104 | data: function (key, value) { | |
| 105 | 0 | if (value) { |
| 106 | 0 | this.contextVars[key] = value; |
| 107 | 0 | return this; |
| 108 | } | |
| 109 | ||
| 110 | 0 | return this.contextVars[key]; |
| 111 | }, | |
| 112 | ||
| 113 | /** | |
| 114 | * Increment the number of executed assertions | |
| 115 | * | |
| 116 | * @method incrementExpectations | |
| 117 | * @chainable | |
| 118 | */ | |
| 119 | ||
| 120 | incrementExpectations: function () { | |
| 121 | 0 | this.runnedExpactations++; |
| 122 | 0 | return this; |
| 123 | }, | |
| 124 | ||
| 125 | /** | |
| 126 | * Increment the number of failed assertions | |
| 127 | * | |
| 128 | * @method incrementFailedAssertions | |
| 129 | * @chainable | |
| 130 | */ | |
| 131 | ||
| 132 | incrementFailedAssertions: function () { | |
| 133 | 0 | this.failedAssertions++; |
| 134 | 0 | return this; |
| 135 | }, | |
| 136 | ||
| 137 | /** | |
| 138 | * Checks if the runned tests fullfill the set expectations | |
| 139 | * or if no expectations were raised | |
| 140 | * | |
| 141 | * @method checkExpectations | |
| 142 | * @return {bool} checkedExpectations Expectations match | |
| 143 | */ | |
| 144 | ||
| 145 | checkExpectations: function () { | |
| 146 | 0 | return (this.expectation === null || !this.expectation || (this.runnedExpactations === this.expectation)); |
| 147 | }, | |
| 148 | ||
| 149 | /** | |
| 150 | * Checks if all runned assertions passed | |
| 151 | * | |
| 152 | * @method checkAssertions | |
| 153 | * @return {bool} assertionFailed Any expectation failed | |
| 154 | */ | |
| 155 | ||
| 156 | checkAssertions: function () { | |
| 157 | 0 | return this.failedAssertions === 0; |
| 158 | }, | |
| 159 | ||
| 160 | /** | |
| 161 | * Sets up all the bindings needed for a test to run | |
| 162 | * | |
| 163 | * @method done | |
| 164 | * @return {object} result A promise | |
| 165 | * @private | |
| 166 | */ | |
| 167 | ||
| 168 | done: function () { | |
| 169 | 0 | var result = Q.resolve(); |
| 170 | // clear the done error timeout | |
| 171 | 0 | clearTimeout(this.timeoutForDone); |
| 172 | // remove all previously attached event listeners to clear the message queue | |
| 173 | 0 | this.driver.events.removeAllListeners('driver:message'); |
| 174 | // resolve the deferred when the test is finished | |
| 175 | 0 | Unit.testStarted.fin(this._testFinished.bind(this, result)); |
| 176 | 0 | return result; |
| 177 | }, | |
| 178 | ||
| 179 | /** | |
| 180 | * Allow to use custom functions in order to embrace code reuse across | |
| 181 | * multiple files (for example for use in Page Objects). | |
| 182 | * | |
| 183 | * @method andThen | |
| 184 | * @param {function} a function, where 'this' references the test | |
| 185 | * @chainable | |
| 186 | */ | |
| 187 | andThen: function(func) { | |
| 188 | 0 | return func.call(this); |
| 189 | }, | |
| 190 | ||
| 191 | /** | |
| 192 | * Adds a node style function (with node err callback) style to the test. | |
| 193 | * | |
| 194 | * @method node | |
| 195 | * @param {function} a node function that is executed in the context of the test. | |
| 196 | * @chainable | |
| 197 | */ | |
| 198 | node: function(nodeFunction) { | |
| 199 | 0 | var deferred = Q.defer(), |
| 200 | that = this; | |
| 201 | 0 | nodeFunction.call(this, function(err) { |
| 202 | 0 | if (typeof err !== 'undefined') { |
| 203 | 0 | that.reporter.emit('error', err); |
| 204 | 0 | that.incrementFailedAssertions(); |
| 205 | 0 | deferred.reject(); |
| 206 | } else { | |
| 207 | 0 | deferred.resolve(); |
| 208 | } | |
| 209 | }); | |
| 210 | 0 | this.promise(deferred); |
| 211 | 0 | return this; |
| 212 | }, | |
| 213 | ||
| 214 | /** | |
| 215 | * Adds a promise to the chain of tests. | |
| 216 | * | |
| 217 | * @method promise | |
| 218 | * @param {promise} a q promise | |
| 219 | * @chainable | |
| 220 | */ | |
| 221 | promise: function(deferred) { | |
| 222 | 0 | this.actionPromiseQueue.push(deferred); |
| 223 | 0 | return this; |
| 224 | }, | |
| 225 | ||
| 226 | /** | |
| 227 | * Emits the test finished events & resolves all promises | |
| 228 | * when its done | |
| 229 | * | |
| 230 | * @method _testFinished | |
| 231 | * @param {object} result Promised result var | |
| 232 | * @return {object} result Promised result var | |
| 233 | * @private | |
| 234 | */ | |
| 235 | ||
| 236 | _testFinished: function (result) { | |
| 237 | // add a last deferred function on the end of the action queue, | |
| 238 | // to tell that this test is finished | |
| 239 | 0 | this.actionPromiseQueue.push(this._testFin.bind(this)); |
| 240 | ||
| 241 | // initialize all of the event receiver functions, | |
| 242 | // that later take the driver result | |
| 243 | 0 | this.actionPromiseQueue.forEach(function (f) { |
| 244 | 0 | result = result.then(f).fail(function () { |
| 245 | 0 | console.error(arguments); |
| 246 | 0 | process.exit(0); |
| 247 | }); | |
| 248 | }.bind(this)); | |
| 249 | ||
| 250 | // run the driver when all actions are stored in the queue | |
| 251 | 0 | Q.allSettled(this.actionPromiseQueue) |
| 252 | .then(this.driver.run.bind(this.driver)); | |
| 253 | ||
| 254 | 0 | return result; |
| 255 | }, | |
| 256 | ||
| 257 | /** | |
| 258 | * Emits the test started event | |
| 259 | * | |
| 260 | * @method _reportTestStarted | |
| 261 | * @param {string} name Name of the test | |
| 262 | * @chainable | |
| 263 | * @private | |
| 264 | */ | |
| 265 | ||
| 266 | _reportTestStarted: function (name) { | |
| 267 | 0 | this.reporter.emit('report:test:started', {name: name}); |
| 268 | 0 | return this; |
| 269 | }, | |
| 270 | ||
| 271 | /** | |
| 272 | * Checks if the test run is complete & emits/resolves | |
| 273 | * all the needed events/promises when the run is complete | |
| 274 | * | |
| 275 | * @method _onDriverMessage | |
| 276 | * @param {object} data Data that is returned by the driver:message event | |
| 277 | * @chainable | |
| 278 | * @private | |
| 279 | */ | |
| 280 | ||
| 281 | _onDriverMessage: function (data) { | |
| 282 | // check if the test run is complete | |
| 283 | 0 | if (data && data.key === 'run.complete') { |
| 284 | // emit the test finish events & resolve the deferred | |
| 285 | 0 | this._emitConcreteTestFinished(); |
| 286 | 0 | this._emitAssertionStatus(); |
| 287 | 0 | this._emitTestFinished(); |
| 288 | 0 | this.deferred.resolve(); |
| 289 | } | |
| 290 | ||
| 291 | 0 | return this; |
| 292 | }, | |
| 293 | ||
| 294 | /** | |
| 295 | * Emits an event, that the current test run has been finished | |
| 296 | * | |
| 297 | * @method _emitConcreteTestFinished | |
| 298 | * @chainable | |
| 299 | * @private | |
| 300 | */ | |
| 301 | ||
| 302 | _emitConcreteTestFinished: function () { | |
| 303 | 0 | this.events.emit('test:' + this._uid + ':finished', 'test:finished', this); |
| 304 | 0 | return this; |
| 305 | }, | |
| 306 | ||
| 307 | /** | |
| 308 | * Emits an event that describes the current state of all assertions | |
| 309 | * | |
| 310 | * @method _emitAssertionStatus | |
| 311 | * @chainable | |
| 312 | * @private | |
| 313 | */ | |
| 314 | ||
| 315 | _emitAssertionStatus: function () { | |
| 316 | 0 | this.reporter.emit('report:assertion:status', { |
| 317 | expected: (this.expectation ? this.expectation : this.runnedExpactations), | |
| 318 | run: this.runnedExpactations, | |
| 319 | status: this._testStatus() | |
| 320 | }); | |
| 321 | 0 | return this; |
| 322 | }, | |
| 323 | ||
| 324 | /** | |
| 325 | * Get the overall test status (assertions & expectation) | |
| 326 | * | |
| 327 | * @method _testStatus | |
| 328 | * @return {bool} status The test status | |
| 329 | * @chainable | |
| 330 | * @private | |
| 331 | */ | |
| 332 | ||
| 333 | _testStatus: function () { | |
| 334 | 0 | return this.checkExpectations() && this.checkAssertions(); |
| 335 | }, | |
| 336 | ||
| 337 | /** | |
| 338 | * Emits an event that describes the current state of all assertions. | |
| 339 | * The event should be fired when a test is finished | |
| 340 | * | |
| 341 | * @method _emitTestFinished | |
| 342 | * @chainable | |
| 343 | * @private | |
| 344 | */ | |
| 345 | ||
| 346 | _emitTestFinished: function () { | |
| 347 | 0 | this.reporter.emit('report:test:finished', { |
| 348 | name: this.name, | |
| 349 | id: this._uid, | |
| 350 | passedAssertions: this.runnedExpactations - this.failedAssertions, | |
| 351 | failedAssertions: this.failedAssertions, | |
| 352 | runnedExpactations: this.runnedExpactations, | |
| 353 | status: this._testStatus(), | |
| 354 | nl: true | |
| 355 | }); | |
| 356 | ||
| 357 | 0 | return this; |
| 358 | }, | |
| 359 | ||
| 360 | /** | |
| 361 | * Kicks off the test & binds all promises/events | |
| 362 | * | |
| 363 | * @method _testFin | |
| 364 | * @return {object} promise A promise | |
| 365 | * @private | |
| 366 | */ | |
| 367 | ||
| 368 | _testFin: function () { | |
| 369 | 0 | this.deferred = Q.defer(); |
| 370 | ||
| 371 | 0 | if (_.isFunction(this.driver.end)) { |
| 372 | 0 | this.driver.end(); |
| 373 | } | |
| 374 | ||
| 375 | // emit report startet event | |
| 376 | 0 | this._reportTestStarted(this.name); |
| 377 | ||
| 378 | // listen to all the messages from the driver | |
| 379 | 0 | this.driver.events.on('driver:message', this._onDriverMessage.bind(this)); |
| 380 | 0 | return this.deferred.promise; |
| 381 | }, | |
| 382 | ||
| 383 | /** | |
| 384 | * Copies assertion methods | |
| 385 | * | |
| 386 | * @method _inheritAssertions | |
| 387 | * @param {Test} test Instacne of test | |
| 388 | * @chainable | |
| 389 | * @private | |
| 390 | */ | |
| 391 | ||
| 392 | _inheritAssertions: function (test) { | |
| 393 | 0 | ['is'].forEach(function (method) { |
| 394 | 0 | test[method] = test.assert[method].bind(test.assert); |
| 395 | }); | |
| 396 | 0 | return test; |
| 397 | }, | |
| 398 | ||
| 399 | /** | |
| 400 | * Copies assertion helper methods | |
| 401 | * | |
| 402 | * @method _inheritAssertions | |
| 403 | * @param {Test} test Instacne of test | |
| 404 | * @chainable | |
| 405 | * @private | |
| 406 | */ | |
| 407 | ||
| 408 | _inheritAssertionHelpers: function (test) { | |
| 409 | 0 | ['not', 'between', 'gt', 'gte', 'lt', 'lte', 'equalsCaseInsensitive'].forEach(function (method) { |
| 410 | 0 | test.is[method] = test.assert[method].bind(test.assert); |
| 411 | 0 | test.assert.is[method] = test.assert[method].bind(test.assert); |
| 412 | }); | |
| 413 | 0 | ['contain', 'match'].forEach(function (method) { |
| 414 | 0 | test.to = test.to || {}; |
| 415 | 0 | test.assert.to = test.assert.to || {}; |
| 416 | ||
| 417 | 0 | test.to[method] = test.assert[method].bind(test.assert); |
| 418 | 0 | test.assert.to[method] = test.assert[method].bind(test.assert); |
| 419 | }); | |
| 420 | 0 | ['notContain'].forEach(function (method) { |
| 421 | 0 | var apiName = method.substr(3, 1).toLowerCase() + method.substr(4); |
| 422 | 0 | test.to.not = test.to.not || {}; |
| 423 | 0 | test.assert.to.not = test.assert.to.not || {}; |
| 424 | ||
| 425 | 0 | test.to.not[apiName] = test.assert[method].bind(test.assert); |
| 426 | 0 | test.assert.to.not[apiName] = test.assert[method].bind(test.assert); |
| 427 | }); | |
| 428 | 0 | return test; |
| 429 | }, | |
| 430 | ||
| 431 | /** | |
| 432 | * Set up the instance | |
| 433 | * | |
| 434 | * @method _inheritAssertions | |
| 435 | * @param {Test} test Instacne of test | |
| 436 | * @param {object} opts Options | |
| 437 | * @chainable | |
| 438 | * @private | |
| 439 | */ | |
| 440 | ||
| 441 | _initialize: function (test, opts) { | |
| 442 | 0 | test._uid = _.uniqueId('test'); |
| 443 | 0 | test.events = opts.events; |
| 444 | 0 | test.driver = opts.driver; |
| 445 | 0 | test.reporter = opts.reporter; |
| 446 | 0 | return test; |
| 447 | } | |
| 448 | ||
| 449 | }; | |
| 450 | ||
| 451 | /** | |
| 452 | * Alias for 'andThen'; use if it is the first function called in the test case. | |
| 453 | * | |
| 454 | * @method start | |
| 455 | * @chainable | |
| 456 | */ | |
| 457 | 1 | Unit.prototype.start = Unit.prototype.andThen; |
| 458 | ||
| 459 | // export a function that generates a new test instance | |
| 460 | 1 | module.exports = function (opts) { |
| 461 | // mixin assertions, actions & getters | |
| 462 | 0 | Unit.prototype = _.extend(Unit.prototype, actions({reporter: opts.reporter}).prototype); |
| 463 | 0 | var unit = new Unit(opts); |
| 464 | 0 | unit.assert = new (assertions())({test: unit}); |
| 465 | 0 | unit.assert.done = unit.done.bind(this); |
| 466 | 0 | unit.assert.query = unit.query.bind(unit.assert); |
| 467 | 0 | unit.assert.$ = unit.query.bind(unit.assert); |
| 468 | 0 | unit.end = unit.assert.end.bind(unit.assert); |
| 469 | ||
| 470 | // copy log methods | |
| 471 | 0 | unit.log = {}; |
| 472 | 0 | unit.log.dom = unit.logger.dom.bind(unit); |
| 473 | 0 | unit.log.message = unit.logger.message.bind(unit); |
| 474 | ||
| 475 | // copy assertions methods | |
| 476 | 0 | unit = unit._inheritAssertions(unit); |
| 477 | ||
| 478 | // copy assertion helper methods | |
| 479 | 0 | unit = unit._inheritAssertionHelpers(unit); |
| 480 | ||
| 481 | // initialize the instance | |
| 482 | 0 | unit = unit._initialize(unit, opts); |
| 483 | ||
| 484 | // TODO: Promise driver start | |
| 485 | // so that we can reexecute them and clean the env between tests | |
| 486 | 0 | Unit.testStarted = unit.driver.start(Q); |
| 487 | 0 | return unit; |
| 488 | }; | |
| 489 |
| Line | Hits | Source |
|---|---|---|
| 1 | /*! | |
| 2 | * | |
| 3 | * Copyright (c) 2013 Sebastian Golasch | |
| 4 | * | |
| 5 | * Permission is hereby granted, free of charge, to any person obtaining a | |
| 6 | * copy of this software and associated documentation files (the "Software"), | |
| 7 | * to deal in the Software without restriction, including without limitation | |
| 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
| 9 | * and/or sell copies of the Software, and to permit persons to whom the | |
| 10 | * Software is furnished to do so, subject to the following conditions: | |
| 11 | * | |
| 12 | * The above copyright notice and this permission notice shall be included | |
| 13 | * in all copies or substantial portions of the Software. | |
| 14 | * | |
| 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
| 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
| 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
| 21 | * DEALINGS IN THE SOFTWARE. | |
| 22 | * | |
| 23 | */ | |
| 24 | ||
| 25 | 1 | 'use strict'; |
| 26 | ||
| 27 | 1 | var counter = 0; |
| 28 | ||
| 29 | 1 | module.exports = function() { |
| 30 | 3 | return 'uuid-' + (++counter); |
| 31 | }; | |
| 32 |