Coverage

19%
243
47
196

/home/ubuntu/src/github.com/dalekjs/dalek-internal-actions/index.js

19%
243
47
196
LineHitsSource
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
241'use strict';
25
26// ext. libs
271var Q = require('q');
281var uuid = require('node-uuid');
291var cheerio = require('cheerio');
30
31// int. global
321var 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
441var Actions = function () {
451 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
861Actions.prototype.query = function (selector) {
870 var that = !this.test ? this : this.test;
880 that.lastChain.push('querying');
890 that.selector = selector;
900 that.querying = true;
910 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
1031Actions.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
1161Actions.prototype.mouseEvent = function (type, selector) {
1170 var hash = uuid.v4();
1180 var cb = this._generateCallbackAssertion('mouseEvent', 'mouseEvent', type, selector, hash);
1190 this._addToActionQueue([type, selector, hash], 'mouseEvent', cb);
1200 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
1731Actions.prototype.setHttpAuth = function (username, password) {
1740 var hash = uuid.v4();
1750 var cb = this._generateCallbackAssertion('setHttpAuth', 'setHttpAuth', username, password, hash);
1760 this._addToActionQueue([username, password, hash], 'setHttpAuth', cb);
1770 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
2131Actions.prototype.toFrame = function (selector) {
2140 var hash = uuid.v4();
215
2160 if (this.querying === true) {
2170 selector = this.selector;
218 }
219
2200 var cb = this._generateCallbackAssertion('toFrame', 'toFrame', selector, hash);
2210 this._addToActionQueue([selector, hash], 'toFrame', cb);
2220 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
2521Actions.prototype.toParent = function () {
2530 var hash = uuid.v4();
2540 var cb = this._generateCallbackAssertion('toFrame', 'toFrame', null, hash);
2550 this._addToActionQueue([null, hash], 'toFrame', cb);
2560 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
2921Actions.prototype.toWindow = function (name) {
2930 var hash = uuid.v4();
2940 var cb = this._generateCallbackAssertion('toWindow', 'toWindow', name, hash);
2950 this._addToActionQueue([name, hash], 'toWindow', cb);
2960 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
3261Actions.prototype.toParentWindow = function () {
3270 var hash = uuid.v4();
3280 var cb = this._generateCallbackAssertion('toWindow', 'toWindow', null, hash);
3290 this._addToActionQueue([null, hash], 'toWindow', cb);
3300 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
3441Actions.prototype.waitForResource = function (ressource, timeout) {
3450 var hash = uuid.v4();
3460 var cb = this._generateCallbackAssertion('waitForResource', 'waitForResource', ressource, timeout, hash);
3470 this._addToActionQueue([ressource, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitForResource', cb);
3480 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
3621Actions.prototype.waitForText = function (text, timeout) {
3630 var hash = uuid.v4();
3640 var cb = this._generateCallbackAssertion('waitForText', 'waitForText', text, timeout, hash);
3650 this._addToActionQueue([text, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitForText', cb);
3660 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
3801Actions.prototype.waitUntilVisible = function (selector, timeout) {
3810 var hash = uuid.v4();
382
3830 if (this.querying === true) {
3840 timeout = selector;
3850 selector = this.selector;
386 }
387
3880 var cb = this._generateCallbackAssertion('waitUntilVisible', 'waitUntilVisible', selector, timeout, hash);
3890 this._addToActionQueue([selector, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitUntilVisible', cb);
3900 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
4041Actions.prototype.waitWhileVisible = function (selector, timeout) {
4050 var hash = uuid.v4();
406
4070 if (this.querying === true) {
4080 timeout = selector;
4090 selector = this.selector;
410 }
411
4120 var cb = this._generateCallbackAssertion('waitWhileVisible', 'waitWhileVisible', selector, timeout, hash);
4130 this._addToActionQueue([selector, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitWhileVisible', cb);
4140 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
4591Actions.prototype.screenshot = function (pathname) {
4600 var hash = uuid.v4();
4610 var cb = this._generateCallbackAssertion('screenshot', 'screenshot', pathname, hash);
4620 this._addToActionQueue(['', pathname, hash], 'screenshot', cb);
4630 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
4991Actions.prototype.wait = function (timeout) {
5000 var hash = uuid.v4();
5010 var cb = this._generateCallbackAssertion('wait', 'wait', timeout, hash);
5020 this._addToActionQueue([(timeout ? parseInt(timeout, 10) : 5000), hash], 'wait', cb);
5030 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
5221Actions.prototype.reload = function () {
5230 var hash = uuid.v4();
5240 var cb = this._generateCallbackAssertion('refresh', 'refresh', '', hash);
5250 this._addToActionQueue([hash], 'refresh', cb);
5260 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
5501Actions.prototype.forward = function () {
5510 var hash = uuid.v4();
5520 var cb = this._generateCallbackAssertion('forward', 'forward', '', hash);
5530 this._addToActionQueue([hash], 'forward', cb);
5540 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
5781Actions.prototype.back = function () {
5790 var hash = uuid.v4();
5800 var cb = this._generateCallbackAssertion('back', 'back', '', hash);
5810 this._addToActionQueue([hash], 'back', cb);
5820 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
6191Actions.prototype.click = function (selector) {
6200 var hash = uuid.v4();
621
6220 if (this.querying === true) {
6230 selector = this.selector;
624 }
625
6260 var cb = this._generateCallbackAssertion('click', 'click', selector, hash);
6270 this._addToActionQueue([selector, hash], 'click', cb);
6280 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
6561Actions.prototype.submit = function (selector) {
6570 var hash = uuid.v4();
658
6590 if (this.querying === true) {
6600 selector = this.selector;
661 }
662
6630 var cb = this._generateCallbackAssertion('submit', 'submit', selector, hash);
6640 this._addToActionQueue([selector, hash], 'submit', cb);
6650 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
6871Actions.prototype.open = function (location) {
6880 var hash = uuid.v4();
6890 var cb = this._generateCallbackAssertion('open', 'open', location, hash);
6900 this._addToActionQueue([location, hash], 'open', cb);
6910 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
7331Actions.prototype.type = function (selector, keystrokes) {
7340 var hash = uuid.v4();
735
7360 if (this.querying === true) {
7370 keystrokes = selector;
7380 selector = this.selector;
739 }
740
7410 var cb = this._generateCallbackAssertion('type', 'type', selector, keystrokes, hash);
7420 this._addToActionQueue([selector, keystrokes], 'type', cb);
7430 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
7681Actions.prototype.sendKeys = function (selector, keystrokes) {
7690 var hash = uuid.v4();
770
7710 if (this.querying === true) {
7720 keystrokes = selector;
7730 selector = this.selector;
774 }
775
7760 var cb = this._generateCallbackAssertion('sendKeys', 'sendKeys', selector, keystrokes, hash);
7770 this._addToActionQueue([selector, keystrokes], 'sendKeys', cb);
7780 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
8081Actions.prototype.answer = function (keystrokes) {
8090 var hash = uuid.v4();
8100 var cb = this._generateCallbackAssertion('promptText', 'promptText', keystrokes, hash);
8110 this._addToActionQueue([keystrokes, hash], 'promptText', cb);
8120 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
8461Actions.prototype.execute = function (script) {
8470 var hash = uuid.v4();
8480 var args = [this.contextVars].concat(Array.prototype.slice.call(arguments, 1) || []);
8490 var cb = this._generateCallbackAssertion('execute', 'execute', script, args, hash);
8500 this._addToActionQueue([script, args, hash], 'execute', cb);
8510 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
8881Actions.prototype.waitFor = function (script, args, timeout) {
8890 var hash = uuid.v4();
8900 timeout = timeout || 5000;
8910 var cb = this._generateCallbackAssertion('waitFor', 'waitFor', script, args, timeout, hash);
8920 this._addToActionQueue([script, args, timeout, hash], 'waitFor', cb);
8930 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
9221Actions.prototype.accept = function () {
9230 var hash = uuid.v4();
9240 var cb = this._generateCallbackAssertion('acceptAlert', 'acceptAlert', hash);
9250 this._addToActionQueue([hash], 'acceptAlert', cb);
9260 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
9561Actions.prototype.dismiss = function () {
9570 var hash = uuid.v4();
9580 var cb = this._generateCallbackAssertion('dismissAlert', 'dismissAlert', hash);
9590 this._addToActionQueue([hash], 'dismissAlert', cb);
9600 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
10021Actions.prototype.resize = function (dimensions) {
10030 var hash = uuid.v4();
10040 var cb = this._generateCallbackAssertion('resize', 'resize', dimensions, hash);
10050 this._addToActionQueue([dimensions, hash], 'resize', cb);
10060 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
10461Actions.prototype.maximize = function () {
10470 var hash = uuid.v4();
10480 var cb = this._generateCallbackAssertion('maximize', 'maximize', hash);
10490 this._addToActionQueue([hash], 'maximize', cb);
10500 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
10701Actions.prototype.setCookie = function (name, contents) {
10710 var hash = uuid.v4();
10720 var cb = this._generateCallbackAssertion('setCookie', 'setCookie', name, contents, hash);
10730 this._addToActionQueue([name, contents, hash], 'setCookie', cb);
10740 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
11031Actions.prototype.waitForElement = function (selector, timeout) {
11040 var hash = uuid.v4();
1105
11060 if (this.querying === true) {
11070 timeout = selector;
11080 selector = this.selector;
1109 }
1110
11110 var cb = this._generateCallbackAssertion('waitForElement', 'waitForElement', selector + ' : ' + timeout, hash);
11120 this._addToActionQueue([selector, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitForElement', cb);
11130 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
11361Actions.prototype.setValue = function (selector, value) {
11370 var hash = uuid.v4();
1138
11390 if (this.querying === true) {
11400 value = selector;
11410 selector = this.selector;
1142 }
1143
11440 var cb = this._generateCallbackAssertion('setValue', 'setValue', selector + ' : ' + value, hash);
11450 this._addToActionQueue([selector, value, hash], 'setValue', cb);
11460 return this;
1147};
1148
1149// LOG (May should live in its own module)
1150// ---------------------------------------
1151
11521Actions.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
11841Actions.prototype.logger.dom = function (selector) {
11850 var hash = uuid.v4();
1186
11870 var cb = function logDomCb (data) {
11880 if (data && data.key === 'source' && !this.uuids[data.uuid]) {
11890 this.uuids[data.uuid] = true;
11900 var $ = cheerio.load(data.value);
11910 var result = selector ? $(selector).html() : $.html();
11920 selector = selector ? selector : ' ';
11930 result = !result ? ' Not found' : result;
11940 this.reporter.emit('report:log:user', 'DOM: ' + selector + ' ' + result);
1195 }
1196 }.bind(this);
1197
11980 this._addToActionQueue([hash], 'source', cb);
11990 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
12301Actions.prototype.logger.message = function (message) {
12310 var hash = uuid.v4();
1232
12330 var cb = function logMessageCb (data) {
12340 if (data && data.key === 'noop' && !this.uuids[data.hash]) {
12350 this.uuids[data.hash] = true;
12360 var result = (typeof(data.value) === 'function') ? data.value.bind(this)() : data.value;
12370 this.reporter.emit('report:log:user', 'MESSAGE: ' + result);
1238 }
1239 }.bind(this);
1240
12410 this._addToActionQueue([message, hash], 'noop', cb);
12420 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
12571Actions.prototype._generateCallbackAssertion = function (key, type) {
12580 var cb = function (data) {
12590 if (data && data.key === key && !this.uuids[data.uuid]) {
12600 if (!data || (data.value && data.value === null)) {
12610 data.value = '';
1262 }
1263
12640 if (key === 'execute') {
12650 Object.keys(data.value.dalek).forEach(function (key) {
12660 this.contextVars[key] = data.value.dalek[key];
1267 }.bind(this));
1268
12690 data.value.test.forEach(function (test) {
12700 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
12780 this.incrementExpectations();
1279
12800 if (!test.ok) {
12810 this.incrementFailedAssertions();
1282 }
1283 }.bind(this));
1284
12850 data.value = '';
1286 }
1287
12880 this.uuids[data.uuid] = true;
12890 reporter.emit('report:action', {
1290 value: data.value,
1291 type: type,
1292 uuid: data.uuid
1293 });
1294 }
1295 }.bind(this);
12960 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
13101Actions.prototype._addToActionQueue = function (opts, driverMethod, cb) {
13110 this.actionPromiseQueue.push(function () {
13120 var deferred = Q.defer();
1313 // add a generic identifier as the last argument to any action method call
13140 opts.push(uuid.v4());
1315 // check the method on the driver object && the callback function
13160 if (typeof(this.driver[driverMethod]) === 'function' && typeof(cb) === 'function') {
1317 // call the method on the driver object
13180 this.driver[driverMethod].apply(this.driver, opts);
13190 deferred.resolve();
1320 } else {
13210 deferred.reject();
1322 }
1323
1324 // listen to driver message events & apply the callback argument
13250 this.driver.events.on('driver:message', cb);
13260 return deferred.promise;
1327 }.bind(this));
13280 return this;
1329};
1330
1331/**
1332 * @module DalekJS
1333 */
1334
13351module.exports = function (opts) {
13361 reporter = opts.reporter;
13371 return Actions;
1338};
1339