Skip to content

Commit dd2da4b

Browse files
committed
[js] Add support for W3C compliant servers
1 parent 3e80b6a commit dd2da4b

File tree

7 files changed

+414
-109
lines changed

7 files changed

+414
-109
lines changed

javascript/node/selenium-webdriver/CHANGES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010
command to test for the presence of an element:
1111

1212
driver.findElements(By.css('.foo')).then(found => !!found.length);
13+
* Added support for W3C-spec compliant servers.
14+
15+
### Changes for W3C WebDriver Spec Compliance
16+
17+
* Changed `element.sendKeys(...)` to send the key sequence as an array where
18+
each element defines a single key. The legacy wire protocol permits arrays
19+
where each element is a string of arbitrary length. This change is solely
20+
at the protocol level and should have no user-visible effect.
1321

1422

1523
## v2.52.0

javascript/node/selenium-webdriver/error.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,7 @@ const ERROR_CODE_TO_TYPE = new Map([
546546
* @param {*} data The response data to check.
547547
* @return {*} The response data if it was not an encoded error.
548548
* @throws {WebDriverError} the decoded error, if present in the data object.
549+
* @deprecated Use {@link #throwDecodedError(data)} instead.
549550
* @see https://w3c.github.io/webdriver/webdriver-spec.html#protocol
550551
*/
551552
function checkResponse(data) {
@@ -557,6 +558,23 @@ function checkResponse(data) {
557558
}
558559

559560

561+
/**
562+
* Throws an error coded from the W3C protocol. A generic error will be thrown
563+
* if the privded `data` is not a valid encoded error.
564+
*
565+
* @param {{error: string, message: string}} data The error data to decode.
566+
* @throws {WebDriverError} the decoded error.
567+
* @see https://w3c.github.io/webdriver/webdriver-spec.html#protocol
568+
*/
569+
function throwDecodedError(data) {
570+
if (data && typeof data === 'object' && typeof data.error === 'string') {
571+
let ctor = ERROR_CODE_TO_TYPE.get(data.error) || WebDriverError;
572+
throw new ctor(data.message);
573+
}
574+
throw new WebDriverError('Unknown error: ' + JSON.stringify(data));
575+
}
576+
577+
560578
/**
561579
* Checks a legacy response from the Selenium 2.0 wire protocol for an error.
562580
* @param {*} responseObj the response object to check.
@@ -626,3 +644,4 @@ exports.UnsupportedOperationError = UnsupportedOperationError;
626644

627645
exports.checkResponse = checkResponse;
628646
exports.checkLegacyResponse = checkLegacyResponse;
647+
exports.throwDecodedError = throwDecodedError;

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

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,11 @@ const COMMAND_MAP = new Map([
156156
[cmd.Name.ELEMENT_EQUALS, get('/session/:sessionId/element/:id/equals/:other')],
157157
[cmd.Name.TAKE_ELEMENT_SCREENSHOT, get('/session/:sessionId/element/:id/screenshot')],
158158
[cmd.Name.SWITCH_TO_WINDOW, post('/session/:sessionId/window')],
159-
[cmd.Name.MAXIMIZE_WINDOW, post('/session/:sessionId/window/:windowHandle/maximize')],
160-
[cmd.Name.GET_WINDOW_POSITION, get('/session/:sessionId/window/:windowHandle/position')],
161-
[cmd.Name.SET_WINDOW_POSITION, post('/session/:sessionId/window/:windowHandle/position')],
162-
[cmd.Name.GET_WINDOW_SIZE, get('/session/:sessionId/window/:windowHandle/size')],
163-
[cmd.Name.SET_WINDOW_SIZE, post('/session/:sessionId/window/:windowHandle/size')],
159+
[cmd.Name.MAXIMIZE_WINDOW, post('/session/:sessionId/window/current/maximize')],
160+
[cmd.Name.GET_WINDOW_POSITION, get('/session/:sessionId/window/current/position')],
161+
[cmd.Name.SET_WINDOW_POSITION, post('/session/:sessionId/window/current/position')],
162+
[cmd.Name.GET_WINDOW_SIZE, get('/session/:sessionId/window/current/size')],
163+
[cmd.Name.SET_WINDOW_SIZE, post('/session/:sessionId/window/current/size')],
164164
[cmd.Name.SWITCH_TO_FRAME, post('/session/:sessionId/frame')],
165165
[cmd.Name.GET_PAGE_SOURCE, get('/session/:sessionId/source')],
166166
[cmd.Name.GET_TITLE, get('/session/:sessionId/title')],
@@ -196,6 +196,14 @@ const COMMAND_MAP = new Map([
196196
]);
197197

198198

199+
/** @const {!Map<string, {method: string, path: string}>} */
200+
const W3C_COMMAND_MAP = new Map([
201+
[cmd.Name.GET_WINDOW_SIZE, get('/session/:sessionId/window/size')],
202+
[cmd.Name.SET_WINDOW_SIZE, post('/session/:sessionId/window/size')],
203+
[cmd.Name.MAXIMIZE_WINDOW, post('/session/:sessionId/window/maximize')],
204+
]);
205+
206+
199207
/**
200208
* A basic HTTP client used to send messages to a remote end.
201209
*/
@@ -377,6 +385,15 @@ function sendRequest(options, onOk, onError, opt_data, opt_proxy) {
377385

378386
/**
379387
* A command executor that communicates with the server using HTTP + JSON.
388+
*
389+
* By default, each instance of this class will use the legacy wire protocol
390+
* from [Selenium project][json]. The executor will automatically switch to the
391+
* [W3C wire protocol][w3c] if the remote end returns a compliant response to
392+
* a new session command.
393+
*
394+
* [json]: https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol
395+
* [w3c]: https://w3c.github.io/webdriver/webdriver-spec.html
396+
*
380397
* @implements {cmd.Executor}
381398
*/
382399
class Executor {
@@ -388,6 +405,15 @@ class Executor {
388405
/** @private {!HttpClient} */
389406
this.client_ = client;
390407

408+
/**
409+
* Whether this executor should use the W3C wire protocol. The executor
410+
* will automatically switch if the remote end sends a compliant response
411+
* to a new session command, however, this property may be directly set to
412+
* `true` to force the executor into W3C mode.
413+
* @type {boolean}
414+
*/
415+
this.w3c = false;
416+
391417
/** @private {Map<string, {method: string, path: string}>} */
392418
this.customCommands_ = null;
393419

@@ -419,6 +445,7 @@ class Executor {
419445
execute(command) {
420446
let resource =
421447
(this.customCommands_ && this.customCommands_.get(command.getName()))
448+
|| (this.w3c && W3C_COMMAND_MAP.get(command.getName()))
422449
|| COMMAND_MAP.get(command.getName());
423450
if (!resource) {
424451
throw new error.UnknownCommandError(
@@ -431,19 +458,26 @@ class Executor {
431458

432459
let log = this.log_;
433460
log.finer(() => '>>>\n' + request);
434-
return this.client_.send(request).then(function(response) {
461+
return this.client_.send(request).then(response => {
435462
log.finer(() => '<<<\n' + response);
436463

437-
let value = parseHttpResponse(/** @type {!HttpResponse} */ (response));
438-
let isResponseObj = (value && typeof value === 'object');
464+
let parsed =
465+
parseHttpResponse(/** @type {!HttpResponse} */ (response), this.w3c);
466+
439467
if (command.getName() === cmd.Name.NEW_SESSION) {
440-
if (!isResponseObj) {
468+
if (!parsed || !parsed['sessionId']) {
441469
throw new error.WebDriverError(
442470
'Unable to parse new session response: ' + response.body);
443471
}
444-
return new Session(value['sessionId'], value['value']);
472+
473+
// The remote end is a W3C compliant server if there is no `status`
474+
// field in the response.
475+
this.w3c = this.w3c || !('status' in parsed);
476+
477+
return new Session(parsed['sessionId'], parsed['value']);
445478
}
446-
return isResponseObj ? value['value'] : value;
479+
480+
return parsed ? (parsed['value'] || null) : parsed;
447481
});
448482
}
449483
}
@@ -466,29 +500,46 @@ function tryParse(str) {
466500
* Callback used to parse {@link HttpResponse} objects from a
467501
* {@link HttpClient}.
468502
* @param {!HttpResponse} httpResponse The HTTP response to parse.
469-
* @return {!Object} The parsed response.
503+
* @param {boolean} w3c Whether the response should be processed using the
504+
* W3C wire protocol.
505+
* @return {{value: ?}} The parsed response.
506+
* @throws {WebDriverError} If the HTTP response is an error.
470507
*/
471-
function parseHttpResponse(httpResponse) {
508+
function parseHttpResponse(httpResponse, w3c) {
472509
let parsed = tryParse(httpResponse.body);
473510
if (parsed !== undefined) {
474-
error.checkLegacyResponse(parsed);
475-
return parsed;
511+
if (w3c) {
512+
if (httpResponse.status > 399) {
513+
error.throwDecodedError(parsed);
514+
}
515+
516+
if (httpResponse.status < 200) {
517+
// This should never happen, but throw the raw response so
518+
// users report it.
519+
throw error.WebDriverError(
520+
`Unexpected HTTP response:\n${httpResponse}`);
521+
}
522+
} else {
523+
error.checkLegacyResponse(parsed);
524+
}
525+
526+
if (!parsed || typeof parsed !== 'object') {
527+
parsed = {value: parsed};
528+
}
529+
return parsed
476530
}
477531

478532
let value = httpResponse.body.replace(/\r\n/g, '\n');
479-
if (!value) {
480-
return null;
481-
}
482533

483-
// 404 represents an unknown command; anything else is a generic unknown
534+
// 404 represents an unknown command; anything else > 399 is a generic unknown
484535
// error.
485536
if (httpResponse.status == 404) {
486537
throw new error.UnsupportedOperationError(value);
487538
} else if (httpResponse.status >= 400) {
488539
throw new error.WebDriverError(value);
489540
}
490541

491-
return value;
542+
return value ? {value: value} : null;
492543
}
493544

494545

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1861,7 +1861,19 @@ class WebElement {
18611861
// ignore the jsdoc and give us a number (which ends up causing problems on
18621862
// the server, which requires strings).
18631863
let keys = promise.all(Array.prototype.slice.call(arguments, 0)).
1864-
then(keys => keys.map(String));
1864+
then(keys => {
1865+
let ret = [];
1866+
keys.forEach(key => {
1867+
if (typeof key !== 'string') {
1868+
key = String(key);
1869+
}
1870+
1871+
// The W3C protocol requires keys to be specified as an array where
1872+
// each element is a single key.
1873+
ret.push.apply(ret, key.split(''));
1874+
});
1875+
return ret;
1876+
});
18651877
if (!this.driver_.fileDetector_) {
18661878
return this.schedule_(
18671879
new command.Command(command.Name.SEND_KEYS_TO_ELEMENT).

javascript/node/selenium-webdriver/test/error_test.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,79 @@ describe('error', function() {
7777
}
7878
});
7979

80+
describe('throwDecodedError', function() {
81+
it('defaults to WebDriverError if type is unrecognized', function() {
82+
assert.throws(
83+
() => error.throwDecodedError({error: 'foo', message: 'hi there'}),
84+
(e) => {
85+
assert.equal(e.constructor, error.WebDriverError);
86+
assert.equal(e.code, error.ErrorCode.UNKNOWN_ERROR);
87+
return true;
88+
});
89+
});
90+
91+
it('throws generic error if encoded data is not valid', function() {
92+
assert.throws(
93+
() => error.throwDecodedError({error: 123, message: 'abc123'}),
94+
(e) => {
95+
assert.strictEqual(e.constructor, error.WebDriverError);
96+
return true;
97+
});
98+
99+
assert.throws(
100+
() => error.throwDecodedError('null'),
101+
(e) => {
102+
assert.strictEqual(e.constructor, error.WebDriverError);
103+
return true;
104+
});
105+
106+
assert.throws(
107+
() => error.throwDecodedError(''),
108+
(e) => {
109+
assert.strictEqual(e.constructor, error.WebDriverError);
110+
return true;
111+
});
112+
});
113+
114+
test('unknown error', error.WebDriverError);
115+
test('element not selectable', error.ElementNotSelectableError);
116+
test('element not visible', error.ElementNotVisibleError);
117+
test('invalid argument', error.InvalidArgumentError);
118+
test('invalid cookie domain', error.InvalidCookieDomainError);
119+
test('invalid element coordinates', error.InvalidElementCoordinatesError);
120+
test('invalid element state', error.InvalidElementStateError);
121+
test('invalid selector', error.InvalidSelectorError);
122+
test('invalid session id', error.InvalidSessionIdError);
123+
test('javascript error', error.JavascriptError);
124+
test('move target out of bounds', error.MoveTargetOutOfBoundsError);
125+
test('no such alert', error.NoSuchAlertError);
126+
test('no such element', error.NoSuchElementError);
127+
test('no such frame', error.NoSuchFrameError);
128+
test('no such window', error.NoSuchWindowError);
129+
test('script timeout', error.ScriptTimeoutError);
130+
test('session not created', error.SessionNotCreatedError);
131+
test('stale element reference', error.StaleElementReferenceError);
132+
test('timeout', error.TimeoutError);
133+
test('unable to set cookie', error.UnableToSetCookieError);
134+
test('unable to capture screen', error.UnableToCaptureScreenError);
135+
test('unexpected alert open', error.UnexpectedAlertOpenError);
136+
test('unknown command', error.UnknownCommandError);
137+
test('unknown method', error.UnknownMethodError);
138+
test('unsupported operation', error.UnsupportedOperationError);
139+
140+
function test(status, expectedType) {
141+
it(`"${status}" => ${expectedType.name}`, function() {
142+
assert.throws(
143+
() => error.throwDecodedError({error: status, message: 'oops'}),
144+
(e) => {
145+
assert.strictEqual(e.constructor, expectedType);
146+
assert.strictEqual(e.message, 'oops');
147+
return true;
148+
});
149+
});
150+
}
151+
});
152+
80153
describe('checkLegacyResponse', function() {
81154
it('does not throw for success', function() {
82155
let resp = {status: error.ErrorCode.SUCCESS};

0 commit comments

Comments
 (0)