/* global document:false */
/**
* @module leadfoot/Server
*/
var keys = require('./keys');
var lang = require('dojo/lang');
var Promise = require('dojo/Promise');
var request = require('dojo/request');
var Session = require('./Session');
var statusCodes = require('./lib/statusCodes');
var urlUtil = require('url');
var util = require('./lib/util');
function isMsEdge(capabilities, minVersion, maxVersion) {
if (capabilities.browserName !== 'MicrosoftEdge') {
return false;
}
if (minVersion != null || maxVersion != null) {
var version = parseFloat(capabilities.browserVersion);
if (minVersion != null && version < minVersion) {
return false;
}
if (maxVersion != null && version > maxVersion) {
return false;
}
}
return true;
}
function isMacSafari(capabilities) {
return capabilities.browserName === 'safari' &&
capabilities.platform === 'MAC' &&
capabilities.platformName !== 'ios';
}
function isGeckodriver(capabilities) {
return capabilities.browserName === 'firefox' &&
parseFloat(capabilities.version) >= 49;
}
function isMacGeckodriver(capabilities) {
return isGeckodriver(capabilities) && capabilities.platform === 'MAC';
}
/**
* Creates a function that performs an HTTP request to a JsonWireProtocol endpoint.
*
* @param {string} method The HTTP method to fix.
* @returns {Function}
*/
function createHttpRequest(method) {
/**
* A function that performs an HTTP request to a JsonWireProtocol endpoint and normalises response status and
* data.
*
* @param {string} path
* The path-part of the JsonWireProtocol URL. May contain placeholders in the form `/\$\d/` that will be
* replaced by entries in the `pathParts` argument.
*
* @param {Object} requestData
* The payload for the request.
*
* @param {Array.<string>=} pathParts Optional placeholder values to inject into the path of the URL.
*
* @returns {Promise.<Object>}
*/
return function sendRequest(path, requestData, pathParts) {
var url = this.url + path.replace(/\$(\d)/, function (_, index) {
return encodeURIComponent(pathParts[index]);
});
var defaultRequestHeaders = {
// At least FirefoxDriver on Selenium 2.40.0 will throw a NullPointerException when retrieving
// session capabilities if an Accept header is not provided. (It is a good idea to provide one
// anyway)
'Accept': 'application/json,text/plain;q=0.9'
};
var kwArgs = lang.delegate(this.requestOptions, {
followRedirects: false,
handleAs: 'text',
headers: lang.mixin({}, defaultRequestHeaders),
method: method
});
if (requestData) {
kwArgs.data = JSON.stringify(requestData);
kwArgs.headers['Content-Type'] = 'application/json;charset=UTF-8';
// At least ChromeDriver 2.9.248307 will not process request data if the length of the data is not
// provided. (It is a good idea to provide one anyway)
kwArgs.headers['Content-Length'] = Buffer.byteLength(kwArgs.data, 'utf8');
}
else {
// At least Selenium 2.41.0 - 2.42.2 running as a grid hub will throw an exception and drop the current
// session if a Content-Length header is not provided with a DELETE or POST request, regardless of whether
// the request actually contains any request data.
kwArgs.headers['Content-Length'] = 0;
}
var trace = {};
Error.captureStackTrace(trace, sendRequest);
return request(url, kwArgs).then(function handleResponse(response) {
/*jshint maxcomplexity:24 */
// The JsonWireProtocol specification prior to June 2013 stated that creating a new session should
// perform a 3xx redirect to the session capabilities URL, instead of simply returning the returning
// data about the session; as a result, we need to follow all redirects to get consistent data
if (response.statusCode >= 300 && response.statusCode < 400 && response.getHeader('Location')) {
var redirectUrl = response.getHeader('Location');
// If redirectUrl isn't an absolute URL, resolve it based on the orignal URL used to create the session
if (!/^\w+:/.test(redirectUrl)) {
redirectUrl = urlUtil.resolve(url, redirectUrl);
}
return request(redirectUrl, {
method: 'GET',
headers: defaultRequestHeaders
}).then(handleResponse);
}
var responseType = response.getHeader('Content-Type');
var data;
if (responseType && responseType.indexOf('application/json') === 0 && response.data) {
data = JSON.parse(response.data);
}
// Some drivers will respond to a DELETE request with 204; in this case, we know the operation
// completed successfully, so just create an expected response data structure for a successful
// operation to avoid any special conditions elsewhere in the code caused by different HTTP return
// values
if (response.statusCode === 204) {
data = {
status: 0,
sessionId: null,
value: null
};
}
else if (response.statusCode >= 400 || (data && data.status > 0)) {
var error = new Error();
// "The client should interpret a 404 Not Found response from the server as an "Unknown command"
// response. All other 4xx and 5xx responses from the server that do not define a status field
// should be interpreted as "Unknown error" responses."
// - http://code.google.com/p/selenium/wiki/JsonWireProtocol#Response_Status_Codes
if (!data) {
data = {
status: response.statusCode === 404 || response.statusCode === 501 ? 9 : 13,
value: {
message: response.text
}
};
}
// ios-driver 0.6.6-SNAPSHOT April 2014 incorrectly implements the specification: does not return
// error data on the `value` key, and does not return the correct HTTP status for unknown commands
else if (!data.value && ('message' in data)) {
data = {
status: response.statusCode === 404 || response.statusCode === 501 ||
data.message.indexOf('cannot find command') > -1 ? 9 : 13,
value: data
};
}
// At least Appium April 2014 responds with the HTTP status Not Implemented but a Selenium
// status UnknownError for commands that are not implemented; these errors are more properly
// represented to end-users using the Selenium status UnknownCommand, so we make the appropriate
// coercion here
if (response.statusCode === 501 && data.status === 13) {
data.status = 9;
}
// At least BrowserStack in May 2016 responds with HTTP 500 and a message value of "Invalid Command" for
// at least some unknown commands. These errors are more properly represented to end-users using the
// Selenium status UnknownCommand, so we make the appropriate coercion here
if (response.statusCode === 500 && data.value && data.value.message === 'Invalid Command') {
data.status = 9;
}
// At least FirefoxDriver 2.40.0 responds with HTTP status codes other than Not Implemented and a
// Selenium status UnknownError for commands that are not implemented; however, it provides a
// reliable indicator that the operation was unsupported by the type of the exception that was
// thrown, so also coerce this back into an UnknownCommand response for end-user code
if (data.status === 13 && data.value && data.value.class &&
(data.value.class.indexOf('UnsupportedOperationException') > -1 ||
data.value.class.indexOf('UnsupportedCommandException') > -1)
) {
data.status = 9;
}
// At least InternetExplorerDriver 2.41.0 & SafariDriver 2.41.0 respond with HTTP status codes
// other than Not Implemented and a Selenium status UnknownError for commands that are not
// implemented; like FirefoxDriver they provide a reliable indicator of unsupported commands
if (response.statusCode === 500 && data.value && data.value.message &&
(
data.value.message.indexOf('Command not found') > -1 ||
data.value.message.indexOf('Unknown command') > -1
)
) {
data.status = 9;
}
// At least GhostDriver 1.1.0 incorrectly responds with HTTP 405 instead of HTTP 501 for
// unimplemented commands
if (response.statusCode === 405 && data.value && data.value.message &&
data.value.message.indexOf('Invalid Command Method') > -1
) {
data.status = 9;
}
var statusText = statusCodes[data.status];
if (statusText) {
error.name = statusText[0];
error.message = statusText[1];
}
if (data.value && data.value.message) {
error.message = data.value.message;
}
if (data.value && data.value.screen) {
data.value.screen = new Buffer(data.value.screen, 'base64');
}
error.status = data.status;
error.detail = data.value;
error.request = {
url: url,
method: method,
data: requestData
};
error.response = response;
var sanitizedUrl = (function () {
var parsedUrl = urlUtil.parse(url);
if (parsedUrl.auth) {
parsedUrl.auth = '(redacted)';
}
return urlUtil.format(parsedUrl);
})();
error.message = '[' + method + ' ' + sanitizedUrl +
(requestData ? ' / ' + JSON.stringify(requestData) : '') +
'] ' + error.message;
error.stack = error.message + util.trimStack(trace.stack);
throw error;
}
return data;
}).catch(function (error) {
error.stack = error.message + util.trimStack(trace.stack);
throw error;
});
};
}
/**
* Returns the actual response value from the remote environment.
*
* @param {Object} response JsonWireProtocol response object.
* @returns {any} The actual response value.
*/
function returnValue(response) {
return response.value;
}
/**
* The Server class represents a remote HTTP server implementing the WebDriver wire protocol that can be used to
* generate new remote control sessions.
*
* @constructor module:leadfoot/Server
* @param {(Object|string)} url
* The fully qualified URL to the JsonWireProtocol endpoint on the server. The default endpoint for a
* JsonWireProtocol HTTP server is http://localhost:4444/wd/hub. You may also pass a parsed URL object which will
* be converted to a string.
* @param {{ proxy: string }=} options
* Additional request options to be used for requests to the server.
*/
function Server(url, options) {
if (typeof url === 'object') {
url = Object.create(url);
if (url.username || url.password || url.accessKey) {
url.auth = encodeURIComponent(url.username) + ':' + encodeURIComponent(url.password || url.accessKey);
}
}
this.url = urlUtil.format(url).replace(/\/*$/, '/');
this.requestOptions = options || {};
}
/**
* @lends module:leadfoot/Server#
*/
Server.prototype = {
constructor: Server,
/**
* An alternative session constructor. Defaults to the standard {@link module:leadfoot/Session} constructor if
* one is not provided.
*
* @type {module:leadfoot/Session}
* @default Session
*/
sessionConstructor: Session,
/**
* Whether or not to perform capabilities testing and correction when creating a new Server.
* @type {boolean}
* @default
*/
fixSessionCapabilities: true,
_get: createHttpRequest('GET'),
_post: createHttpRequest('POST'),
_delete: createHttpRequest('DELETE'),
/**
* Gets the status of the remote server.
*
* @returns {Promise.<Object>} An object containing arbitrary properties describing the status of the remote
* server.
*/
getStatus: function () {
return this._get('status').then(returnValue);
},
/**
* Creates a new remote control session on the remote server.
*
* @param {Capabilities} desiredCapabilities
* A hash map of desired capabilities of the remote environment. The server may return an environment that does
* not match all the desired capabilities if one is not available.
*
* @param {Capabilities=} requiredCapabilities
* A hash map of required capabilities of the remote environment. The server will not return an environment that
* does not match all the required capabilities if one is not available.
*
* @returns {Promise.<module:leadfoot/Session>}
*/
createSession: function (desiredCapabilities, requiredCapabilities) {
var self = this;
var fixSessionCapabilities = desiredCapabilities.fixSessionCapabilities !== false &&
self.fixSessionCapabilities;
// Don’t send `fixSessionCapabilities` to the server
if ('fixSessionCapabilities' in desiredCapabilities) {
desiredCapabilities = lang.mixin({}, desiredCapabilities);
desiredCapabilities.fixSessionCapabilities = undefined;
}
return this._post('session', {
desiredCapabilities: desiredCapabilities,
requiredCapabilities: requiredCapabilities
}).then(function (response) {
var session = new self.sessionConstructor(response.sessionId, self, response.value);
if (fixSessionCapabilities) {
return self._fillCapabilities(session).catch(function (error) {
// The session was started on the server, but we did not resolve the Promise yet. If a failure
// occurs during capabilities filling, we should quit the session on the server too since the
// caller will not be aware that it ever got that far and will have no access to the session to
// quit itself.
return session.quit().finally(function () {
throw error;
});
});
}
else {
return session;
}
});
},
_fillCapabilities: function (session) {
/*jshint maxlen:140 */
var capabilities = session.capabilities;
function supported() { return true; }
function unsupported() { return false; }
function maybeSupported(error) { return error.name !== 'UnknownCommand'; }
var broken = supported;
var works = unsupported;
/**
* Adds the capabilities listed in the `testedCapabilities` object to the hash of capabilities for
* the current session. If a tested capability value is a function, it is assumed that it still needs to
* be executed serially in order to resolve the correct value of that particular capability.
*/
function addCapabilities(testedCapabilities) {
return new Promise(function (resolve, reject) {
var keys = Object.keys(testedCapabilities);
var i = 0;
(function next() {
var key = keys[i++];
if (!key) {
resolve();
return;
}
var value = testedCapabilities[key];
if (typeof value === 'function') {
value().then(function (value) {
capabilities[key] = value;
next();
}, reject);
}
else {
capabilities[key] = value;
next();
}
})();
});
}
function get(page) {
if (capabilities.supportsNavigationDataUris !== false) {
return session.get('data:text/html;charset=utf-8,' + encodeURIComponent(page));
}
// Internet Explorer 9 and earlier, and Microsoft Edge build 10240 and earlier, hang when attempting to do
// navigate after a `document.write` is performed to reset the tab content; we can still do some limited
// testing in these browsers by using the initial browser URL page and injecting some content through
// innerHTML, though it is unfortunately a quirks-mode file so testing is limited
if (
(capabilities.browserName === 'internet explorer' && parseFloat(capabilities.version) < 10) ||
isMsEdge(capabilities)
) {
// Edge driver doesn't provide an initialBrowserUrl
var initialUrl = capabilities.browserName === 'internet explorer' ? capabilities.initialBrowserUrl :
'about:blank';
return session.get(initialUrl).then(function () {
return session.execute('document.body.innerHTML = arguments[0];', [
// The DOCTYPE does not apply, for obvious reasons, but also old IE will discard invisible
// elements like `<script>` and `<style>` if they are the first elements injected with
// `innerHTML`, so an extra text node is added before the rest of the content instead
page.replace('<!DOCTYPE html>', 'x')
]);
});
}
return session.get('about:blank').then(function () {
return session.execute('document.write(arguments[0]);', [ page ]);
});
}
function discoverFeatures() {
// jshint maxcomplexity:15
var testedCapabilities = {};
// At least SafariDriver 2.41.0 fails to allow stand-alone feature testing because it does not inject user
// scripts for URLs that are not http/https
if (isMacSafari(capabilities)) {
return {
nativeEvents: false,
rotatable: false,
locationContextEnabled: false,
webStorageEnabled: false,
applicationCacheEnabled: false,
supportsNavigationDataUris: true,
supportsCssTransforms: true,
supportsExecuteAsync: true,
mouseEnabled: true,
touchEnabled: false,
dynamicViewport: true,
shortcutKey: keys.COMMAND
};
}
// Firefox 49+ (via geckodriver) only supports W3C locator strategies
if (isGeckodriver(capabilities)) {
testedCapabilities.isWebDriver = true;
}
// At least MS Edge 14316 supports alerts but does not specify the capability
if (isMsEdge(capabilities, 37.14316) && !('handlesAlerts' in capabilities)) {
testedCapabilities.handlesAlerts = true;
}
// Appium iOS as of April 2014 supports rotation but does not specify the capability
if (!('rotatable' in capabilities)) {
testedCapabilities.rotatable = session.getOrientation().then(supported, unsupported);
}
// At least FirefoxDriver 2.40.0 and ios-driver 0.6.0 claim they support geolocation in their returned
// capabilities map, when they do not
if (capabilities.locationContextEnabled) {
testedCapabilities.locationContextEnabled = session.getGeolocation()
.then(supported, function (error) {
return error.name !== 'UnknownCommand' &&
error.message.indexOf('not mapped : GET_LOCATION') === -1;
});
}
// At least FirefoxDriver 2.40.0 claims it supports web storage in the returned capabilities map, when
// it does not
if (capabilities.webStorageEnabled) {
testedCapabilities.webStorageEnabled = session.getLocalStorageLength()
.then(supported, maybeSupported);
}
// At least FirefoxDriver 2.40.0 claims it supports application cache in the returned capabilities map,
// when it does not
if (capabilities.applicationCacheEnabled) {
testedCapabilities.applicationCacheEnabled = session.getApplicationCacheStatus()
.then(supported, maybeSupported);
}
// IE11 will take screenshots, but it's very slow
if (capabilities.browserName === 'internet explorer' && capabilities.version == '11') {
testedCapabilities.takesScreenshot = true;
}
// At least Selendroid 0.9.0 will fail to take screenshots in certain device configurations, usually
// emulators with hardware acceleration enabled
else {
testedCapabilities.takesScreenshot = session.takeScreenshot().then(supported, unsupported);
}
// At least ios-driver 0.6.6-SNAPSHOT April 2014 does not support execute_async
testedCapabilities.supportsExecuteAsync = session.executeAsync('arguments[0](true);').catch(unsupported);
// Some additional, currently-non-standard capabilities are needed in order to know about supported
// features of a given platform
if (!('mouseEnabled' in capabilities)) {
// Using mouse services such as doubleclick will hang Firefox 49+ session on the Mac.
if (isMacGeckodriver(capabilities)) {
testedCapabilities.mouseEnabled = true;
}
else {
testedCapabilities.mouseEnabled = function () {
return session.doubleClick()
.then(supported, maybeSupported);
};
}
}
// Don't check for touch support if the environment reports that no touchscreen is available
if (capabilities.hasTouchScreen === false) {
testedCapabilities.touchEnabled = false;
}
else if (!('touchEnabled' in capabilities)) {
testedCapabilities.touchEnabled = session.doubleTap()
.then(supported, maybeSupported);
}
// ChromeDriver 2.19 claims that it supports touch but it does not implement all of the touch endpoints
// from JsonWireProtocol
else if (capabilities.browserName === 'chrome') {
testedCapabilities.touchEnabled = false;
}
if (!('dynamicViewport' in capabilities)) {
testedCapabilities.dynamicViewport = session.getWindowSize().then(function (originalSize) {
return session.setWindowSize(originalSize.width, originalSize.height);
}).then(supported, unsupported);
}
// At least Internet Explorer 11 and earlier do not allow data URIs to be used for navigation
testedCapabilities.supportsNavigationDataUris = function () {
return get('<!DOCTYPE html><title>a</title>').then(function () {
return session.getPageTitle();
}).then(function (pageTitle) {
return pageTitle === 'a';
}).catch(unsupported);
};
testedCapabilities.supportsCssTransforms = function () {
// It is not possible to test this since the feature tests runs in quirks-mode on IE<10, but we
// know that IE9 supports CSS transforms
if (capabilities.browserName === 'internet explorer' && parseFloat(capabilities.version) === 9) {
return Promise.resolve(true);
}
/*jshint maxlen:240 */
return get('<!DOCTYPE html><style>#a{width:8px;height:8px;-ms-transform:scale(0.5);-moz-transform:scale(0.5);-webkit-transform:scale(0.5);transform:scale(0.5);}</style><div id="a"></div>').then(function () {
return session.execute(/* istanbul ignore next */ function () {
var bbox = document.getElementById('a').getBoundingClientRect();
return bbox.right - bbox.left === 4;
});
}).catch(unsupported);
};
testedCapabilities.shortcutKey = (function () {
var platform = capabilities.platform.toLowerCase();
if (platform.indexOf('mac') === 0) {
return keys.COMMAND;
}
if (platform.indexOf('ios') === 0) {
return null;
}
return keys.CONTROL;
})();
return Promise.all(testedCapabilities);
}
function discoverDefects() {
var testedCapabilities = {};
// At least SafariDriver 2.41.0 fails to allow stand-alone feature testing because it does not inject user
// scripts for URLs that are not http/https
if (isMacSafari(capabilities)) {
return {
brokenDeleteCookie: false,
brokenExecuteElementReturn: false,
brokenExecuteUndefinedReturn: false,
brokenElementDisplayedOpacity: false,
brokenElementDisplayedOffscreen: false,
brokenSubmitElement: true,
brokenWindowSwitch: true,
brokenDoubleClick: false,
brokenCssTransformedSize: true,
fixedLogTypes: false,
brokenHtmlTagName: false,
brokenNullGetSpecAttribute: false,
// SafariDriver-specific
brokenActiveElement: true,
brokenNavigation: true,
brokenMouseEvents: true,
brokenWindowPosition: true,
brokenSendKeys: true,
brokenExecuteForNonHttpUrl: true,
// SafariDriver 2.41.0 cannot delete cookies, at all, ever
brokenCookies: true
};
}
// Internet Explorer 8 and earlier will simply crash the server if we attempt to return the parent
// frame via script, so never even attempt to do so
testedCapabilities.scriptedParentFrameCrashesBrowser =
capabilities.browserName === 'internet explorer' && parseFloat(capabilities.version) < 9;
// At least ChromeDriver 2.9 and MS Edge 10240 does not implement /element/active
testedCapabilities.brokenActiveElement = session.getActiveElement().then(works, function (error) {
return error.name === 'UnknownCommand';
});
// At least Selendroid 0.9.0 and MS Edge have broken cookie deletion.
if (capabilities.browserName === 'selendroid') {
// This test is very hard to get working properly in other environments so only test when Selendroid is
// the browser
testedCapabilities.brokenDeleteCookie = function () {
return session.get('about:blank').then(function () {
return session.clearCookies();
}).then(function () {
return session.setCookie({ name: 'foo', value: 'foo' });
}).then(function () {
return session.deleteCookie('foo');
}).then(function () {
return session.getCookies();
}).then(function (cookies) {
return cookies.length > 0;
}).catch(function () {
return true;
}).then(function (isBroken) {
return session.clearCookies().finally(function () {
return isBroken();
});
});
};
}
else if (isMsEdge(capabilities)) {
testedCapabilities.brokenDeleteCookie = true;
}
// At least Firefox 49 + geckodriver can't POST empty data
if (isGeckodriver(capabilities)) {
testedCapabilities.brokenEmptyPost = true;
}
// At least MS Edge may return an 'element is obscured' error when trying to click on visible elements.
if (isMsEdge(capabilities)) {
testedCapabilities.brokenClick = true;
}
// At least Selendroid 0.9.0 incorrectly returns HTML tag names in uppercase, which is a violation
// of the JsonWireProtocol spec
testedCapabilities.brokenHtmlTagName = session.findByTagName('html').then(function (element) {
return element.getTagName();
}).then(function (tagName) {
return tagName !== 'html';
}).catch(broken);
// At least ios-driver 0.6.6-SNAPSHOT incorrectly returns empty string instead of null for attributes
// that do not exist
testedCapabilities.brokenNullGetSpecAttribute = session.findByTagName('html').then(function (element) {
return element.getSpecAttribute('nonexisting');
}).then(function (value) {
return value !== null;
}).catch(broken);
// At least MS Edge 10240 doesn't properly deserialize web elements passed as `execute` arguments
testedCapabilities.brokenElementSerialization = function () {
return get('<!DOCTYPE html><div id="a"></div>').then(function () {
return session.findById('a')
}).then(function (element) {
return session.execute(function (element) {
return element.getAttribute('id');
}, [ element ]);
}).then(function (attribute) {
return attribute !== 'a';
}).catch(broken);
};
// At least Selendroid 0.16.0 incorrectly returns `undefined` instead of `null` when an undefined
// value is returned by an `execute` call
testedCapabilities.brokenExecuteUndefinedReturn = session.execute(
'return undefined;'
).then(function (value) {
return value !== null;
}, broken);
// At least Selendroid 0.9.0 always returns invalid element handles from JavaScript
testedCapabilities.brokenExecuteElementReturn = function () {
return get('<!DOCTYPE html><div id="a"></div>').then(function () {
return session.execute('return document.getElementById("a");');
}).then(function (element) {
return element && element.getTagName();
}).then(works, broken);
};
// At least Selendroid 0.9.0 treats fully transparent elements as displayed, but all others do not
testedCapabilities.brokenElementDisplayedOpacity = function () {
return get('<!DOCTYPE html><div id="a" style="opacity: .1;">a</div>').then(function () {
// IE<9 do not support CSS opacity so should not be involved in this test
return session.execute('var o = document.getElementById("a").style.opacity; return o && o.charAt(0) === "0";');
}).then(function (supportsOpacity) {
if (!supportsOpacity) {
return works();
}
else {
return session.execute('document.getElementById("a").style.opacity = "0";')
.then(function () {
return session.findById('a');
})
.then(function (element) {
return element.isDisplayed();
});
}
}).catch(broken);
};
// At least ChromeDriver 2.9 treats elements that are offscreen as displayed, but others do not
testedCapabilities.brokenElementDisplayedOffscreen = function () {
var pageText = '<!DOCTYPE html><div id="a" style="left: 0; position: absolute; top: -1000px;">a</div>';
return get(pageText).then(function () {
return session.findById('a');
}).then(function (element) {
return element.isDisplayed();
}).catch(broken);
};
// At least MS Edge Driver 14316 doesn't support sending keys to a file input. See
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7194303/
//
// The existing feature test for this caused some browsers to hang, so just flag it for Edge for now.
if (isMsEdge(capabilities, null, 38.14366)) {
testedCapabilities.brokenFileSendKeys = true;
}
// testedCapabilities.brokenFileSendKeys = function () {
// return get('<!DOCTYPE html><input type="file" id="i1">').then(function () {
// var element;
// return session.findById('i1')
// .then(function (element) {
// return element.type('./Server.js');
// }).then(function () {
// return session.execute(function () {
// return document.getElementById('i1').value;
// });
// }).then(function (text) {
// if (!/Server.js$/.test(text)) {
// throw new Error('mismatch');
// }
// });
// }).then(works, broken);
// };
// At least MS Edge Driver 14316 doesn't normalize whitespace properly when retrieving text. Text may
// contain "\r\n" pairs rather than "\n", and there may be extraneous whitespace adjacent to "\r\n" pairs
// and at the start and end of the text.
testedCapabilities.brokenWhitespaceNormalization = function () {
return get('<!DOCTYPE html><div id="d">This is\n<br>a test\n</div>').then(function () {
return session.findById('d')
.then(function (element) {
return element.getVisibleText();
}).then(function (text) {
if (/\r\n/.test(text) || /\s+$/.test(text)) {
throw new Error('invalid whitespace');
}
});
}).then(works, broken);
};
// At least MS Edge Driver 14316 doesn't return elements' computed styles
testedCapabilities.brokenComputedStyles = function () {
var pageText = '<!DOCTYPE html><style>a { background: purple }</style><a id="a1">foo</a>';
return get(pageText).then(function () {
return session.findById('a1');
}).then(function (element) {
return element.getComputedStyle('background-color');
}).then(function (value) {
if (!value) {
throw new Error('empty style');
}
}).then(works, broken);
};
// IE11 will hang during this check, although option selection does work with it
if (capabilities.browserName !== 'internet explorer' && capabilities.version !== '11') {
// At least MS Edge Driver 14316 doesn't allow selection option elements to be clicked.
testedCapabilities.brokenOptionSelect = function () {
return get(
'<!DOCTYPE html><select id="d"><option id="o1" value="foo">foo</option>' +
'<option id="o2" value="bar" selected>bar</option></select>'
).then(function () {
return session.findById('d');
}).then(function (element) {
return element.click();
}).then(function () {
return session.findById('o1');
}).then(function (element) {
return element.click();
}).then(works, broken);
};
}
// At least MS Edge driver 10240 doesn't support getting the page source
testedCapabilities.brokenPageSource = session.getPageSource().then(works, broken);
// IE11 will hang during this check if nativeEvents are enabled
if (capabilities.browserName !== 'internet explorer' && capabilities.version !== '11') {
testedCapabilities.brokenSubmitElement = true;
}
else {
// There is inconsistency across all drivers as to whether or not submitting a form button should cause
// the form button to be submitted along with the rest of the form; it seems most likely that tests
// do want the specified button to act as though someone clicked it when it is submitted, so the
// behaviour needs to be normalised
testedCapabilities.brokenSubmitElement = function () {
/*jshint maxlen:200 */
return get(
'<!DOCTYPE html><form method="get" action="about:blank">' +
'<input id="a" type="submit" name="a" value="a"></form>'
).then(function () {
return session.findById('a');
}).then(function (element) {
return element.submit();
}).then(function () {
return session.getCurrentUrl();
}).then(function (url) {
return url.indexOf('a=a') === -1;
}).catch(broken);
};
}
// At least MS Edge 10586 becomes unresponsive after calling DELETE window, and window.close() requires user
// interaction. This capability is distinct from brokenDeleteWindow as this capability indicates that there
// is no way to close a Window.
if (isMsEdge(capabilities, null, 25.10586)) {
testedCapabilities.brokenWindowClose = true;
}
// At least MS Edge driver 10240 doesn't support window sizing commands
testedCapabilities.brokenWindowSize = session.getWindowSize().then(works, broken);
// At least Selendroid 0.9.0 has a bug where it catastrophically fails to retrieve available types;
// they have tried to hardcode the available log types in this version so we can just return the
// same hardcoded list ourselves.
// At least InternetExplorerDriver 2.41.0 also fails to provide log types.
// Firefox 49+ (via geckodriver) doesn't support retrieving logs or log types, and may hang the session.
if (isMacGeckodriver(capabilities)) {
testedCapabilities.fixedLogTypes = [];
}
else {
testedCapabilities.fixedLogTypes = session.getAvailableLogTypes().then(unsupported, function (error) {
if (capabilities.browserName === 'selendroid' && !error.response.text.length) {
return [ 'logcat' ];
}
return [];
});
}
// At least Microsoft Edge 10240 doesn't support timeout values of 0.
testedCapabilities.brokenZeroTimeout = session.setTimeout('implicit', 0).then(works, broken);
// At least ios-driver 0.6.6-SNAPSHOT April 2014 corrupts its internal state when performing window
// switches and gets permanently stuck; we cannot feature detect, so platform sniffing it is
if (capabilities.browserName === 'Safari' && capabilities.platformName === 'IOS') {
testedCapabilities.brokenWindowSwitch = true;
}
else {
testedCapabilities.brokenWindowSwitch = session.getCurrentWindowHandle().then(function (handle) {
return session.switchToWindow(handle);
}).then(works, broken);
}
// At least selendroid 0.12.0-SNAPSHOT doesn't support switching to the parent frame
if (capabilities.browserName === 'android' && capabilities.deviceName === 'Android Emulator') {
testedCapabilities.brokenParentFrameSwitch = true;
}
else {
testedCapabilities.brokenParentFrameSwitch = session.switchToParentFrame().then(works, broken);
}
var scrollTestUrl = '<!DOCTYPE html><div id="a" style="margin: 3000px;"></div>';
// ios-driver 0.6.6-SNAPSHOT April 2014 calculates position based on a bogus origin and does not
// account for scrolling
testedCapabilities.brokenElementPosition = function () {
return get(scrollTestUrl).then(function () {
return session.findById('a');
}).then(function (element) {
return element.getPosition();
}).then(function (position) {
return position.x !== 3000 || position.y !== 3000;
}).catch(broken);
};
// At least ios-driver 0.6.6-SNAPSHOT April 2014 will never complete a refresh call
testedCapabilities.brokenRefresh = function () {
return session.get('about:blank?1').then(function () {
return new Promise(function (resolve, reject, progress, setCanceler) {
function cleanup() {
clearTimeout(timer);
refresh.cancel();
}
setCanceler(cleanup);
var refresh = session.refresh().then(function () {
cleanup();
resolve(false);
}, function () {
cleanup();
resolve(true);
});
var timer = setTimeout(function () {
cleanup();
}, 2000);
});
}).catch(broken);
};
if (isGeckodriver(capabilities)) {
// At least geckodriver 0.11 and Firefox 49 don't implement mouse control, so everything will need to be
// simulated.
testedCapabilities.brokenMouseEvents = true;
}
else if (capabilities.mouseEnabled) {
// At least IE 10 and 11 on SauceLabs don't fire native mouse events consistently even though they
// support moveMouseTo
testedCapabilities.brokenMouseEvents = function () {
return get(
'<!DOCTYPE html><div id="foo">foo</div>' +
'<script>window.counter = 0; var d = document; d.onmousemove = function () { window.counter++; };</script>'
).then(function () {
return session.findById('foo');
}).then(function (element) {
return session.moveMouseTo(element, 20, 20);
}).then(function () {
return util.sleep(100);
}).then(function () {
return session.execute('return window.counter;');
}).then(
function (counter) {
return counter > 0 ? works() : broken();
},
broken
);
};
// At least ChromeDriver 2.12 through 2.19 will throw an error if mouse movement relative to the <html>
// element is attempted
testedCapabilities.brokenHtmlMouseMove = function () {
return get('<!DOCTYPE html><html></html>').then(function () {
return session.findByTagName('html').then(function (element) {
return session.moveMouseTo(element, 0, 0);
});
}).then(works, broken);
};
// At least ChromeDriver 2.9.248307 does not correctly emit the entire sequence of events that would
// normally occur during a double-click
testedCapabilities.brokenDoubleClick = function retry() {
/*jshint maxlen:200 */
// InternetExplorerDriver is not buggy, but IE9 in quirks-mode is; since we cannot do feature
// tests in standards-mode in IE<10, force the value to false since it is not broken in this
// browser
if (capabilities.browserName === 'internet explorer' && capabilities.version === '9') {
return Promise.resolve(false);
}
return get('<!DOCTYPE html><script>window.counter = 0; var d = document; d.onclick = d.onmousedown = d.onmouseup = function () { window.counter++; };</script>').then(function () {
return session.findByTagName('html');
}).then(function (element) {
return session.moveMouseTo(element);
}).then(function () {
return util.sleep(100);
}).then(function () {
return session.doubleClick();
}).then(function () {
return session.execute('return window.counter;');
}).then(function (counter) {
// InternetExplorerDriver 2.41.0 has a race condition that makes this test sometimes fail
/* istanbul ignore if: inconsistent race condition */
if (counter === 0) {
return retry();
}
return counter !== 6;
}).catch(broken);
};
}
if (capabilities.touchEnabled) {
// At least Selendroid 0.9.0 fails to perform a long tap due to an INJECT_EVENTS permission failure
testedCapabilities.brokenLongTap = session.findByTagName('body').then(function (element) {
return session.longTap(element);
}).then(works, broken);
// At least ios-driver 0.6.6-SNAPSHOT April 2014 claims to support touch press/move/release but
// actually fails when you try to use the commands
testedCapabilities.brokenMoveFinger = session.pressFinger(0, 0).then(works, function (error) {
return error.name === 'UnknownCommand' || error.message.indexOf('need to specify the JS') > -1;
});
// Touch scroll in ios-driver 0.6.6-SNAPSHOT is broken, does not scroll at all;
// in selendroid 0.9.0 it ignores the element argument
testedCapabilities.brokenTouchScroll = function () {
return get(scrollTestUrl).then(function () {
return session.touchScroll(0, 20);
}).then(function () {
return session.execute('return window.scrollY !== 20;');
}).then(function (isBroken) {
if (isBroken) {
return true;
}
return session.findById('a').then(function (element) {
return session.touchScroll(element, 0, 0);
}).then(function () {
return session.execute('return window.scrollY !== 3000;');
});
})
.catch(broken);
};
// Touch flick in ios-driver 0.6.6-SNAPSHOT is broken, does not scroll at all except in very
// broken ways if very tiny speeds are provided and the flick goes in the wrong direction
testedCapabilities.brokenFlickFinger = function () {
return get(scrollTestUrl).then(function () {
return session.flickFinger(0, 400);
}).then(function () {
return session.execute('return window.scrollY === 0;');
})
.catch(broken);
};
}
if (capabilities.supportsCssTransforms) {
testedCapabilities.brokenCssTransformedSize = function () {
/*jshint maxlen:240 */
return get('<!DOCTYPE html><style>#a{width:8px;height:8px;-ms-transform:scale(0.5);-moz-transform:scale(0.5);-webkit-transform:scale(0.5);transform:scale(0.5);}</style><div id="a"></div>').then(function () {
return session.execute('return document.getElementById("a");').then(function (element) {
return element.getSize();
}).then(function (dimensions) {
return dimensions.width !== 4 || dimensions.height !== 4;
});
}).catch(broken);
};
}
return Promise.all(testedCapabilities);
}
function discoverServerFeatures() {
var testedCapabilities = {};
/* jshint maxlen:300 */
// Check that the remote server will accept file uploads. There is a secondary test in discoverDefects that
// checks whether the server allows typing into file inputs.
testedCapabilities.remoteFiles = function () {
return session._post('file', {
file: 'UEsDBAoAAAAAAD0etkYAAAAAAAAAAAAAAAAIABwAdGVzdC50eHRVVAkAA2WnXlVlp15VdXgLAAEE8gMAAATyAwAAUEsBAh4DCgAAAAAAPR62RgAAAAAAAAAAAAAAAAgAGAAAAAAAAAAAAKSBAAAAAHRlc3QudHh0VVQFAANlp15VdXgLAAEE8gMAAATyAwAAUEsFBgAAAAABAAEATgAAAEIAAAAAAA=='
}).then(function (filename) {
return filename && filename.indexOf('test.txt') > -1;
}).catch(unsupported);
};
// The window sizing commands in the W3C standard don't use window handles, but they do under the
// JsonWireProtocol. By default, Session assumes handles are used. When the result of this check is added to
// capabilities, Session will take it into account.
testedCapabilities.implicitWindowHandles = session.getWindowSize().then(unsupported, function (error) {
return error.name === 'UnknownCommand';
});
// At least SafariDriver 2.41.0 fails to allow stand-alone feature testing because it does not inject user
// scripts for URLs that are not http/https
if (!isMacSafari(capabilities)) {
// At least MS Edge 14316 returns immediately from a click request immediately rather than waiting for
// default action to occur.
if (isMsEdge(capabilities)) {
testedCapabilities.returnsFromClickImmediately = true;
}
else {
testedCapabilities.returnsFromClickImmediately = function () {
function assertSelected(expected) {
return function (actual) {
if (expected !== actual) {
throw new Error('unexpected selection state');
}
};
}
return get(
'<!DOCTYPE html><input type="checkbox" id="c">'
).then(function () {
return session.findById('c');
}).then(function (element) {
return element.click().then(function () {
return element.isSelected();
}).then(assertSelected(true))
.then(function () {
return element.click().then(function () {
return element.isSelected();
});
}).then(assertSelected(false))
.then(function () {
return element.click().then(function () {
return element.isSelected();
});
}).then(assertSelected(true));
}).then(works, broken);
};
}
}
// The W3C WebDriver standard does not support the session-level /keys command, but JsonWireProtocol does.
if (isGeckodriver(capabilities)) {
testedCapabilities.supportsKeysCommand = false;
}
else {
testedCapabilities.supportsKeysCommand = session._post('keys', { value: [ 'a' ] }).then(supported,
unsupported);
}
return Promise.all(testedCapabilities);
}
if (capabilities._filled) {
return Promise.resolve(session);
}
// At least geckodriver 0.11 and Firefox 49+ may hang when getting 'about:blank' in the first request
var promise = isGeckodriver(capabilities) ? Promise.resolve(session) : session.get('about:blank');
return promise
.then(discoverServerFeatures)
.then(addCapabilities)
.then(discoverFeatures)
.then(addCapabilities)
.then(function () {
return session.get('about:blank');
})
.then(discoverDefects)
.then(addCapabilities)
.then(function () {
Object.defineProperty(capabilities, '_filled', {
value: true,
configurable: true
});
return session.get('about:blank').finally(function () {
return session;
});
});
},
/**
* Gets a list of all currently active remote control sessions on this server.
*
* @returns {Promise.<Object[]>}
*/
getSessions: function () {
return this._get('sessions').then(function (sessions) {
// At least BrowserStack is now returning an array for the sessions response
if (sessions && !Array.isArray(sessions)) {
sessions = returnValue(sessions);
}
// At least ChromeDriver 2.19 uses the wrong keys
// https://code.google.com/p/chromedriver/issues/detail?id=1229
sessions.forEach(function (session) {
if (session.sessionId && !session.id) {
session.id = session.sessionId;
}
});
return sessions;
});
},
/**
* Gets information on the capabilities of a given session from the server. The list of capabilities returned
* by this command will not include any of the extra session capabilities detected by Leadfoot and may be
* inaccurate.
*
* @param {string} sessionId
* @returns {Promise.<Object>}
*/
getSessionCapabilities: function (sessionId) {
return this._get('session/$0', null, [ sessionId ]).then(returnValue);
},
/**
* Terminates a session on the server.
*
* @param {string} sessionId
* @returns {Promise.<void>}
*/
deleteSession: function (sessionId) {
return this._delete('session/$0', null, [ sessionId ]).then(returnValue);
}
};
module.exports = Server;