|
25 | 25 |
|
26 | 26 | 'use strict';
|
27 | 27 |
|
| 28 | +const fs = require('fs'); |
| 29 | +const path = require('path'); |
| 30 | + |
28 | 31 | const cmd = require('./command');
|
| 32 | +const devmode = require('./devmode'); |
29 | 33 | const error = require('./error');
|
30 | 34 | const logging = require('./logging');
|
31 | 35 | const promise = require('./promise');
|
@@ -110,13 +114,77 @@ class Response {
|
110 | 114 | }
|
111 | 115 |
|
112 | 116 |
|
| 117 | +const DEV_ROOT = '../../../../buck-out/gen/javascript/'; |
| 118 | + |
| 119 | +/** @enum {string} */ |
| 120 | +const Atom = { |
| 121 | + GET_ATTRIBUTE: devmode |
| 122 | + ? path.join(__dirname, DEV_ROOT, 'webdriver/atoms/getAttribute.js') |
| 123 | + : path.join(__dirname, 'atoms/getAttribute.js'), |
| 124 | + IS_DISPLAYED: devmode |
| 125 | + ? path.join(__dirname, DEV_ROOT, 'atoms/fragments/is-displayed.js') |
| 126 | + : path.join(__dirname, 'atoms/isDisplayed.js'), |
| 127 | +}; |
| 128 | + |
| 129 | + |
| 130 | +const ATOMS = /** !Map<string, !Promise<string>> */new Map(); |
| 131 | +const LOG = logging.getLogger('webdriver.http'); |
| 132 | + |
| 133 | +/** |
| 134 | + * @param {Atom} file The atom file to load. |
| 135 | + * @return {!Promise<string>} A promise that will resolve to the contents of the |
| 136 | + * file. |
| 137 | + */ |
| 138 | +function loadAtom(file) { |
| 139 | + if (ATOMS.has(file)) { |
| 140 | + return ATOMS.get(file); |
| 141 | + } |
| 142 | + let contents = /** !Promise<string> */new Promise((resolve, reject) => { |
| 143 | + LOG.finest(() => `Loading atom ${file}`); |
| 144 | + fs.readFile(file, 'utf8', function(err, data) { |
| 145 | + if (err) { |
| 146 | + reject(err); |
| 147 | + } else { |
| 148 | + resolve(data); |
| 149 | + } |
| 150 | + }); |
| 151 | + }); |
| 152 | + ATOMS.set(file, contents); |
| 153 | + return contents; |
| 154 | +} |
| 155 | + |
| 156 | + |
113 | 157 | function post(path) { return resource('POST', path); }
|
114 | 158 | function del(path) { return resource('DELETE', path); }
|
115 | 159 | function get(path) { return resource('GET', path); }
|
116 | 160 | function resource(method, path) { return {method: method, path: path}; }
|
117 | 161 |
|
118 | 162 |
|
119 |
| -/** @const {!Map<string, {method: string, path: string}>} */ |
| 163 | +/** @typedef {{method: string, path: string}} */ |
| 164 | +var CommandSpec; |
| 165 | + |
| 166 | + |
| 167 | +/** @typedef {function(!cmd.Command): !Promise<!cmd.Command>} */ |
| 168 | +var CommandTransformer; |
| 169 | + |
| 170 | + |
| 171 | +/** |
| 172 | + * @param {!cmd.Command} command The initial command. |
| 173 | + * @param {Atom} atom The name of the atom to execute. |
| 174 | + * @return {!Promise<!cmd.Command>} The transformed command to execute. |
| 175 | + */ |
| 176 | +function toExecuteAtomCommand(command, atom, ...params) { |
| 177 | + return loadAtom(atom).then(atom => { |
| 178 | + return new cmd.Command(cmd.Name.EXECUTE_SCRIPT) |
| 179 | + .setParameter('sessionId', command.getParameter('sessionId')) |
| 180 | + .setParameter('script', `return (${atom}).apply(null, arguments)`) |
| 181 | + .setParameter('args', params.map(param => command.getParameter(param))); |
| 182 | + }); |
| 183 | +} |
| 184 | + |
| 185 | + |
| 186 | + |
| 187 | +/** @const {!Map<string, CommandSpec>} */ |
120 | 188 | const COMMAND_MAP = new Map([
|
121 | 189 | [cmd.Name.GET_SERVER_STATUS, get('/status')],
|
122 | 190 | [cmd.Name.NEW_SESSION, post('/session')],
|
@@ -195,9 +263,15 @@ const COMMAND_MAP = new Map([
|
195 | 263 | ]);
|
196 | 264 |
|
197 | 265 |
|
198 |
| -/** @const {!Map<string, {method: string, path: string}>} */ |
| 266 | +/** @const {!Map<string, (CommandSpec|CommandTransformer)>} */ |
199 | 267 | const W3C_COMMAND_MAP = new Map([
|
200 | 268 | [cmd.Name.GET_ACTIVE_ELEMENT, get('/session/:sessionId/element/active')],
|
| 269 | + [cmd.Name.GET_ELEMENT_ATTRIBUTE, (cmd) => { |
| 270 | + return toExecuteAtomCommand(cmd, Atom.GET_ATTRIBUTE, 'id', 'name'); |
| 271 | + }], |
| 272 | + [cmd.Name.IS_ELEMENT_DISPLAYED, (cmd) => { |
| 273 | + return toExecuteAtomCommand(cmd, Atom.IS_DISPLAYED, 'id'); |
| 274 | + }], |
201 | 275 | [cmd.Name.MAXIMIZE_WINDOW, post('/session/:sessionId/window/maximize')],
|
202 | 276 | [cmd.Name.GET_WINDOW_POSITION, get('/session/:sessionId/window/position')],
|
203 | 277 | [cmd.Name.SET_WINDOW_POSITION, post('/session/:sessionId/window/position')],
|
@@ -249,6 +323,53 @@ function doSend(executor, request) {
|
249 | 323 | }
|
250 | 324 |
|
251 | 325 |
|
| 326 | +/** |
| 327 | + * @param {Map<string, CommandSpec>} customCommands |
| 328 | + * A map of custom command definitions. |
| 329 | + * @param {boolean} w3c Whether to use W3C command mappings. |
| 330 | + * @param {!cmd.Command} command The command to resolve. |
| 331 | + * @return {!Promise<!Request>} A promise that will resolve with the |
| 332 | + * command to execute. |
| 333 | + */ |
| 334 | +function buildRequest(customCommands, w3c, command) { |
| 335 | + LOG.finest(() => `Translating command: ${command.getName()}`); |
| 336 | + let spec = customCommands && customCommands.get(command.getName()); |
| 337 | + if (spec) { |
| 338 | + return toHttpRequest(spec); |
| 339 | + } |
| 340 | + |
| 341 | + if (w3c) { |
| 342 | + spec = W3C_COMMAND_MAP.get(command.getName()); |
| 343 | + if (typeof spec === 'function') { |
| 344 | + LOG.finest(() => `Transforming command for W3C: ${command.getName()}`); |
| 345 | + return spec(command) |
| 346 | + .then(newCommand => buildRequest(customCommands, w3c, newCommand)); |
| 347 | + } else if (spec) { |
| 348 | + return toHttpRequest(spec); |
| 349 | + } |
| 350 | + } |
| 351 | + |
| 352 | + spec = COMMAND_MAP.get(command.getName()); |
| 353 | + if (spec) { |
| 354 | + return toHttpRequest(spec); |
| 355 | + } |
| 356 | + return Promise.reject( |
| 357 | + new error.UnknownCommandError( |
| 358 | + 'Unrecognized command: ' + command.getName())); |
| 359 | + |
| 360 | + /** |
| 361 | + * @param {CommandSpec} resource |
| 362 | + * @return {!Promise<!Request>} |
| 363 | + */ |
| 364 | + function toHttpRequest(resource) { |
| 365 | + LOG.finest(() => `Building HTTP request: ${JSON.stringify(resource)}`); |
| 366 | + let parameters = command.getParameters(); |
| 367 | + let path = buildPath(resource.path, parameters); |
| 368 | + return Promise.resolve(new Request(resource.method, path, parameters)); |
| 369 | + } |
| 370 | +} |
| 371 | + |
| 372 | + |
252 | 373 | /**
|
253 | 374 | * A command executor that communicates with the server using JSON over HTTP.
|
254 | 375 | *
|
@@ -280,7 +401,7 @@ class Executor {
|
280 | 401 | */
|
281 | 402 | this.w3c = false;
|
282 | 403 |
|
283 |
| - /** @private {Map<string, {method: string, path: string}>} */ |
| 404 | + /** @private {Map<string, CommandSpec>} */ |
284 | 405 | this.customCommands_ = null;
|
285 | 406 |
|
286 | 407 | /** @private {!logging.Logger} */
|
@@ -309,51 +430,40 @@ class Executor {
|
309 | 430 |
|
310 | 431 | /** @override */
|
311 | 432 | execute(command) {
|
312 |
| - let resource = |
313 |
| - (this.customCommands_ && this.customCommands_.get(command.getName())) |
314 |
| - || (this.w3c && W3C_COMMAND_MAP.get(command.getName())) |
315 |
| - || COMMAND_MAP.get(command.getName()); |
316 |
| - if (!resource) { |
317 |
| - throw new error.UnknownCommandError( |
318 |
| - 'Unrecognized command: ' + command.getName()); |
319 |
| - } |
320 |
| - |
321 |
| - let parameters = command.getParameters(); |
322 |
| - let path = buildPath(resource.path, parameters); |
323 |
| - let request = new Request(resource.method, path, parameters); |
324 |
| - |
325 |
| - let log = this.log_; |
326 |
| - log.finer(() => '>>>\n' + request); |
327 |
| - return doSend(this, request).then(response => { |
328 |
| - log.finer(() => '<<<\n' + response); |
329 |
| - |
330 |
| - let parsed = |
331 |
| - parseHttpResponse(/** @type {!Response} */ (response), this.w3c); |
332 |
| - |
333 |
| - if (command.getName() === cmd.Name.NEW_SESSION |
334 |
| - || command.getName() === cmd.Name.DESCRIBE_SESSION) { |
335 |
| - if (!parsed || !parsed['sessionId']) { |
336 |
| - throw new error.WebDriverError( |
337 |
| - 'Unable to parse new session response: ' + response.body); |
| 433 | + let request = buildRequest(this.customCommands_, this.w3c, command); |
| 434 | + return request.then(request => { |
| 435 | + this.log_.finer(() => `>>> ${request.method} ${request.path}`); |
| 436 | + return doSend(this, request).then(response => { |
| 437 | + this.log_.finer(() => `>>>\n${request}\n<<<\n${response}`); |
| 438 | + |
| 439 | + let parsed = |
| 440 | + parseHttpResponse(/** @type {!Response} */ (response), this.w3c); |
| 441 | + |
| 442 | + if (command.getName() === cmd.Name.NEW_SESSION |
| 443 | + || command.getName() === cmd.Name.DESCRIBE_SESSION) { |
| 444 | + if (!parsed || !parsed['sessionId']) { |
| 445 | + throw new error.WebDriverError( |
| 446 | + 'Unable to parse new session response: ' + response.body); |
| 447 | + } |
| 448 | + |
| 449 | + // The remote end is a W3C compliant server if there is no `status` |
| 450 | + // field in the response. This is not appliable for the DESCRIBE_SESSION |
| 451 | + // command, which is not defined in the W3C spec. |
| 452 | + if (command.getName() === cmd.Name.NEW_SESSION) { |
| 453 | + this.w3c = this.w3c || !('status' in parsed); |
| 454 | + } |
| 455 | + |
| 456 | + return new Session(parsed['sessionId'], parsed['value']); |
338 | 457 | }
|
339 | 458 |
|
340 |
| - // The remote end is a W3C compliant server if there is no `status` |
341 |
| - // field in the response. This is not appliable for the DESCRIBE_SESSION |
342 |
| - // command, which is not defined in the W3C spec. |
343 |
| - if (command.getName() === cmd.Name.NEW_SESSION) { |
344 |
| - this.w3c = this.w3c || !('status' in parsed); |
| 459 | + if (parsed |
| 460 | + && typeof parsed === 'object' |
| 461 | + && 'value' in parsed) { |
| 462 | + let value = parsed['value']; |
| 463 | + return typeof value === 'undefined' ? null : value; |
345 | 464 | }
|
346 |
| - |
347 |
| - return new Session(parsed['sessionId'], parsed['value']); |
348 |
| - } |
349 |
| - |
350 |
| - if (parsed |
351 |
| - && typeof parsed === 'object' |
352 |
| - && 'value' in parsed) { |
353 |
| - let value = parsed['value']; |
354 |
| - return typeof value === 'undefined' ? null : value; |
355 |
| - } |
356 |
| - return parsed; |
| 465 | + return parsed; |
| 466 | + }); |
357 | 467 | });
|
358 | 468 | }
|
359 | 469 | }
|
|
0 commit comments