/*!
*
* Copyright (c) 2013 Sebastian Golasch
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
'use strict';
// ext. libs
var Q = require('q');
var uuid = require('./uuid');
var cheerio = require('cheerio');
// int. global
var reporter = null;
/**
* Actions are a way to control your browsers, e.g. simulate user interactions
* like clicking elements, open urls, filling out input fields, etc.
*
* @class Actions
* @constructor
* @part Actions
* @api
*/
var Actions = function () {
this.uuids = {};
};
/**
* It can be really cumbersome to repeat selectors all over when performing
* multiple actions or assertions on the same element(s).
* When you use the query method (or its alias $), you're able to specify a
* selector once instead of repeating it all over the place.
*
* So, instead of writing this:
*
* ```javascript
* test.open('http://doctorwhotv.co.uk/')
* .assert.text('#nav').is('Navigation')
* .assert.visible('#nav')
* .assert.attr('#nav', 'data-nav', 'true')
* .click('#nav')
* .done();
* ```
*
* you can write this:
*
* ```javascript
* test.open('http://doctorwhotv.co.uk/')
* .query('#nav')
* .assert.text().is('Navigation')
* .assert.visible()
* .assert.attr('data-nav', 'true')
* .click()
* .end()
* .done();
* ```
*
* Always make sure to terminate it with the [end](assertions.html#meth-end) method!
*
* @api
* @method query
* @param {string} selector Selector of the element to query
* @chainable
*/
Actions.prototype.query = function (selector) {
var that = !this.test ? this : this.test;
that.lastChain.push('querying');
that.selector = selector;
that.querying = true;
return this.test ? this : that;
};
/**
* Alias of query
*
* @api
* @method $
* @param {string} selector Selector of the element to query
* @chainable
*/
Actions.prototype.$ = Actions.prototype.query;
/**
* Triggers a mouse event on the first element found matching the provided selector.
* Supported events are mouseup, mousedown, click, mousemove, mouseover and mouseout.
* TODO: IMPLEMENT
*
* @method mouseEvent
* @param {string} type
* @param {string} selector
* @chainable
*/
Actions.prototype.mouseEvent = function (type, selector) {
var hash = uuid();
var cb = this._generateCallbackAssertion('mouseEvent', 'mouseEvent', type, selector, hash);
this._addToActionQueue([type, selector, hash], 'mouseEvent', cb);
return this;
};
/**
* Sets HTTP_AUTH_USER and HTTP_AUTH_PW values for HTTP based authentication systems.
*
* If your site is behind a HTTP basic auth, you're able to set the username and the password
*
* ```javascript
* test.setHttpAuth('OSWIN', 'rycbrar')
* .open('http://admin.therift.com');
* ```
*
* Most of the time, you`re not storing your passwords within files that will be checked
* in your vcs, for this scenario, you have two options:
*
* The first option is, to use daleks cli capabilities to generate config variables
* from the command line, like this
*
* ```batch
* $ dalek --vars USER=OSWIN,PASS=rycbrar
* ```
*
* ```javascript
* test.setHttpAuth(test.config.get('USER'), test.config.get('PASS'))
* .open('http://admin.therift.com');
* ```
*
* The second option is, to use env variables to generate config variables
* from the command line, like this
*
* ```batch
* $ SET USER=OSWIN
* $ SET PASS=rycbrar
* $ dalek
* ```
*
* ```javascript
* test.setHttpAuth(test.config.get('USER'), test.config.get('PASS'))
* .open('http://admin.therift.com');
* ```
*
* If both, dalek variables & env variables are set, the dalek variables win.
* For more information about this, I recommend to check out the [configuration docs](/docs/config.html)
*
* TODO: IMPLEMENT
*
* @method setHttpAuth
* @param {string} username
* @param {string} password
* @return {Actions}
*/
Actions.prototype.setHttpAuth = function (username, password) {
var hash = uuid();
var cb = this._generateCallbackAssertion('setHttpAuth', 'setHttpAuth', username, password, hash);
this._addToActionQueue([username, password, hash], 'setHttpAuth', cb);
return this;
};
/**
* Switches to an iFrame context
*
* Sometimes you encounter situations, where you need to drive/access an iFrame sitting in your page.
* You can access such frames with this method, but be aware of the fact, that the complete test context
* than switches to the iframe context, every action and assertion will be executed within the iFrame context.
* Btw.: The domain of the IFrame can be whatever you want, this method has no same origin policy restrictions.
*
* If you wan't to get back to the parents context, you have to use the [toParent](#meth-toParent) method.
*
* ```html
* <div>
* <iframe id="login" src="/login.html"/>
* </div>
* ```
*
* ```javascript
* test.open('http://adomain.withiframe.com')
* .assert.title().is('Title of a page that embeds an iframe')
* .toFrame('#login')
* .assert.title().is('Title of a page that can be embedded as an iframe')
* .toParent()
* .done();
* ```
*
* > NOTE: Buggy in Firefox
*
* @api
* @method toFrame
* @param {string} selector Selector of the frame to switch to
* @chainable
*/
Actions.prototype.toFrame = function (selector) {
var hash = uuid();
if (this.querying === true) {
selector = this.selector;
}
var cb = this._generateCallbackAssertion('toFrame', 'toFrame', selector, hash);
this._addToActionQueue([selector, hash], 'toFrame', cb);
return this;
};
/**
* Switches back to the parent page context when the test context has been
* switched to an iFrame context
*
* ```html
* <div>
* <iframe id="login" src="/login.html"/>
* </div>
* ```
*
* ```javascript
* test.open('http://adomain.withiframe.com')
* .assert.title().is('Title of a page that embeds an iframe')
* .toFrame('#login')
* .assert.title().is('Title of a page that can be embedded as an iframe')
* .toParent()
* .assert.title().is('Title of a page that embeds an iframe')
* .done();
* ```
*
* > NOTE: Buggy in Firefox
*
* @api
* @method toParent
* @chainable
*/
Actions.prototype.toParent = function () {
var hash = uuid();
var cb = this._generateCallbackAssertion('toFrame', 'toFrame', null, hash);
this._addToActionQueue([null, hash], 'toFrame', cb);
return this;
};
/**
* Switches to a different window context
*
* Sometimes you encounter situations, where you need to access a different window, like popup windows.
* You can access such windows with this method, but be aware of the fact, that the complete test context
* than switches to the window context, every action and assertion will be executed within the chosen window context.
* Btw.: The domain of the window can be whatever you want, this method has no same origin policy restrictions.
*
* If you want to get back to the parents context, you have to use the [toParentWindow](#meth-toParentWindow) method.
*
* ```html
* <div>
* <a onclick="window.open('http://google.com','goog','width=480, height=300')">Open Google</a>
* </div>
* ```
*
* ```javascript
* test.open('http://adomain.com')
* .assert.title().is('Title of a page that can open a popup window')
* .toWindow('goog')
* .assert.title().is('Google')
* .toParentWindow()
* .done();
* ```
*
* > NOTE: Buggy in Firefox
*
* @api
* @method toWindow
* @param {string} name Name of the window to switch to
* @chainable
*/
Actions.prototype.toWindow = function (name) {
var hash = uuid();
var cb = this._generateCallbackAssertion('toWindow', 'toWindow', name, hash);
this._addToActionQueue([name, hash], 'toWindow', cb);
return this;
};
/**
* Switches back to the parent window context when the test context has been
* switched to a different window context
*
* ```html
* <div>
* <a onclick="window.open('http://google.com','goog','width=480, height=300')">Open Google</a>
* </div>
* ```
*
* ```javascript
* test.open('http://adomain.com')
* .assert.title().is('Title of a page that can open a popup window')
* .toWindow('goog')
* .assert.title().is('Google')
* .toParentWindow()
* .assert.title().is('Title of a page that can open a popup window')
* .done();
* ```
*
* > NOTE: Buggy in Firefox
*
* @api
* @method toParentWindow
* @chainable
*/
Actions.prototype.toParentWindow = function () {
var hash = uuid();
var cb = this._generateCallbackAssertion('toWindow', 'toWindow', null, hash);
this._addToActionQueue([null, hash], 'toWindow', cb);
return this;
};
/**
* Wait until a resource that matches the given testFx is loaded to process a next step.
*
* TODO: IMPLEMENT
*
* @method waitForResource
* @param {string} ressource URL of the ressource that should be waited for
* @param {number} timeout Timeout in miliseconds
* @chainable
*/
Actions.prototype.waitForResource = function (ressource, timeout) {
var hash = uuid();
var cb = this._generateCallbackAssertion('waitForResource', 'waitForResource', ressource, timeout, hash);
this._addToActionQueue([ressource, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitForResource', cb);
return this;
};
/**
* Waits until the passed text is present in the page contents before processing the immediate next step.
*
* TODO: IMPLEMENT
*
* @method waitForText
* @param {string} text Text to be waited for
* @param {number} timeout Timeout in miliseconds
* @chainable
*/
Actions.prototype.waitForText = function (text, timeout) {
var hash = uuid();
var cb = this._generateCallbackAssertion('waitForText', 'waitForText', text, timeout, hash);
this._addToActionQueue([text, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitForText', cb);
return this;
};
/**
* Waits until an element matching the provided selector expression is visible in the remote DOM to process a next step.
*
* TODO: IMPLEMENT
*
* @method waitUntilVisible
* @param {string} selector Selector of the element that should be waited to become invisible
* @param {number} timeout Timeout in miliseconds
* @chainable
*/
Actions.prototype.waitUntilVisible = function (selector, timeout) {
var hash = uuid();
if (this.querying === true) {
timeout = selector;
selector = this.selector;
}
var cb = this._generateCallbackAssertion('waitUntilVisible', 'waitUntilVisible', selector, timeout, hash);
this._addToActionQueue([selector, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitUntilVisible', cb);
return this;
};
/**
* Waits until an element matching the provided selector expression is no longer visible in remote DOM to process a next step.
*
* TODO: IMPLEMENT
*
* @method waitWhileVisible
* @param {string} selector Selector of the element that should be waited to become visible
* @param {number} timeout Timeout in miliseconds
* @chainable
*/
Actions.prototype.waitWhileVisible = function (selector, timeout) {
var hash = uuid();
if (this.querying === true) {
timeout = selector;
selector = this.selector;
}
var cb = this._generateCallbackAssertion('waitWhileVisible', 'waitWhileVisible', selector, timeout, hash);
this._addToActionQueue([selector, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitWhileVisible', cb);
return this;
};
/**
* Take a screenshot of the current page or css element.
*
* The pathname argument takes some placeholders that will be replaced
* Placeholder:
*
* - `:browser` - The browser name (e.g. 'Chrome', 'Safari', 'Firefox', etc.)
* - `:version` - The browser version (e.g. '10_0', '23_11_5', etc.)
* - `:os` - The operating system (e.g. `OSX`, `Windows`, `Linux`)
* - `:osVersion` - The operating system version (e.g `XP`, `7`, `10_8`, etc.)
* - `:viewport` - The current viewport in pixels (e.g. `w1024_h768`)
* - `:timestamp` - UNIX like timestapm (e.g. `637657345`)
* - `:date` - Current date in format MM_DD_YYYY (e.g. `12_24_2013`)
* - `:datetime` - Current datetime in format MM_DD_YYYY_HH_mm_ss (e.g. `12_24_2013_14_55_23`)
*
* ```javascript
* // creates 'my/folder/my_file.png'
* test.screenshot('my/folder/my_file.png');
* // creates 'my/page/in/safari/homepage.png'
* test.screenshot('my/page/in/:browser/homepage.png');
* // creates 'my/page/in/safari_6_0_1/homepage.png'
* test.screenshot('my/page/in/:browser_:version/homepage.png');
* // creates 'my/page/in/safari_6_0_1/on/osx/homepage.png'
* test.screenshot('my/page/in/:browser_:version/on/:os/homepage.png');
* // creates 'my/page/in/safari_6_0_1/on/osx_10_8/homepage.png'
* test.screenshot('my/page/in/:browser_:version/on/:os_:osVersion/homepage.png');
* // creates 'my/page/at/w1024_h768/homepage.png'
* test.screenshot('my/page/at/:viewport/homepage.png');
* // creates 'my/page/at/637657345/homepage.png'
* test.screenshot('my/page/in_time/:timestamp/homepage.png');
* // creates 'my/page/at/12_24_2013/homepage.png'
* test.screenshot('my/page/in_time/:date/homepage.png');
* // creates 'my/page/at/12_24_2013_14_55_23/homepage.png'
* test.screenshot('my/page/in_time/:datetime/homepage.png');
* ```
*
* @api
* @method screenshot
* @param {string} pathname Name of the folder and file the screenshot should be saved to
* @param {string} css selector of element should be screeshoted
* @return chainable
*/
Actions.prototype.screenshot = function (pathname, selector) {
var hash = uuid();
if (this.querying === true) {
selector = this.selector;
}
var opts = {
realpath : undefined,
selector : selector
};
this.screenshotParams = opts;
var screenshotcb = this._generatePlainCallback('screenshot', hash, opts, 'realpath', typeof selector === 'undefined');
this._addToActionQueue(['', pathname, hash], 'screenshot', screenshotcb.bind(this));
if (selector) {
var imagecutcb = this._generateCallbackAssertion('imagecut', 'screenshot element', opts, hash);
this._addToActionQueue([opts, hash], 'imagecut', imagecutcb);
}
this.reporter.emit('report:screenshot', {
'pathname' : pathname,
'uuid' : hash
});
return this;
};
/**
* Generates a callback that will be fired when the action has been completed.
* The callback will then store value into opts variable.
*
* @method _generateCallbackAssertion
* @param {string} type Type of the action (normalle the actions name)
* @param {string} hash Unique id of the action
* @param {string} opts Variable where will be stored result of execution of the action
* @param {string} key Name of the property where will be stored result of execution of the action
* @return {function} The generated callback function
* @private
*/
Actions.prototype._generatePlainCallback = function (type, hash, opts, property, last) {
var cb = function (data) {
if (data.hash === hash && data.key === type && !this.uuids[data.uuid]) {
if (typeof opts === 'object' && typeof property === 'string') {
opts[property] = data.value;
}
if (data.key === 'screenshot') {
this.reporter.emit('report:action', {
value: data.value,
type: type,
uuid: data.uuid
});
}
if (last) {
this.uuids[data.uuid] = true;
}
}
};
return cb;
};
/**
* Pause steps suite execution for a given amount of time, and optionally execute a step on done.
*
* This makes sense, if you have a ticker for example, tht scrolls like every ten seconds
* & you want to assure that the visible content changes every ten seconds
*
* ```javascript
* test.open('http://myticker.org')
* .assert.visible('.ticker-element:first-child', 'First ticker element is visible')
* .wait(10000)
* .assert.visible('.ticker-element:nth-child(2)', 'Snd. ticker element is visible')
* .wait(10000)
* .assert.visible('.ticker-element:last-child', 'Third ticker element is visible')
* .done();
* ```
* If no timeout argument is given, a default timeout of 5 seconds will be used
*
* ```javascript
* test.open('http://myticker.org')
* .assert.visible('.ticker-element:first-child', 'First ticker element is visible')
* .wait()
* .assert.visible('.ticker-element:nth-child(2)', 'Snd. ticker element is visible')
* .wait()
* .assert.visible('.ticker-element:last-child', 'Third ticker element is visible')
* .done();
* ```
*
* @api
* @method wait
* @param {number} timeout in milliseconds
* @chainable
*/
Actions.prototype.wait = function (timeout) {
var hash = uuid();
var cb = this._generateCallbackAssertion('wait', 'wait', timeout, hash);
this._addToActionQueue([(timeout ? parseInt(timeout, 10) : 5000), hash], 'wait', cb);
return this;
};
/**
* Reloads current page location.
*
* This is basically the same as hitting F5/refresh in your browser
*
* ```javascript
* test.open('http://google.com')
* .reload()
* .done();
* ```
*
* @api
* @method reload
* @chainable
*/
Actions.prototype.reload = function () {
var hash = uuid();
var cb = this._generateCallbackAssertion('refresh', 'refresh', '', hash);
this._addToActionQueue([hash], 'refresh', cb);
return this;
};
/**
* Moves a step forward in browser's history.
*
* This is basically the same as hitting the forward button in your browser
*
* ```javascript
* test.open('http://google.com')
* .open('https://github.com')
* .assert.url.is('https://github.com/', 'We are at GitHub')
* .back()
* .assert.url.is('http://google.com', 'We are at Google!')
* .forward()
* .assert.url.is('https://github.com/', 'Back at GitHub! Timetravel FTW')
* .done();
* ```
*
* @api
* @method forward
* @chainable
*/
Actions.prototype.forward = function () {
var hash = uuid();
var cb = this._generateCallbackAssertion('forward', 'forward', '', hash);
this._addToActionQueue([hash], 'forward', cb);
return this;
};
/**
* Moves back a step in browser's history.
*
* This is basically the same as hitting the back button in your browser
*
* ```javascript
* test.open('http://google.com')
* .open('https://github.com')
* .assert.url.is('https://github.com/', 'We are at GitHub')
* .back()
* .assert.url.is('http://google.com', 'We are at Google!')
* .forward()
* .assert.url.is('https://github.com/', 'Back at GitHub! Timetravel FTW');
* .done();
* ```
*
* @api
* @method back
* @chainable
*/
Actions.prototype.back = function () {
var hash = uuid();
var cb = this._generateCallbackAssertion('back', 'back', '', hash);
this._addToActionQueue([hash], 'back', cb);
return this;
};
/**
* Performs a click on the element matching the provided selector expression.
*
* If we take Daleks homepage (the one you're probably visiting right now),
* the HTML looks something like this (it does not really, but hey, lets assume this for a second)
*
* ```html
* <nav>
* <ul>
* <li><a id="homeapge" href="/index.html">DalekJS</a></li>
* <li><a id="docs" href="/docs.html">Documentation</a></li>
* <li><a id="faq" href="/faq.html">F.A.Q</a></li>
* </ul>
* </nav>
* ```
*
* ```javascript
* test.open('http://dalekjs.com')
* .click('#faq')
* .assert.title().is('DalekJS - Frequently asked questions', 'What the F.A.Q.')
* .done();
* ```
*
* By default, this performs a left click.
* In the future it might become the ability to also execute a "right button" click.
*
* > Note: Does not work correctly in Firefox when used on `<select>` & `<option>` elements
*
* @api
* @method click
* @param {string} selector Selector of the element to be clicked
* @chainable
*/
Actions.prototype.click = function (selector) {
var hash = uuid();
if (this.querying === true) {
selector = this.selector;
}
var cb = this._generateCallbackAssertion('click', 'click', selector, hash);
this._addToActionQueue([selector, hash], 'click', cb);
return this;
};
/**
* Submits a form.
*
* ```html
* <form id="skaaro" action="skaaro.php" method="GET">
* <input type="hidden" name="intheshadows" value="itis"/>
* <input type="text" name="truth" id="truth" value=""/>
* </form>
* ```
*
* ```javascript
* test.open('http://home.dalek.com')
* .type('#truth', 'out there is')
* .submit('#skaaro')
* .done();
* ```
*
* > Note: Does not work in Firefox yet
*
* @api
* @method submit
* @param {string} selector Selector of the form to be submitted
* @chainable
*/
Actions.prototype.submit = function (selector) {
var hash = uuid();
if (this.querying === true) {
selector = this.selector;
}
var cb = this._generateCallbackAssertion('submit', 'submit', selector, hash);
this._addToActionQueue([selector, hash], 'submit', cb);
return this;
};
/**
* Performs an HTTP request for opening a given location.
* You can forge GET, POST, PUT, DELETE and HEAD requests.
*
* Basically the same as typing a location into your browsers URL bar and
* hitting return.
*
* ```javascript
* test.open('http://dalekjs.com')
* .assert.url().is('http://dalekjs.com', 'DalekJS I\'m in you')
* .done();
* ```
*
* @api
* @method open
* @param {string} location URL of the page to open
* @chainable
*/
Actions.prototype.open = function (location) {
//see if we should prepend the location with the configured base url is available and needed
if(location.substr(0, 1) === '/' && this.driver.config.config.baseUrl) {
location = this.driver.config.config.baseUrl + location;
}
var hash = uuid();
var cb = this._generateCallbackAssertion('open', 'open', location, hash);
this._addToActionQueue([location, hash], 'open', cb);
return this;
};
/**
* Types a text into an input field or text area.
* And yes, it really types, character for character, like you would
* do when using your keyboard.
*
*
* ```html
* <form id="skaaro" action="skaaro.php" method="GET">
* <input type="hidden" name="intheshadows" value="itis"/>
* <input type="text" name="truth" id="truth" value=""/>
* </form>
* ```
*
* ```javascript
* test.open('http://home.dalek.com')
* .type('#truth', 'out there is')
* .assert.val('#truth', 'out there is', 'Text has been set')
* .done();
* ```
*
* You can also send special keys using unicode.
*
* * ```javascript
* test.open('http://home.dalek.com')
* .type('#truth', 'out \uE008there\uE008 is')
* .assert.val('#truth', 'out THERE is', 'Text has been set')
* .done();
* ```
* 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).
*
* > Note: Does not work correctly in Firefox with special keys
*
* @api
* @method type
* @param {string} selector Selector of the form field to be filled
* @param {string} keystrokes Text to be applied to the element
* @chainable
*/
Actions.prototype.type = function (selector, keystrokes) {
var hash = uuid();
if (this.querying === true) {
keystrokes = selector;
selector = this.selector;
}
var cb = this._generateCallbackAssertion('type', 'type', selector, keystrokes, hash);
this._addToActionQueue([selector, keystrokes], 'type', cb);
return this;
};
/**
* This acts just like .type() with a key difference.
* This action can be used on non-input elements (useful for test site wide keyboard shortcuts and the like).
* So assumeing we have a keyboard shortcut that display an alert box, we could test that with something like this:
*
* ```javascript
* test.open('http://home.dalek.com')
* .sendKeys('body', '\uE00C')
* .assert.dialogText('press the escape key give this alert text')
* .done();
* ```
*
*
* > Note: Does not work correctly in Firefox with special keys
*
* @api
* @method sendKeys
* @param {string} selector Selector of the form field to be filled
* @param {string} keystrokes Text to be applied to the element
* @chainable
*/
Actions.prototype.sendKeys = function (selector, keystrokes) {
var hash = uuid();
if (this.querying === true) {
keystrokes = selector;
selector = this.selector;
}
var cb = this._generateCallbackAssertion('sendKeys', 'sendKeys', selector, keystrokes, hash);
this._addToActionQueue([selector, keystrokes], 'sendKeys', cb);
return this;
};
/**
* Types a text into the text input field of a prompt dialog.
* Like you would do when using your keyboard.
*
* ```html
* <div>
* <a id="aquestion" onclick="this.innerText = window.prompt('Your favourite companion:')">????</a>
* </div>
* ```
*
* ```javascript
* test.open('http://adomain.com')
* .click('#aquestion')
* .answer('Rose')
* .assert.text('#aquestion').is('Rose', 'Awesome she was!')
* .done();
* ```
*
*
* > Note: Does not work in Firefox & PhantomJS
*
* @api
* @method answer
* @param {string} keystrokes Text to be applied to the element
* @return chainable
*/
Actions.prototype.answer = function (keystrokes) {
var hash = uuid();
var cb = this._generateCallbackAssertion('promptText', 'promptText', keystrokes, hash);
this._addToActionQueue([keystrokes, hash], 'promptText', cb);
return this;
};
/**
* Executes a JavaScript function within the browser context
*
* ```javascript
* test.open('http://adomain.com')
* .execute(function () {
* window.myFramework.addRow('foo');
* window.myFramework.addRow('bar');
* })
* .done();
* ```
*
* You can also apply arguments to the function
*
* ```javascript
* test.open('http://adomain.com')
* .execute(function (paramFoo, aBar) {
* window.myFramework.addRow(paramFoo);
* window.myFramework.addRow(aBar);
* }, 'foo', 'bar')
* .done();
* ```
*
* > Note: Buggy in Firefox
*
* @api
* @method execute
* @param {function} script JavaScript function that should be executed
* @return chainable
*/
Actions.prototype.execute = function (script) {
var hash = uuid();
var args = [this.contextVars].concat(Array.prototype.slice.call(arguments, 1) || []);
var cb = this._generateCallbackAssertion('execute', 'execute', script, args, hash);
this._addToActionQueue([script, args, hash], 'execute', cb);
return this;
};
/**
* Waits until a function returns true to process any next step.
*
* You can also set a callback on timeout using the onTimeout argument,
* and set the timeout using the timeout one, in milliseconds. The default timeout is set to 5000ms.
*
* ```javascript
* test.open('http://adomain.com')
* .waitFor(function () {
* return window.myCheck === true;
* })
* .done();
* ```
*
* You can also apply arguments to the function, as well as a timeout
*
* ```javascript
* test.open('http://adomain.com')
* .waitFor(function (aCheck) {
* return window.myThing === aCheck;
* }, ['arg1', 'arg2'], 10000)
* .done();
* ```
*
* > Note: Buggy in Firefox
*
* @method waitFor
* @param {function} fn Async function that resolves an promise when ready
* @param {array} args Additional arguments
* @param {number} timeout Timeout in miliseconds
* @chainable
* @api
*/
Actions.prototype.waitFor = function (script, args, timeout) {
var hash = uuid();
timeout = timeout || 5000;
args = [this.contextVars].concat(Array.prototype.slice.call(arguments, 1) || []);
var cb = this._generateCallbackAssertion('waitFor', 'waitFor', script, args, timeout, hash);
this._addToActionQueue([script, args, timeout, hash], 'waitFor', cb);
return this;
};
/**
* Accepts an alert/prompt/confirm dialog. This is basically the same actions as when
* you are clicking okay or hitting return in one of that dialogs.
*
* ```html
* <div>
* <a id="attentione" onclick="window.alert('Alonsy!')">ALERT!ALERT!</a>
* </div>
* ```
*
* ```javascript
* test.open('http://adomain.com')
* // alert appears
* .click('#attentione')
* // alert is gone
* .accept()
* .done();
* ```
*
* > Note: Does not work in Firefox & PhantomJS
*
* @api
* @method accept
* @return chainable
*/
Actions.prototype.accept = function () {
var hash = uuid();
var cb = this._generateCallbackAssertion('acceptAlert', 'acceptAlert', hash);
this._addToActionQueue([hash], 'acceptAlert', cb);
return this;
};
/**
* Dismisses an prompt/confirm dialog. This is basically the same actions as when
* you are clicking cancel in one of that dialogs.
*
* ```html
* <div>
* <a id="nonono" onclick="(this.innerText = window.confirm('No classic doctors in the 50th?') ? 'Buh!' : ':(') ">What!</a>
* </div>
* ```
*
* ```javascript
* test.open('http://adomain.com')
* // prompt appears
* .click('#nonono')
* // prompt is gone
* .dismiss()
* .assert.text('#nonono').is(':(', 'So sad')
* .done();
* ```
*
* > Note: Does not work in Firefox & PhantomJS
*
* @api
* @method dismiss
* @return chainable
*/
Actions.prototype.dismiss = function () {
var hash = uuid();
var cb = this._generateCallbackAssertion('dismissAlert', 'dismissAlert', hash);
this._addToActionQueue([hash], 'dismissAlert', cb);
return this;
};
/**
* Resizes the browser window to a set of given dimensions (in px).
* The default configuration of dalek opening pages is a width of 1280px
* and a height of 1024px. You can specify your own default in the configuration.
*
* ```html
* <div>
* <span id="magicspan">The span in the fireplace</span>
* </div>
* ```
*
* ```css
* #magicspan {
* display: inline;
* }
*
* // @media all and (max-width: 500px) and (min-width: 300px)
* #magicspan {
* display: none;
* }
* ```
*
* ```javascript
* test.open('http://adomain.com')
* .assert.visible('#magicspan', 'Big screen, visible span')
* .resize({width: 400, height: 500})
* .assert.notVisible('#magicspan', 'Small screen, no visible span magic!')
* .done();
* ```
*
*
* > Note: Does not work in Firefox
*
* @api
* @method resize
* @param {object} dimensions Width and height as properties to apply
* @chainable
*/
Actions.prototype.resize = function (dimensions) {
var hash = uuid();
var cb = this._generateCallbackAssertion('resize', 'resize', dimensions, hash);
this._addToActionQueue([dimensions, hash], 'resize', cb);
return this;
};
/**
* Maximizes the browser window.
*
* ```html
* <div>
* <span id="magicspan">The span in the fireplace</span>
* </div>
* ```
*
* ```css
* #magicspan {
* display: inline;
* }
*
* @media all and (max-width: 500px) and (min-width: 300px) {
* #magicspan {
* display: none;
* }
* }
* ```
*
* ```javascript
* test.open('http://adomain.com')
* .resize({width: 400, height: 500})
* .assert.notVisible('#magicspan', 'Small screen, no visible span magic!')
* .maximize()
* .assert.visible('#magicspan', 'Big screen, visible span')
* .done();
* ```
*
* > Note: Does not work in Firefox and PhantomJS
*
* @api
* @method maximize
* @chainable
*/
Actions.prototype.maximize = function () {
var hash = uuid();
var cb = this._generateCallbackAssertion('maximize', 'maximize', hash);
this._addToActionQueue([hash], 'maximize', cb);
return this;
};
/**
* Sets a cookie.
* More configuration options will be implemented in the future,
* by now, you can only set a cookie with a specific name and contents.
* This will be a domain wide set cookie.
*
* ```javascript
* test.open('http://adomain.com')
* .setCookie('my_cookie_name', 'my=content')
* .done();
* ```
*
* @api
* @method setCookie
* @chainable
*/
Actions.prototype.setCookie = function (name, contents) {
var hash = uuid();
var cb = this._generateCallbackAssertion('setCookie', 'setCookie', name, contents, hash);
this._addToActionQueue([name, contents, hash], 'setCookie', cb);
return this;
};
/**
* Waits until an element matching the provided
* selector expression exists in remote DOM to process any next step.
*
* Lets assume we have a ticker that loads its contents via AJAX,
* and appends new elements, when the call has been successfully answered:
*
* ```javascript
* test.open('http://myticker.org')
* .assert.text('.ticker-element:first-child', 'First!', 'First ticker element is visible')
* // now we load the next ticker element, defsult timeout is 5 seconds
* .waitForElement('.ticker-element:nth-child(2)')
* .assert.text('.ticker-element:nth-child(2)', 'Me snd. one', 'Snd. ticker element is visible')
* // Lets assume that this AJAX call can take longer, so we raise the default timeout to 10 seconds
* .waitForElement('.ticker-element:last-child', 10000)
* .assert.text('.ticker-element:last-child', 'Me, third one!', 'Third ticker element is visible')
* .done();
* ```
*
* Note that the function exits succesfully when the first element is found, matching the given selector
*
* @api
* @method waitForElement
* @param {string} selector Selector that matches the element to wait for
* @param {number} timeout Timeout in milliseconds
* @chainable
*/
Actions.prototype.waitForElement = function (selector, timeout) {
var hash = uuid();
if (this.querying === true) {
timeout = selector;
selector = this.selector;
}
var cb = this._generateCallbackAssertion('waitForElement', 'waitForElement', selector + ' : ' + timeout, hash);
this._addToActionQueue([selector, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitForElement', cb);
return this;
};
/**
* Fills the fields of a form with given values.
*
* ```html
* <input type="text" value="not really a value" id="ijustwannahaveavalue"/>
* ```
*
* ```javascript
* test.open('http://dalekjs.com')
* .setValue('#ijustwannahaveavalue', 'a value')
* .assert.val('#ijustwannahaveavalue', 'a value', 'Value is changed');
* ```
*
* @api
* @method setValue
* @param {string} selector
* @param {string} value
* @return {Actions}
*/
Actions.prototype.setValue = function (selector, value) {
var hash = uuid();
if (this.querying === true) {
value = selector;
selector = this.selector;
}
var cb = this._generateCallbackAssertion('setValue', 'setValue', selector + ' : ' + value, hash);
this._addToActionQueue([selector, value, hash], 'setValue', cb);
return this;
};
// LOG (May should live in its own module)
// ---------------------------------------
Actions.prototype.logger = {};
/**
* Logs a part of the remote dom
*
* ```html
* <body>
* <div id="smth">
* <input type="hidden" value="not really a value" id="ijustwannahaveavalue"/>
* </div>
* </body>
* ```
*
* ```javascript
* test.open('http://dalekjs.com/guineapig')
* .log.dom('#smth')
* .done();
* ```
*
* Will output this:
*
* ```html
* DOM: #smth <input type="hidden" value="not really a value" id="ijustwannahaveavalue"/>
* ```
*
* @api
* @method log.dom
* @param {string} selector CSS selector
* @chainable
*/
Actions.prototype.logger.dom = function (selector) {
var hash = uuid();
var cb = function logDomCb (data) {
if (data && data.key === 'source' && !this.uuids[data.uuid]) {
this.uuids[data.uuid] = true;
var $ = cheerio.load(data.value);
var result = selector ? $(selector).html() : $.html();
selector = selector ? selector : ' ';
result = !result ? ' Not found' : result;
this.reporter.emit('report:log:user', 'DOM: ' + selector + ' ' + result);
}
}.bind(this);
this._addToActionQueue([hash], 'source', cb);
return this;
};
/**
* Logs a user defined message
*
* ```javascript
* test.open('http://dalekjs.com/guineapig')
* .execute(function () {
* this.data('aKey', 'aValue');
* })
* .log.message(function () {
* return test.data('aKey'); // outputs MESSAGE: 'aValue'
* })
* .done();
* ```
*
* 'Normal' messages can be logged too:
*
* ```javascript
* test.open('http://dalekjs.com/guineapig')
* .log.message('FooBar') // outputs MESSAGE: FooBar
* .done();
* ```
*
* @api
* @method log.message
* @param {function|string} message
* @chainable
*/
Actions.prototype.logger.message = function (message) {
var hash = uuid();
var cb = function logMessageCb (data) {
if (data && data.key === 'noop' && !this.uuids[data.hash]) {
this.uuids[data.hash] = true;
var result = (typeof(data.value) === 'function') ? data.value.bind(this)() : data.value;
this.reporter.emit('report:log:user', 'MESSAGE: ' + result);
}
}.bind(this);
this._addToActionQueue([message, hash], 'noop', cb);
return this;
};
/**
* Generates a callback that will be fired when the action has been completed.
* The callback itself will then validate the answer and will also emit an event
* that the action has been successfully executed.
*
* @method _generateCallbackAssertion
* @param {string} key Unique key of the action
* @param {string} type Type of the action (normalle the actions name)
* @return {function} The generated callback function
* @private
*/
Actions.prototype._generateCallbackAssertion = function (key, type) {
var cb = function (data) {
if (data && data.key === key && !this.uuids[data.uuid]) {
if (!data || (data.value && data.value === null)) {
data.value = '';
}
if (key === 'execute') {
Object.keys(data.value.dalek).forEach(function (key) {
this.contextVars[key] = data.value.dalek[key];
}.bind(this));
data.value.test.forEach(function (test) {
this.reporter.emit('report:assertion', {
success: test.ok,
expected: true,
value: test.ok,
message: test.message,
type: 'OK'
});
this.incrementExpectations();
if (!test.ok) {
this.incrementFailedAssertions();
}
}.bind(this));
data.value = '';
}
this.uuids[data.uuid] = true;
reporter.emit('report:action', {
value: data.value,
type: type,
uuid: data.uuid
});
}
}.bind(this);
return cb;
};
/**
* Adds a method to the queue of actions/assertions to execute
*
* @method _addToActionQueue
* @param {object} opts Options of the action to invoke
* @param {string} driverMethod Name of the method to call on the driver
* @param {function} A callback function that will be executed when the action has been executed
* @private
* @chainable
*/
Actions.prototype._addToActionQueue = function (opts, driverMethod, cb) {
if (driverMethod !== 'screenshot' && driverMethod !== 'imagecut') {
this.screenshotParams = undefined;
}
this.actionPromiseQueue.push(function () {
var deferred = Q.defer();
// add a generic identifier as the last argument to any action method call
opts.push(uuid());
// check the method on the driver object && the callback function
if (typeof(this.driver[driverMethod]) === 'function' && typeof(cb) === 'function') {
// call the method on the driver object
this.driver[driverMethod].apply(this.driver, opts);
deferred.resolve();
} else {
deferred.reject();
}
// listen to driver message events & apply the callback argument
this.driver.events.on('driver:message', cb);
return deferred.promise;
}.bind(this));
return this;
};
Actions.prototype._button = function(button) {
var buttons = {LEFT: 0, MIDDLE: 1, RIGHT: 2};
if (button === undefined) {
button = 0;
} else if (typeof button !== 'number') {
button = buttons[button.toUpperCase()] || 0;
}
return button;
};
// http://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/click
Actions.prototype.buttonClick = function (button) {
var hash = uuid();
button = this._button(button);
var cb = this._generateCallbackAssertion('buttonClick', 'buttonClick');
this._addToActionQueue([button, hash], 'buttonClick', cb);
return this;
};
// http://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/moveto
Actions.prototype.moveTo = function (selector, x, y) {
var hash = uuid();
if (this.querying === true) {
selector = this.selector;
}
if (x === undefined) {
x = null;
}
if (y === undefined) {
y = null;
}
// move to coordinate
var cb = this._generateCallbackAssertion('moveto', 'moveto');
this._addToActionQueue([selector, x, y, hash], 'moveto', cb);
return this;
};
/**
* Close the active window and automatically selects the parent window.
*
* ```javascript
* this.test.toWindow('test');
* this.test.close();
*
* //you can now write your code as if the original parent window was selected because .close()
* //selects that automatically for you so you don't have to call .toParentWindow() everytime
* ```
*
* @api
* @method close
* @chainable
*/
Actions.prototype.close = function () {
var hash = uuid();
var cb = this._generateCallbackAssertion('close', 'close', hash);
this._addToActionQueue([hash], 'close', cb);
//since the current window is now closed, make sense to automatically select the parent window since you would have to do this anyway
this.toParentWindow();
return this;
};
/**
* @module DalekJS
*/
module.exports = function (opts) {
reporter = opts.reporter;
return Actions;
};