Server.js

  1. /* global document:false */
  2. /**
  3. * @module leadfoot/Server
  4. */
  5. var keys = require('./keys');
  6. var lang = require('dojo/lang');
  7. var Promise = require('dojo/Promise');
  8. var request = require('dojo/request');
  9. var Session = require('./Session');
  10. var statusCodes = require('./lib/statusCodes');
  11. var urlUtil = require('url');
  12. var util = require('./lib/util');
  13. function isMsEdge(capabilities, minVersion, maxVersion) {
  14. if (capabilities.browserName !== 'MicrosoftEdge') {
  15. return false;
  16. }
  17. if (minVersion != null || maxVersion != null) {
  18. var version = parseFloat(capabilities.browserVersion);
  19. if (minVersion != null && version < minVersion) {
  20. return false;
  21. }
  22. if (maxVersion != null && version > maxVersion) {
  23. return false;
  24. }
  25. }
  26. return true;
  27. }
  28. function isMacSafari(capabilities) {
  29. return capabilities.browserName === 'safari' &&
  30. capabilities.platform === 'MAC' &&
  31. capabilities.platformName !== 'ios';
  32. }
  33. function isGeckodriver(capabilities) {
  34. return capabilities.browserName === 'firefox' &&
  35. parseFloat(capabilities.version) >= 49;
  36. }
  37. function isMacGeckodriver(capabilities) {
  38. return isGeckodriver(capabilities) && capabilities.platform === 'MAC';
  39. }
  40. /**
  41. * Creates a function that performs an HTTP request to a JsonWireProtocol endpoint.
  42. *
  43. * @param {string} method The HTTP method to fix.
  44. * @returns {Function}
  45. */
  46. function createHttpRequest(method) {
  47. /**
  48. * A function that performs an HTTP request to a JsonWireProtocol endpoint and normalises response status and
  49. * data.
  50. *
  51. * @param {string} path
  52. * The path-part of the JsonWireProtocol URL. May contain placeholders in the form `/\$\d/` that will be
  53. * replaced by entries in the `pathParts` argument.
  54. *
  55. * @param {Object} requestData
  56. * The payload for the request.
  57. *
  58. * @param {Array.<string>=} pathParts Optional placeholder values to inject into the path of the URL.
  59. *
  60. * @returns {Promise.<Object>}
  61. */
  62. return function sendRequest(path, requestData, pathParts) {
  63. var url = this.url + path.replace(/\$(\d)/, function (_, index) {
  64. return encodeURIComponent(pathParts[index]);
  65. });
  66. var defaultRequestHeaders = {
  67. // At least FirefoxDriver on Selenium 2.40.0 will throw a NullPointerException when retrieving
  68. // session capabilities if an Accept header is not provided. (It is a good idea to provide one
  69. // anyway)
  70. 'Accept': 'application/json,text/plain;q=0.9'
  71. };
  72. var kwArgs = lang.delegate(this.requestOptions, {
  73. followRedirects: false,
  74. handleAs: 'text',
  75. headers: lang.mixin({}, defaultRequestHeaders),
  76. method: method
  77. });
  78. if (requestData) {
  79. kwArgs.data = JSON.stringify(requestData);
  80. kwArgs.headers['Content-Type'] = 'application/json;charset=UTF-8';
  81. // At least ChromeDriver 2.9.248307 will not process request data if the length of the data is not
  82. // provided. (It is a good idea to provide one anyway)
  83. kwArgs.headers['Content-Length'] = Buffer.byteLength(kwArgs.data, 'utf8');
  84. }
  85. else {
  86. // At least Selenium 2.41.0 - 2.42.2 running as a grid hub will throw an exception and drop the current
  87. // session if a Content-Length header is not provided with a DELETE or POST request, regardless of whether
  88. // the request actually contains any request data.
  89. kwArgs.headers['Content-Length'] = 0;
  90. }
  91. var trace = {};
  92. Error.captureStackTrace(trace, sendRequest);
  93. return request(url, kwArgs).then(function handleResponse(response) {
  94. /*jshint maxcomplexity:24 */
  95. // The JsonWireProtocol specification prior to June 2013 stated that creating a new session should
  96. // perform a 3xx redirect to the session capabilities URL, instead of simply returning the returning
  97. // data about the session; as a result, we need to follow all redirects to get consistent data
  98. if (response.statusCode >= 300 && response.statusCode < 400 && response.getHeader('Location')) {
  99. var redirectUrl = response.getHeader('Location');
  100. // If redirectUrl isn't an absolute URL, resolve it based on the orignal URL used to create the session
  101. if (!/^\w+:/.test(redirectUrl)) {
  102. redirectUrl = urlUtil.resolve(url, redirectUrl);
  103. }
  104. return request(redirectUrl, {
  105. method: 'GET',
  106. headers: defaultRequestHeaders
  107. }).then(handleResponse);
  108. }
  109. var responseType = response.getHeader('Content-Type');
  110. var data;
  111. if (responseType && responseType.indexOf('application/json') === 0 && response.data) {
  112. data = JSON.parse(response.data);
  113. }
  114. // Some drivers will respond to a DELETE request with 204; in this case, we know the operation
  115. // completed successfully, so just create an expected response data structure for a successful
  116. // operation to avoid any special conditions elsewhere in the code caused by different HTTP return
  117. // values
  118. if (response.statusCode === 204) {
  119. data = {
  120. status: 0,
  121. sessionId: null,
  122. value: null
  123. };
  124. }
  125. else if (response.statusCode >= 400 || (data && data.status > 0)) {
  126. var error = new Error();
  127. // "The client should interpret a 404 Not Found response from the server as an "Unknown command"
  128. // response. All other 4xx and 5xx responses from the server that do not define a status field
  129. // should be interpreted as "Unknown error" responses."
  130. // - http://code.google.com/p/selenium/wiki/JsonWireProtocol#Response_Status_Codes
  131. if (!data) {
  132. data = {
  133. status: response.statusCode === 404 || response.statusCode === 501 ? 9 : 13,
  134. value: {
  135. message: response.text
  136. }
  137. };
  138. }
  139. // ios-driver 0.6.6-SNAPSHOT April 2014 incorrectly implements the specification: does not return
  140. // error data on the `value` key, and does not return the correct HTTP status for unknown commands
  141. else if (!data.value && ('message' in data)) {
  142. data = {
  143. status: response.statusCode === 404 || response.statusCode === 501 ||
  144. data.message.indexOf('cannot find command') > -1 ? 9 : 13,
  145. value: data
  146. };
  147. }
  148. // At least Appium April 2014 responds with the HTTP status Not Implemented but a Selenium
  149. // status UnknownError for commands that are not implemented; these errors are more properly
  150. // represented to end-users using the Selenium status UnknownCommand, so we make the appropriate
  151. // coercion here
  152. if (response.statusCode === 501 && data.status === 13) {
  153. data.status = 9;
  154. }
  155. // At least BrowserStack in May 2016 responds with HTTP 500 and a message value of "Invalid Command" for
  156. // at least some unknown commands. These errors are more properly represented to end-users using the
  157. // Selenium status UnknownCommand, so we make the appropriate coercion here
  158. if (response.statusCode === 500 && data.value && data.value.message === 'Invalid Command') {
  159. data.status = 9;
  160. }
  161. // At least FirefoxDriver 2.40.0 responds with HTTP status codes other than Not Implemented and a
  162. // Selenium status UnknownError for commands that are not implemented; however, it provides a
  163. // reliable indicator that the operation was unsupported by the type of the exception that was
  164. // thrown, so also coerce this back into an UnknownCommand response for end-user code
  165. if (data.status === 13 && data.value && data.value.class &&
  166. (data.value.class.indexOf('UnsupportedOperationException') > -1 ||
  167. data.value.class.indexOf('UnsupportedCommandException') > -1)
  168. ) {
  169. data.status = 9;
  170. }
  171. // At least InternetExplorerDriver 2.41.0 & SafariDriver 2.41.0 respond with HTTP status codes
  172. // other than Not Implemented and a Selenium status UnknownError for commands that are not
  173. // implemented; like FirefoxDriver they provide a reliable indicator of unsupported commands
  174. if (response.statusCode === 500 && data.value && data.value.message &&
  175. (
  176. data.value.message.indexOf('Command not found') > -1 ||
  177. data.value.message.indexOf('Unknown command') > -1
  178. )
  179. ) {
  180. data.status = 9;
  181. }
  182. // At least GhostDriver 1.1.0 incorrectly responds with HTTP 405 instead of HTTP 501 for
  183. // unimplemented commands
  184. if (response.statusCode === 405 && data.value && data.value.message &&
  185. data.value.message.indexOf('Invalid Command Method') > -1
  186. ) {
  187. data.status = 9;
  188. }
  189. var statusText = statusCodes[data.status];
  190. if (statusText) {
  191. error.name = statusText[0];
  192. error.message = statusText[1];
  193. }
  194. if (data.value && data.value.message) {
  195. error.message = data.value.message;
  196. }
  197. if (data.value && data.value.screen) {
  198. data.value.screen = new Buffer(data.value.screen, 'base64');
  199. }
  200. error.status = data.status;
  201. error.detail = data.value;
  202. error.request = {
  203. url: url,
  204. method: method,
  205. data: requestData
  206. };
  207. error.response = response;
  208. var sanitizedUrl = (function () {
  209. var parsedUrl = urlUtil.parse(url);
  210. if (parsedUrl.auth) {
  211. parsedUrl.auth = '(redacted)';
  212. }
  213. return urlUtil.format(parsedUrl);
  214. })();
  215. error.message = '[' + method + ' ' + sanitizedUrl +
  216. (requestData ? ' / ' + JSON.stringify(requestData) : '') +
  217. '] ' + error.message;
  218. error.stack = error.message + util.trimStack(trace.stack);
  219. throw error;
  220. }
  221. return data;
  222. }).catch(function (error) {
  223. error.stack = error.message + util.trimStack(trace.stack);
  224. throw error;
  225. });
  226. };
  227. }
  228. /**
  229. * Returns the actual response value from the remote environment.
  230. *
  231. * @param {Object} response JsonWireProtocol response object.
  232. * @returns {any} The actual response value.
  233. */
  234. function returnValue(response) {
  235. return response.value;
  236. }
  237. /**
  238. * The Server class represents a remote HTTP server implementing the WebDriver wire protocol that can be used to
  239. * generate new remote control sessions.
  240. *
  241. * @constructor module:leadfoot/Server
  242. * @param {(Object|string)} url
  243. * The fully qualified URL to the JsonWireProtocol endpoint on the server. The default endpoint for a
  244. * JsonWireProtocol HTTP server is http://localhost:4444/wd/hub. You may also pass a parsed URL object which will
  245. * be converted to a string.
  246. * @param {{ proxy: string }=} options
  247. * Additional request options to be used for requests to the server.
  248. */
  249. function Server(url, options) {
  250. if (typeof url === 'object') {
  251. url = Object.create(url);
  252. if (url.username || url.password || url.accessKey) {
  253. url.auth = encodeURIComponent(url.username) + ':' + encodeURIComponent(url.password || url.accessKey);
  254. }
  255. }
  256. this.url = urlUtil.format(url).replace(/\/*$/, '/');
  257. this.requestOptions = options || {};
  258. }
  259. /**
  260. * @lends module:leadfoot/Server#
  261. */
  262. Server.prototype = {
  263. constructor: Server,
  264. /**
  265. * An alternative session constructor. Defaults to the standard {@link module:leadfoot/Session} constructor if
  266. * one is not provided.
  267. *
  268. * @type {module:leadfoot/Session}
  269. * @default Session
  270. */
  271. sessionConstructor: Session,
  272. /**
  273. * Whether or not to perform capabilities testing and correction when creating a new Server.
  274. * @type {boolean}
  275. * @default
  276. */
  277. fixSessionCapabilities: true,
  278. _get: createHttpRequest('GET'),
  279. _post: createHttpRequest('POST'),
  280. _delete: createHttpRequest('DELETE'),
  281. /**
  282. * Gets the status of the remote server.
  283. *
  284. * @returns {Promise.<Object>} An object containing arbitrary properties describing the status of the remote
  285. * server.
  286. */
  287. getStatus: function () {
  288. return this._get('status').then(returnValue);
  289. },
  290. /**
  291. * Creates a new remote control session on the remote server.
  292. *
  293. * @param {Capabilities} desiredCapabilities
  294. * A hash map of desired capabilities of the remote environment. The server may return an environment that does
  295. * not match all the desired capabilities if one is not available.
  296. *
  297. * @param {Capabilities=} requiredCapabilities
  298. * A hash map of required capabilities of the remote environment. The server will not return an environment that
  299. * does not match all the required capabilities if one is not available.
  300. *
  301. * @returns {Promise.<module:leadfoot/Session>}
  302. */
  303. createSession: function (desiredCapabilities, requiredCapabilities) {
  304. var self = this;
  305. var fixSessionCapabilities = desiredCapabilities.fixSessionCapabilities !== false &&
  306. self.fixSessionCapabilities;
  307. // Don’t send `fixSessionCapabilities` to the server
  308. if ('fixSessionCapabilities' in desiredCapabilities) {
  309. desiredCapabilities = lang.mixin({}, desiredCapabilities);
  310. desiredCapabilities.fixSessionCapabilities = undefined;
  311. }
  312. return this._post('session', {
  313. desiredCapabilities: desiredCapabilities,
  314. requiredCapabilities: requiredCapabilities
  315. }).then(function (response) {
  316. var session = new self.sessionConstructor(response.sessionId, self, response.value);
  317. if (fixSessionCapabilities) {
  318. return self._fillCapabilities(session).catch(function (error) {
  319. // The session was started on the server, but we did not resolve the Promise yet. If a failure
  320. // occurs during capabilities filling, we should quit the session on the server too since the
  321. // caller will not be aware that it ever got that far and will have no access to the session to
  322. // quit itself.
  323. return session.quit().finally(function () {
  324. throw error;
  325. });
  326. });
  327. }
  328. else {
  329. return session;
  330. }
  331. });
  332. },
  333. _fillCapabilities: function (session) {
  334. /*jshint maxlen:140 */
  335. var capabilities = session.capabilities;
  336. function supported() { return true; }
  337. function unsupported() { return false; }
  338. function maybeSupported(error) { return error.name !== 'UnknownCommand'; }
  339. var broken = supported;
  340. var works = unsupported;
  341. /**
  342. * Adds the capabilities listed in the `testedCapabilities` object to the hash of capabilities for
  343. * the current session. If a tested capability value is a function, it is assumed that it still needs to
  344. * be executed serially in order to resolve the correct value of that particular capability.
  345. */
  346. function addCapabilities(testedCapabilities) {
  347. return new Promise(function (resolve, reject) {
  348. var keys = Object.keys(testedCapabilities);
  349. var i = 0;
  350. (function next() {
  351. var key = keys[i++];
  352. if (!key) {
  353. resolve();
  354. return;
  355. }
  356. var value = testedCapabilities[key];
  357. if (typeof value === 'function') {
  358. value().then(function (value) {
  359. capabilities[key] = value;
  360. next();
  361. }, reject);
  362. }
  363. else {
  364. capabilities[key] = value;
  365. next();
  366. }
  367. })();
  368. });
  369. }
  370. function get(page) {
  371. if (capabilities.supportsNavigationDataUris !== false) {
  372. return session.get('data:text/html;charset=utf-8,' + encodeURIComponent(page));
  373. }
  374. // Internet Explorer 9 and earlier, and Microsoft Edge build 10240 and earlier, hang when attempting to do
  375. // navigate after a `document.write` is performed to reset the tab content; we can still do some limited
  376. // testing in these browsers by using the initial browser URL page and injecting some content through
  377. // innerHTML, though it is unfortunately a quirks-mode file so testing is limited
  378. if (
  379. (capabilities.browserName === 'internet explorer' && parseFloat(capabilities.version) < 10) ||
  380. isMsEdge(capabilities)
  381. ) {
  382. // Edge driver doesn't provide an initialBrowserUrl
  383. var initialUrl = capabilities.browserName === 'internet explorer' ? capabilities.initialBrowserUrl :
  384. 'about:blank';
  385. return session.get(initialUrl).then(function () {
  386. return session.execute('document.body.innerHTML = arguments[0];', [
  387. // The DOCTYPE does not apply, for obvious reasons, but also old IE will discard invisible
  388. // elements like `<script>` and `<style>` if they are the first elements injected with
  389. // `innerHTML`, so an extra text node is added before the rest of the content instead
  390. page.replace('<!DOCTYPE html>', 'x')
  391. ]);
  392. });
  393. }
  394. return session.get('about:blank').then(function () {
  395. return session.execute('document.write(arguments[0]);', [ page ]);
  396. });
  397. }
  398. function discoverFeatures() {
  399. // jshint maxcomplexity:15
  400. var testedCapabilities = {};
  401. // At least SafariDriver 2.41.0 fails to allow stand-alone feature testing because it does not inject user
  402. // scripts for URLs that are not http/https
  403. if (isMacSafari(capabilities)) {
  404. return {
  405. nativeEvents: false,
  406. rotatable: false,
  407. locationContextEnabled: false,
  408. webStorageEnabled: false,
  409. applicationCacheEnabled: false,
  410. supportsNavigationDataUris: true,
  411. supportsCssTransforms: true,
  412. supportsExecuteAsync: true,
  413. mouseEnabled: true,
  414. touchEnabled: false,
  415. dynamicViewport: true,
  416. shortcutKey: keys.COMMAND
  417. };
  418. }
  419. // Firefox 49+ (via geckodriver) only supports W3C locator strategies
  420. if (isGeckodriver(capabilities)) {
  421. testedCapabilities.isWebDriver = true;
  422. }
  423. // At least MS Edge 14316 supports alerts but does not specify the capability
  424. if (isMsEdge(capabilities, 37.14316) && !('handlesAlerts' in capabilities)) {
  425. testedCapabilities.handlesAlerts = true;
  426. }
  427. // Appium iOS as of April 2014 supports rotation but does not specify the capability
  428. if (!('rotatable' in capabilities)) {
  429. testedCapabilities.rotatable = session.getOrientation().then(supported, unsupported);
  430. }
  431. // At least FirefoxDriver 2.40.0 and ios-driver 0.6.0 claim they support geolocation in their returned
  432. // capabilities map, when they do not
  433. if (capabilities.locationContextEnabled) {
  434. testedCapabilities.locationContextEnabled = session.getGeolocation()
  435. .then(supported, function (error) {
  436. return error.name !== 'UnknownCommand' &&
  437. error.message.indexOf('not mapped : GET_LOCATION') === -1;
  438. });
  439. }
  440. // At least FirefoxDriver 2.40.0 claims it supports web storage in the returned capabilities map, when
  441. // it does not
  442. if (capabilities.webStorageEnabled) {
  443. testedCapabilities.webStorageEnabled = session.getLocalStorageLength()
  444. .then(supported, maybeSupported);
  445. }
  446. // At least FirefoxDriver 2.40.0 claims it supports application cache in the returned capabilities map,
  447. // when it does not
  448. if (capabilities.applicationCacheEnabled) {
  449. testedCapabilities.applicationCacheEnabled = session.getApplicationCacheStatus()
  450. .then(supported, maybeSupported);
  451. }
  452. // IE11 will take screenshots, but it's very slow
  453. if (capabilities.browserName === 'internet explorer' && capabilities.version == '11') {
  454. testedCapabilities.takesScreenshot = true;
  455. }
  456. // At least Selendroid 0.9.0 will fail to take screenshots in certain device configurations, usually
  457. // emulators with hardware acceleration enabled
  458. else {
  459. testedCapabilities.takesScreenshot = session.takeScreenshot().then(supported, unsupported);
  460. }
  461. // At least ios-driver 0.6.6-SNAPSHOT April 2014 does not support execute_async
  462. testedCapabilities.supportsExecuteAsync = session.executeAsync('arguments[0](true);').catch(unsupported);
  463. // Some additional, currently-non-standard capabilities are needed in order to know about supported
  464. // features of a given platform
  465. if (!('mouseEnabled' in capabilities)) {
  466. // Using mouse services such as doubleclick will hang Firefox 49+ session on the Mac.
  467. if (isMacGeckodriver(capabilities)) {
  468. testedCapabilities.mouseEnabled = true;
  469. }
  470. else {
  471. testedCapabilities.mouseEnabled = function () {
  472. return session.doubleClick()
  473. .then(supported, maybeSupported);
  474. };
  475. }
  476. }
  477. // Don't check for touch support if the environment reports that no touchscreen is available
  478. if (capabilities.hasTouchScreen === false) {
  479. testedCapabilities.touchEnabled = false;
  480. }
  481. else if (!('touchEnabled' in capabilities)) {
  482. testedCapabilities.touchEnabled = session.doubleTap()
  483. .then(supported, maybeSupported);
  484. }
  485. // ChromeDriver 2.19 claims that it supports touch but it does not implement all of the touch endpoints
  486. // from JsonWireProtocol
  487. else if (capabilities.browserName === 'chrome') {
  488. testedCapabilities.touchEnabled = false;
  489. }
  490. if (!('dynamicViewport' in capabilities)) {
  491. testedCapabilities.dynamicViewport = session.getWindowSize().then(function (originalSize) {
  492. return session.setWindowSize(originalSize.width, originalSize.height);
  493. }).then(supported, unsupported);
  494. }
  495. // At least Internet Explorer 11 and earlier do not allow data URIs to be used for navigation
  496. testedCapabilities.supportsNavigationDataUris = function () {
  497. return get('<!DOCTYPE html><title>a</title>').then(function () {
  498. return session.getPageTitle();
  499. }).then(function (pageTitle) {
  500. return pageTitle === 'a';
  501. }).catch(unsupported);
  502. };
  503. testedCapabilities.supportsCssTransforms = function () {
  504. // It is not possible to test this since the feature tests runs in quirks-mode on IE<10, but we
  505. // know that IE9 supports CSS transforms
  506. if (capabilities.browserName === 'internet explorer' && parseFloat(capabilities.version) === 9) {
  507. return Promise.resolve(true);
  508. }
  509. /*jshint maxlen:240 */
  510. 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 () {
  511. return session.execute(/* istanbul ignore next */ function () {
  512. var bbox = document.getElementById('a').getBoundingClientRect();
  513. return bbox.right - bbox.left === 4;
  514. });
  515. }).catch(unsupported);
  516. };
  517. testedCapabilities.shortcutKey = (function () {
  518. var platform = capabilities.platform.toLowerCase();
  519. if (platform.indexOf('mac') === 0) {
  520. return keys.COMMAND;
  521. }
  522. if (platform.indexOf('ios') === 0) {
  523. return null;
  524. }
  525. return keys.CONTROL;
  526. })();
  527. return Promise.all(testedCapabilities);
  528. }
  529. function discoverDefects() {
  530. var testedCapabilities = {};
  531. // At least SafariDriver 2.41.0 fails to allow stand-alone feature testing because it does not inject user
  532. // scripts for URLs that are not http/https
  533. if (isMacSafari(capabilities)) {
  534. return {
  535. brokenDeleteCookie: false,
  536. brokenExecuteElementReturn: false,
  537. brokenExecuteUndefinedReturn: false,
  538. brokenElementDisplayedOpacity: false,
  539. brokenElementDisplayedOffscreen: false,
  540. brokenSubmitElement: true,
  541. brokenWindowSwitch: true,
  542. brokenDoubleClick: false,
  543. brokenCssTransformedSize: true,
  544. fixedLogTypes: false,
  545. brokenHtmlTagName: false,
  546. brokenNullGetSpecAttribute: false,
  547. // SafariDriver-specific
  548. brokenActiveElement: true,
  549. brokenNavigation: true,
  550. brokenMouseEvents: true,
  551. brokenWindowPosition: true,
  552. brokenSendKeys: true,
  553. brokenExecuteForNonHttpUrl: true,
  554. // SafariDriver 2.41.0 cannot delete cookies, at all, ever
  555. brokenCookies: true
  556. };
  557. }
  558. // Internet Explorer 8 and earlier will simply crash the server if we attempt to return the parent
  559. // frame via script, so never even attempt to do so
  560. testedCapabilities.scriptedParentFrameCrashesBrowser =
  561. capabilities.browserName === 'internet explorer' && parseFloat(capabilities.version) < 9;
  562. // At least ChromeDriver 2.9 and MS Edge 10240 does not implement /element/active
  563. testedCapabilities.brokenActiveElement = session.getActiveElement().then(works, function (error) {
  564. return error.name === 'UnknownCommand';
  565. });
  566. // At least Selendroid 0.9.0 and MS Edge have broken cookie deletion.
  567. if (capabilities.browserName === 'selendroid') {
  568. // This test is very hard to get working properly in other environments so only test when Selendroid is
  569. // the browser
  570. testedCapabilities.brokenDeleteCookie = function () {
  571. return session.get('about:blank').then(function () {
  572. return session.clearCookies();
  573. }).then(function () {
  574. return session.setCookie({ name: 'foo', value: 'foo' });
  575. }).then(function () {
  576. return session.deleteCookie('foo');
  577. }).then(function () {
  578. return session.getCookies();
  579. }).then(function (cookies) {
  580. return cookies.length > 0;
  581. }).catch(function () {
  582. return true;
  583. }).then(function (isBroken) {
  584. return session.clearCookies().finally(function () {
  585. return isBroken();
  586. });
  587. });
  588. };
  589. }
  590. else if (isMsEdge(capabilities)) {
  591. testedCapabilities.brokenDeleteCookie = true;
  592. }
  593. // At least Firefox 49 + geckodriver can't POST empty data
  594. if (isGeckodriver(capabilities)) {
  595. testedCapabilities.brokenEmptyPost = true;
  596. }
  597. // At least MS Edge may return an 'element is obscured' error when trying to click on visible elements.
  598. if (isMsEdge(capabilities)) {
  599. testedCapabilities.brokenClick = true;
  600. }
  601. // At least Selendroid 0.9.0 incorrectly returns HTML tag names in uppercase, which is a violation
  602. // of the JsonWireProtocol spec
  603. testedCapabilities.brokenHtmlTagName = session.findByTagName('html').then(function (element) {
  604. return element.getTagName();
  605. }).then(function (tagName) {
  606. return tagName !== 'html';
  607. }).catch(broken);
  608. // At least ios-driver 0.6.6-SNAPSHOT incorrectly returns empty string instead of null for attributes
  609. // that do not exist
  610. testedCapabilities.brokenNullGetSpecAttribute = session.findByTagName('html').then(function (element) {
  611. return element.getSpecAttribute('nonexisting');
  612. }).then(function (value) {
  613. return value !== null;
  614. }).catch(broken);
  615. // At least MS Edge 10240 doesn't properly deserialize web elements passed as `execute` arguments
  616. testedCapabilities.brokenElementSerialization = function () {
  617. return get('<!DOCTYPE html><div id="a"></div>').then(function () {
  618. return session.findById('a')
  619. }).then(function (element) {
  620. return session.execute(function (element) {
  621. return element.getAttribute('id');
  622. }, [ element ]);
  623. }).then(function (attribute) {
  624. return attribute !== 'a';
  625. }).catch(broken);
  626. };
  627. // At least Selendroid 0.16.0 incorrectly returns `undefined` instead of `null` when an undefined
  628. // value is returned by an `execute` call
  629. testedCapabilities.brokenExecuteUndefinedReturn = session.execute(
  630. 'return undefined;'
  631. ).then(function (value) {
  632. return value !== null;
  633. }, broken);
  634. // At least Selendroid 0.9.0 always returns invalid element handles from JavaScript
  635. testedCapabilities.brokenExecuteElementReturn = function () {
  636. return get('<!DOCTYPE html><div id="a"></div>').then(function () {
  637. return session.execute('return document.getElementById("a");');
  638. }).then(function (element) {
  639. return element && element.getTagName();
  640. }).then(works, broken);
  641. };
  642. // At least Selendroid 0.9.0 treats fully transparent elements as displayed, but all others do not
  643. testedCapabilities.brokenElementDisplayedOpacity = function () {
  644. return get('<!DOCTYPE html><div id="a" style="opacity: .1;">a</div>').then(function () {
  645. // IE<9 do not support CSS opacity so should not be involved in this test
  646. return session.execute('var o = document.getElementById("a").style.opacity; return o && o.charAt(0) === "0";');
  647. }).then(function (supportsOpacity) {
  648. if (!supportsOpacity) {
  649. return works();
  650. }
  651. else {
  652. return session.execute('document.getElementById("a").style.opacity = "0";')
  653. .then(function () {
  654. return session.findById('a');
  655. })
  656. .then(function (element) {
  657. return element.isDisplayed();
  658. });
  659. }
  660. }).catch(broken);
  661. };
  662. // At least ChromeDriver 2.9 treats elements that are offscreen as displayed, but others do not
  663. testedCapabilities.brokenElementDisplayedOffscreen = function () {
  664. var pageText = '<!DOCTYPE html><div id="a" style="left: 0; position: absolute; top: -1000px;">a</div>';
  665. return get(pageText).then(function () {
  666. return session.findById('a');
  667. }).then(function (element) {
  668. return element.isDisplayed();
  669. }).catch(broken);
  670. };
  671. // At least MS Edge Driver 14316 doesn't support sending keys to a file input. See
  672. // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7194303/
  673. //
  674. // The existing feature test for this caused some browsers to hang, so just flag it for Edge for now.
  675. if (isMsEdge(capabilities, null, 38.14366)) {
  676. testedCapabilities.brokenFileSendKeys = true;
  677. }
  678. // testedCapabilities.brokenFileSendKeys = function () {
  679. // return get('<!DOCTYPE html><input type="file" id="i1">').then(function () {
  680. // var element;
  681. // return session.findById('i1')
  682. // .then(function (element) {
  683. // return element.type('./Server.js');
  684. // }).then(function () {
  685. // return session.execute(function () {
  686. // return document.getElementById('i1').value;
  687. // });
  688. // }).then(function (text) {
  689. // if (!/Server.js$/.test(text)) {
  690. // throw new Error('mismatch');
  691. // }
  692. // });
  693. // }).then(works, broken);
  694. // };
  695. // At least MS Edge Driver 14316 doesn't normalize whitespace properly when retrieving text. Text may
  696. // contain "\r\n" pairs rather than "\n", and there may be extraneous whitespace adjacent to "\r\n" pairs
  697. // and at the start and end of the text.
  698. testedCapabilities.brokenWhitespaceNormalization = function () {
  699. return get('<!DOCTYPE html><div id="d">This is\n<br>a test\n</div>').then(function () {
  700. return session.findById('d')
  701. .then(function (element) {
  702. return element.getVisibleText();
  703. }).then(function (text) {
  704. if (/\r\n/.test(text) || /\s+$/.test(text)) {
  705. throw new Error('invalid whitespace');
  706. }
  707. });
  708. }).then(works, broken);
  709. };
  710. // At least MS Edge Driver 14316 doesn't return elements' computed styles
  711. testedCapabilities.brokenComputedStyles = function () {
  712. var pageText = '<!DOCTYPE html><style>a { background: purple }</style><a id="a1">foo</a>';
  713. return get(pageText).then(function () {
  714. return session.findById('a1');
  715. }).then(function (element) {
  716. return element.getComputedStyle('background-color');
  717. }).then(function (value) {
  718. if (!value) {
  719. throw new Error('empty style');
  720. }
  721. }).then(works, broken);
  722. };
  723. // IE11 will hang during this check, although option selection does work with it
  724. if (capabilities.browserName !== 'internet explorer' && capabilities.version !== '11') {
  725. // At least MS Edge Driver 14316 doesn't allow selection option elements to be clicked.
  726. testedCapabilities.brokenOptionSelect = function () {
  727. return get(
  728. '<!DOCTYPE html><select id="d"><option id="o1" value="foo">foo</option>' +
  729. '<option id="o2" value="bar" selected>bar</option></select>'
  730. ).then(function () {
  731. return session.findById('d');
  732. }).then(function (element) {
  733. return element.click();
  734. }).then(function () {
  735. return session.findById('o1');
  736. }).then(function (element) {
  737. return element.click();
  738. }).then(works, broken);
  739. };
  740. }
  741. // At least MS Edge driver 10240 doesn't support getting the page source
  742. testedCapabilities.brokenPageSource = session.getPageSource().then(works, broken);
  743. // IE11 will hang during this check if nativeEvents are enabled
  744. if (capabilities.browserName !== 'internet explorer' && capabilities.version !== '11') {
  745. testedCapabilities.brokenSubmitElement = true;
  746. }
  747. else {
  748. // There is inconsistency across all drivers as to whether or not submitting a form button should cause
  749. // the form button to be submitted along with the rest of the form; it seems most likely that tests
  750. // do want the specified button to act as though someone clicked it when it is submitted, so the
  751. // behaviour needs to be normalised
  752. testedCapabilities.brokenSubmitElement = function () {
  753. /*jshint maxlen:200 */
  754. return get(
  755. '<!DOCTYPE html><form method="get" action="about:blank">' +
  756. '<input id="a" type="submit" name="a" value="a"></form>'
  757. ).then(function () {
  758. return session.findById('a');
  759. }).then(function (element) {
  760. return element.submit();
  761. }).then(function () {
  762. return session.getCurrentUrl();
  763. }).then(function (url) {
  764. return url.indexOf('a=a') === -1;
  765. }).catch(broken);
  766. };
  767. }
  768. // At least MS Edge 10586 becomes unresponsive after calling DELETE window, and window.close() requires user
  769. // interaction. This capability is distinct from brokenDeleteWindow as this capability indicates that there
  770. // is no way to close a Window.
  771. if (isMsEdge(capabilities, null, 25.10586)) {
  772. testedCapabilities.brokenWindowClose = true;
  773. }
  774. // At least MS Edge driver 10240 doesn't support window sizing commands
  775. testedCapabilities.brokenWindowSize = session.getWindowSize().then(works, broken);
  776. // At least Selendroid 0.9.0 has a bug where it catastrophically fails to retrieve available types;
  777. // they have tried to hardcode the available log types in this version so we can just return the
  778. // same hardcoded list ourselves.
  779. // At least InternetExplorerDriver 2.41.0 also fails to provide log types.
  780. // Firefox 49+ (via geckodriver) doesn't support retrieving logs or log types, and may hang the session.
  781. if (isMacGeckodriver(capabilities)) {
  782. testedCapabilities.fixedLogTypes = [];
  783. }
  784. else {
  785. testedCapabilities.fixedLogTypes = session.getAvailableLogTypes().then(unsupported, function (error) {
  786. if (capabilities.browserName === 'selendroid' && !error.response.text.length) {
  787. return [ 'logcat' ];
  788. }
  789. return [];
  790. });
  791. }
  792. // At least Microsoft Edge 10240 doesn't support timeout values of 0.
  793. testedCapabilities.brokenZeroTimeout = session.setTimeout('implicit', 0).then(works, broken);
  794. // At least ios-driver 0.6.6-SNAPSHOT April 2014 corrupts its internal state when performing window
  795. // switches and gets permanently stuck; we cannot feature detect, so platform sniffing it is
  796. if (capabilities.browserName === 'Safari' && capabilities.platformName === 'IOS') {
  797. testedCapabilities.brokenWindowSwitch = true;
  798. }
  799. else {
  800. testedCapabilities.brokenWindowSwitch = session.getCurrentWindowHandle().then(function (handle) {
  801. return session.switchToWindow(handle);
  802. }).then(works, broken);
  803. }
  804. // At least selendroid 0.12.0-SNAPSHOT doesn't support switching to the parent frame
  805. if (capabilities.browserName === 'android' && capabilities.deviceName === 'Android Emulator') {
  806. testedCapabilities.brokenParentFrameSwitch = true;
  807. }
  808. else {
  809. testedCapabilities.brokenParentFrameSwitch = session.switchToParentFrame().then(works, broken);
  810. }
  811. var scrollTestUrl = '<!DOCTYPE html><div id="a" style="margin: 3000px;"></div>';
  812. // ios-driver 0.6.6-SNAPSHOT April 2014 calculates position based on a bogus origin and does not
  813. // account for scrolling
  814. testedCapabilities.brokenElementPosition = function () {
  815. return get(scrollTestUrl).then(function () {
  816. return session.findById('a');
  817. }).then(function (element) {
  818. return element.getPosition();
  819. }).then(function (position) {
  820. return position.x !== 3000 || position.y !== 3000;
  821. }).catch(broken);
  822. };
  823. // At least ios-driver 0.6.6-SNAPSHOT April 2014 will never complete a refresh call
  824. testedCapabilities.brokenRefresh = function () {
  825. return session.get('about:blank?1').then(function () {
  826. return new Promise(function (resolve, reject, progress, setCanceler) {
  827. function cleanup() {
  828. clearTimeout(timer);
  829. refresh.cancel();
  830. }
  831. setCanceler(cleanup);
  832. var refresh = session.refresh().then(function () {
  833. cleanup();
  834. resolve(false);
  835. }, function () {
  836. cleanup();
  837. resolve(true);
  838. });
  839. var timer = setTimeout(function () {
  840. cleanup();
  841. }, 2000);
  842. });
  843. }).catch(broken);
  844. };
  845. if (isGeckodriver(capabilities)) {
  846. // At least geckodriver 0.11 and Firefox 49 don't implement mouse control, so everything will need to be
  847. // simulated.
  848. testedCapabilities.brokenMouseEvents = true;
  849. }
  850. else if (capabilities.mouseEnabled) {
  851. // At least IE 10 and 11 on SauceLabs don't fire native mouse events consistently even though they
  852. // support moveMouseTo
  853. testedCapabilities.brokenMouseEvents = function () {
  854. return get(
  855. '<!DOCTYPE html><div id="foo">foo</div>' +
  856. '<script>window.counter = 0; var d = document; d.onmousemove = function () { window.counter++; };</script>'
  857. ).then(function () {
  858. return session.findById('foo');
  859. }).then(function (element) {
  860. return session.moveMouseTo(element, 20, 20);
  861. }).then(function () {
  862. return util.sleep(100);
  863. }).then(function () {
  864. return session.execute('return window.counter;');
  865. }).then(
  866. function (counter) {
  867. return counter > 0 ? works() : broken();
  868. },
  869. broken
  870. );
  871. };
  872. // At least ChromeDriver 2.12 through 2.19 will throw an error if mouse movement relative to the <html>
  873. // element is attempted
  874. testedCapabilities.brokenHtmlMouseMove = function () {
  875. return get('<!DOCTYPE html><html></html>').then(function () {
  876. return session.findByTagName('html').then(function (element) {
  877. return session.moveMouseTo(element, 0, 0);
  878. });
  879. }).then(works, broken);
  880. };
  881. // At least ChromeDriver 2.9.248307 does not correctly emit the entire sequence of events that would
  882. // normally occur during a double-click
  883. testedCapabilities.brokenDoubleClick = function retry() {
  884. /*jshint maxlen:200 */
  885. // InternetExplorerDriver is not buggy, but IE9 in quirks-mode is; since we cannot do feature
  886. // tests in standards-mode in IE<10, force the value to false since it is not broken in this
  887. // browser
  888. if (capabilities.browserName === 'internet explorer' && capabilities.version === '9') {
  889. return Promise.resolve(false);
  890. }
  891. return get('<!DOCTYPE html><script>window.counter = 0; var d = document; d.onclick = d.onmousedown = d.onmouseup = function () { window.counter++; };</script>').then(function () {
  892. return session.findByTagName('html');
  893. }).then(function (element) {
  894. return session.moveMouseTo(element);
  895. }).then(function () {
  896. return util.sleep(100);
  897. }).then(function () {
  898. return session.doubleClick();
  899. }).then(function () {
  900. return session.execute('return window.counter;');
  901. }).then(function (counter) {
  902. // InternetExplorerDriver 2.41.0 has a race condition that makes this test sometimes fail
  903. /* istanbul ignore if: inconsistent race condition */
  904. if (counter === 0) {
  905. return retry();
  906. }
  907. return counter !== 6;
  908. }).catch(broken);
  909. };
  910. }
  911. if (capabilities.touchEnabled) {
  912. // At least Selendroid 0.9.0 fails to perform a long tap due to an INJECT_EVENTS permission failure
  913. testedCapabilities.brokenLongTap = session.findByTagName('body').then(function (element) {
  914. return session.longTap(element);
  915. }).then(works, broken);
  916. // At least ios-driver 0.6.6-SNAPSHOT April 2014 claims to support touch press/move/release but
  917. // actually fails when you try to use the commands
  918. testedCapabilities.brokenMoveFinger = session.pressFinger(0, 0).then(works, function (error) {
  919. return error.name === 'UnknownCommand' || error.message.indexOf('need to specify the JS') > -1;
  920. });
  921. // Touch scroll in ios-driver 0.6.6-SNAPSHOT is broken, does not scroll at all;
  922. // in selendroid 0.9.0 it ignores the element argument
  923. testedCapabilities.brokenTouchScroll = function () {
  924. return get(scrollTestUrl).then(function () {
  925. return session.touchScroll(0, 20);
  926. }).then(function () {
  927. return session.execute('return window.scrollY !== 20;');
  928. }).then(function (isBroken) {
  929. if (isBroken) {
  930. return true;
  931. }
  932. return session.findById('a').then(function (element) {
  933. return session.touchScroll(element, 0, 0);
  934. }).then(function () {
  935. return session.execute('return window.scrollY !== 3000;');
  936. });
  937. })
  938. .catch(broken);
  939. };
  940. // Touch flick in ios-driver 0.6.6-SNAPSHOT is broken, does not scroll at all except in very
  941. // broken ways if very tiny speeds are provided and the flick goes in the wrong direction
  942. testedCapabilities.brokenFlickFinger = function () {
  943. return get(scrollTestUrl).then(function () {
  944. return session.flickFinger(0, 400);
  945. }).then(function () {
  946. return session.execute('return window.scrollY === 0;');
  947. })
  948. .catch(broken);
  949. };
  950. }
  951. if (capabilities.supportsCssTransforms) {
  952. testedCapabilities.brokenCssTransformedSize = function () {
  953. /*jshint maxlen:240 */
  954. 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 () {
  955. return session.execute('return document.getElementById("a");').then(function (element) {
  956. return element.getSize();
  957. }).then(function (dimensions) {
  958. return dimensions.width !== 4 || dimensions.height !== 4;
  959. });
  960. }).catch(broken);
  961. };
  962. }
  963. return Promise.all(testedCapabilities);
  964. }
  965. function discoverServerFeatures() {
  966. var testedCapabilities = {};
  967. /* jshint maxlen:300 */
  968. // Check that the remote server will accept file uploads. There is a secondary test in discoverDefects that
  969. // checks whether the server allows typing into file inputs.
  970. testedCapabilities.remoteFiles = function () {
  971. return session._post('file', {
  972. file: 'UEsDBAoAAAAAAD0etkYAAAAAAAAAAAAAAAAIABwAdGVzdC50eHRVVAkAA2WnXlVlp15VdXgLAAEE8gMAAATyAwAAUEsBAh4DCgAAAAAAPR62RgAAAAAAAAAAAAAAAAgAGAAAAAAAAAAAAKSBAAAAAHRlc3QudHh0VVQFAANlp15VdXgLAAEE8gMAAATyAwAAUEsFBgAAAAABAAEATgAAAEIAAAAAAA=='
  973. }).then(function (filename) {
  974. return filename && filename.indexOf('test.txt') > -1;
  975. }).catch(unsupported);
  976. };
  977. // The window sizing commands in the W3C standard don't use window handles, but they do under the
  978. // JsonWireProtocol. By default, Session assumes handles are used. When the result of this check is added to
  979. // capabilities, Session will take it into account.
  980. testedCapabilities.implicitWindowHandles = session.getWindowSize().then(unsupported, function (error) {
  981. return error.name === 'UnknownCommand';
  982. });
  983. // At least SafariDriver 2.41.0 fails to allow stand-alone feature testing because it does not inject user
  984. // scripts for URLs that are not http/https
  985. if (!isMacSafari(capabilities)) {
  986. // At least MS Edge 14316 returns immediately from a click request immediately rather than waiting for
  987. // default action to occur.
  988. if (isMsEdge(capabilities)) {
  989. testedCapabilities.returnsFromClickImmediately = true;
  990. }
  991. else {
  992. testedCapabilities.returnsFromClickImmediately = function () {
  993. function assertSelected(expected) {
  994. return function (actual) {
  995. if (expected !== actual) {
  996. throw new Error('unexpected selection state');
  997. }
  998. };
  999. }
  1000. return get(
  1001. '<!DOCTYPE html><input type="checkbox" id="c">'
  1002. ).then(function () {
  1003. return session.findById('c');
  1004. }).then(function (element) {
  1005. return element.click().then(function () {
  1006. return element.isSelected();
  1007. }).then(assertSelected(true))
  1008. .then(function () {
  1009. return element.click().then(function () {
  1010. return element.isSelected();
  1011. });
  1012. }).then(assertSelected(false))
  1013. .then(function () {
  1014. return element.click().then(function () {
  1015. return element.isSelected();
  1016. });
  1017. }).then(assertSelected(true));
  1018. }).then(works, broken);
  1019. };
  1020. }
  1021. }
  1022. // The W3C WebDriver standard does not support the session-level /keys command, but JsonWireProtocol does.
  1023. if (isGeckodriver(capabilities)) {
  1024. testedCapabilities.supportsKeysCommand = false;
  1025. }
  1026. else {
  1027. testedCapabilities.supportsKeysCommand = session._post('keys', { value: [ 'a' ] }).then(supported,
  1028. unsupported);
  1029. }
  1030. return Promise.all(testedCapabilities);
  1031. }
  1032. if (capabilities._filled) {
  1033. return Promise.resolve(session);
  1034. }
  1035. // At least geckodriver 0.11 and Firefox 49+ may hang when getting 'about:blank' in the first request
  1036. var promise = isGeckodriver(capabilities) ? Promise.resolve(session) : session.get('about:blank');
  1037. return promise
  1038. .then(discoverServerFeatures)
  1039. .then(addCapabilities)
  1040. .then(discoverFeatures)
  1041. .then(addCapabilities)
  1042. .then(function () {
  1043. return session.get('about:blank');
  1044. })
  1045. .then(discoverDefects)
  1046. .then(addCapabilities)
  1047. .then(function () {
  1048. Object.defineProperty(capabilities, '_filled', {
  1049. value: true,
  1050. configurable: true
  1051. });
  1052. return session.get('about:blank').finally(function () {
  1053. return session;
  1054. });
  1055. });
  1056. },
  1057. /**
  1058. * Gets a list of all currently active remote control sessions on this server.
  1059. *
  1060. * @returns {Promise.<Object[]>}
  1061. */
  1062. getSessions: function () {
  1063. return this._get('sessions').then(function (sessions) {
  1064. // At least BrowserStack is now returning an array for the sessions response
  1065. if (sessions && !Array.isArray(sessions)) {
  1066. sessions = returnValue(sessions);
  1067. }
  1068. // At least ChromeDriver 2.19 uses the wrong keys
  1069. // https://code.google.com/p/chromedriver/issues/detail?id=1229
  1070. sessions.forEach(function (session) {
  1071. if (session.sessionId && !session.id) {
  1072. session.id = session.sessionId;
  1073. }
  1074. });
  1075. return sessions;
  1076. });
  1077. },
  1078. /**
  1079. * Gets information on the capabilities of a given session from the server. The list of capabilities returned
  1080. * by this command will not include any of the extra session capabilities detected by Leadfoot and may be
  1081. * inaccurate.
  1082. *
  1083. * @param {string} sessionId
  1084. * @returns {Promise.<Object>}
  1085. */
  1086. getSessionCapabilities: function (sessionId) {
  1087. return this._get('session/$0', null, [ sessionId ]).then(returnValue);
  1088. },
  1089. /**
  1090. * Terminates a session on the server.
  1091. *
  1092. * @param {string} sessionId
  1093. * @returns {Promise.<void>}
  1094. */
  1095. deleteSession: function (sessionId) {
  1096. return this._delete('session/$0', null, [ sessionId ]).then(returnValue);
  1097. }
  1098. };
  1099. module.exports = Server;