| 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('node-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 | 1 | 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, you terminate it with the [assertions.html#meth-end](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.v4(); |
| 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.v4(); |
| 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 mehtod, 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.v4(); |
| 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.v4(); |
| 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 . |
| 263 | | * You can access such frames with this mehtod, 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.v4(); |
| 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 windoe context when the test context has been |
| 301 | | * switched to a different windoe 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.v4(); |
| 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.v4(); |
| 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.v4(); |
| 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.v4(); |
| 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.v4(); |
| 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. |
| 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'); |
| 435 | | * // creates 'my/page/in/safari/homepage.png' |
| 436 | | * test.screenshot('my/page/in/:browser/homepage'); |
| 437 | | * // creates 'my/page/in/safari_6_0_1/homepage.png' |
| 438 | | * test.screenshot('my/page/in/:browser_:version/homepage'); |
| 439 | | * // creates 'my/page/in/safari_6_0_1/on/osx/homepage.png' |
| 440 | | * test.screenshot('my/page/in/:browser_:version/on/:os/homepage'); |
| 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'); |
| 443 | | * // creates 'my/page/at/w1024_h768/homepage.png' |
| 444 | | * test.screenshot('my/page/at/:viewport/homepage'); |
| 445 | | * // creates 'my/page/at/637657345/homepage.png' |
| 446 | | * test.screenshot('my/page/in_time/:timestamp/homepage'); |
| 447 | | * // creates 'my/page/at/12_24_2013/homepage.png' |
| 448 | | * test.screenshot('my/page/in_time/:date/homepage'); |
| 449 | | * // creates 'my/page/at/12_24_2013_14_55_23/homepage.png' |
| 450 | | * test.screenshot('my/page/in_time/:datetime/homepage'); |
| 451 | | * ``` |
| 452 | | * |
| 453 | | * @api |
| 454 | | * @method screenshot |
| 455 | | * @param {string} pathname Name of the folder and file the screenshot should be saved to |
| 456 | | * @return chainable |
| 457 | | */ |
| 458 | | |
| 459 | 1 | Actions.prototype.screenshot = function (pathname) { |
| 460 | 0 | var hash = uuid.v4(); |
| 461 | 0 | var cb = this._generateCallbackAssertion('screenshot', 'screenshot', pathname, hash); |
| 462 | 0 | this._addToActionQueue(['', pathname, hash], 'screenshot', cb); |
| 463 | 0 | return this; |
| 464 | | }; |
| 465 | | |
| 466 | | /** |
| 467 | | * Pause steps suite execution for a given amount of time, and optionally execute a step on done. |
| 468 | | * |
| 469 | | * This makes sense, if you have a ticker for example, tht scrolls like every ten seconds |
| 470 | | * & you want to assure that the visible content changes every ten seconds |
| 471 | | * |
| 472 | | * ```javascript |
| 473 | | * test.open('http://myticker.org') |
| 474 | | * .assert.visible('.ticker-element:first-child', 'First ticker element is visible') |
| 475 | | * .wait(10000) |
| 476 | | * .assert.visible('.ticker-element:nth-child(2)', 'Snd. ticker element is visible') |
| 477 | | * .wait(10000) |
| 478 | | * .assert.visible('.ticker-element:last-child', 'Third ticker element is visible') |
| 479 | | * .done(); |
| 480 | | * ``` |
| 481 | | * If no timeout argument is given, a default timeout of 5 seconds will be used |
| 482 | | * |
| 483 | | * ```javascript |
| 484 | | * test.open('http://myticker.org') |
| 485 | | * .assert.visible('.ticker-element:first-child', 'First ticker element is visible') |
| 486 | | * .wait() |
| 487 | | * .assert.visible('.ticker-element:nth-child(2)', 'Snd. ticker element is visible') |
| 488 | | * .wait() |
| 489 | | * .assert.visible('.ticker-element:last-child', 'Third ticker element is visible') |
| 490 | | * .done(); |
| 491 | | * ``` |
| 492 | | * |
| 493 | | * @api |
| 494 | | * @method wait |
| 495 | | * @param {number} timeout in milliseconds |
| 496 | | * @chainable |
| 497 | | */ |
| 498 | | |
| 499 | 1 | Actions.prototype.wait = function (timeout) { |
| 500 | 0 | var hash = uuid.v4(); |
| 501 | 0 | var cb = this._generateCallbackAssertion('wait', 'wait', timeout, hash); |
| 502 | 0 | this._addToActionQueue([(timeout ? parseInt(timeout, 10) : 5000), hash], 'wait', cb); |
| 503 | 0 | return this; |
| 504 | | }; |
| 505 | | |
| 506 | | /** |
| 507 | | * Reloads current page location. |
| 508 | | * |
| 509 | | * This is basically the same as hitting F5/refresh in your browser |
| 510 | | * |
| 511 | | * ```javascript |
| 512 | | * test.open('http://google.com') |
| 513 | | * .reload() |
| 514 | | * .done(); |
| 515 | | * ``` |
| 516 | | * |
| 517 | | * @api |
| 518 | | * @method reload |
| 519 | | * @chainable |
| 520 | | */ |
| 521 | | |
| 522 | 1 | Actions.prototype.reload = function () { |
| 523 | 0 | var hash = uuid.v4(); |
| 524 | 0 | var cb = this._generateCallbackAssertion('refresh', 'refresh', '', hash); |
| 525 | 0 | this._addToActionQueue([hash], 'refresh', cb); |
| 526 | 0 | return this; |
| 527 | | }; |
| 528 | | |
| 529 | | /** |
| 530 | | * Moves a step forward in browser's history. |
| 531 | | * |
| 532 | | * This is basically the same as hitting the forward button in your browser |
| 533 | | * |
| 534 | | * ```javascript |
| 535 | | * test.open('http://google.com') |
| 536 | | * .open('https://github.com') |
| 537 | | * .assert.url.is('https://github.com/', 'We are at GitHub') |
| 538 | | * .back() |
| 539 | | * .assert.url.is('http://google.com', 'We are at Google!') |
| 540 | | * .forward() |
| 541 | | * .assert.url.is('https://github.com/', 'Back at GitHub! Timetravel FTW') |
| 542 | | * .done(); |
| 543 | | * ``` |
| 544 | | * |
| 545 | | * @api |
| 546 | | * @method forward |
| 547 | | * @chainable |
| 548 | | */ |
| 549 | | |
| 550 | 1 | Actions.prototype.forward = function () { |
| 551 | 0 | var hash = uuid.v4(); |
| 552 | 0 | var cb = this._generateCallbackAssertion('forward', 'forward', '', hash); |
| 553 | 0 | this._addToActionQueue([hash], 'forward', cb); |
| 554 | 0 | return this; |
| 555 | | }; |
| 556 | | |
| 557 | | /** |
| 558 | | * Moves back a step in browser's history. |
| 559 | | * |
| 560 | | * This is basically the same as hitting the back button in your browser |
| 561 | | * |
| 562 | | * ```javascript |
| 563 | | * test.open('http://google.com') |
| 564 | | * .open('https://github.com') |
| 565 | | * .assert.url.is('https://github.com/', 'We are at GitHub') |
| 566 | | * .back() |
| 567 | | * .assert.url.is('http://google.com', 'We are at Google!') |
| 568 | | * .forward() |
| 569 | | * .assert.url.is('https://github.com/', 'Back at GitHub! Timetravel FTW'); |
| 570 | | * .done(); |
| 571 | | * ``` |
| 572 | | * |
| 573 | | * @api |
| 574 | | * @method back |
| 575 | | * @chainable |
| 576 | | */ |
| 577 | | |
| 578 | 1 | Actions.prototype.back = function () { |
| 579 | 0 | var hash = uuid.v4(); |
| 580 | 0 | var cb = this._generateCallbackAssertion('back', 'back', '', hash); |
| 581 | 0 | this._addToActionQueue([hash], 'back', cb); |
| 582 | 0 | return this; |
| 583 | | }; |
| 584 | | |
| 585 | | /** |
| 586 | | * Performs a click on the element matching the provided selector expression. |
| 587 | | * |
| 588 | | * If we take Daleks homepage (the one you're probably visiting right now), |
| 589 | | * the HTML looks something like this (it does not really, but hey, lets assume this for a second) |
| 590 | | * |
| 591 | | * ```html |
| 592 | | * <nav> |
| 593 | | * <ul> |
| 594 | | * <li><a id="homeapge" href="/index.html">DalekJS</a></li> |
| 595 | | * <li><a id="docs" href="/docs.html">Documentation</a></li> |
| 596 | | * <li><a id="faq" href="/faq.html">F.A.Q</a></li> |
| 597 | | * </ul> |
| 598 | | * </nav> |
| 599 | | * ``` |
| 600 | | * |
| 601 | | * ```javascript |
| 602 | | * test.open('http://dalekjs.com') |
| 603 | | * .click('#faq') |
| 604 | | * .assert.title().is('DalekJS - Frequently asked questions', 'What the F.A.Q.') |
| 605 | | * .done(); |
| 606 | | * ``` |
| 607 | | * |
| 608 | | * By default, this performs a left click. |
| 609 | | * In the future it might become the ability to also execute a "right button" click. |
| 610 | | * |
| 611 | | * > Note: Does not work correctly in Firefox when used on `<select>` & `<option>` elements |
| 612 | | * |
| 613 | | * @api |
| 614 | | * @method click |
| 615 | | * @param {string} selector Selector of the element to be clicked |
| 616 | | * @chainable |
| 617 | | */ |
| 618 | | |
| 619 | 1 | Actions.prototype.click = function (selector) { |
| 620 | 0 | var hash = uuid.v4(); |
| 621 | | |
| 622 | 0 | if (this.querying === true) { |
| 623 | 0 | selector = this.selector; |
| 624 | | } |
| 625 | | |
| 626 | 0 | var cb = this._generateCallbackAssertion('click', 'click', selector, hash); |
| 627 | 0 | this._addToActionQueue([selector, hash], 'click', cb); |
| 628 | 0 | return this; |
| 629 | | }; |
| 630 | | |
| 631 | | /** |
| 632 | | * Submits a form. |
| 633 | | * |
| 634 | | * ```html |
| 635 | | * <form id="skaaro" action="skaaro.php" method="GET"> |
| 636 | | * <input type="hidden" name="intheshadows" value="itis"/> |
| 637 | | * <input type="text" name="truth" id="truth" value=""/> |
| 638 | | * </form> |
| 639 | | * ``` |
| 640 | | * |
| 641 | | * ```javascript |
| 642 | | * test.open('http://home.dalek.com') |
| 643 | | * .type('#truth', 'out there is') |
| 644 | | * .submit('#skaaro') |
| 645 | | * .done(); |
| 646 | | * ``` |
| 647 | | * |
| 648 | | * > Note: Does not work in Firefox yet |
| 649 | | * |
| 650 | | * @api |
| 651 | | * @method submit |
| 652 | | * @param {string} selector Selector of the form to be submitted |
| 653 | | * @chainable |
| 654 | | */ |
| 655 | | |
| 656 | 1 | Actions.prototype.submit = function (selector) { |
| 657 | 0 | var hash = uuid.v4(); |
| 658 | | |
| 659 | 0 | if (this.querying === true) { |
| 660 | 0 | selector = this.selector; |
| 661 | | } |
| 662 | | |
| 663 | 0 | var cb = this._generateCallbackAssertion('submit', 'submit', selector, hash); |
| 664 | 0 | this._addToActionQueue([selector, hash], 'submit', cb); |
| 665 | 0 | return this; |
| 666 | | }; |
| 667 | | |
| 668 | | /** |
| 669 | | * Performs an HTTP request for opening a given location. |
| 670 | | * You can forge GET, POST, PUT, DELETE and HEAD requests. |
| 671 | | * |
| 672 | | * Basically the same as typing a location into your browsers URL bar and |
| 673 | | * hitting return. |
| 674 | | * |
| 675 | | * ```javascript |
| 676 | | * test.open('http://dalekjs.com') |
| 677 | | * .assert.url().is('http://dalekjs.com', 'DalekJS I'm in you') |
| 678 | | * .done(); |
| 679 | | * ``` |
| 680 | | * |
| 681 | | * @api |
| 682 | | * @method open |
| 683 | | * @param {string} location URL of the page to open |
| 684 | | * @chainable |
| 685 | | */ |
| 686 | | |
| 687 | 1 | Actions.prototype.open = function (location) { |
| 688 | 0 | var hash = uuid.v4(); |
| 689 | 0 | var cb = this._generateCallbackAssertion('open', 'open', location, hash); |
| 690 | 0 | this._addToActionQueue([location, hash], 'open', cb); |
| 691 | 0 | return this; |
| 692 | | }; |
| 693 | | |
| 694 | | /** |
| 695 | | * Types a text into an input field or text area. |
| 696 | | * And yes, it really types, character for character, like you would |
| 697 | | * do when using your keyboard. |
| 698 | | * |
| 699 | | * |
| 700 | | * ```html |
| 701 | | * <form id="skaaro" action="skaaro.php" method="GET"> |
| 702 | | * <input type="hidden" name="intheshadows" value="itis"/> |
| 703 | | * <input type="text" name="truth" id="truth" value=""/> |
| 704 | | * </form> |
| 705 | | * ``` |
| 706 | | * |
| 707 | | * ```javascript |
| 708 | | * test.open('http://home.dalek.com') |
| 709 | | * .type('#truth', 'out there is') |
| 710 | | * .assert.val('#truth', 'out there is', 'Text has been set') |
| 711 | | * .done(); |
| 712 | | * ``` |
| 713 | | * |
| 714 | | * You can also send special keys using unicode. |
| 715 | | * |
| 716 | | * * ```javascript |
| 717 | | * test.open('http://home.dalek.com') |
| 718 | | * .type('#truth', 'out \uE008there\uE008 is') |
| 719 | | * .assert.val('#truth', 'out THERE is', 'Text has been set') |
| 720 | | * .done(); |
| 721 | | * ``` |
| 722 | | * 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). |
| 723 | | * |
| 724 | | * > Note: Does not work correctly in Firefox with special keys |
| 725 | | * |
| 726 | | * @api |
| 727 | | * @method type |
| 728 | | * @param {string} selector Selector of the form field to be filled |
| 729 | | * @param {string} keystrokes Text to be applied to the element |
| 730 | | * @chainable |
| 731 | | */ |
| 732 | | |
| 733 | 1 | Actions.prototype.type = function (selector, keystrokes) { |
| 734 | 0 | var hash = uuid.v4(); |
| 735 | | |
| 736 | 0 | if (this.querying === true) { |
| 737 | 0 | keystrokes = selector; |
| 738 | 0 | selector = this.selector; |
| 739 | | } |
| 740 | | |
| 741 | 0 | var cb = this._generateCallbackAssertion('type', 'type', selector, keystrokes, hash); |
| 742 | 0 | this._addToActionQueue([selector, keystrokes], 'type', cb); |
| 743 | 0 | return this; |
| 744 | | }; |
| 745 | | |
| 746 | | /** |
| 747 | | * This acts just like .type() with a key difference. |
| 748 | | * This action can be used on non-input elements (useful for test site wide keyboard shortcuts and the like). |
| 749 | | * So assumeing we have a keyboard shortcut that display an alert box, we could test that with something like this: |
| 750 | | * |
| 751 | | * ```javascript |
| 752 | | * test.open('http://home.dalek.com') |
| 753 | | * .sendKeys('body', '\uE00C') |
| 754 | | * .assert.dialogText('press the escape key give this alert text') |
| 755 | | * .done(); |
| 756 | | * ``` |
| 757 | | * |
| 758 | | * |
| 759 | | * > Note: Does not work correctly in Firefox with special keys |
| 760 | | * |
| 761 | | * @api |
| 762 | | * @method sendKeys |
| 763 | | * @param {string} selector Selector of the form field to be filled |
| 764 | | * @param {string} keystrokes Text to be applied to the element |
| 765 | | * @chainable |
| 766 | | */ |
| 767 | | |
| 768 | 1 | Actions.prototype.sendKeys = function (selector, keystrokes) { |
| 769 | 0 | var hash = uuid.v4(); |
| 770 | | |
| 771 | 0 | if (this.querying === true) { |
| 772 | 0 | keystrokes = selector; |
| 773 | 0 | selector = this.selector; |
| 774 | | } |
| 775 | | |
| 776 | 0 | var cb = this._generateCallbackAssertion('sendKeys', 'sendKeys', selector, keystrokes, hash); |
| 777 | 0 | this._addToActionQueue([selector, keystrokes], 'sendKeys', cb); |
| 778 | 0 | return this; |
| 779 | | }; |
| 780 | | |
| 781 | | /** |
| 782 | | * Types a text into the text inout field of a prompt dialog. |
| 783 | | * Like you would do when using your keyboard. |
| 784 | | * |
| 785 | | * ```html |
| 786 | | * <div> |
| 787 | | * <a id="aquestion" onclick="this.innerText = window.prompt('Your favourite companion:')">????</a> |
| 788 | | * </div> |
| 789 | | * ``` |
| 790 | | * |
| 791 | | * ```javascript |
| 792 | | * test.open('http://adomain.com') |
| 793 | | * .click('#aquestion') |
| 794 | | * .answer('Rose') |
| 795 | | * .assert.text('#aquestion').is('Rose', 'Awesome she was!') |
| 796 | | * .done(); |
| 797 | | * ``` |
| 798 | | * |
| 799 | | * |
| 800 | | * > Note: Does not work in Firefox & PhantomJS |
| 801 | | * |
| 802 | | * @api |
| 803 | | * @method answer |
| 804 | | * @param {string} keystrokes Text to be applied to the element |
| 805 | | * @return chainable |
| 806 | | */ |
| 807 | | |
| 808 | 1 | Actions.prototype.answer = function (keystrokes) { |
| 809 | 0 | var hash = uuid.v4(); |
| 810 | 0 | var cb = this._generateCallbackAssertion('promptText', 'promptText', keystrokes, hash); |
| 811 | 0 | this._addToActionQueue([keystrokes, hash], 'promptText', cb); |
| 812 | 0 | return this; |
| 813 | | }; |
| 814 | | |
| 815 | | /** |
| 816 | | * Executes a JavaScript function within the browser context |
| 817 | | * |
| 818 | | * ```javascript |
| 819 | | * test.open('http://adomain.com') |
| 820 | | * .execute(function () { |
| 821 | | * window.myFramework.addRow('foo'); |
| 822 | | * window.myFramework.addRow('bar'); |
| 823 | | * }) |
| 824 | | * .done(); |
| 825 | | * ``` |
| 826 | | * |
| 827 | | * You can also apply arguments to the function |
| 828 | | * |
| 829 | | * ```javascript |
| 830 | | * test.open('http://adomain.com') |
| 831 | | * .execute(function (paramFoo, aBar) { |
| 832 | | * window.myFramework.addRow(paramFoo); |
| 833 | | * window.myFramework.addRow(aBar); |
| 834 | | * }, 'foo', 'bar') |
| 835 | | * .done(); |
| 836 | | * ``` |
| 837 | | * |
| 838 | | * > Note: Buggy in Firefox |
| 839 | | * |
| 840 | | * @api |
| 841 | | * @method execute |
| 842 | | * @param {function} script JavaScript function that should be executed |
| 843 | | * @return chainable |
| 844 | | */ |
| 845 | | |
| 846 | 1 | Actions.prototype.execute = function (script) { |
| 847 | 0 | var hash = uuid.v4(); |
| 848 | 0 | var args = [this.contextVars].concat(Array.prototype.slice.call(arguments, 1) || []); |
| 849 | 0 | var cb = this._generateCallbackAssertion('execute', 'execute', script, args, hash); |
| 850 | 0 | this._addToActionQueue([script, args, hash], 'execute', cb); |
| 851 | 0 | return this; |
| 852 | | }; |
| 853 | | |
| 854 | | /** |
| 855 | | * Waits until a function returns true to process any next step. |
| 856 | | * |
| 857 | | * You can also set a callback on timeout using the onTimeout argument, |
| 858 | | * and set the timeout using the timeout one, in milliseconds. The default timeout is set to 5000ms. |
| 859 | | * |
| 860 | | * ```javascript |
| 861 | | * test.open('http://adomain.com') |
| 862 | | * .waitFor(function () { |
| 863 | | * return window.myCheck === true; |
| 864 | | * }) |
| 865 | | * .done(); |
| 866 | | * ``` |
| 867 | | * |
| 868 | | * You can also apply arguments to the function, as well as a timeout |
| 869 | | * |
| 870 | | * ```javascript |
| 871 | | * test.open('http://adomain.com') |
| 872 | | * .waitFor(function (aCheck) { |
| 873 | | * return window.myThing === aCheck; |
| 874 | | * }, 'aValue', 10000) |
| 875 | | * .done(); |
| 876 | | * ``` |
| 877 | | * |
| 878 | | * > Note: Buggy in Firefox |
| 879 | | * |
| 880 | | * @method waitFor |
| 881 | | * @param {function} fn Async function that resolves an promise when ready |
| 882 | | * @param {array} args Additional arguments |
| 883 | | * @param {number} timeout Timeout in miliseconds |
| 884 | | * @chainable |
| 885 | | * @api |
| 886 | | */ |
| 887 | | |
| 888 | 1 | Actions.prototype.waitFor = function (script, args, timeout) { |
| 889 | 0 | var hash = uuid.v4(); |
| 890 | 0 | timeout = timeout || 5000; |
| 891 | 0 | var cb = this._generateCallbackAssertion('waitFor', 'waitFor', script, args, timeout, hash); |
| 892 | 0 | this._addToActionQueue([script, args, timeout, hash], 'waitFor', cb); |
| 893 | 0 | return this; |
| 894 | | }; |
| 895 | | |
| 896 | | /** |
| 897 | | * Accepts an alert/prompt/confirm dialog. This is basically the same actions as when |
| 898 | | * you are clicking okay or hitting return in one of that dialogs. |
| 899 | | * |
| 900 | | * ```html |
| 901 | | * <div> |
| 902 | | * <a id="attentione" onclick="window.alert('Alonsy!')">ALERT!ALERT!</a> |
| 903 | | * </div> |
| 904 | | * ``` |
| 905 | | * |
| 906 | | * ```javascript |
| 907 | | * test.open('http://adomain.com') |
| 908 | | * // alert appears |
| 909 | | * .click('#attentione') |
| 910 | | * // alert is gone |
| 911 | | * .accept() |
| 912 | | * .done(); |
| 913 | | * ``` |
| 914 | | * |
| 915 | | * > Note: Does not work in Firefox & PhantomJS |
| 916 | | * |
| 917 | | * @api |
| 918 | | * @method accept |
| 919 | | * @return chainable |
| 920 | | */ |
| 921 | | |
| 922 | 1 | Actions.prototype.accept = function () { |
| 923 | 0 | var hash = uuid.v4(); |
| 924 | 0 | var cb = this._generateCallbackAssertion('acceptAlert', 'acceptAlert', hash); |
| 925 | 0 | this._addToActionQueue([hash], 'acceptAlert', cb); |
| 926 | 0 | return this; |
| 927 | | }; |
| 928 | | |
| 929 | | /** |
| 930 | | * Dismisses an prompt/confirm dialog. This is basically the same actions as when |
| 931 | | * you are clicking cancel in one of that dialogs. |
| 932 | | * |
| 933 | | * ```html |
| 934 | | * <div> |
| 935 | | * <a id="nonono" onclick="(this.innerText = window.confirm('No classic doctors in the 50th?') ? 'Buh!' : ':(') ">What!</a> |
| 936 | | * </div> |
| 937 | | * ``` |
| 938 | | * |
| 939 | | * ```javascript |
| 940 | | * test.open('http://adomain.com') |
| 941 | | * // prompt appears |
| 942 | | * .click('#nonono') |
| 943 | | * // prompt is gone |
| 944 | | * .dismiss() |
| 945 | | * .assert.text('#nonono').is(':(', 'So sad') |
| 946 | | * .done(); |
| 947 | | * ``` |
| 948 | | * |
| 949 | | * > Note: Does not work in Firefox & PhantomJS |
| 950 | | * |
| 951 | | * @api |
| 952 | | * @method dismiss |
| 953 | | * @return chainable |
| 954 | | */ |
| 955 | | |
| 956 | 1 | Actions.prototype.dismiss = function () { |
| 957 | 0 | var hash = uuid.v4(); |
| 958 | 0 | var cb = this._generateCallbackAssertion('dismissAlert', 'dismissAlert', hash); |
| 959 | 0 | this._addToActionQueue([hash], 'dismissAlert', cb); |
| 960 | 0 | return this; |
| 961 | | }; |
| 962 | | |
| 963 | | /** |
| 964 | | * Resizes the browser window to a set of given dimensions (in px). |
| 965 | | * The default configuration of dalek opening pages is a width of 1280px |
| 966 | | * and a height of 1024px. You can specify your own default in the configuration. |
| 967 | | * |
| 968 | | * ```html |
| 969 | | * <div> |
| 970 | | * <span id="magicspan">The span in the fireplace</span> |
| 971 | | * </div> |
| 972 | | * ``` |
| 973 | | * |
| 974 | | * ```css |
| 975 | | * #magicspan { |
| 976 | | * display: inline; |
| 977 | | * } |
| 978 | | * |
| 979 | | * // @media all and (max-width: 500px) and (min-width: 300px) |
| 980 | | * #magicspan { |
| 981 | | * display: none; |
| 982 | | * } |
| 983 | | * ``` |
| 984 | | * |
| 985 | | * ```javascript |
| 986 | | * test.open('http://adomain.com') |
| 987 | | * .assert.visible('#magicspan', 'Big screen, visible span') |
| 988 | | * .resize({width: 400, height: 500}) |
| 989 | | * .assert.notVisible('#magicspan', 'Small screen, no visible span magic!') |
| 990 | | * .done(); |
| 991 | | * ``` |
| 992 | | * |
| 993 | | * |
| 994 | | * > Note: Does not work in Firefox |
| 995 | | * |
| 996 | | * @api |
| 997 | | * @method resize |
| 998 | | * @param {object} dimensions Width and height as properties to apply |
| 999 | | * @chainable |
| 1000 | | */ |
| 1001 | | |
| 1002 | 1 | Actions.prototype.resize = function (dimensions) { |
| 1003 | 0 | var hash = uuid.v4(); |
| 1004 | 0 | var cb = this._generateCallbackAssertion('resize', 'resize', dimensions, hash); |
| 1005 | 0 | this._addToActionQueue([dimensions, hash], 'resize', cb); |
| 1006 | 0 | return this; |
| 1007 | | }; |
| 1008 | | |
| 1009 | | /** |
| 1010 | | * Maximizes the browser window. |
| 1011 | | * |
| 1012 | | * ```html |
| 1013 | | * <div> |
| 1014 | | * <span id="magicspan">The span in the fireplace</span> |
| 1015 | | * </div> |
| 1016 | | * ``` |
| 1017 | | * |
| 1018 | | * ```css |
| 1019 | | * #magicspan { |
| 1020 | | * display: inline; |
| 1021 | | * } |
| 1022 | | * |
| 1023 | | * @media all and (max-width: 500px) and (min-width: 300px) { |
| 1024 | | * #magicspan { |
| 1025 | | * display: none; |
| 1026 | | * } |
| 1027 | | * } |
| 1028 | | * ``` |
| 1029 | | * |
| 1030 | | * ```javascript |
| 1031 | | * test.open('http://adomain.com') |
| 1032 | | * .resize({width: 400, height: 500}) |
| 1033 | | * .assert.notVisible('#magicspan', 'Small screen, no visible span magic!') |
| 1034 | | * .maximize() |
| 1035 | | * .assert.visible('#magicspan', 'Big screen, visible span') |
| 1036 | | * .done(); |
| 1037 | | * ``` |
| 1038 | | * |
| 1039 | | * > Note: Does not work in Firefox and PhantomJS |
| 1040 | | * |
| 1041 | | * @api |
| 1042 | | * @method maximize |
| 1043 | | * @chainable |
| 1044 | | */ |
| 1045 | | |
| 1046 | 1 | Actions.prototype.maximize = function () { |
| 1047 | 0 | var hash = uuid.v4(); |
| 1048 | 0 | var cb = this._generateCallbackAssertion('maximize', 'maximize', hash); |
| 1049 | 0 | this._addToActionQueue([hash], 'maximize', cb); |
| 1050 | 0 | return this; |
| 1051 | | }; |
| 1052 | | |
| 1053 | | /** |
| 1054 | | * Sets a cookie. |
| 1055 | | * More configuration options will be implemented in the future, |
| 1056 | | * by now, you can only set a cookie with a specific name and contents. |
| 1057 | | * This will be a domain wide set cookie. |
| 1058 | | * |
| 1059 | | * ```javascript |
| 1060 | | * test.open('http://adomain.com') |
| 1061 | | * .setCookie('my_cookie_name', 'my=content') |
| 1062 | | * .done(); |
| 1063 | | * ``` |
| 1064 | | * |
| 1065 | | * @api |
| 1066 | | * @method setCookie |
| 1067 | | * @chainable |
| 1068 | | */ |
| 1069 | | |
| 1070 | 1 | Actions.prototype.setCookie = function (name, contents) { |
| 1071 | 0 | var hash = uuid.v4(); |
| 1072 | 0 | var cb = this._generateCallbackAssertion('setCookie', 'setCookie', name, contents, hash); |
| 1073 | 0 | this._addToActionQueue([name, contents, hash], 'setCookie', cb); |
| 1074 | 0 | return this; |
| 1075 | | }; |
| 1076 | | |
| 1077 | | /** |
| 1078 | | * Waits until an element matching the provided |
| 1079 | | * selector expression exists in remote DOM to process any next step. |
| 1080 | | * |
| 1081 | | * Lets assume we have a ticker that loads its contents via AJAX, |
| 1082 | | * and appends new elements, when the call has been successfully answered: |
| 1083 | | * |
| 1084 | | * ```javascript |
| 1085 | | * test.open('http://myticker.org') |
| 1086 | | * .assert.text('.ticker-element:first-child', 'First!', 'First ticker element is visible') |
| 1087 | | * // now we load the next ticker element, defsult timeout is 5 seconds |
| 1088 | | * .waitForElement('.ticker-element:nth-child(2)') |
| 1089 | | * .assert.text('.ticker-element:nth-child(2)', 'Me snd. one', 'Snd. ticker element is visible') |
| 1090 | | * // Lets assume that this AJAX call can take longer, so we raise the default timeout to 10 seconds |
| 1091 | | * .waitForElement('.ticker-element:last-child', 10000) |
| 1092 | | * .assert.text('.ticker-element:last-child', 'Me, third one!', 'Third ticker element is visible') |
| 1093 | | * .done(); |
| 1094 | | * ``` |
| 1095 | | * |
| 1096 | | * @api |
| 1097 | | * @method waitForElement |
| 1098 | | * @param {string} selector Selector that matches the element to wait for |
| 1099 | | * @param {number} timeout Timeout in milliseconds |
| 1100 | | * @chainable |
| 1101 | | */ |
| 1102 | | |
| 1103 | 1 | Actions.prototype.waitForElement = function (selector, timeout) { |
| 1104 | 0 | var hash = uuid.v4(); |
| 1105 | | |
| 1106 | 0 | if (this.querying === true) { |
| 1107 | 0 | timeout = selector; |
| 1108 | 0 | selector = this.selector; |
| 1109 | | } |
| 1110 | | |
| 1111 | 0 | var cb = this._generateCallbackAssertion('waitForElement', 'waitForElement', selector + ' : ' + timeout, hash); |
| 1112 | 0 | this._addToActionQueue([selector, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitForElement', cb); |
| 1113 | 0 | return this; |
| 1114 | | }; |
| 1115 | | |
| 1116 | | /** |
| 1117 | | * Fills the fields of a form with given values. |
| 1118 | | * |
| 1119 | | * ```html |
| 1120 | | * <input type="hidden" value="not really a value" id="ijustwannahaveavalue"/> |
| 1121 | | * ``` |
| 1122 | | * |
| 1123 | | * ```javascript |
| 1124 | | * test.open('http://dalekjs.com') |
| 1125 | | * .setValue('#ijustwannahaveavalue', 'a value') |
| 1126 | | * .title().is('DalekJS - Frequently asked questions', 'What the F.A.Q.'); |
| 1127 | | * ``` |
| 1128 | | * |
| 1129 | | * @api |
| 1130 | | * @method setValue |
| 1131 | | * @param {string} selector |
| 1132 | | * @param {string} value |
| 1133 | | * @return {Actions} |
| 1134 | | */ |
| 1135 | | |
| 1136 | 1 | Actions.prototype.setValue = function (selector, value) { |
| 1137 | 0 | var hash = uuid.v4(); |
| 1138 | | |
| 1139 | 0 | if (this.querying === true) { |
| 1140 | 0 | value = selector; |
| 1141 | 0 | selector = this.selector; |
| 1142 | | } |
| 1143 | | |
| 1144 | 0 | var cb = this._generateCallbackAssertion('setValue', 'setValue', selector + ' : ' + value, hash); |
| 1145 | 0 | this._addToActionQueue([selector, value, hash], 'setValue', cb); |
| 1146 | 0 | return this; |
| 1147 | | }; |
| 1148 | | |
| 1149 | | // LOG (May should live in its own module) |
| 1150 | | // --------------------------------------- |
| 1151 | | |
| 1152 | 1 | Actions.prototype.logger = {}; |
| 1153 | | |
| 1154 | | /** |
| 1155 | | * Logs a part of the remote dom |
| 1156 | | * |
| 1157 | | * ```html |
| 1158 | | * <body> |
| 1159 | | * <div id="smth"> |
| 1160 | | * <input type="hidden" value="not really a value" id="ijustwannahaveavalue"/> |
| 1161 | | * </div> |
| 1162 | | * </body> |
| 1163 | | * ``` |
| 1164 | | * |
| 1165 | | * ```javascript |
| 1166 | | * test.open('http://dalekjs.com/guineapig') |
| 1167 | | * .log.dom('#smth') |
| 1168 | | * .done(); |
| 1169 | | * ``` |
| 1170 | | * |
| 1171 | | * Will output this: |
| 1172 | | * |
| 1173 | | * ```html |
| 1174 | | * DOM: #smth <input type="hidden" value="not really a value" id="ijustwannahaveavalue"/> |
| 1175 | | * ``` |
| 1176 | | |
| 1177 | | * |
| 1178 | | * @api |
| 1179 | | * @method log.dom |
| 1180 | | * @param {string} selector CSS selector |
| 1181 | | * @chainable |
| 1182 | | */ |
| 1183 | | |
| 1184 | 1 | Actions.prototype.logger.dom = function (selector) { |
| 1185 | 0 | var hash = uuid.v4(); |
| 1186 | | |
| 1187 | 0 | var cb = function logDomCb (data) { |
| 1188 | 0 | if (data && data.key === 'source' && !this.uuids[data.uuid]) { |
| 1189 | 0 | this.uuids[data.uuid] = true; |
| 1190 | 0 | var $ = cheerio.load(data.value); |
| 1191 | 0 | var result = selector ? $(selector).html() : $.html(); |
| 1192 | 0 | selector = selector ? selector : ' '; |
| 1193 | 0 | result = !result ? ' Not found' : result; |
| 1194 | 0 | this.reporter.emit('report:log:user', 'DOM: ' + selector + ' ' + result); |
| 1195 | | } |
| 1196 | | }.bind(this); |
| 1197 | | |
| 1198 | 0 | this._addToActionQueue([hash], 'source', cb); |
| 1199 | 0 | return this; |
| 1200 | | }; |
| 1201 | | |
| 1202 | | /** |
| 1203 | | * Logs a user defined message |
| 1204 | | * |
| 1205 | | * ```javascript |
| 1206 | | * test.open('http://dalekjs.com/guineapig') |
| 1207 | | * .execute(function () { |
| 1208 | | * this.data('aKey', 'aValue'); |
| 1209 | | * }) |
| 1210 | | * .log.message(function () { |
| 1211 | | * return test.data('aKey'); // outputs MESSAGE: 'aValue' |
| 1212 | | * }) |
| 1213 | | * .done(); |
| 1214 | | * ``` |
| 1215 | | * |
| 1216 | | * 'Normal' messages can be logged too: |
| 1217 | | * |
| 1218 | | * ```javascript |
| 1219 | | * test.open('http://dalekjs.com/guineapig') |
| 1220 | | * .log.message('FooBar') // outputs MESSAGE: FooBar |
| 1221 | | * .done(); |
| 1222 | | * ``` |
| 1223 | | * |
| 1224 | | * @api |
| 1225 | | * @method log.message |
| 1226 | | * @param {function|string} message |
| 1227 | | * @chainable |
| 1228 | | */ |
| 1229 | | |
| 1230 | 1 | Actions.prototype.logger.message = function (message) { |
| 1231 | 0 | var hash = uuid.v4(); |
| 1232 | | |
| 1233 | 0 | var cb = function logMessageCb (data) { |
| 1234 | 0 | if (data && data.key === 'noop' && !this.uuids[data.hash]) { |
| 1235 | 0 | this.uuids[data.hash] = true; |
| 1236 | 0 | var result = (typeof(data.value) === 'function') ? data.value.bind(this)() : data.value; |
| 1237 | 0 | this.reporter.emit('report:log:user', 'MESSAGE: ' + result); |
| 1238 | | } |
| 1239 | | }.bind(this); |
| 1240 | | |
| 1241 | 0 | this._addToActionQueue([message, hash], 'noop', cb); |
| 1242 | 0 | return this; |
| 1243 | | }; |
| 1244 | | |
| 1245 | | /** |
| 1246 | | * Generates a callback that will be fired when the action has been completed. |
| 1247 | | * The callback itself will then validate the answer and will also emit an event |
| 1248 | | * that the action has been successfully executed. |
| 1249 | | * |
| 1250 | | * @method _generateCallbackAssertion |
| 1251 | | * @param {string} key Unique key of the action |
| 1252 | | * @param {string} type Type of the action (normalle the actions name) |
| 1253 | | * @return {function} The generated callback function |
| 1254 | | * @private |
| 1255 | | */ |
| 1256 | | |
| 1257 | 1 | Actions.prototype._generateCallbackAssertion = function (key, type) { |
| 1258 | 0 | var cb = function (data) { |
| 1259 | 0 | if (data && data.key === key && !this.uuids[data.uuid]) { |
| 1260 | 0 | if (!data || (data.value && data.value === null)) { |
| 1261 | 0 | data.value = ''; |
| 1262 | | } |
| 1263 | | |
| 1264 | 0 | if (key === 'execute') { |
| 1265 | 0 | Object.keys(data.value.dalek).forEach(function (key) { |
| 1266 | 0 | this.contextVars[key] = data.value.dalek[key]; |
| 1267 | | }.bind(this)); |
| 1268 | | |
| 1269 | 0 | data.value.test.forEach(function (test) { |
| 1270 | 0 | this.reporter.emit('report:assertion', { |
| 1271 | | success: test.ok, |
| 1272 | | expected: true, |
| 1273 | | value: test.ok, |
| 1274 | | message: test.message, |
| 1275 | | type: 'OK' |
| 1276 | | }); |
| 1277 | | |
| 1278 | 0 | this.incrementExpectations(); |
| 1279 | | |
| 1280 | 0 | if (!test.ok) { |
| 1281 | 0 | this.incrementFailedAssertions(); |
| 1282 | | } |
| 1283 | | }.bind(this)); |
| 1284 | | |
| 1285 | 0 | data.value = ''; |
| 1286 | | } |
| 1287 | | |
| 1288 | 0 | this.uuids[data.uuid] = true; |
| 1289 | 0 | reporter.emit('report:action', { |
| 1290 | | value: data.value, |
| 1291 | | type: type, |
| 1292 | | uuid: data.uuid |
| 1293 | | }); |
| 1294 | | } |
| 1295 | | }.bind(this); |
| 1296 | 0 | return cb; |
| 1297 | | }; |
| 1298 | | |
| 1299 | | /** |
| 1300 | | * Adds a method to the queue of actions/assertions to execute |
| 1301 | | * |
| 1302 | | * @method _addToActionQueue |
| 1303 | | * @param {object} opts Options of the action to invoke |
| 1304 | | * @param {string} driverMethod Name of the method to call on the driver |
| 1305 | | * @param {function} A callback function that will be executed when the action has been executed |
| 1306 | | * @private |
| 1307 | | * @chainable |
| 1308 | | */ |
| 1309 | | |
| 1310 | 1 | Actions.prototype._addToActionQueue = function (opts, driverMethod, cb) { |
| 1311 | 0 | this.actionPromiseQueue.push(function () { |
| 1312 | 0 | var deferred = Q.defer(); |
| 1313 | | // add a generic identifier as the last argument to any action method call |
| 1314 | 0 | opts.push(uuid.v4()); |
| 1315 | | // check the method on the driver object && the callback function |
| 1316 | 0 | if (typeof(this.driver[driverMethod]) === 'function' && typeof(cb) === 'function') { |
| 1317 | | // call the method on the driver object |
| 1318 | 0 | this.driver[driverMethod].apply(this.driver, opts); |
| 1319 | 0 | deferred.resolve(); |
| 1320 | | } else { |
| 1321 | 0 | deferred.reject(); |
| 1322 | | } |
| 1323 | | |
| 1324 | | // listen to driver message events & apply the callback argument |
| 1325 | 0 | this.driver.events.on('driver:message', cb); |
| 1326 | 0 | return deferred.promise; |
| 1327 | | }.bind(this)); |
| 1328 | 0 | return this; |
| 1329 | | }; |
| 1330 | | |
| 1331 | | /** |
| 1332 | | * @module DalekJS |
| 1333 | | */ |
| 1334 | | |
| 1335 | 1 | module.exports = function (opts) { |
| 1336 | 1 | reporter = opts.reporter; |
| 1337 | 1 | return Actions; |
| 1338 | | }; |
| 1339 | | |