@theintern/leadfoot
Version:
Leadfoot. A JavaScript client library that brings cross-platform consistency to the Selenium WebDriver API.
983 lines • 91.6 kB
JavaScript
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isFirefox = exports.isSafari = exports.isInternetExplorer = exports.isMsEdge = void 0;
var http_1 = require("http");
var https_1 = require("https");
var keys_1 = __importDefault(require("./keys"));
var common_1 = require("@theintern/common");
var Session_1 = __importDefault(require("./Session"));
var statusCodes_1 = __importDefault(require("./lib/statusCodes"));
var url_1 = require("url");
var util_1 = require("./lib/util");
var Server = /** @class */ (function () {
/**
* The Server class represents a remote HTTP server implementing the
* WebDriver wire protocol that can be used to generate new remote control
* sessions.
*
* @param 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 options Additional request options to be used for requests to the
* server.
*/
// TODO: NodeRequestOptions doesn't take a type in dojo-core alpha 20
function Server(url, options) {
/**
* An alternative session constructor. Defaults to the standard [[Session]]
* constructor if one is not provided.
*/
this.sessionConstructor = Session_1.default;
/**
* Whether or not to detect and/or correct environment capabilities when
* creating a new Server. If the value is "no-detect", capabilities will be
* updated with already-known features and defects based on the platform, but
* no tests will be run.
*/
this.fixSessionCapabilities = true;
// Use custom agents with keepAlive enabled to improve test efficiency,
// particularly with remote services such as BrowserStack. See
// https://github.com/browserstack/fast-selenium-scripts/blob/master/node/fast-selenium.js
this._httpAgent = new http_1.Agent({
keepAlive: true,
keepAliveMsecs: 30000
});
this._httpsAgent = new https_1.Agent({
keepAlive: true,
keepAliveMsecs: 30000
});
if (typeof url === 'object') {
url = __assign({}, url);
if (url.username || url.password || url.accessKey) {
url.auth =
encodeURIComponent(url.username || '') +
':' +
encodeURIComponent(url.password || url.accessKey || '');
}
}
this.url = url_1.format(url).replace(/\/*$/, '/');
this.requestOptions = options || {};
}
/**
* A function that performs an HTTP request to a JsonWireProtocol endpoint
* and normalises response status and data.
*
* @param method The HTTP method to fix
*
* @param 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 requestData The payload for the request.
*
* @param pathParts Optional placeholder values to inject into the path of
* the URL.
*/
Server.prototype._sendRequest = function (method, 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 headers = __assign({}, defaultRequestHeaders);
var kwArgs = __assign(__assign({}, this.requestOptions), { followRedirects: false, handleAs: 'text', httpAgent: this._httpAgent, httpsAgent: this._httpsAgent, headers: headers, method: method });
if (requestData) {
kwArgs.data = JSON.stringify(requestData);
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)
headers['Content-Length'] = String(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.
headers['Content-Length'] = '0';
}
var trace = {};
Error.captureStackTrace(trace, this._sendRequest);
return new common_1.Task(function (resolve, reject) {
common_1.request(url, kwArgs)
.then(resolve, reject)
.finally(function () {
var error = new Error('Canceled');
error.name = 'CancelError';
reject(error);
});
})
.then(function handleResponse(response) {
// 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.status >= 300 &&
response.status < 400 &&
response.headers.get('Location')) {
var redirectUrl = response.headers.get('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 = url_1.resolve(url, redirectUrl);
}
return common_1.request(redirectUrl, {
headers: defaultRequestHeaders
}).then(handleResponse);
}
return response.text().then(function (data) {
return { response: response, data: data };
});
})
.then(function (responseData) {
var response = responseData.response;
var responseType = response.headers.get('Content-Type');
var data;
if (responseType &&
responseType.indexOf('application/json') === 0 &&
responseData.data) {
data = JSON.parse(responseData.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.status === 204) {
data = {
status: 0,
sessionId: null,
value: null
};
}
else if (response.status >= 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 = {
value: {
message: responseData.data
}
};
}
else if (!data.value && 'message' in data) {
// 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
data = {
status: response.status === 404 ||
response.status === 501 ||
data.message.indexOf('cannot find command') > -1
? 9
: 13,
value: data
};
}
// At least BrowserStack in December 2020 returns response data with a value but no status
if (!data.status) {
data.status =
response.status === 404 || response.status === 501 ? 9 : 13;
}
// At least InternetExplorerDriver 3.141.59 includes `status` and
// `value` fields but uses HTTP status codes
if (data.status === 404 || data.status === 501) {
data.status = 9;
}
// 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.status === 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.status === 500 &&
data.value &&
data.value.message === 'Invalid Command') {
data.status = 9;
}
// At least BrowserStack in Aug 2020 responds with HTTP 422
// 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.status === 422 &&
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.status === 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.status === 405 &&
data.value &&
data.value.message &&
data.value.message.indexOf('Invalid Command Method') > -1) {
data.status = 9;
}
var statusCode = statusCodes_1.default[data.status];
if (statusCode) {
var name_1 = statusCode[0], message = statusCode[1];
if (name_1 && message) {
error.name = name_1;
error.message = message;
}
}
if (data.value && data.value.message) {
error.message = data.value.message;
}
if (data.value && data.value.screen) {
data.value.screen = Buffer.from(data.value.screen, 'base64');
}
error.status = data.status;
error.detail = data.value;
error.name = getErrorName(error);
error.request = {
url: url,
method: method,
data: requestData
};
error.response = response;
var sanitizedUrl = (function () {
var parsedUrl = url_1.parse(url);
if (parsedUrl.auth) {
parsedUrl.auth = '(redacted)';
}
return url_1.format(parsedUrl);
})();
error.message =
"[" + method + " " + sanitizedUrl +
(requestData ? " / " + JSON.stringify(requestData) : '') +
("] " + error.message);
error.stack = error.message + util_1.trimStack(trace.stack);
throw error;
}
return data;
})
.catch(function (error) {
error.stack = error.message + util_1.trimStack(trace.stack);
throw error;
});
};
Server.prototype.get = function (path, requestData, pathParts) {
return this._sendRequest('GET', path, requestData, pathParts);
};
Server.prototype.post = function (path, requestData, pathParts) {
return this._sendRequest('POST', path, requestData, pathParts);
};
Server.prototype.delete = function (path, requestData, pathParts) {
return this._sendRequest('DELETE', path, requestData, pathParts);
};
/**
* Gets the status of the remote server.
*
* @returns An object containing arbitrary properties describing the status
* of the remote server.
*/
Server.prototype.getStatus = function () {
return this.get('status').then(returnValue);
};
/**
* Creates a new remote control session on the remote server.
*
* @param 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 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.
*/
Server.prototype.createSession = function (desiredCapabilities, requiredCapabilities) {
var _this = this;
var fixSessionCapabilities = this.fixSessionCapabilities;
if (desiredCapabilities.fixSessionCapabilities != null) {
fixSessionCapabilities = desiredCapabilities.fixSessionCapabilities;
// Don’t send `fixSessionCapabilities` to the server
desiredCapabilities = __assign({}, desiredCapabilities);
desiredCapabilities.fixSessionCapabilities = undefined;
}
return this.post('session', {
desiredCapabilities: desiredCapabilities,
requiredCapabilities: requiredCapabilities
}).then(function (response) {
var responseData;
var sessionId;
if (response.value.sessionId && response.value.capabilities) {
// At least geckodriver 0.16 - 0.19 return the sessionId and
// capabilities as value.sessionId and value.capabilities.
responseData = response.value.capabilities;
sessionId = response.value.sessionId;
}
else if (response.value.value && response.value.sessionId) {
// At least geckodriver 0.15.0 returns the sessionId and
// capabilities as value.sessionId and value.value.
responseData = response.value.value;
sessionId = response.value.sessionId;
}
else {
// Selenium and chromedriver return the sessionId as a top
// level property in the response, and the capabilities in a
// 'value' property.
responseData = response.value;
sessionId = response.sessionId;
}
var session = new _this.sessionConstructor(sessionId, _this, responseData);
// Add any desired capabilities that were originally specified but aren't
// present in the capabilities returned by the server. This will allow
// for feature flags to be manually set.
var userKeys = Object.keys(desiredCapabilities).filter(function (key) { return !(key in session.capabilities); });
for (var _i = 0, userKeys_1 = userKeys; _i < userKeys_1.length; _i++) {
var key = userKeys_1[_i];
session.capabilities[key] = desiredCapabilities[key];
}
if (fixSessionCapabilities) {
return _this._fillCapabilities(session, fixSessionCapabilities !== 'no-detect')
.catch(function (error) {
// The session was started on the server, but we did
// not resolve the Task 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;
});
})
.then(function () { return session; });
}
else {
return session;
}
});
};
/**
* Fill in known capabilities/defects and optionally run tests to detect
* more
*/
Server.prototype._fillCapabilities = function (session, detectCapabilities) {
if (detectCapabilities === void 0) { detectCapabilities = true; }
Object.assign(session.capabilities, this._getKnownCapabilities(session));
return (detectCapabilities
? this._detectCapabilities(session)
: common_1.Task.resolve(session)).then(function () {
Object.defineProperty(session.capabilities, '_filled', {
value: true,
configurable: true
});
return session;
});
};
/**
* Return capabilities and defects that don't require running tests
*/
Server.prototype._getKnownCapabilities = function (session) {
var capabilities = session.capabilities;
var updates = {};
// Safari 10 and 11 report their versions on a 'version' property using
// non-contiguous version numbers. Versions < 10 use standard numbers on
// a 'version' property, while versions >= 12 use standard numbers on a
// browserVersion property.
if (isSafari(session.capabilities) &&
!session.capabilities.browserVersion) {
var version = session.capabilities.version;
var versionNum = parseFloat(version);
if (versionNum > 12000 && versionNum < 13000) {
session.capabilities.browserVersion = '10';
}
else if (versionNum > 13000) {
session.capabilities.browserVersion = '11';
}
}
// At least geckodriver 0.15.0 only returns platformName (not platform)
// and browserVersion (not version) in its capabilities.
if (capabilities.platform && !capabilities.platformName) {
capabilities.platformName = capabilities.platform;
}
if (capabilities.version && !capabilities.browserVersion) {
capabilities.browserVersion = capabilities.version;
}
// 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 (isSafari(capabilities)) {
if (isMac(capabilities)) {
if (isValidVersion(capabilities, 0, 11)) {
Object.assign(updates, {
nativeEvents: false,
rotatable: false,
locationContextEnabled: false,
webStorageEnabled: false,
applicationCacheEnabled: false,
supportsNavigationDataUris: true,
supportsCssTransforms: true,
supportsExecuteAsync: true,
mouseEnabled: true,
touchEnabled: false,
dynamicViewport: true,
shortcutKey: keys_1.default.COMMAND,
// This must be set; running it as a server test will cause
// SafariDriver to emit errors with the text "undefined is not
// an object (evaluating 'a.postMessage')", and the session
// will become unresponsive
returnsFromClickImmediately: false,
brokenDeleteCookie: false,
brokenExecuteElementReturn: false,
brokenExecuteUndefinedReturn: false,
brokenElementDisplayedOpacity: false,
brokenElementDisplayedOffscreen: false,
brokenSubmitElement: true,
brokenWindowSwitch: true,
brokenDoubleClick: false,
brokenCssTransformedSize: true,
fixedLogTypes: false,
brokenHtmlTagName: false,
brokenNullGetSpecAttribute: false
});
}
if (isValidVersion(capabilities, 0, 10)) {
Object.assign(updates, {
// SafariDriver, which shows versions up to 9.x, doesn't support file
// uploads
remoteFiles: false,
brokenActiveElement: true,
brokenExecuteForNonHttpUrl: true,
brokenMouseEvents: true,
brokenNavigation: true,
brokenOptionSelect: false,
brokenSendKeys: true,
brokenWindowPosition: true,
brokenWindowSize: true,
// SafariDriver 2.41.0 cannot delete cookies, at all, ever
brokenCookies: true
});
}
if (isValidVersion(capabilities, 10, 12)) {
Object.assign(updates, {
brokenLinkTextLocator: true,
// At least Safari 11 will hang on the brokenOptionSelect test
brokenOptionSelect: true,
brokenWhitespaceNormalization: true,
brokenMouseEvents: true,
brokenWindowClose: true,
usesWebDriverActiveElement: true
});
}
if (isValidVersion(capabilities, 12, 13)) {
Object.assign(updates, {
// At least Safari 12 uses W3C webdriver standard, including
// /attribute/:attr
usesWebDriverElementAttribute: true,
// At least Safari 12 will sometimes close a tab or window other
// than the current top-level browsing context when using DELETE
// /window
brokenDeleteWindow: true
});
}
if (isValidVersion(capabilities, 13, Infinity)) {
Object.assign(updates, {
// Safari 13 clicks the wrong location when clicking an element
// See https://github.com/SeleniumHQ/selenium/issues/7649
brokenClick: true,
// Sessions for Safari 13 on BrowserStack can become unresponsive
// when the `buttonup` call is used
brokenMouseEvents: true,
// Simulated events in Safari 13 do not change select values
brokenOptionSelect: true,
// Trying to close a window in Safari 13 will cause Safari to exit
brokenWindowClose: true
});
}
}
// 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 (isIos(capabilities)) {
updates.brokenWindowSwitch = true;
}
return updates;
}
if (isFirefox(capabilities)) {
if (isValidVersion(capabilities, 49, Infinity)) {
// The W3C WebDriver standard does not support the session-level
// /keys command, but JsonWireProtocol does.
updates.noKeysCommand = true;
// Firefox 49+ (via geckodriver) only supports W3C locator
// strategies
updates.usesWebDriverLocators = true;
// Non-W3C Firefox 49+ (via geckodriver) requires keys sent to an element
// to be a flat array
updates.usesFlatKeysArray = true;
// At least Firefox 49 + geckodriver can't POST empty data
updates.brokenEmptyPost = true;
// At least geckodriver 0.11 and Firefox 49 don't implement mouse
// control, so everything will need to be simulated.
updates.brokenMouseEvents = true;
// Firefox 49+ (via geckodriver) doesn't support retrieving logs or
// log types, and may hang the session.
updates.fixedLogTypes = [];
}
if (isValidVersion(capabilities, 49, 53)) {
// At least geckodriver 0.15.0 and Firefox 51 will stop responding
// to commands when performing window switches.
updates.brokenWindowSwitch = true;
}
// Using mouse services such as doubleclick will hang Firefox 49+
// session on the Mac.
if (capabilities.mouseEnabled == null &&
isValidVersion(capabilities, 49, Infinity) &&
isMac(capabilities)) {
updates.mouseEnabled = true;
}
}
if (isMsEdge(capabilities)) {
// At least MS Edge 14316 returns immediately from a click request
// immediately rather than waiting for default action to occur.
updates.returnsFromClickImmediately = true;
// At least MS Edge before 44.17763 may return an 'element is obscured'
// error when trying to click on visible elements.
if (isValidVersion(capabilities, 0, 44.17763)) {
updates.brokenClick = true;
}
// File uploads don't work on Edge as of May 2017
updates.remoteFiles = false;
// 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 (isValidVersion(capabilities, 25.10586)) {
updates.brokenWindowClose = true;
}
// 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 (isValidVersion(capabilities, 38.14366)) {
updates.brokenFileSendKeys = true;
}
// At least MS Edge 14316 supports alerts but does not specify the
// capability
if (isValidVersion(capabilities, 37.14316) &&
!('handlesAlerts' in capabilities)) {
updates.handlesAlerts = true;
}
// MS Edge 17763+ do not support the JWP execute commands. However, they
// return a JavaScriptError rather than something indicating that the
// command isn't supported.
if (isValidVersion(capabilities, 44.17763)) {
if (capabilities.usesWebDriverExecuteSync == null) {
updates.usesWebDriverExecuteSync = true;
}
if (capabilities.usesWebDriverExecuteAsync == null) {
updates.usesWebDriverExecuteAsync = true;
}
}
// MS Edge < 18 (44.17763) doesn't properly support cookie deletion
if (isValidVersion(capabilities, 0, 43)) {
updates.brokenDeleteCookie = true;
}
}
if (isInternetExplorer(capabilities)) {
// Internet Explorer does not allow data URIs to be used for navigation
updates.supportsNavigationDataUris = false;
if (isValidVersion(capabilities, 10, Infinity)) {
// At least IE10+ don't support the /frame/parent command
updates.brokenParentFrameSwitch = true;
}
if (isValidVersion(capabilities, 11)) {
// IE11 will take screenshots, but it's very slow
updates.takesScreenshot = true;
// IE11 will hang during this check if nativeEvents are enabled
updates.brokenSubmitElement = true;
// IE11 will hang during this check, but it does support window
// switching
updates.brokenWindowSwitch = false;
// IE11 doesn't support the /frame/parent command
updates.brokenParentFrameSwitch = true;
}
if (isValidVersion(capabilities, 11, Infinity)) {
// At least IE11 will hang during this check, although option
// selection does work with it
updates.brokenOptionSelect = false;
// At least IE11 will fail this feature test because it only supports
// the POST endpoint for timeouts
updates.supportsGetTimeouts = false;
// At least IE11 will fail this feature test because it only supports
// the POST endpoint for timeouts
updates.brokenZeroTimeout = true;
}
// 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 (isValidVersion(capabilities, 9)) {
updates.supportsCssTransforms = 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
updates.scriptedParentFrameCrashesBrowser = isValidVersion(capabilities, 0, 9);
}
// Don't check for touch support if the environment reports that no
// touchscreen is available.
if (capabilities.hasTouchScreen === false) {
updates.touchEnabled = false;
}
updates.shortcutKey = (function () {
if (isIos(capabilities)) {
return null;
}
if (isMac(capabilities)) {
return keys_1.default.COMMAND;
}
return keys_1.default.CONTROL;
})();
// At least selendroid 0.12.0-SNAPSHOT doesn't support switching to the
// parent frame
if (isAndroid(capabilities) && isAndroidEmulator(capabilities)) {
updates.brokenParentFrameSwitch = true;
}
return updates;
};
/**
* Run tests to detect capabilities/defects
*/
Server.prototype._detectCapabilities = function (session) {
var _this = this;
var capabilities = session.capabilities;
var supported = function () { return true; };
var unsupported = function () { return false; };
var maybeSupported = function (error) {
if (error.name === 'UnknownCommand') {
return false;
}
if (/\bunimplemented command\b/.test(error.message)) {
return false;
}
if (/The command .* not found/.test(error.message)) {
return false;
}
// At least Firefox 73 returns an error with the message "HTTP method not
// allowed" when POSTing to touch endpoints
if (/HTTP method not allowed/.test(error.message)) {
return false;
}
return true;
};
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.
*/
var addCapabilities = function (testedCapabilities) {
return Object.keys(testedCapabilities).reduce(function (previous, key) {
return previous.then(function () {
var value = testedCapabilities[key];
var task = typeof value === 'function' ? value() : common_1.Task.resolve(value);
return task.then(function (value) {
capabilities[key] = value;
});
});
}, common_1.Task.resolve());
};
var get = function (page) {
if (capabilities.supportsNavigationDataUris !== false) {
return session.get('data:text/html;charset=utf-8,' + encodeURIComponent(page));
}
// Internet Explorer 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 (isInternetExplorer(capabilities) || isMsEdge(capabilities)) {
// Edge driver doesn't provide an initialBrowserUrl
var initialUrl = 'about:blank';
// As of version 3.3.0.1, IEDriverServer provides IE-specific
// options, including the initialBrowserUrl, under an
// 'se:ieOptions' property rather than directly on
// capabilities.
// https://github.com/SeleniumHQ/selenium/blob/e60b607a97b9b7588d59e0c26ef9a6d1d1350911/cpp/iedriverserver/CHANGELOG
if (isInternetExplorer(capabilities) && capabilities['se:ieOptions']) {
initialUrl = capabilities['se:ieOptions'].initialBrowserUrl;
}
else if (capabilities.initialBrowserUrl) {
initialUrl = capabilities.initialBrowserUrl;
}
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]);
});
};
var discoverServerFeatures = function () {
var testedCapabilities = {};
// 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.
if (capabilities.remoteFiles == null) {
testedCapabilities.remoteFiles = function () {
// TODO: _post probably shouldn't be private
return session
.serverPost('file', {
file: 'UEsDBAoAAAAAAD0etkYAAAAAAAAAAAAA' +
'AAAIABwAdGVzdC50eHRVVAkAA2WnXlVl' +
'p15VdXgLAAEE8gMAAATyAwAAUEsBAh4D' +
'CgAAAAAAPR62RgAAAAAAAAAAAAAAAAgA' +
'GAAAAAAAAAAAAKSBAAAAAHRlc3QudHh0' +
'VVQFAANlp15VdXgLAAEE8gMAAATyAwAA' +
'UEsFBgAAAAABAAEATgAAAEIAAAAAAA=='
})
.then(function (filename) { return filename && filename.indexOf('test.txt') > -1; })
.catch(unsupported);
};
}
if (capabilities.supportsSessionCommand == null) {
testedCapabilities.supportsSessionCommands = function () {
return _this.get('session/$0', undefined, [session.sessionId]).then(supported, unsupported);
};
}
if (capabilities.supportsGetTimeouts == null) {
testedCapabilities.supportsGetTimeouts = function () {
return session.serverGet('timeouts').then(supported, unsupported);
};
}
if (capabilities.usesWebDriverTimeouts == null) {
testedCapabilities.usesWebDriverTimeouts = function () {
return (session
// Try to set a timeout using W3C semantics
.serverPost('timeouts', { implicit: 1234 })
.then(function () {
// At least IE 11 on BrowserStack doesn't support GET for
// timeouts. If we got here, though, the driver supports setting
// W3C timeouts.
if (isInternetExplorer(capabilities)) {
return supported();
}
// Verify that the timeout was set; at least Firefox 77 with
// geckodriver 0.26 on BrowserStack will allow some properties
// to be set using W3C-style data, but will fail to set the
// `implicit` timeout this way. Note that IE11 doesn't support
// GET for timeouts, so will always fail this test.
return session
.serverGet('timeouts')
.then(function (timeouts) { return timeouts.implicit === 1234; })
.catch(unsupported);
}, unsupported));
};
}
if (capabilities.usesWebDriverWindowCommands == null) {
testedCapabilities.usesWebDriverWindowCommands = function () {
return session.serverGet('window/rect').then(supported, unsupported);
};
}
// The W3C standard says window commands should take a 'handle'
// parameter, while the JsonWireProtocol used a 'name' parameter.
if (capabilities.usesHandleParameter == null) {
testedCapabilities.usesHandleParameter = function () {
return session
.switchToWindow('current')
.then(unsupported, function (error) {
return error.name === 'InvalidArgument' ||
/missing .*handle/i.test(error.message);
});
};
}
// Sauce Labs will not return a list of sessions at least as of May
// 2017
if (capabilities.brokenSessionList == null) {
testedCapabilities.brokenSessionList = function () {
return _this.getSessions().then(works, broken);
};
}
if (capabilities.returnsFromClickImmediately == null) {
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 (capabilities.noKeysCommand == null) {
testedCapabilities.noKeysCommand = function () {
return session
.serverPost('keys', { value: ['a'] })
.then(function () { return false; }, function () { return true; });
};
}
// The W3C WebDriver standard does not support the /displayed endpoint
if (capabilities.noElementDisplayed == null) {
testedCapabilities.noElementDisplayed = function () {
return session
.findByCssSelector('html')
.then(function (element) { return element.isDisplayed(); })
.then(function () { return false; }, function () { return true; });
};
}
return common_1.Task.all(Object.keys(testedCapabilities).map(function (key) { return testedCapabilities[key]; })).then(function () { return testedCapabilities; });
};
var discoverFeatures = function () {
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 (isSafari(capabilities, 0, 10) && isMac(capabilities)) {
return common_1.Task.resolve({});
}
// Appium iOS as of April 2014 supports rotation but does not
// specify the capability
if (capabilities.rotatable == null) {
testedCapabilities.rotatable = function () {
return session.getOrientation().then(supported, unsupported);
};
}
if (capabilities.locationContextEnabled) {
testedCapabilities.locationContextEnabled = function () {
return session.getGeolocation().then(supported, function (error) {
// 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 (error.message.indexOf('not mapped : GET_LOCATION') !== -1) {
return false;
}
// At least chromedriver 2.25 requires the location to
// be set first. At least chromedriver 2.25 will throw
// a CastClassException while trying to retrieve a
// geolocation.
if (error.message.indexOf('Location must be set') !== -1) {
return session
.setGeolocation({
latitude: 12.1,
longitude: -22.33,
altitude: 1000.2
})
.then(function () { return session.getGeolocation(); })
.then(supported, unsupported);
}
return false;
});
};
}
// 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 = function () {
return 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 = function () {
return session.getApplicationCacheStatus().then(supported, maybeSupported);
};
}
if (capabilities.takesScreenshot == null) {
// At least Selendroid 0.9.0 will fail to take screenshots in
// certain device configurations, usually emulators with
// hardware acceleration enabled
testedCapabilities.takesScreenshot = function () {
return session.takeScreenshot().then(supported, unsupported);
};
}
// At least ios-driver 0.6.6-SNAPSHOT April 2014 does not support
// execute_async
if (capabilities.supportsExecuteAsync == null) {
testedCapabilities.supportsExecuteAsync = function () {
return session.executeAsync('arguments[0](true);').catch(unsupported);
};
}
// Using mouse services su