Skip to content

Commit f76d614

Browse files
committed
[js] Use the atoms for getAttribute and isDisplayed
Fixes #2301
1 parent 6fa3c01 commit f76d614

File tree

7 files changed

+193
-62
lines changed

7 files changed

+193
-62
lines changed

Rakefile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -560,8 +560,9 @@ namespace :node do
560560
task :deploy => [
561561
"//cpp:noblur",
562562
"//cpp:noblur64",
563+
"//javascript/atoms/fragments:is-displayed",
563564
"//javascript/firefox-driver:webdriver",
564-
"//javascript/safari-driver:client",
565+
"//javascript/webdriver/atoms:getAttribute",
565566
] do
566567
cmd = "node javascript/node/deploy.js" <<
567568
" --output=build/javascript/node/selenium-webdriver" <<
@@ -571,7 +572,8 @@ namespace :node do
571572
" --resource=build/cpp/amd64/libnoblur64.so:firefox/amd64/libnoblur64.so" <<
572573
" --resource=build/cpp/i386/libnoblur.so:firefox/i386/libnoblur.so" <<
573574
" --resource=build/javascript/firefox-driver/webdriver.xpi:firefox/webdriver.xpi" <<
574-
" --resource=buck-out/gen/javascript/safari-driver/client.js:safari/client.js" <<
575+
" --resource=buck-out/gen/javascript/webdriver/atoms/getAttribute.js:atoms/getAttribute.js" <<
576+
" --resource=buck-out/gen/javascript/atoms/fragments/is-displayed.js:atoms/isDisplayed.js" <<
575577
" --resource=common/src/web/:test/data/" <<
576578
" --exclude_resource=common/src/web/Bin" <<
577579
" --exclude_resource=.gitignore" <<

javascript/node/selenium-webdriver/CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
`SELENIUM_PROMISE_MANAGER=0`. This is part of a larger plan to remove the
2121
promise manager, as documented at
2222
<https://github.com/SeleniumHQ/selenium/issues/2969>
23+
* When communicating with a W3C-compliant remote end, use the atoms library for
24+
the `WebElement.getAttribute()` and `WebElement.isDisplayed()` commands. This
25+
behavior is consistent with the java, .net, python, and ruby clients.
2326

2427

2528
### API Changes

javascript/node/selenium-webdriver/lib/http.js

Lines changed: 155 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@
2525

2626
'use strict';
2727

28+
const fs = require('fs');
29+
const path = require('path');
30+
2831
const cmd = require('./command');
32+
const devmode = require('./devmode');
2933
const error = require('./error');
3034
const logging = require('./logging');
3135
const promise = require('./promise');
@@ -110,13 +114,77 @@ class Response {
110114
}
111115

112116

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+
113157
function post(path) { return resource('POST', path); }
114158
function del(path) { return resource('DELETE', path); }
115159
function get(path) { return resource('GET', path); }
116160
function resource(method, path) { return {method: method, path: path}; }
117161

118162

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>} */
120188
const COMMAND_MAP = new Map([
121189
[cmd.Name.GET_SERVER_STATUS, get('/status')],
122190
[cmd.Name.NEW_SESSION, post('/session')],
@@ -195,9 +263,15 @@ const COMMAND_MAP = new Map([
195263
]);
196264

197265

198-
/** @const {!Map<string, {method: string, path: string}>} */
266+
/** @const {!Map<string, (CommandSpec|CommandTransformer)>} */
199267
const W3C_COMMAND_MAP = new Map([
200268
[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+
}],
201275
[cmd.Name.MAXIMIZE_WINDOW, post('/session/:sessionId/window/maximize')],
202276
[cmd.Name.GET_WINDOW_POSITION, get('/session/:sessionId/window/position')],
203277
[cmd.Name.SET_WINDOW_POSITION, post('/session/:sessionId/window/position')],
@@ -249,6 +323,53 @@ function doSend(executor, request) {
249323
}
250324

251325

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+
252373
/**
253374
* A command executor that communicates with the server using JSON over HTTP.
254375
*
@@ -280,7 +401,7 @@ class Executor {
280401
*/
281402
this.w3c = false;
282403

283-
/** @private {Map<string, {method: string, path: string}>} */
404+
/** @private {Map<string, CommandSpec>} */
284405
this.customCommands_ = null;
285406

286407
/** @private {!logging.Logger} */
@@ -309,51 +430,40 @@ class Executor {
309430

310431
/** @override */
311432
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']);
338457
}
339458

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;
345464
}
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+
});
357467
});
358468
}
359469
}

javascript/node/selenium-webdriver/lib/test/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ var browsersToTest = (function() {
114114
console.log('Running tests using loopback address')
115115
}
116116
}
117+
console.log(
118+
'Promise manager is enabled? ' + webdriver.promise.USE_PROMISE_MANAGER);
117119

118120
return browsers;
119121
})();
@@ -216,6 +218,15 @@ function suite(fn, opt_options) {
216218

217219
try {
218220

221+
before(function() {
222+
if (isDevMode) {
223+
return build.of(
224+
'//javascript/atoms/fragments:is-displayed',
225+
'//javascript/webdriver/atoms:getAttribute')
226+
.onlyOnce().go();
227+
}
228+
});
229+
219230
// Server is only started if required for a specific config.
220231
after(function() {
221232
if (seleniumServer) {

javascript/node/selenium-webdriver/lib/webdriver.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1905,7 +1905,7 @@ class WebElement {
19051905
* @private
19061906
*/
19071907
schedule_(command, description) {
1908-
command.setParameter('id', this.getId());
1908+
command.setParameter('id', this);
19091909
return this.driver_.schedule(command, description);
19101910
}
19111911

javascript/node/selenium-webdriver/test/lib/http_test.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,14 @@ describe('http', function() {
8484

8585
describe('command routing', function() {
8686
it('rejects unrecognized commands', function() {
87-
assert.throws(
88-
() => executor.execute(new Command('fake-name')),
89-
function (err) {
90-
return err instanceof error.UnknownCommandError
91-
&& 'Unrecognized command: fake-name' === err.message;
92-
});
87+
return executor.execute(new Command('fake-name'))
88+
.then(assert.fail, err => {
89+
if (err instanceof error.UnknownCommandError
90+
&& 'Unrecognized command: fake-name' === err.message) {
91+
return;
92+
}
93+
throw err;
94+
})
9395
});
9496

9597
it('rejects promise if client fails to send request', function() {

0 commit comments

Comments
 (0)