http.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. // Licensed to the Software Freedom Conservancy (SFC) under one
  2. // or more contributor license agreements. See the NOTICE file
  3. // distributed with this work for additional information
  4. // regarding copyright ownership. The SFC licenses this file
  5. // to you under the Apache License, Version 2.0 (the
  6. // "License"); you may not use this file except in compliance
  7. // with the License. You may obtain a copy of the License at
  8. //
  9. // http://www.apache.org/licenses/LICENSE-2.0
  10. //
  11. // Unless required by applicable law or agreed to in writing,
  12. // software distributed under the License is distributed on an
  13. // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  14. // KIND, either express or implied. See the License for the
  15. // specific language governing permissions and limitations
  16. // under the License.
  17. /**
  18. * @fileoverview Defines an environment agnostic {@linkplain cmd.Executor
  19. * command executor} that communicates with a remote end using JSON over HTTP.
  20. *
  21. * Clients should implement the {@link Client} interface, which is used by
  22. * the {@link Executor} to send commands to the remote end.
  23. */
  24. 'use strict';
  25. const cmd = require('./command');
  26. const error = require('./error');
  27. const logging = require('./logging');
  28. const promise = require('./promise');
  29. const Session = require('./session').Session;
  30. const WebElement = require('./webdriver').WebElement;
  31. const {getAttribute, isDisplayed} = /** @suppress {undefinedVars|uselessCode} */(function() {
  32. try {
  33. return {
  34. getAttribute: require('./atoms/get-attribute.js'),
  35. isDisplayed: require('./atoms/is-displayed.js')
  36. };
  37. } catch (ex) {
  38. throw Error(
  39. 'Failed to import atoms modules. If running in devmode, you need to run'
  40. + ' `./go node:atoms` from the project root: ' + ex);
  41. }
  42. })();
  43. /**
  44. * Converts a headers map to a HTTP header block string.
  45. * @param {!Map<string, string>} headers The map to convert.
  46. * @return {string} The headers as a string.
  47. */
  48. function headersToString(headers) {
  49. let ret = [];
  50. headers.forEach(function(value, name) {
  51. ret.push(`${name.toLowerCase()}: ${value}`);
  52. });
  53. return ret.join('\n');
  54. }
  55. /**
  56. * Represents a HTTP request message. This class is a "partial" request and only
  57. * defines the path on the server to send a request to. It is each client's
  58. * responsibility to build the full URL for the final request.
  59. * @final
  60. */
  61. class Request {
  62. /**
  63. * @param {string} method The HTTP method to use for the request.
  64. * @param {string} path The path on the server to send the request to.
  65. * @param {Object=} opt_data This request's non-serialized JSON payload data.
  66. */
  67. constructor(method, path, opt_data) {
  68. this.method = /** string */method;
  69. this.path = /** string */path;
  70. this.data = /** Object */opt_data;
  71. this.headers = /** !Map<string, string> */new Map(
  72. [['Accept', 'application/json; charset=utf-8']]);
  73. }
  74. /** @override */
  75. toString() {
  76. let ret = `${this.method} ${this.path} HTTP/1.1\n`;
  77. ret += headersToString(this.headers) + '\n\n';
  78. if (this.data) {
  79. ret += JSON.stringify(this.data);
  80. }
  81. return ret;
  82. }
  83. }
  84. /**
  85. * Represents a HTTP response message.
  86. * @final
  87. */
  88. class Response {
  89. /**
  90. * @param {number} status The response code.
  91. * @param {!Object<string>} headers The response headers. All header names
  92. * will be converted to lowercase strings for consistent lookups.
  93. * @param {string} body The response body.
  94. */
  95. constructor(status, headers, body) {
  96. this.status = /** number */status;
  97. this.body = /** string */body;
  98. this.headers = /** !Map<string, string>*/new Map;
  99. for (let header in headers) {
  100. this.headers.set(header.toLowerCase(), headers[header]);
  101. }
  102. }
  103. /** @override */
  104. toString() {
  105. let ret = `HTTP/1.1 ${this.status}\n${headersToString(this.headers)}\n\n`;
  106. if (this.body) {
  107. ret += this.body;
  108. }
  109. return ret;
  110. }
  111. }
  112. const DEV_ROOT = '../../../../buck-out/gen/javascript/';
  113. /** @enum {!Function} */
  114. const Atom = {
  115. GET_ATTRIBUTE: getAttribute,
  116. IS_DISPLAYED: isDisplayed
  117. };
  118. const LOG = logging.getLogger('webdriver.http');
  119. function post(path) { return resource('POST', path); }
  120. function del(path) { return resource('DELETE', path); }
  121. function get(path) { return resource('GET', path); }
  122. function resource(method, path) { return {method: method, path: path}; }
  123. /** @typedef {{method: string, path: string}} */
  124. var CommandSpec;
  125. /** @typedef {function(!cmd.Command): !Promise<!cmd.Command>} */
  126. var CommandTransformer;
  127. class InternalTypeError extends TypeError {}
  128. /**
  129. * @param {!cmd.Command} command The initial command.
  130. * @param {Atom} atom The name of the atom to execute.
  131. * @return {!Promise<!cmd.Command>} The transformed command to execute.
  132. */
  133. function toExecuteAtomCommand(command, atom, ...params) {
  134. return new Promise((resolve, reject) => {
  135. if (typeof atom !== 'function') {
  136. reject(new InternalTypeError('atom is not a function: ' + typeof atom));
  137. return;
  138. }
  139. let newCmd = new cmd.Command(cmd.Name.EXECUTE_SCRIPT)
  140. .setParameter('sessionId', command.getParameter('sessionId'))
  141. .setParameter('script', `return (${atom}).apply(null, arguments)`)
  142. .setParameter('args', params.map(param => command.getParameter(param)));
  143. resolve(newCmd);
  144. });
  145. }
  146. /** @const {!Map<string, CommandSpec>} */
  147. const COMMAND_MAP = new Map([
  148. [cmd.Name.GET_SERVER_STATUS, get('/status')],
  149. [cmd.Name.NEW_SESSION, post('/session')],
  150. [cmd.Name.GET_SESSIONS, get('/sessions')],
  151. [cmd.Name.DESCRIBE_SESSION, get('/session/:sessionId')],
  152. [cmd.Name.QUIT, del('/session/:sessionId')],
  153. [cmd.Name.CLOSE, del('/session/:sessionId/window')],
  154. [cmd.Name.GET_CURRENT_WINDOW_HANDLE, get('/session/:sessionId/window_handle')],
  155. [cmd.Name.GET_WINDOW_HANDLES, get('/session/:sessionId/window_handles')],
  156. [cmd.Name.GET_CURRENT_URL, get('/session/:sessionId/url')],
  157. [cmd.Name.GET, post('/session/:sessionId/url')],
  158. [cmd.Name.GO_BACK, post('/session/:sessionId/back')],
  159. [cmd.Name.GO_FORWARD, post('/session/:sessionId/forward')],
  160. [cmd.Name.REFRESH, post('/session/:sessionId/refresh')],
  161. [cmd.Name.ADD_COOKIE, post('/session/:sessionId/cookie')],
  162. [cmd.Name.GET_ALL_COOKIES, get('/session/:sessionId/cookie')],
  163. [cmd.Name.DELETE_ALL_COOKIES, del('/session/:sessionId/cookie')],
  164. [cmd.Name.DELETE_COOKIE, del('/session/:sessionId/cookie/:name')],
  165. [cmd.Name.FIND_ELEMENT, post('/session/:sessionId/element')],
  166. [cmd.Name.FIND_ELEMENTS, post('/session/:sessionId/elements')],
  167. [cmd.Name.GET_ACTIVE_ELEMENT, post('/session/:sessionId/element/active')],
  168. [cmd.Name.FIND_CHILD_ELEMENT, post('/session/:sessionId/element/:id/element')],
  169. [cmd.Name.FIND_CHILD_ELEMENTS, post('/session/:sessionId/element/:id/elements')],
  170. [cmd.Name.CLEAR_ELEMENT, post('/session/:sessionId/element/:id/clear')],
  171. [cmd.Name.CLICK_ELEMENT, post('/session/:sessionId/element/:id/click')],
  172. [cmd.Name.SEND_KEYS_TO_ELEMENT, post('/session/:sessionId/element/:id/value')],
  173. [cmd.Name.SUBMIT_ELEMENT, post('/session/:sessionId/element/:id/submit')],
  174. [cmd.Name.GET_ELEMENT_TEXT, get('/session/:sessionId/element/:id/text')],
  175. [cmd.Name.GET_ELEMENT_TAG_NAME, get('/session/:sessionId/element/:id/name')],
  176. [cmd.Name.IS_ELEMENT_SELECTED, get('/session/:sessionId/element/:id/selected')],
  177. [cmd.Name.IS_ELEMENT_ENABLED, get('/session/:sessionId/element/:id/enabled')],
  178. [cmd.Name.IS_ELEMENT_DISPLAYED, get('/session/:sessionId/element/:id/displayed')],
  179. [cmd.Name.GET_ELEMENT_LOCATION, get('/session/:sessionId/element/:id/location')],
  180. [cmd.Name.GET_ELEMENT_SIZE, get('/session/:sessionId/element/:id/size')],
  181. [cmd.Name.GET_ELEMENT_ATTRIBUTE, get('/session/:sessionId/element/:id/attribute/:name')],
  182. [cmd.Name.GET_ELEMENT_VALUE_OF_CSS_PROPERTY, get('/session/:sessionId/element/:id/css/:propertyName')],
  183. [cmd.Name.ELEMENT_EQUALS, get('/session/:sessionId/element/:id/equals/:other')],
  184. [cmd.Name.TAKE_ELEMENT_SCREENSHOT, get('/session/:sessionId/element/:id/screenshot')],
  185. [cmd.Name.SWITCH_TO_WINDOW, post('/session/:sessionId/window')],
  186. [cmd.Name.MAXIMIZE_WINDOW, post('/session/:sessionId/window/current/maximize')],
  187. [cmd.Name.GET_WINDOW_POSITION, get('/session/:sessionId/window/current/position')],
  188. [cmd.Name.SET_WINDOW_POSITION, post('/session/:sessionId/window/current/position')],
  189. [cmd.Name.GET_WINDOW_SIZE, get('/session/:sessionId/window/current/size')],
  190. [cmd.Name.SET_WINDOW_SIZE, post('/session/:sessionId/window/current/size')],
  191. [cmd.Name.SWITCH_TO_FRAME, post('/session/:sessionId/frame')],
  192. [cmd.Name.GET_PAGE_SOURCE, get('/session/:sessionId/source')],
  193. [cmd.Name.GET_TITLE, get('/session/:sessionId/title')],
  194. [cmd.Name.EXECUTE_SCRIPT, post('/session/:sessionId/execute')],
  195. [cmd.Name.EXECUTE_ASYNC_SCRIPT, post('/session/:sessionId/execute_async')],
  196. [cmd.Name.SCREENSHOT, get('/session/:sessionId/screenshot')],
  197. [cmd.Name.GET_TIMEOUT, get('/session/:sessionId/timeouts')],
  198. [cmd.Name.SET_TIMEOUT, post('/session/:sessionId/timeouts')],
  199. [cmd.Name.MOVE_TO, post('/session/:sessionId/moveto')],
  200. [cmd.Name.CLICK, post('/session/:sessionId/click')],
  201. [cmd.Name.DOUBLE_CLICK, post('/session/:sessionId/doubleclick')],
  202. [cmd.Name.MOUSE_DOWN, post('/session/:sessionId/buttondown')],
  203. [cmd.Name.MOUSE_UP, post('/session/:sessionId/buttonup')],
  204. [cmd.Name.MOVE_TO, post('/session/:sessionId/moveto')],
  205. [cmd.Name.SEND_KEYS_TO_ACTIVE_ELEMENT, post('/session/:sessionId/keys')],
  206. [cmd.Name.TOUCH_SINGLE_TAP, post('/session/:sessionId/touch/click')],
  207. [cmd.Name.TOUCH_DOUBLE_TAP, post('/session/:sessionId/touch/doubleclick')],
  208. [cmd.Name.TOUCH_DOWN, post('/session/:sessionId/touch/down')],
  209. [cmd.Name.TOUCH_UP, post('/session/:sessionId/touch/up')],
  210. [cmd.Name.TOUCH_MOVE, post('/session/:sessionId/touch/move')],
  211. [cmd.Name.TOUCH_SCROLL, post('/session/:sessionId/touch/scroll')],
  212. [cmd.Name.TOUCH_LONG_PRESS, post('/session/:sessionId/touch/longclick')],
  213. [cmd.Name.TOUCH_FLICK, post('/session/:sessionId/touch/flick')],
  214. [cmd.Name.ACCEPT_ALERT, post('/session/:sessionId/accept_alert')],
  215. [cmd.Name.DISMISS_ALERT, post('/session/:sessionId/dismiss_alert')],
  216. [cmd.Name.GET_ALERT_TEXT, get('/session/:sessionId/alert_text')],
  217. [cmd.Name.SET_ALERT_TEXT, post('/session/:sessionId/alert_text')],
  218. [cmd.Name.SET_ALERT_CREDENTIALS, post('/session/:sessionId/alert/credentials')],
  219. [cmd.Name.GET_LOG, post('/session/:sessionId/log')],
  220. [cmd.Name.GET_AVAILABLE_LOG_TYPES, get('/session/:sessionId/log/types')],
  221. [cmd.Name.GET_SESSION_LOGS, post('/logs')],
  222. [cmd.Name.UPLOAD_FILE, post('/session/:sessionId/file')],
  223. ]);
  224. /** @const {!Map<string, (CommandSpec|CommandTransformer)>} */
  225. const W3C_COMMAND_MAP = new Map([
  226. [cmd.Name.GET_ACTIVE_ELEMENT, get('/session/:sessionId/element/active')],
  227. [cmd.Name.GET_ALERT_TEXT, get('/session/:sessionId/alert/text')],
  228. [cmd.Name.SET_ALERT_TEXT, post('/session/:sessionId/alert/text')],
  229. [cmd.Name.ACCEPT_ALERT, post('/session/:sessionId/alert/accept')],
  230. [cmd.Name.DISMISS_ALERT, post('/session/:sessionId/alert/dismiss')],
  231. [cmd.Name.GET_ELEMENT_ATTRIBUTE, (cmd) => {
  232. return toExecuteAtomCommand(cmd, Atom.GET_ATTRIBUTE, 'id', 'name');
  233. }],
  234. [cmd.Name.GET_ELEMENT_LOCATION, get('/session/:sessionId/element/:id/rect')],
  235. [cmd.Name.GET_ELEMENT_SIZE, get('/session/:sessionId/element/:id/rect')],
  236. [cmd.Name.IS_ELEMENT_DISPLAYED, (cmd) => {
  237. return toExecuteAtomCommand(cmd, Atom.IS_DISPLAYED, 'id');
  238. }],
  239. [cmd.Name.EXECUTE_SCRIPT, post('/session/:sessionId/execute/sync')],
  240. [cmd.Name.EXECUTE_ASYNC_SCRIPT, post('/session/:sessionId/execute/async')],
  241. [cmd.Name.MAXIMIZE_WINDOW, post('/session/:sessionId/window/maximize')],
  242. [cmd.Name.GET_WINDOW_POSITION, get('/session/:sessionId/window/position')],
  243. [cmd.Name.SET_WINDOW_POSITION, post('/session/:sessionId/window/position')],
  244. [cmd.Name.GET_WINDOW_SIZE, get('/session/:sessionId/window/size')],
  245. [cmd.Name.SET_WINDOW_SIZE, post('/session/:sessionId/window/size')],
  246. [cmd.Name.GET_CURRENT_WINDOW_HANDLE, get('/session/:sessionId/window')],
  247. [cmd.Name.GET_WINDOW_HANDLES, get('/session/:sessionId/window/handles')],
  248. ]);
  249. /**
  250. * Handles sending HTTP messages to a remote end.
  251. *
  252. * @interface
  253. */
  254. class Client {
  255. /**
  256. * Sends a request to the server. The client will automatically follow any
  257. * redirects returned by the server, fulfilling the returned promise with the
  258. * final response.
  259. *
  260. * @param {!Request} httpRequest The request to send.
  261. * @return {!Promise<Response>} A promise that will be fulfilled with the
  262. * server's response.
  263. */
  264. send(httpRequest) {}
  265. }
  266. const CLIENTS =
  267. /** !WeakMap<!Executor, !(Client|IThenable<!Client>)> */new WeakMap;
  268. /**
  269. * Sends a request using the given executor.
  270. * @param {!Executor} executor
  271. * @param {!Request} request
  272. * @return {!Promise<Response>}
  273. */
  274. function doSend(executor, request) {
  275. const client = CLIENTS.get(executor);
  276. if (promise.isPromise(client)) {
  277. return client.then(client => {
  278. CLIENTS.set(executor, client);
  279. return client.send(request);
  280. });
  281. } else {
  282. return client.send(request);
  283. }
  284. }
  285. /**
  286. * @param {Map<string, CommandSpec>} customCommands
  287. * A map of custom command definitions.
  288. * @param {boolean} w3c Whether to use W3C command mappings.
  289. * @param {!cmd.Command} command The command to resolve.
  290. * @return {!Promise<!Request>} A promise that will resolve with the
  291. * command to execute.
  292. */
  293. function buildRequest(customCommands, w3c, command) {
  294. LOG.finest(() => `Translating command: ${command.getName()}`);
  295. let spec = customCommands && customCommands.get(command.getName());
  296. if (spec) {
  297. return toHttpRequest(spec);
  298. }
  299. if (w3c) {
  300. spec = W3C_COMMAND_MAP.get(command.getName());
  301. if (typeof spec === 'function') {
  302. LOG.finest(() => `Transforming command for W3C: ${command.getName()}`);
  303. return spec(command)
  304. .then(newCommand => buildRequest(customCommands, w3c, newCommand));
  305. } else if (spec) {
  306. return toHttpRequest(spec);
  307. }
  308. }
  309. spec = COMMAND_MAP.get(command.getName());
  310. if (spec) {
  311. return toHttpRequest(spec);
  312. }
  313. return Promise.reject(
  314. new error.UnknownCommandError(
  315. 'Unrecognized command: ' + command.getName()));
  316. /**
  317. * @param {CommandSpec} resource
  318. * @return {!Promise<!Request>}
  319. */
  320. function toHttpRequest(resource) {
  321. LOG.finest(() => `Building HTTP request: ${JSON.stringify(resource)}`);
  322. let parameters = command.getParameters();
  323. let path = buildPath(resource.path, parameters);
  324. return Promise.resolve(new Request(resource.method, path, parameters));
  325. }
  326. }
  327. /**
  328. * A command executor that communicates with the server using JSON over HTTP.
  329. *
  330. * By default, each instance of this class will use the legacy wire protocol
  331. * from [Selenium project][json]. The executor will automatically switch to the
  332. * [W3C wire protocol][w3c] if the remote end returns a compliant response to
  333. * a new session command.
  334. *
  335. * [json]: https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol
  336. * [w3c]: https://w3c.github.io/webdriver/webdriver-spec.html
  337. *
  338. * @implements {cmd.Executor}
  339. */
  340. class Executor {
  341. /**
  342. * @param {!(Client|IThenable<!Client>)} client The client to use for sending
  343. * requests to the server, or a promise-like object that will resolve to
  344. * to the client.
  345. */
  346. constructor(client) {
  347. CLIENTS.set(this, client);
  348. /**
  349. * Whether this executor should use the W3C wire protocol. The executor
  350. * will automatically switch if the remote end sends a compliant response
  351. * to a new session command, however, this property may be directly set to
  352. * `true` to force the executor into W3C mode.
  353. * @type {boolean}
  354. */
  355. this.w3c = false;
  356. /** @private {Map<string, CommandSpec>} */
  357. this.customCommands_ = null;
  358. /** @private {!logging.Logger} */
  359. this.log_ = logging.getLogger('webdriver.http.Executor');
  360. }
  361. /**
  362. * Defines a new command for use with this executor. When a command is sent,
  363. * the {@code path} will be preprocessed using the command's parameters; any
  364. * path segments prefixed with ":" will be replaced by the parameter of the
  365. * same name. For example, given "/person/:name" and the parameters
  366. * "{name: 'Bob'}", the final command path will be "/person/Bob".
  367. *
  368. * @param {string} name The command name.
  369. * @param {string} method The HTTP method to use when sending this command.
  370. * @param {string} path The path to send the command to, relative to
  371. * the WebDriver server's command root and of the form
  372. * "/path/:variable/segment".
  373. */
  374. defineCommand(name, method, path) {
  375. if (!this.customCommands_) {
  376. this.customCommands_ = new Map;
  377. }
  378. this.customCommands_.set(name, {method, path});
  379. }
  380. /** @override */
  381. execute(command) {
  382. let request = buildRequest(this.customCommands_, this.w3c, command);
  383. return request.then(request => {
  384. this.log_.finer(() => `>>> ${request.method} ${request.path}`);
  385. return doSend(this, request).then(response => {
  386. this.log_.finer(() => `>>>\n${request}\n<<<\n${response}`);
  387. let httpResponse = /** @type {!Response} */(response);
  388. let {isW3C, value} = parseHttpResponse(command, httpResponse);
  389. if (command.getName() === cmd.Name.NEW_SESSION
  390. || command.getName() === cmd.Name.DESCRIBE_SESSION) {
  391. if (!value || !value.sessionId) {
  392. throw new error.WebDriverError(
  393. `Unable to parse new session response: ${response.body}`);
  394. }
  395. // The remote end is a W3C compliant server if there is no `status`
  396. // field in the response. This is not applicable for the DESCRIBE_SESSION
  397. // command, which is not defined in the W3C spec.
  398. if (command.getName() === cmd.Name.NEW_SESSION) {
  399. this.w3c = this.w3c || isW3C;
  400. }
  401. // No implementations use the `capabilities` key yet...
  402. let capabilities = value.capabilities || value.value;
  403. return new Session(value.sessionId, capabilities);
  404. }
  405. return typeof value === 'undefined' ? null : value;
  406. });
  407. });
  408. }
  409. }
  410. /**
  411. * @param {string} str .
  412. * @return {?} .
  413. */
  414. function tryParse(str) {
  415. try {
  416. return JSON.parse(str);
  417. } catch (ignored) {
  418. // Do nothing.
  419. }
  420. }
  421. /**
  422. * Callback used to parse {@link Response} objects from a
  423. * {@link HttpClient}.
  424. *
  425. * @param {!cmd.Command} command The command the response is for.
  426. * @param {!Response} httpResponse The HTTP response to parse.
  427. * @return {{isW3C: boolean, value: ?}} An object describing the parsed
  428. * response. This object will have two fields: `isW3C` indicates whether
  429. * the response looks like it came from a remote end that conforms with the
  430. * W3C WebDriver spec, and `value`, the actual response value.
  431. * @throws {WebDriverError} If the HTTP response is an error.
  432. */
  433. function parseHttpResponse(command, httpResponse) {
  434. if (httpResponse.status < 200) {
  435. // This should never happen, but throw the raw response so users report it.
  436. throw new error.WebDriverError(
  437. `Unexpected HTTP response:\n${httpResponse}`);
  438. }
  439. let parsed = tryParse(httpResponse.body);
  440. if (parsed && typeof parsed === 'object') {
  441. let value = parsed.value;
  442. let isW3C =
  443. value !== null && typeof value === 'object'
  444. && typeof parsed.status === 'undefined';
  445. if (!isW3C) {
  446. error.checkLegacyResponse(parsed);
  447. // Adjust legacy new session responses to look like W3C to simplify
  448. // later processing.
  449. if (command.getName() === cmd.Name.NEW_SESSION
  450. || command.getName() == cmd.Name.DESCRIBE_SESSION) {
  451. value = parsed;
  452. }
  453. } else if (httpResponse.status > 399) {
  454. error.throwDecodedError(value);
  455. }
  456. return {isW3C, value};
  457. }
  458. if (parsed !== undefined) {
  459. return {isW3C: false, value: parsed};
  460. }
  461. let value = httpResponse.body.replace(/\r\n/g, '\n');
  462. // 404 represents an unknown command; anything else > 399 is a generic unknown
  463. // error.
  464. if (httpResponse.status == 404) {
  465. throw new error.UnsupportedOperationError(value);
  466. } else if (httpResponse.status >= 400) {
  467. throw new error.WebDriverError(value);
  468. }
  469. return {isW3C: false, value: value || null};
  470. }
  471. /**
  472. * Builds a fully qualified path using the given set of command parameters. Each
  473. * path segment prefixed with ':' will be replaced by the value of the
  474. * corresponding parameter. All parameters spliced into the path will be
  475. * removed from the parameter map.
  476. * @param {string} path The original resource path.
  477. * @param {!Object<*>} parameters The parameters object to splice into the path.
  478. * @return {string} The modified path.
  479. */
  480. function buildPath(path, parameters) {
  481. let pathParameters = path.match(/\/:(\w+)\b/g);
  482. if (pathParameters) {
  483. for (let i = 0; i < pathParameters.length; ++i) {
  484. let key = pathParameters[i].substring(2); // Trim the /:
  485. if (key in parameters) {
  486. let value = parameters[key];
  487. if (WebElement.isId(value)) {
  488. // When inserting a WebElement into the URL, only use its ID value,
  489. // not the full JSON.
  490. value = WebElement.extractId(value);
  491. }
  492. path = path.replace(pathParameters[i], '/' + value);
  493. delete parameters[key];
  494. } else {
  495. throw new error.InvalidArgumentError(
  496. 'Missing required parameter: ' + key);
  497. }
  498. }
  499. }
  500. return path;
  501. }
  502. // PUBLIC API
  503. exports.Executor = Executor;
  504. exports.Client = Client;
  505. exports.Request = Request;
  506. exports.Response = Response;
  507. exports.buildPath = buildPath; // Exported for testing.