UNPKG

@theintern/leadfoot

Version:

Leadfoot. A JavaScript client library that brings cross-platform consistency to the Selenium WebDriver API.

983 lines 91.6 kB
"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