/*global document:false, window:false */
/**
* @module leadfoot/Session
*/
var Element = require('./Element');
var findDisplayed = require('./lib/findDisplayed');
var fs = require('fs');
var JsZip = require('jszip');
var lang = require('dojo/lang');
var path = require('path');
var Promise = require('dojo/Promise');
var statusCodes = require('./lib/statusCodes');
var storage = require('./lib/storage');
var strategies = require('./lib/strategies');
var util = require('./lib/util');
var waitForDeleted = require('./lib/waitForDeleted');
/**
* Finds and converts serialised DOM element objects into fully-featured typed Elements.
*
* @private
* @param session The session from which the Element was retrieved.
* @param value An object or array that may be, or may contain, serialised DOM element objects.
* @returns The input value, with all serialised DOM element objects converted to typed Elements.
*/
function convertToElements(session, value) {
// TODO: Unit test elements attached to objects
function convert(value) {
if (Array.isArray(value)) {
value = value.map(convert);
}
else if (typeof value === 'object' && value !== null) {
if (value.ELEMENT) {
value = new Element(value, session);
}
else {
for (var k in value) {
value[k] = convert(value[k]);
}
}
}
return value;
}
return convert(value);
}
/**
* Delegates the HTTP request for a method to the underlying {@link module:leadfoot/Server} object.
*
* @private
* @param {string} method
* @returns {function(string, Object, Array.<string>=): Promise.<{ sessionId: string, status: number, value: any }>}
*/
function delegateToServer(method) {
return function (path, requestData, pathParts) {
var self = this;
path = 'session/' + this._sessionId + (path ? ('/' + path) : '');
if (method === '_post' && !requestData && this.capabilities.brokenEmptyPost) {
requestData = {};
}
return new Promise(function (resolve, reject, progress, setCanceller) {
var cancelled = false;
setCanceller(function (reason) {
cancelled = true;
throw reason;
});
// The promise is cleared from `_nextRequest` once it has been resolved in order to avoid
// infinitely long chains of promises retaining values that are not used any more
var thisRequest;
function clearNextRequest() {
if (self._nextRequest === thisRequest) {
self._nextRequest = null;
}
}
function runRequest() {
// `runRequest` is normally called once the previous request is finished. If this request
// is cancelled before the previous request is finished, then it should simply never run.
// (This Promise will have been rejected already by the cancellation.)
if (cancelled) {
clearNextRequest();
return;
}
var response = self._server[method](path, requestData, pathParts).then(returnValue);
response.finally(clearNextRequest);
// The value of the response always needs to be taken directly from the server call
// rather than from the chained `_nextRequest` promise, since if an undefined value is
// returned by the server call and that value is returned through `finally(runRequest)`,
// the *previous* Promise’s resolved value will be used as the resolved value, which is
// wrong
resolve(response);
return response;
}
// At least ChromeDriver 2.19 will just hard close connections if parallel requests are made to the server,
// so any request sent to the server for a given session must be serialised. Other servers like Selendroid
// have been known to have issues with parallel requests as well, so serialisation is applied universally,
// even though it has negative performance implications
if (self._nextRequest) {
thisRequest = self._nextRequest = self._nextRequest.finally(runRequest);
}
else {
thisRequest = self._nextRequest = runRequest();
}
});
};
}
/**
* As of Selenium 2.40.0 (March 2014), all drivers incorrectly transmit an UnknownError instead of a
* JavaScriptError when user code fails to execute correctly. This method corrects this status code, under the
* assumption that drivers will follow the spec in future.
*
* @private
*/
function fixExecuteError(error) {
if (error.name === 'UnknownError') {
error.status = 17;
error.name = statusCodes[error.status][0];
}
throw error;
}
function noop() {
// At least ios-driver 0.6.6 returns an empty object for methods that are supposed to return no value at all,
// which is not correct
}
/**
* HTTP cookies are transmitted as semicolon-delimited strings, with a `key=value` pair giving the cookie’s name and
* value, then additional information about the cookie (expiry, path, domain, etc.) as additional k-v pairs. This
* method takes an Array describing the parts of a cookie (`target`), and a hash map containing the additional
* information (`source`), and pushes the properties from the source object onto the target array as properly
* escaped key-value strings.
*
* @private
* @param {Array} target
* @param {Object} source
*/
function pushCookieProperties(target, source) {
Object.keys(source).forEach(function (key) {
var value = source[key];
if (key === 'name' || key === 'value' || (key === 'domain' && value === 'http')) {
return;
}
if (typeof value === 'boolean') {
value && target.push(key);
}
// JsonWireProtocol uses the key 'expiry' but JavaScript cookies use the key 'expires'
else if (key === 'expiry') {
if (typeof value === 'number') {
value = new Date(value * 1000);
}
if (value instanceof Date) {
value = Date.toUTCString();
}
target.push('expires=' + encodeURIComponent(value));
}
else {
target.push(key + '=' + encodeURIComponent(value));
}
});
}
/**
* Returns the actual response value from the remote environment.
*
* @private
* @param {Object} response JsonWireProtocol response object.
* @returns {any} The actual response value.
*/
function returnValue(response) {
return response.value;
}
/* istanbul ignore next */
/**
* Simulates a keyboard event as it would occur on Safari 7.
*
* @private
* @param {Array.<string>} keys Keys to type.
*/
function simulateKeys(keys) {
var target = document.activeElement;
function dispatch(kwArgs) {
var event;
if (typeof KeyboardEvent === 'function') {
event = new KeyboardEvent(kwArgs.type, {
bubbles: true,
cancelable: kwArgs.cancelable || false,
view: window,
key: kwArgs.key || '',
location: 3
});
}
else {
event = document.createEvent('KeyboardEvent');
event.initKeyboardEvent(
kwArgs.type,
true,
kwArgs.cancelable || false,
window,
kwArgs.key || '',
3,
'',
0,
''
);
}
return target.dispatchEvent(event);
}
function dispatchInput() {
var event;
if (typeof Event === 'function') {
event = new Event('input', { bubbles: true, cancelable: false });
}
else {
event = document.createEvent('Event');
event.initEvent('input', true, false);
}
return target.dispatchEvent(event);
}
keys = Array.prototype.concat.apply([], keys.map(function (keys) {
return keys.split('');
}));
for (var i = 0, j = keys.length; i < j; ++i) {
var key = keys[i];
var performDefault = true;
performDefault = dispatch({ type: 'keydown', cancelable: true, key: key });
performDefault = performDefault && dispatch({ type: 'keypress', cancelable: true, key: key });
if (performDefault) {
if ('value' in target) {
target.value = target.value.slice(0, target.selectionStart) + key +
target.value.slice(target.selectionEnd);
dispatchInput();
}
else if (target.isContentEditable) {
var node = document.createTextNode(key);
var selection = window.getSelection();
var range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(node);
range.setStartAfter(node);
range.setEndAfter(node);
selection.removeAllRanges();
selection.addRange(range);
}
}
dispatch({ type: 'keyup', cancelable: true, key: key });
}
}
/* istanbul ignore next */
/**
* Simulates a mouse event as it would occur on Safari 7.
*
* @private
* @param {Object} kwArgs Parameters for the mouse event.
*/
function simulateMouse(kwArgs) {
var position = kwArgs.position;
function dispatch(kwArgs) {
var event;
if (typeof MouseEvent === 'function') {
event = new MouseEvent(kwArgs.type, {
bubbles: 'bubbles' in kwArgs ? kwArgs.bubbles : true,
cancelable: kwArgs.cancelable || false,
view: window,
detail: kwArgs.detail || 0,
screenX: window.screenX + position.x,
screenY: window.screenY + position.y,
clientX: position.x,
clientY: position.y,
ctrlKey: kwArgs.ctrlKey || false,
shiftKey: kwArgs.shiftKey || false,
altKey: kwArgs.altKey || false,
metaKey: kwArgs.metaKey || false,
button: kwArgs.button || 0,
relatedTarget: kwArgs.relatedTarget
});
}
else {
event = document.createEvent('MouseEvents');
event.initMouseEvent(
kwArgs.type,
kwArgs.bubbles || true,
kwArgs.cancelable || false,
window,
kwArgs.detail || 0,
window.screenX + position.x,
window.screenY + position.y,
position.x,
position.y,
kwArgs.ctrlKey || false,
kwArgs.altKey || false,
kwArgs.shiftKey || false,
kwArgs.metaKey || false,
kwArgs.button || 0,
kwArgs.relatedTarget || null
);
}
return kwArgs.target.dispatchEvent(event);
}
function click(target, button, detail) {
if (!down(target, button)) {
return false;
}
if (!up(target, button)) {
return false;
}
return dispatch({
button: button,
cancelable: true,
detail: detail,
target: target,
type: 'click'
});
}
function down(target, button) {
return dispatch({
button: button,
cancelable: true,
target: target,
type: 'mousedown'
});
}
function up(target, button) {
return dispatch({
button: button,
cancelable: true,
target: target,
type: 'mouseup'
});
}
function move(currentElement, newElement, xOffset, yOffset) {
if (newElement) {
var bbox = newElement.getBoundingClientRect();
if (xOffset == null) {
xOffset = (bbox.right - bbox.left) * 0.5;
}
if (yOffset == null) {
yOffset = (bbox.bottom - bbox.top) * 0.5;
}
position = { x: bbox.left + xOffset, y: bbox.top + yOffset };
}
else {
position.x += xOffset || 0;
position.y += yOffset || 0;
newElement = document.elementFromPoint(position.x, position.y);
}
if (currentElement !== newElement) {
dispatch({ type: 'mouseout', target: currentElement, relatedTarget: newElement });
dispatch({ type: 'mouseleave', target: currentElement, relatedTarget: newElement, bubbles: false });
dispatch({ type: 'mouseenter', target: newElement, relatedTarget: currentElement, bubbles: false });
dispatch({ type: 'mouseover', target: newElement, relatedTarget: currentElement });
}
dispatch({ type: 'mousemove', target: newElement, bubbles: true });
return position;
}
var target = document.elementFromPoint(position.x, position.y);
if (kwArgs.action === 'mousemove') {
return move(target, kwArgs.element, kwArgs.xOffset, kwArgs.yOffset);
}
else if (kwArgs.action === 'mousedown') {
return down(target, kwArgs.button);
}
else if (kwArgs.action === 'mouseup') {
return up(target, kwArgs.button);
}
else if (kwArgs.action === 'click') {
return click(target, kwArgs.button, 0);
}
else if (kwArgs.action === 'dblclick') {
if (!click(target, kwArgs.button, 0)) {
return false;
}
if (!click(target, kwArgs.button, 1)) {
return false;
}
return dispatch({
type: 'dblclick',
target: target,
button: kwArgs.button,
detail: 2,
cancelable: true
});
}
}
/**
* A Session represents a connection to a remote environment that can be driven programmatically.
*
* @constructor module:leadfoot/Session
* @param {string} sessionId The ID of the session, as provided by the remote.
* @param {module:leadfoot/Server} server The server that the session belongs to.
* @param {Capabilities} capabilities A map of bugs and features that the remote environment exposes.
*/
function Session(sessionId, server, capabilities) {
this._sessionId = sessionId;
this._server = server;
this._capabilities = capabilities;
this._closedWindows = {};
this._timeouts = {
script: Promise.resolve(0),
implicit: Promise.resolve(0),
'page load': Promise.resolve(Infinity)
};
}
/**
* @lends module:leadfoot/Session#
*/
Session.prototype = {
constructor: Session,
_movedToElement: false,
_lastMousePosition: null,
_lastAltitude: null,
_closedWindows: null,
// TODO: Timeouts are held so that we can fiddle with the implicit wait timeout to add efficient `waitFor`
// and `waitForDeleted` convenience methods. Technically only the implicit timeout is necessary.
_timeouts: {},
/**
* Information about the available features and bugs in the remote environment.
*
* @member {Capabilities} capabilities
* @memberOf module:leadfoot/Session#
* @readonly
*/
get capabilities() {
return this._capabilities;
},
/**
* The current session ID.
*
* @member {string} sessionId
* @memberOf module:leadfoot/Session#
* @readonly
*/
get sessionId() {
return this._sessionId;
},
/**
* The Server that the session runs on.
*
* @member {module:leadfoot/Server} server
* @memberOf module:leadfoot/Session#
* @readonly
*/
get server() {
return this._server;
},
_get: delegateToServer('_get'),
_post: delegateToServer('_post'),
_delete: delegateToServer('_delete'),
/**
* Gets the current value of a timeout for the session.
*
* @param {string} type The type of timeout to retrieve. One of 'script', 'implicit', or 'page load'.
* @returns {Promise.<number>} The timeout, in milliseconds.
*/
getTimeout: function (type) {
return this._timeouts[type];
},
/**
* Sets the value of a timeout for the session.
*
* @param {string} type
* The type of timeout to set. One of 'script', 'implicit', or 'page load'.
*
* @param {number} ms
* The length of time to use for the timeout, in milliseconds. A value of 0 will cause operations to time out
* immediately.
*
* @returns {Promise.<void>}
*/
setTimeout: function (type, ms) {
// Infinity cannot be serialised by JSON
if (ms === Infinity) {
// It seems that at least ChromeDriver 2.10 has a limit here that is near the 32-bit signed integer limit,
// and IEDriverServer 2.42.2 has an even lower limit; 2.33 hours should be infinite enough for testing
ms = Math.pow(2, 23) - 1;
}
// If the target doesn't support a timeout of 0, use 1.
if (this.capabilities.brokenZeroTimeout && ms === 0) {
ms = 1;
}
var self = this;
var promise = this._post('timeouts', {
type: type,
ms: ms
}).catch(function (error) {
// Appium as of April 2014 complains that `timeouts` is unsupported, so try the more specific
// endpoints if they exist
if (error.name === 'UnknownCommand') {
if (type === 'script') {
return self._post('timeouts/async_script', { ms: ms });
}
else if (type === 'implicit') {
return self._post('timeouts/implicit_wait', { ms: ms });
}
}
throw error;
}).then(noop);
this._timeouts[type] = promise.then(function () {
return ms;
});
return promise;
},
/**
* Gets the identifier for the window that is currently focused.
*
* @returns {Promise.<string>} A window handle identifier that can be used with other window handling functions.
*/
getCurrentWindowHandle: function () {
var self = this;
return this._get('window_handle').then(function (handle) {
if (self.capabilities.brokenDeleteWindow && self._closedWindows[handle]) {
var error = new Error();
error.status = 23;
error.name = statusCodes[error.status][0];
error.message = statusCodes[error.status][1];
throw error;
}
return handle;
});
},
/**
* Gets a list of identifiers for all currently open windows.
*
* @returns {Promise.<string[]>}
*/
getAllWindowHandles: function () {
var self = this;
return this._get('window_handles').then(function (handles) {
if (self.capabilities.brokenDeleteWindow) {
return handles.filter(function (handle) { return !self._closedWindows[handle]; });
}
return handles;
});
},
/**
* Gets the URL that is loaded in the focused window/frame.
*
* @returns {Promise.<string>}
*/
getCurrentUrl: function () {
return this._get('url');
},
/**
* Navigates the focused window/frame to a new URL.
*
* @param {string} url
* @returns {Promise.<void>}
*/
get: function (url) {
this._movedToElement = false;
if (this.capabilities.brokenMouseEvents) {
this._lastMousePosition = { x: 0, y: 0 };
}
return this._post('url', {
url: url
}).then(noop);
},
/**
* Navigates the focused window/frame forward one page using the browser’s navigation history.
*
* @returns {Promise.<void>}
*/
goForward: function () {
// TODO: SPEC: Seems like this and `back` should return the newly navigated URL.
return this._post('forward').then(noop);
},
/**
* Navigates the focused window/frame back one page using the browser’s navigation history.
*
* @returns {Promise.<void>}
*/
goBack: function () {
return this._post('back').then(noop);
},
/**
* Reloads the current browser window/frame.
*
* @returns {Promise.<void>}
*/
refresh: function () {
if (this.capabilities.brokenRefresh) {
return this.execute('location.reload();');
}
return this._post('refresh').then(noop);
},
/**
* Executes JavaScript code within the focused window/frame. The code should return a value synchronously.
*
* @see {@link module:leadfoot/Session#executeAsync} to execute code that returns values asynchronously.
*
* @param {Function|string} script
* The code to execute. This function will always be converted to a string, sent to the remote environment, and
* reassembled as a new anonymous function on the remote end. This means that you cannot access any variables
* through closure. If your code needs to get data from variables on the local end, they should be passed using
* `args`.
*
* @param {any[]} args
* An array of arguments that will be passed to the executed code. Only values that can be serialised to JSON, plus
* {@link module:leadfoot/Element} objects, can be specified as arguments.
*
* @returns {Promise.<any>}
* The value returned by the remote code. Only values that can be serialised to JSON, plus DOM elements, can be
* returned.
*/
execute: function (script, args) {
// At least FirefoxDriver 2.40.0 will throw a confusing NullPointerException if args is not an array;
// provide a friendlier error message to users that accidentally pass a non-array
if (typeof args !== 'undefined' && !Array.isArray(args)) {
throw new Error('Arguments passed to execute must be an array');
}
var result = this._post('execute', {
script: util.toExecuteString(script),
args: args || []
}).then(lang.partial(convertToElements, this), fixExecuteError);
if (this.capabilities.brokenExecuteUndefinedReturn) {
result = result.then(function (value) {
if (value === undefined) {
value = null;
}
return value;
});
}
return result;
},
/**
* Executes JavaScript code within the focused window/frame. The code must invoke the provided callback in
* order to signal that it has completed execution.
*
* @see {@link module:leadfoot/Session#execute} to execute code that returns values synchronously.
* @see {@link module:leadfoot/Session#setExecuteAsyncTimeout} to set the time until an asynchronous script is
* considered timed out.
*
* @param {Function|string} script
* The code to execute. This function will always be converted to a string, sent to the remote environment, and
* reassembled as a new anonymous function on the remote end. This means that you cannot access any variables
* through closure. If your code needs to get data from variables on the local end, they should be passed using
* `args`.
*
* @param {any[]} args
* An array of arguments that will be passed to the executed code. Only values that can be serialised to JSON, plus
* {@link module:leadfoot/Element} objects, can be specified as arguments. In addition to these arguments, a
* callback function will always be passed as the final argument to the function specified in `script`. This
* callback function must be invoked in order to signal that execution has completed. The return value of the
* execution, if any, should be passed to this callback function.
*
* @returns {Promise.<any>}
* The value returned by the remote code. Only values that can be serialised to JSON, plus DOM elements, can be
* returned.
*/
executeAsync: function (script, args) {
// At least FirefoxDriver 2.40.0 will throw a confusing NullPointerException if args is not an array;
// provide a friendlier error message to users that accidentally pass a non-array
if (typeof args !== 'undefined' && !Array.isArray(args)) {
throw new Error('Arguments passed to executeAsync must be an array');
}
return this._post('execute_async', {
script: util.toExecuteString(script),
args: args || []
}).then(lang.partial(convertToElements, this), fixExecuteError);
},
/**
* Gets a screenshot of the focused window and returns it in PNG format.
*
* @returns {Promise.<Buffer>} A buffer containing a PNG image.
*/
takeScreenshot: function () {
return this._get('screenshot').then(function (data) {
/*jshint node:true */
return new Buffer(data, 'base64');
});
},
/**
* Gets a list of input method editor engines available to the remote environment.
* As of April 2014, no known remote environments support IME functions.
*
* @returns {Promise.<string[]>}
*/
getAvailableImeEngines: function () {
return this._get('ime/available_engines');
},
/**
* Gets the currently active input method editor for the remote environment.
* As of April 2014, no known remote environments support IME functions.
*
* @returns {Promise.<string>}
*/
getActiveImeEngine: function () {
return this._get('ime/active_engine');
},
/**
* Returns whether or not an input method editor is currently active in the remote environment.
* As of April 2014, no known remote environments support IME functions.
*
* @returns {Promise.<boolean>}
*/
isImeActivated: function () {
return this._get('ime/activated');
},
/**
* Deactivates any active input method editor in the remote environment.
* As of April 2014, no known remote environments support IME functions.
*
* @returns {Promise.<void>}
*/
deactivateIme: function () {
return this._post('ime/deactivate');
},
/**
* Activates an input method editor in the remote environment.
* As of April 2014, no known remote environments support IME functions.
*
* @param {string} engine The type of IME to activate.
* @returns {Promise.<void>}
*/
activateIme: function (engine) {
return this._post('ime/activate', {
engine: engine
});
},
/**
* Switches the currently focused frame to a new frame.
*
* @param {string|number|null|Element} id
* The frame to switch to. In most environments, a number or string value corresponds to a key in the
* `window.frames` object of the currently active frame. If `null`, the topmost (default) frame will be used.
* If an Element is provided, it must correspond to a `<frame>` or `<iframe>` element.
*
* @returns {Promise.<void>}
*/
switchToFrame: function (id) {
return this._post('frame', {
id: id
}).then(noop);
},
/**
* Switches the currently focused window to a new window.
*
* @param {string} handle
* The handle of the window to switch to. In mobile environments and environments based on the W3C WebDriver
* standard, this should be a handle as returned by {@link module:leadfoot/Session#getAllWindowHandles}.
*
* In environments using the JsonWireProtocol, this value corresponds to the `window.name` property of a window.
*
* @returns {Promise.<void>}
*/
switchToWindow: function (handle) {
return this._post('window', {
// TODO: Note that in the W3C standard, the property is 'handle'
name: handle
}).then(noop);
},
/**
* Switches the currently focused frame to the parent of the currently focused frame.
*
* @returns {Promise.<void>}
*/
switchToParentFrame: function () {
var self = this;
return this._post('frame/parent').catch(function (error) {
// At least FirefoxDriver 2.40.0 does not implement this command, but we can fake it by retrieving
// the parent frame element using JavaScript and switching to it directly by reference
// At least Selendroid 0.9.0 also does not support this command, but unfortunately throws an incorrect
// error so it looks like a fatal error; see https://github.com/selendroid/selendroid/issues/364
if (error.name === 'UnknownCommand' ||
(
self.capabilities.browserName === 'selendroid' &&
error.message.indexOf('Error occured while communicating with selendroid server') > -1
)
) {
if (self.capabilities.scriptedParentFrameCrashesBrowser) {
throw error;
}
return self.execute('return window.parent.frameElement;').then(function (parent) {
// TODO: Using `null` if no parent frame was returned keeps the request from being invalid,
// but may be incorrect and may cause incorrect frame retargeting on certain platforms;
// At least Selendroid 0.9.0 fails both commands
return self.switchToFrame(parent || null);
});
}
throw error;
}).then(noop);
},
/**
* Closes the currently focused window. In most environments, after the window has been closed, it is necessary
* to explicitly switch to whatever window is now focused.
*
* @returns {Promise.<void>}
*/
closeCurrentWindow: function () {
function manualClose() {
return self.getCurrentWindowHandle().then(function (handle) {
return self.execute('window.close();').then(function () {
self._closedWindows[handle] = true;
});
});
}
var self = this;
if (self.capabilities.brokenDeleteWindow) {
return manualClose();
}
return this._delete('window').catch(function (error) {
// ios-driver 0.6.6-SNAPSHOT April 2014 does not implement close window command
if (error.name === 'UnknownCommand') {
self.capabilities.brokenDeleteWindow = true;
return manualClose();
}
throw error;
}).then(noop);
},
/**
* Sets the dimensions of a window.
*
* @param {string=} windowHandle
* The name of the window to resize. See {@link module:leadfoot/Session#switchToWindow} to learn about valid
* window names. Omit this argument to resize the currently focused window.
*
* @param {number} width
* The new width of the window, in CSS pixels.
*
* @param {number} height
* The new height of the window, in CSS pixels.
*
* @returns {Promise.<void>}
*/
setWindowSize: function (windowHandle, width, height) {
if (typeof height === 'undefined') {
height = width;
width = windowHandle;
windowHandle = null;
}
var data = {
width: width,
height: height
};
if (this.capabilities.implicitWindowHandles) {
if (windowHandle == null) {
return this._post('window/size', data);
}
else {
// User provided a window handle; get the current handle, switch to the new one, get the size, then
// switch back to the original handle.
var self = this;
var error;
return this.getCurrentWindowHandle().then(function (originalHandle) {
return self.switchToWindow(windowHandle).then(function () {
return this._post('window/size', data);
}).catch(function (_error) {
error = error;
}).then(function () {
return self.switchToWindow(originalHandle);
}).then(function () {
if (error) {
throw error;
}
});
});
}
}
else {
if (windowHandle == null) {
windowHandle = 'current';
}
return this._post('window/$0/size', {
width: width,
height: height
}, [ windowHandle ]).then(noop);
}
},
/**
* Gets the dimensions of a window.
*
* @param {string=} windowHandle
* The name of the window to query. See {@link module:leadfoot/Session#switchToWindow} to learn about valid
* window names. Omit this argument to query the currently focused window.
*
* @returns {Promise.<{ width: number, height: number }>}
* An object describing the width and height of the window, in CSS pixels.
*/
getWindowSize: function (windowHandle) {
if (this.capabilities.implicitWindowHandles) {
if (windowHandle == null) {
return this._get('window/size');
}
else {
// User provided a window handle; get the current handle, switch to the new one, get the size, then
// switch back to the original handle.
var self = this;
var error;
var size;
return this.getCurrentWindowHandle().then(function (originalHandle) {
return self.switchToWindow(windowHandle).then(function () {
return self._get('window/size');
}).then(function (_size) {
size = _size;
}, function (_error) {
error = _error;
}).then(function () {
return self.switchToWindow(originalHandle);
}).then(function () {
if (error) {
throw error;
}
return size;
});
});
}
}
else {
if (typeof windowHandle === 'undefined') {
windowHandle = 'current';
}
return this._get('window/$0/size', null, [ windowHandle ]);
}
},
/**
* Sets the position of a window.
*
* Note that this method is not part of the W3C WebDriver standard.
*
* @param {string=} windowHandle
* The name of the window to move. See {@link module:leadfoot/Session#switchToWindow} to learn about valid
* window names. Omit this argument to move the currently focused window.
*
* @param {number} x
* The screen x-coordinate to move to, in CSS pixels, relative to the left edge of the primary monitor.
*
* @param {number} y
* The screen y-coordinate to move to, in CSS pixels, relative to the top edge of the primary monitor.
*
* @returns {Promise.<void>}
*/
setWindowPosition: function (windowHandle, x, y) {
if (typeof y === 'undefined') {
y = x;
x = windowHandle;
windowHandle = 'current';
}
return this._post('window/$0/position', {
x: x,
y: y
}, [ windowHandle ]).then(noop);
},
/**
* Gets the position of a window.
*
* Note that this method is not part of the W3C WebDriver standard.
*
* @param {string=} windowHandle
* The name of the window to query. See {@link module:leadfoot/Session#switchToWindow} to learn about valid
* window names. Omit this argument to query the currently focused window.
*
* @returns {Promise.<{ x: number, y: number }>}
* An object describing the position of the window, in CSS pixels, relative to the top-left corner of the
* primary monitor. If a secondary monitor exists above or to the left of the primary monitor, these values
* will be negative.
*/
getWindowPosition: function (windowHandle) {
if (typeof windowHandle === 'undefined') {
windowHandle = 'current';
}
return this._get('window/$0/position', null, [ windowHandle ]).then(function (position) {
// At least InternetExplorerDriver 2.41.0 on IE9 returns an object containing extra properties
return { x: position.x, y: position.y };
});
},
/**
* Maximises a window according to the platform’s window system behaviour.
*
* @param {string=} windowHandle
* The name of the window to resize. See {@link module:leadfoot/Session#switchToWindow} to learn about valid
* window names. Omit this argument to resize the currently focused window.
*
* @returns {Promise.<void>}
*/
maximizeWindow: function (windowHandle) {
if (typeof windowHandle === 'undefined') {
windowHandle = 'current';
}
return this._post('window/$0/maximize', null, [ windowHandle ]).then(noop);
},
/**
* Gets all cookies set on the current page.
*
* @returns {Promise.<WebDriverCookie[]>}
*/
getCookies: function () {
return this._get('cookie').then(function (cookies) {
// At least SafariDriver 2.41.0 returns cookies with extra class and hCode properties that should not
// exist
return cookies.map(function (badCookie) {
var cookie = {};
for (var key in badCookie) {
if (key === 'name' || key === 'value' || key === 'path' || key === 'domain' ||
key === 'secure' || key === 'httpOnly' || key === 'expiry'
) {
cookie[key] = badCookie[key];
}
}
if (typeof cookie.expiry === 'number') {
cookie.expiry = new Date(cookie.expiry * 1000);
}
return cookie;
});
});
},
/**
* Sets a cookie on the current page.
*
* @param {WebDriverCookie} cookie
* @returns {Promise.<void>}
*/
setCookie: function (cookie) {
var self = this;
if (typeof cookie.expiry === 'string') {
cookie.expiry = new Date(cookie.expiry);
}
if (cookie.expiry instanceof Date) {
cookie.expiry = cookie.expiry.valueOf() / 1000;
}
return this._post('cookie', {
cookie: cookie
}).catch(function (error) {
// At least ios-driver 0.6.0-SNAPSHOT April 2014 does not know how to set cookies
if (error.name === 'UnknownCommand') {
// Per RFC6265 section 4.1.1, cookie names must match `token` (any US-ASCII character except for
// control characters and separators as defined in RFC2616 section 2.2)
if (/[^A-Za-z0-9!#$%&'*+.^_`|~-]/.test(cookie.name)) {
error = new Error();
error.status = 25;
error.name = statusCodes[error.status[0]];
error.message = 'Invalid cookie name';
throw error;
}
if (/[^\u0021\u0023-\u002b\u002d-\u003a\u003c-\u005b\u005d-\u007e]/.test(cookie.value)) {
error = new Error();
error.status = 25;
error.name = statusCodes[error.status[0]];
error.message = 'Invalid cookie value';
throw error;
}
var cookieToSet = [ cookie.name + '=' + cookie.value ];
pushCookieProperties(cookieToSet, cookie);
return self.execute(/* istanbul ignore next */ function (cookie) {
document.cookie = cookie;
}, [ cookieToSet.join(';') ]);
}
throw error;
}).then(noop);
},
/**
* Clears all cookies for the current page.
*
* @returns {Promise.<void>}
*/
clearCookies: function () {
if (this.capabilities.brokenDeleteCookie) {
var self = this;
return this.getCookies().then(function (cookies) {
return cookies.reduce(function (promise, cookie) {
var expiredCookie = [
cookie.name + '=',
'expires=Thu, 01 Jan 1970 00:00:00 GMT'
];
pushCookieProperties(expiredCookie, cookie);
return promise.then(function () {
return self.execute(/* istanbul ignore next */ function (expiredCookie) {
document.cookie = expiredCookie + '; domain=' + encodeURIComponent(document.domain) +
// Assume the cookie was created by Selenium, so it's path is '/'; at least MS Edge
// requires a path to delete a cookie
'; path=/';
}, [ expiredCookie.join(';') ]);
});
}, Promise.resolve());
});
}
return this._delete('cookie').then(noop);
},
/**
* Deletes a cookie on the current page.
*
* @param {string} name The name of the cookie to delete.
* @returns {Promise.<void>}
*/
deleteCookie: function (name) {
if (this.capabilities.brokenDeleteCookie) {
var self = this;
return this.getCookies().then(function (cookies) {
var cookie;
if (cookies.some(function (value) {
if (value.name === name) {
cookie = value;
return true;
}
})) {
var expiredCookie = [
cookie.name + '=',
'expires=Thu, 01 Jan 1970 00:00:00 GMT'
];
pushCookieProperties(expiredCookie, cookie);
return self.execute(/* istanbul ignore next */ function (expiredCookie) {
document.cookie = expiredCookie + '; domain=' + encodeURIComponent(document.domain) +
// Assume the cookie was created by Selenium, so it's path is '/'; at least MS Edge requires
// a path to delete a cookie
'; path=/';
}, [ expiredCookie.join(';') ]);
}
});
}
return this._delete('cookie/$0', null, [ name ]).then(noop);
},
/**
* Gets the HTML loaded in the focused window/frame. This markup is serialised by the remote environment so
* may not exactly match the HTML provided by the Web server.
*
* @returns {Promise.<string>}
*/
getPageSource: function () {
if (this.capabilities.brokenPageSource) {
return this.execute(/* istanbul ignore next */ function () {
return document.documentElement.outerHTML;
});
}
else {
return this._get('source');
}
},
/**
* Gets the title of the top-level browsing context of the current window or tab.
*
* @returns {Promise.<string>}
*/
getPageTitle: function () {
return this._get('title');
},
/**
* Gets the first element from the focused window/frame that matches the given query.
*
* @see {@link module:leadfoot/Session#setFindTimeout} to set the amount of time it the remote environment
* should spend waiting for an element that does not exist at the time of the `find` call before timing
* out.
*
* @param {string} using
* The element retrieval strategy to use. One of 'class name', 'css selector', 'id', 'name', 'link text',
* 'partial link text', 'tag name', 'xpath'.
*
* @param {string} value
* The strategy-specific value to search for. For example, if `using` is 'id', `value` should be the ID of the
* element to retrieve.
*
* @returns {Promise.<module:leadfoot/Element>}
*/
find: function (using, value) {
var self = this;
if (this.capabilities.isWebDriver) {
var locator = strategies.toW3cLocator(using, value);
using = locator.using;
value = locator.value;
}
if (using.indexOf('link text') !== -1 && this.capabilities.brokenWhitespaceNormalization) {
return this.execute(/* istanbul ignore next */ this._manualFindByLinkText, [ using, value ])
.then(function (element) {
if (!element) {
var error = new Error();
error.name = 'NoSuchElement';
throw error;
}
return new Element(element, self);
});
}
return this._post('element', {
using: using,
value: value
}).then(function (element) {
return new Element(element, self);
});
},
/**
* Gets an array of elements from the focused window/frame that match the given query.
*
* @param {string} using
* The element retrieval strategy to use. See {@link module:leadfoot/Session#find} for options.
*
* @param {string} value
* The strategy-specific value to search for. See {@link module:leadfoot/Session#find} for details.
*
* @returns {Promise.<module:leadfoot/Element[]>}
*/
findAll: function (using, value) {
var self = this;
if (this.capabilities.isWebDriver) {
var locator = strategies.toW3cLocator(using, value);
using = locator.using;
value = locator.value;
}
if (using.indexOf('link text') !== -1 && this.capabilities.brokenWhitespaceNormalization) {
return this.execute(/* istanbul ignore next */ this._manualFindByLinkText, [ using, value, true ])
.then(function (elements) {
return elements.map(function (element) {
return new Element(element, self);
});
});
}
return this._post('elements', {
using: using,
value: value
}).then(function (elements) {
return elements.map(function (element) {
return new Element(element, self);
});
});
},
/**
* Gets the currently focused element from the focused window/frame.
*
* @method
* @returns {Promise.<module:leadfoot/Element>}
*/
getActiveElement: util.forCommand(function () {
function getDocumentActiveElement() {
return self.execute('return document.activeElement;');
}
var self = this;
if (this.capabilities.brokenActiveElement) {
return getDocumentActiveElement();
}
else {
return this._post('element/active').then(function (element) {
if (element) {
return new Element(element, self);
}
// The driver will return `null` if the active element is the body element; for consistency with how
// the DOM `document.activeElement` property works, we’ll diverge and always return an element
else {
return getDocumentActiveElement();
}
});
}
}, { createsContext: true }),
/**
* Types into the focused window/frame/element.
*
* @param {string|string[]} keys
* The text to type in the remote environment. It is possible to type keys that do not have normal character
* representations (modifier keys, function keys, etc.) as well as keys that have two different representations
* on a typical US-ASCII keyboard (numpad keys); use the values from {@link module:leadfoot/keys} to type these
* special characters. Any modifier keys that are activated by this call will persist until they are
* deactivated. To deactivate a modifier key, type the same modifier key a second time, or send `\uE000`
* ('NULL') to deactivate all currently active modifier keys.
*
* @returns {Promise.<void>}
*/
pressKeys: function (keys) {
if (!Array.isArray(keys)) {
keys = [ keys ];
}
if (this.capabilities.brokenSendKeys || !this.capabilities.supportsKeysCommand) {
return this.execute(simulateKeys, [ keys ]);
}
return this._post('keys', {
value: keys
}).then(noop);
},
/**
* Gets the current screen orientation.
*
* @returns {Promise.<string>} Either 'portrait' or 'landscape'.
*/
getOrientation: function () {
return this._get('orientation').then(function (orientation) {
return orientation.toLowerCase();
});
},
/**
* Sets the screen orientation.
*
* @param {string} orientation Either 'portrait' or 'landscape'.
* @returns {Promise.<void>}
*/
setOrientation: function (orientation) {
orientation = orientation.toUpperCase();
return this._post('orientation', {
orientation: orientation
}).then(noop);
},
/**
* Gets the text displayed in the currently active alert pop-up.
*
* @returns {Promise.<string>}
*/
getAlertText: function () {
return this._get('alert_text');
},
/**
* Types into the currently active prompt pop-up.
*
* @param {string|string[]} text The text to type into the pop-up’s input box.
* @returns {Promise.<void>}
*/
typeInPrompt: function (text) {
if (Array.isArray(text)) {
text = text.join('');
}
return this._post('alert_text', {
text: text
}).then(noop);
},
/**
* Accepts an alert, prompt, or confirmation pop-up. Equivalent to clicking the 'OK' button.
*
* @returns {Promise.<void>}
*/
acceptAlert: function () {
return this._post('accept_alert').then(noop);
},
/**
* Dismisses an alert, prompt, or confirmation pop-up. Equivalent to clicking the 'OK' button of an alert pop-up
* or the 'Cancel' button of a prompt or confirmation pop-up.
*
* @returns {Promise.<void>}
*/
dismissAlert: function () {
return this._post('dismiss_alert').then(noop);
},
/**
* Moves the remote environment’s mouse cursor to the specified element or relative position. If the element is
* outside of the viewport, the remote driver will attempt to scroll it into view automatically.
*
* @method
* @param {Element=} element
* The element to move the mouse to. If x-offset and y-offset are not specified, the mouse will be moved to the
* centre of the element.
*
* @param {number=} xOffset
* The x-offset of the cursor, maybe in CSS pixels, relative to the left edge of the specified element’s
* bounding client rectangle. If no element is specified, the offset is relative to the previous position of the
* mouse, or to the left edge of the page’s root element if the mouse was never moved before.
*
* @param {number=} yOffset
* The y-offset of the cursor, maybe in CSS pixels, relative to the top edge of the specified element’s bounding
* client rectangle. If no element is specified, the offset is relative to the previous position of the mouse,
* or to the top edge of the page’s root element if the mouse was never moved before.
*
* @returns {Promise.<void>}
*/
moveMouseTo: util.forCommand(function (element, xOffset, yOffset) {
var self = this;
if (typeof yOffset === 'undefined' && typeof xOffset !== 'undefined') {
yOffset = xOffset;
xOffset = element;
element = null;
}
if (this.capabilities.brokenMouseEvents) {
return this.execute(simulateMouse, [ {
action: 'mousemove',
position: self._lastMousePosition,
element: element,
xOffset: xOffset,
yOffset: yOffset
} ]).then(function (newPosition) {
self._lastMousePosition = newPosition;
});
}
if (element) {
element = element.elementId;
}
// If the mouse has not been moved to any element on this page yet, drivers will either throw errors
// (FirefoxDriver 2.40.0) or silently fail (ChromeDriver 2.9) when trying to move the mouse cursor relative
// to the "previous" position; in this case, we just assume that the mouse position defaults to the
// top-left corner of the document
else if (!this._movedToElement) {
if (this.capabilities.brokenHtmlMouseMove) {
return this.execute('return document.body;').then(function (element) {
return element.getPosition().then(function (position) {
return self.moveMouseTo(element, xOffset - position.x, yOffset - position.y);
});
});
}
else {
return this.execute('return document.documentElement;').then(function (element) {
return self.moveMouseTo(element, xOffset, yOffset);
});
}
}
return this._post('moveto', {
element: element,
xoffset: xOffset,
yoffset: yOffset
}).then(function () {
self._movedToElement = true;
});
}, { usesElement: true }),
/**
* Clicks a mouse button at the point where the mouse cursor is currently positioned. This method may fail to
* execute with an error if the mouse has not been moved anywhere since the page was loaded.
*
* @param {number=} button
* The button to click. 0 corresponds to the primary mouse button, 1 to the middle mouse button, 2 to the
* secondary mouse button. Numbers above 2 correspond to any additional buttons a mouse might provide.
*
* @returns {Promise.<void>}
*/
clickMouseButton: function (button) {
if (this.capabilities.brokenMouseEvents) {
return this.execute(simulateMouse, [ {
action: 'click',
button: button,
position: this._lastMousePosition
} ]).then(noop);
}
var self = this;
return this._post('click', {
button: button
}).then(function () {
// ios-driver 0.6.6-SNAPSHOT April 2014 does not wait until the default action for a click event occurs
// before returning
if (self.capabilities.touchEnabled) {
return util.sleep(300);
}
});
},
/**
* Depresses a mouse button without releasing it.
*
* @param {number=} button The button to press. See {@link module:leadfoot/Session#click} for available options.
* @returns {Promise.<void>}
*/
pressMouseButton: function (button) {
if (this.capabilities.brokenMouseEvents) {
return this.execute(simulateMouse, [ {
action: 'mousedown',
button: button,
position: this._lastMousePosition
} ]).then(noop);
}
return this._post('buttondown', {
button: button
}).then(noop);
},
/**
* Releases a previously depressed mouse button.
*
* @param {number=} button The button to press. See {@link module:leadfoot/Session#click} for available options.
* @returns {Promise.<void>}
*/
releaseMouseButton: function (button) {
if (this.capabilities.brokenMouseEvents) {
return this.execute(simulateMouse, [ {
action: 'mouseup',
button: button,
position: this._lastMousePosition
} ]).then(noop);
}
return this._post('buttonup', {
button: button
}).then(noop);
},
/**
* Double-clicks the primary mouse button.
*
* @returns {Promise.<void>}
*/
doubleClick: function () {
if (this.capabilities.brokenMouseEvents) {
return this.execute(simulateMouse, [ {
action: 'dblclick',
button: 0,
position: this._lastMousePosition
} ]).then(noop);
}
else if (this.capabilities.brokenDoubleClick) {
var self = this;
return this.pressMouseButton().then(function () {
return self.releaseMouseButton();
}).then(function () {
return self._post('doubleclick');
});
}
return this._post('doubleclick').then(noop);
},
/**
* Taps an element on a touch screen device. If the element is outside of the viewport, the remote driver will
* attempt to scroll it into view automatically.
*
* @method
* @param {module:leadfoot/Element} element The element to tap.
* @returns {Promise.<void>}
*/
tap: util.forCommand(function (element) {
if (element) {
element = element.elementId;
}
return this._post('touch/click', {
element: element
}).then(noop);
}, { usesElement: true }),
/**
* Depresses a new finger at the given point on a touch screen device without releasing it.
*
* @param {number} x The screen x-coordinate to press, maybe in device pixels.
* @param {number} y The screen y-coordinate to press, maybe in device pixels.
* @returns {Promise.<void>}
*/
pressFinger: function (x, y) {
// TODO: If someone specifies the same coordinates as as an existing finger, will it switch the active finger
// back to that finger instead of adding a new one?
return this._post('touch/down', {
x: x,
y: y
}).then(noop);
},
/**
* Releases whatever finger exists at the given point on a touch screen device.
*
* @param {number} x The screen x-coordinate where a finger is pressed, maybe in device pixels.
* @param {number} y The screen y-coordinate where a finger is pressed, maybe in device pixels.
* @returns {Promise.<void>}
*/
releaseFinger: function (x, y) {
return this._post('touch/up', {
x: x,
y: y
}).then(noop);
},
/**
* Moves the last depressed finger to a new point on the touch screen.
*
* @param {number} x The screen x-coordinate to move to, maybe in device pixels.
* @param {number} y The screen y-coordinate to move to, maybe in device pixels.
* @returns {Promise.<void>}
*/
moveFinger: function (x, y) {
return this._post('touch/move', {
x: x,
y: y
}).then(noop);
},
/**
* Scrolls the currently focused window on a touch screen device.
*
* @method
* @param {Element=} element
* An element to scroll to. The window will be scrolled so the element is as close to the top-left corner of the
* window as possible.
*
* @param {number=} xOffset
* An optional x-offset, relative to the left edge of the element, in CSS pixels. If no element is specified,
* the offset is relative to the previous scroll position of the window.
*
* @param {number=} yOffset
* An optional y-offset, relative to the top edge of the element, in CSS pixels. If no element is specified,
* the offset is relative to the previous scroll position of the window.
*
* @returns {Promise.<void>}
*/
touchScroll: util.forCommand(function (element, xOffset, yOffset) {
if (typeof yOffset === 'undefined' && typeof xOffset !== 'undefined') {
yOffset = xOffset;
xOffset = element;
element = undefined;
}
if (this.capabilities.brokenTouchScroll) {
return this.execute(/* istanbul ignore next */ function (element, x, y) {
var rect = { left: window.scrollX, top: window.scrollY };
if (element) {
var bbox = element.getBoundingClientRect();
rect.left += bbox.left;
rect.top += bbox.top;
}
window.scrollTo(rect.left + x, rect.top + y);
}, [ element, xOffset, yOffset ]);
}
if (element) {
element = element.elementId;
}
// TODO: If using this, please correct for device pixel ratio to ensure consistency
return this._post('touch/scroll', {
element: element,
xoffset: xOffset,
yoffset: yOffset
}).then(noop);
}, { usesElement: true }),
/**
* Performs a double-tap gesture on an element.
*
* @method
* @param {module:leadfoot/Element} element The element to double-tap.
* @returns {Promise.<void>}
*/
doubleTap: util.forCommand(function (element) {
if (element) {
element = element.elementId;
}
return this._post('touch/doubleclick', {
element: element
}).then(noop);
}, { usesElement: true }),
/**
* Performs a long-tap gesture on an element.
*
* @method
* @param {module:leadfoot/Element} element The element to long-tap.
* @returns {Promise.<void>}
*/
longTap: util.forCommand(function (element) {
if (element) {
element = element.elementId;
}
return this._post('touch/longclick', {
element: element
}).then(noop);
}, { usesElement: true }),
/**
* Flicks a finger. Note that this method is currently badly specified and highly dysfunctional and is only
* provided for the sake of completeness.
*
* @method
* @param {module:leadfoot/Element} element The element where the flick should start.
* @param {number} xOffset The x-offset in pixels to flick by.
* @param {number} yOffset The x-offset in pixels to flick by.
* @param {number} speed The speed of the flick, in pixels per *second*. Most human flicks are 100–200ms, so
* this value will be higher than expected.
* @returns {Promise.<void>}
*/
flickFinger: util.forCommand(function (element, xOffset, yOffset, speed) {
if (typeof speed === 'undefined' && typeof yOffset === 'undefined' && typeof xOffset !== 'undefined') {
return this._post('touch/flick', {
xspeed: element,
yspeed: xOffset
}).then(noop);
}
if (element) {
element = element.elementId;
}
return this._post('touch/flick', {
element: element,
xoffset: xOffset,
yoffset: yOffset,
speed: speed
}).then(noop);
}, { usesElement: true }),
/**
* Gets the current geographical location of the remote environment.
*
* @returns {Promise.<Geolocation>}
* Latitude and longitude are specified using standard WGS84 decimal latitude/longitude. Altitude is specified
* as meters above the WGS84 ellipsoid. Not all environments support altitude.
*/
getGeolocation: function () {
var self = this;
return this._get('location').then(function (location) {
// ChromeDriver 2.9 ignores altitude being set and then returns 0; to match the Geolocation API
// specification, we will just pretend that altitude is not supported by the browser at all by
// changing the value to `null` if it is zero but the last set value was not zero
if (location.altitude === 0 && self._lastAltitude !== location.altitude) {
location.altitude = null;
}
return location;
});
},
/**
* Sets the geographical location of the remote environment.
*
* @param {Geolocation} location
* Latitude and longitude are specified using standard WGS84 decimal latitude/longitude. Altitude is specified
* as meters above the WGS84 ellipsoid. Not all environments support altitude.
*
* @returns {Promise.<void>}
*/
setGeolocation: function (location) {
// TODO: Is it weird that this accepts an object argument? `setCookie` does too, but nothing else does.
if (location.altitude !== undefined) {
this._lastAltitude = location.altitude;
}
return this._post('location', {
location: location
}).then(noop);
},
/**
* Gets all logs from the remote environment of the given type. The logs in the remote environment are cleared
* once they have been retrieved.
*
* @param {string} type
* The type of log entries to retrieve. Available log types differ between remote environments. Use
* {@link module:leadfoot/Session#getAvailableLogTypes} to learn what log types are currently available. Not all
* environments support all possible log types.
*
* @returns {Promise.<LogEntry[]>}
* An array of log entry objects. Timestamps in log entries are Unix timestamps, in seconds.
*/
getLogsFor: function (type) {
return this._post('log', {
type: type
}).then(function (logs) {
// At least Selendroid 0.9.0 returns logs as an array of strings instead of an array of log objects,
// which is a spec violation; see https://github.com/selendroid/selendroid/issues/366
if (logs && typeof logs[0] === 'string') {
return logs.map(function (log) {
var logData = /\[([^\]]+)\]\s*\[([^\]]+)\]\s*(.*)/.exec(log);
if (logData) {
return {
timestamp: Date.parse(logData[1]) / 1000,
level: logData[2],
message: logData[3]
};
}
return {
timestamp: NaN,
level: 'INFO',
message: log
};
});
}
return logs;
});
},
/**
* Gets the types of logs that are currently available for retrieval from the remote environment.
*
* @returns {Promise.<string[]>}
*/
getAvailableLogTypes: function () {
if (this.capabilities.fixedLogTypes) {
return Promise.resolve(this.capabilities.fixedLogTypes);
}
return this._get('log/types');
},
/**
* Gets the current state of the HTML5 application cache for the current page.
*
* @returns {Promise.<number>}
* The cache status. One of 0 (uncached), 1 (cached/idle), 2 (checking), 3 (downloading), 4 (update ready), 5
* (obsolete).
*/
getApplicationCacheStatus: function () {
return this._get('application_cache/status');
},
/**
* Terminates the session. No more commands will be accepted by the remote after this point.
*
* @returns {Promise.<void>}
*/
quit: function () {
return this._server.deleteSession(this._sessionId).then(noop);
},
/**
* Searches a document or element subtree for links with the given normalized text. This method works for 'link text'
* and 'partial link text' search strategies.
*
* Note that this method should be passed to an `execute` call, not called directly.
*
* @param {string} using The strategy in use ('link text' or 'partial link text')
* @param {string} value The link text to search for
* @param {boolean} multiple If true, return all matching links
* @param {Element?} element A context element
* @returns {Element|Element[]} The found element or elements
*/
_manualFindByLinkText: function (using, value, multiple, element) {
var check = using === 'link text' ? function (linkText, text) {
return linkText === text;
} : function (linkText, text) {
return linkText.indexOf(text) !== -1;
};
var links = (element || document).getElementsByTagName('a');
var linkText;
if (multiple) {
var found = [];
}
for (var i = 0; i < links.length; i++) {
// Normalize the link text whitespace
linkText = links[i].innerText
.replace(/^\s+/, '')
.replace(/\s+$/, '')
.replace(/\s*\r\n\s*/g, '\n')
.replace(/ +/g, ' ');
if (check(linkText, value)) {
if (!multiple) {
return links[i];
}
found.push(links[i]);
}
}
if (multiple) {
return found;
}
},
/**
* Normalize whitespace in the same way that most browsers generate innerText.
*
* @param {string} text
* @returns {string} Text with leading and trailing whitespace removed, with inner runs of spaces changed to a
* single space, and with "\r\n" pairs converted to "\n".
*/
_normalizeWhitespace: function (text) {
if (text) {
text = text
.replace(/^\s+/, '')
.replace(/\s+$/, '')
.replace(/\s*\r\n\s*/g, '\n')
.replace(/ +/g, ' ');
}
return text;
},
/**
* Uploads a file to a remote Selenium server for use when testing file uploads. This API is not part of the
* WebDriver specification and should not be used directly. To send a file to a server that supports file uploads,
* use {@link module:leadfoot/Element#type} to type the name of the local file into a file input field and the file
* will be transparently transmitted and used by the server.
*
* @private
* @returns {Promise.<string>}
*/
_uploadFile: function (filename) {
var self = this;
return new Promise(function (resolve) {
var content = fs.readFileSync(filename);
var zip = new JsZip();
zip.file(path.basename(filename), content);
var data = zip.generate({ type: 'base64' });
zip = null;
resolve(self._post('file', { file: data }));
});
}
};
/**
* Gets the list of keys set in local storage for the focused window/frame.
*
* @method getLocalStorageKeys
* @memberOf module:leadfoot/Session#
* @returns {Promise.<string[]>}
*/
/**
* Sets a value in local storage for the focused window/frame.
*
* @method setLocalStorageItem
* @memberOf module:leadfoot/Session#
* @param {string} key The key to set.
* @param {string} value The value to set.
* @returns {Promise.<void>}
*/
/**
* Clears all data in local storage for the focused window/frame.
*
* @method clearLocalStorage
* @memberOf module:leadfoot/Session#
* @returns {Promise.<void>}
*/
/**
* Gets a value from local storage for the focused window/frame.
*
* @method getLocalStorageItem
* @memberOf module:leadfoot/Session#
* @param {string} key The key of the data to get.
* @returns {Promise.<string>}
*/
/**
* Deletes a value from local storage for the focused window/frame.
*
* @method deleteLocalStorageItem
* @memberOf module:leadfoot/Session#
* @param {string} key The key of the data to delete.
* @returns {Promise.<void>}
*/
/**
* Gets the number of keys set in local storage for the focused window/frame.
*
* @method getLocalStorageLength
* @memberOf module:leadfoot/Session#
* @returns {Promise.<number>}
*/
storage.applyTo(Session.prototype, 'local');
/**
* Gets the list of keys set in session storage for the focused window/frame.
*
* @method getSessionStorageKeys
* @memberOf module:leadfoot/Session#
* @returns {Promise.<string[]>}
*/
/**
* Sets a value in session storage for the focused window/frame.
*
* @method setSessionStorageItem
* @memberOf module:leadfoot/Session#
* @param {string} key The key to set.
* @param {string} value The value to set.
* @returns {Promise.<void>}
*/
/**
* Clears all data in session storage for the focused window/frame.
*
* @method clearSessionStorage
* @memberOf module:leadfoot/Session#
* @returns {Promise.<void>}
*/
/**
* Gets a value from session storage for the focused window/frame.
*
* @method getSessionStorageItem
* @memberOf module:leadfoot/Session#
* @param {string} key The key of the data to get.
* @returns {Promise.<string>}
*/
/**
* Deletes a value from session storage for the focused window/frame.
*
* @method deleteSessionStorageItem
* @memberOf module:leadfoot/Session#
* @param {string} key The key of the data to delete.
* @returns {Promise.<void>}
*/
/**
* Gets the number of keys set in session storage for the focused window/frame.
*
* @method getSessionStorageLength
* @memberOf module:leadfoot/Session#
* @returns {Promise.<number>}
*/
storage.applyTo(Session.prototype, 'session');
// TODO: The rest of this file are "extra" interfaces; decide where they go more permanently
/**
* Gets the first element in the currently active window/frame matching the given CSS class name.
*
* @method findByClassName
* @memberOf module:leadfoot/Session#
* @param {string} className The CSS class name to search for.
* @returns {Promise.<module:leadfoot/Element>}
*/
/**
* Gets the first element in the currently active window/frame matching the given CSS selector.
*
* @method findByCssSelector
* @memberOf module:leadfoot/Session#
* @param {string} selector The CSS selector to search for.
* @returns {Promise.<module:leadfoot/Element>}
*/
/**
* Gets the first element in the currently active window/frame matching the given ID.
*
* @method findById
* @memberOf module:leadfoot/Session#
* @param {string} id The ID of the element.
* @returns {Promise.<module:leadfoot/Element>}
*/
/**
* Gets the first element in the currently active window/frame matching the given name attribute.
*
* @method findByName
* @memberOf module:leadfoot/Session#
* @param {string} name The name of the element.
* @returns {Promise.<module:leadfoot/Element>}
*/
/**
* Gets the first element in the currently active window/frame matching the given case-insensitive link text.
*
* @method findByLinkText
* @memberOf module:leadfoot/Session#
* @param {string} text The link text of the element.
* @returns {Promise.<module:leadfoot/Element>}
*/
/**
* Gets the first element in the currently active window/frame partially matching the given case-insensitive
* link text.
*
* @method findByPartialLinkText
* @memberOf module:leadfoot/Session#
* @param {string} text The partial link text of the element.
* @returns {Promise.<module:leadfoot/Element>}
*/
/**
* Gets the first element in the currently active window/frame matching the given HTML tag name.
*
* @method findByTagName
* @memberOf module:leadfoot/Session#
* @param {string} tagName The tag name of the element.
* @returns {Promise.<module:leadfoot/Element>}
*/
/**
* Gets the first element in the currently active window/frame matching the given XPath selector.
*
* @method findByXpath
* @memberOf module:leadfoot/Session#
* @param {string} path The XPath selector to search for.
* @returns {Promise.<module:leadfoot/Element>}
*/
/**
* Gets all elements in the currently active window/frame matching the given CSS class name.
*
* @method findAllByClassName
* @memberOf module:leadfoot/Session#
* @param {string} className The CSS class name to search for.
* @returns {Promise.<module:leadfoot/Element[]>}
*/
/**
* Gets all elements in the currently active window/frame matching the given CSS selector.
*
* @method findAllByCssSelector
* @memberOf module:leadfoot/Session#
* @param {string} selector The CSS selector to search for.
* @returns {Promise.<module:leadfoot/Element[]>}
*/
/**
* Gets all elements in the currently active window/frame matching the given name attribute.
*
* @method findAllByName
* @memberOf module:leadfoot/Session#
* @param {string} name The name of the element.
* @returns {Promise.<module:leadfoot/Element[]>}
*/
/**
* Gets all elements in the currently active window/frame matching the given case-insensitive link text.
*
* @method findAllByLinkText
* @memberOf module:leadfoot/Session#
* @param {string} text The link text of the element.
* @returns {Promise.<module:leadfoot/Element[]>}
*/
/**
* Gets all elements in the currently active window/frame partially matching the given case-insensitive
* link text.
*
* @method findAllByPartialLinkText
* @memberOf module:leadfoot/Session#
* @param {string} text The partial link text of the element.
* @returns {Promise.<module:leadfoot/Element[]>}
*/
/**
* Gets all elements in the currently active window/frame matching the given HTML tag name.
*
* @method findAllByTagName
* @memberOf module:leadfoot/Session#
* @param {string} tagName The tag name of the element.
* @returns {Promise.<module:leadfoot/Element[]>}
*/
/**
* Gets all elements in the currently active window/frame matching the given XPath selector.
*
* @method findAllByXpath
* @memberOf module:leadfoot/Session#
* @param {string} path The XPath selector to search for.
* @returns {Promise.<module:leadfoot/Element[]>}
*/
strategies.applyTo(Session.prototype);
/**
* Gets the first {@link module:leadfoot/Element#isDisplayed displayed} element in the currently active window/frame
* matching the given query. This is inherently slower than {@link module:leadfoot/Session#find}, so should only be
* used in cases where the visibility of an element cannot be ensured in advance.
*
* @method findDisplayed
* @memberOf module:leadfoot/Session#
* @since 1.6
*
* @param {string} using
* The element retrieval strategy to use. See {@link module:leadfoot/Session#find} for options.
*
* @param {string} value
* The strategy-specific value to search for. See {@link module:leadfoot/Session#find} for details.
*
* @returns {Promise.<module:leadfoot/Element>}
*/
/**
* Gets the first {@link module:leadfoot/Element#isDisplayed displayed} element in the currently active window/frame
* matching the given CSS class name. This is inherently slower than {@link module:leadfoot/Session#find}, so should
* only be used in cases where the visibility of an element cannot be ensured in advance.
*
* @method findDisplayedByClassName
* @memberOf module:leadfoot/Session#
* @since 1.6
* @param {string} className The CSS class name to search for.
* @returns {Promise.<module:leadfoot/Element>}
*/
/**
* Gets the first {@link module:leadfoot/Element#isDisplayed displayed} element in the currently active window/frame
* matching the given CSS selector. This is inherently slower than {@link module:leadfoot/Session#find}, so should only
* be used in cases where the visibility of an element cannot be ensured in advance.
*
* @method findDisplayedByCssSelector
* @memberOf module:leadfoot/Session#
* @since 1.6
* @param {string} selector The CSS selector to search for.
* @returns {Promise.<module:leadfoot/Element>}
*/
/**
* Gets the first {@link module:leadfoot/Element#isDisplayed displayed} element in the currently active window/frame
* matching the given ID. This is inherently slower than {@link module:leadfoot/Session#find}, so should only be
* used in cases where the visibility of an element cannot be ensured in advance.
*
* @method findDisplayedById
* @memberOf module:leadfoot/Session#
* @since 1.6
* @param {string} id The ID of the element.
* @returns {Promise.<module:leadfoot/Element>}
*/
/**
* Gets the first {@link module:leadfoot/Element#isDisplayed displayed} element in the currently active window/frame
* matching the given name attribute. This is inherently slower than {@link module:leadfoot/Session#find}, so should
* only be used in cases where the visibility of an element cannot be ensured in advance.
*
* @method findDisplayedByName
* @memberOf module:leadfoot/Session#
* @since 1.6
* @param {string} name The name of the element.
* @returns {Promise.<module:leadfoot/Element>}
*/
/**
* Gets the first {@link module:leadfoot/Element#isDisplayed displayed} element in the currently active window/frame
* matching the given case-insensitive link text. This is inherently slower than {@link module:leadfoot/Session#find},
* so should only be used in cases where the visibility of an element cannot be ensured in advance.
*
* @method findDisplayedByLinkText
* @memberOf module:leadfoot/Session#
* @since 1.6
* @param {string} text The link text of the element.
* @returns {Promise.<module:leadfoot/Element>}
*/
/**
* Gets the first {@link module:leadfoot/Element#isDisplayed displayed} element in the currently active window/frame
* partially matching the given case-insensitive link text. This is inherently slower than
* {@link module:leadfoot/Session#find}, so should only be used in cases where the visibility of an element cannot be
* ensured in advance.
*
* @method findDisplayedByPartialLinkText
* @memberOf module:leadfoot/Session#
* @since 1.6
* @param {string} text The partial link text of the element.
* @returns {Promise.<module:leadfoot/Element>}
*/
/**
* Gets the first {@link module:leadfoot/Element#isDisplayed displayed} element in the currently active window/frame
* matching the given HTML tag name. This is inherently slower than {@link module:leadfoot/Session#find}, so should
* only be used in cases where the visibility of an element cannot be ensured in advance.
*
* @method findDisplayedByTagName
* @memberOf module:leadfoot/Session#
* @since 1.6
* @param {string} tagName The tag name of the element.
* @returns {Promise.<module:leadfoot/Element>}
*/
/**
* Gets the first {@link module:leadfoot/Element#isDisplayed displayed} element in the currently active window/frame
* matching the given XPath selector. This is inherently slower than {@link module:leadfoot/Session#find}, so should
* only be used in cases where the visibility of an element cannot be ensured in advance.
*
* @method findDisplayedByXpath
* @memberOf module:leadfoot/Session#
* @since 1.6
* @param {string} path The XPath selector to search for.
* @returns {Promise.<module:leadfoot/Element>}
*/
findDisplayed.applyTo(Session.prototype);
/**
* Waits for all elements in the currently active window/frame to be destroyed.
*
* @method waitForDeleted
* @memberOf module:leadfoot/Session#
*
* @param {string} using
* The element retrieval strategy to use. See {@link module:leadfoot/Session#find} for options.
*
* @param {string} value
* The strategy-specific value to search for. See {@link module:leadfoot/Session#find} for details.
*
* @returns {Promise.<void>}
*/
/**
* Waits for all elements in the currently active window/frame matching the given CSS class name to be
* destroyed.
*
* @method waitForDeletedByClassName
* @memberOf module:leadfoot/Session#
* @param {string} className The CSS class name to search for.
* @returns {Promise.<void>}
*/
/**
* Waits for all elements in the currently active window/frame matching the given CSS selector to be destroyed.
*
* @method waitForDeletedByCssSelector
* @memberOf module:leadfoot/Session#
* @param {string} selector The CSS selector to search for.
* @returns {Promise.<void>}
*/
/**
* Waits for all elements in the currently active window/frame matching the given ID to be destroyed.
*
* @method waitForDeletedById
* @memberOf module:leadfoot/Session#
* @param {string} id The ID of the element.
* @returns {Promise.<void>}
*/
/**
* Waits for all elements in the currently active window/frame matching the given name attribute to be
* destroyed.
*
* @method waitForDeletedByName
* @memberOf module:leadfoot/Session#
* @param {string} name The name of the element.
* @returns {Promise.<void>}
*/
/**
* Waits for all elements in the currently active window/frame matching the given case-insensitive link text
* to be destroyed.
*
* @method waitForDeletedByLinkText
* @memberOf module:leadfoot/Session#
* @param {string} text The link text of the element.
* @returns {Promise.<void>}
*/
/**
* Waits for all elements in the currently active window/frame partially matching the given case-insensitive
* link text to be destroyed.
*
* @method waitForDeletedByPartialLinkText
* @memberOf module:leadfoot/Session#
* @param {string} text The partial link text of the element.
* @returns {Promise.<void>}
*/
/**
* Waits for all elements in the currently active window/frame matching the given HTML tag name to be destroyed.
*
* @method waitForDeletedByTagName
* @memberOf module:leadfoot/Session#
* @param {string} tagName The tag name of the element.
* @returns {Promise.<void>}
*/
/**
* Waits for all elements in the currently active window/frame matching the given XPath selector to be
* destroyed.
*
* @method waitForDeletedByXpath
* @memberOf module:leadfoot/Session#
* @param {string} path The XPath selector to search for.
* @returns {Promise.<void>}
*/
waitForDeleted.applyTo(Session.prototype);
/**
* Gets the timeout for {@link module:leadfoot/Session#executeAsync} calls.
*
* @method getExecuteAsyncTimeout
* @memberOf module:leadfoot/Session#
* @returns {Promise.<number>}
*/
/**
* Sets the timeout for {@link module:leadfoot/Session#executeAsync} calls.
*
* @method setExecuteAsyncTimeout
* @memberOf module:leadfoot/Session#
* @param {number} ms The length of the timeout, in milliseconds.
* @returns {Promise.<void>}
*/
/**
* Gets the timeout for {@link module:leadfoot/Session#find} calls.
*
* @method getFindTimeout
* @memberOf module:leadfoot/Session#
* @returns {Promise.<number>}
*/
/**
* Sets the timeout for {@link module:leadfoot/Session#find} calls.
*
* @method setFindTimeout
* @memberOf module:leadfoot/Session#
* @param {number} ms The length of the timeout, in milliseconds.
* @returns {Promise.<void>}
*/
/**
* Gets the timeout for {@link module:leadfoot/Session#get} calls.
*
* @method getPageLoadTimeout
* @memberOf module:leadfoot/Session#
* @returns {Promise.<number>}
*/
/**
* Sets the timeout for {@link module:leadfoot/Session#get} calls.
*
* @method setPageLoadTimeout
* @memberOf module:leadfoot/Session#
* @param {number} ms The length of the timeout, in milliseconds.
* @returns {Promise.<void>}
*/
(function (prototype) {
var timeouts = {
script: 'ExecuteAsync',
implicit: 'Find',
'page load': 'PageLoad'
};
for (var type in timeouts) {
prototype['get' + timeouts[type] + 'Timeout'] = lang.partial(function (type) {
return this.getTimeout(type);
}, type);
prototype['set' + timeouts[type] + 'Timeout'] = lang.partial(function (type, ms) {
return this.setTimeout(type, ms);
}, type);
}
})(Session.prototype);
module.exports = Session;