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 | /**
|
19 | * @fileoverview The heart of the WebDriver JavaScript API.
|
20 | */
|
21 |
|
22 | ;
|
23 |
|
24 | const actions = require('./actions');
|
25 | const by = require('./by');
|
26 | const Capabilities = require('./capabilities').Capabilities;
|
27 | const command = require('./command');
|
28 | const error = require('./error');
|
29 | const input = require('./input');
|
30 | const logging = require('./logging');
|
31 | const {Session} = require('./session');
|
32 | const Symbols = require('./symbols');
|
33 | const promise = require('./promise');
|
34 |
|
35 |
|
36 | /**
|
37 | * Defines a condition for use with WebDriver's {@linkplain WebDriver#wait wait
|
38 | * command}.
|
39 | *
|
40 | * @template OUT
|
41 | */
|
42 | class Condition {
|
43 | /**
|
44 | * @param {string} message A descriptive error message. Should complete the
|
45 | * sentence "Waiting [...]"
|
46 | * @param {function(!WebDriver): OUT} fn The condition function to
|
47 | * evaluate on each iteration of the wait loop.
|
48 | */
|
49 | constructor(message, fn) {
|
50 | /** @private {string} */
|
51 | this.description_ = 'Waiting ' + message;
|
52 |
|
53 | /** @type {function(!WebDriver): OUT} */
|
54 | this.fn = fn;
|
55 | }
|
56 |
|
57 | /** @return {string} A description of this condition. */
|
58 | description() {
|
59 | return this.description_;
|
60 | }
|
61 | }
|
62 |
|
63 |
|
64 | /**
|
65 | * Defines a condition that will result in a {@link WebElement}.
|
66 | *
|
67 | * @extends {Condition<!(WebElement|IThenable<!WebElement>)>}
|
68 | */
|
69 | class WebElementCondition extends Condition {
|
70 | /**
|
71 | * @param {string} message A descriptive error message. Should complete the
|
72 | * sentence "Waiting [...]"
|
73 | * @param {function(!WebDriver): !(WebElement|IThenable<!WebElement>)}
|
74 | * fn The condition function to evaluate on each iteration of the wait
|
75 | * loop.
|
76 | */
|
77 | constructor(message, fn) {
|
78 | super(message, fn);
|
79 | }
|
80 | }
|
81 |
|
82 |
|
83 | //////////////////////////////////////////////////////////////////////////////
|
84 | //
|
85 | // WebDriver
|
86 | //
|
87 | //////////////////////////////////////////////////////////////////////////////
|
88 |
|
89 |
|
90 | /**
|
91 | * Translates a command to its wire-protocol representation before passing it
|
92 | * to the given `executor` for execution.
|
93 | * @param {!command.Executor} executor The executor to use.
|
94 | * @param {!command.Command} command The command to execute.
|
95 | * @return {!Promise} A promise that will resolve with the command response.
|
96 | */
|
97 | function executeCommand(executor, command) {
|
98 | return toWireValue(command.getParameters()).
|
99 | then(function(parameters) {
|
100 | command.setParameters(parameters);
|
101 | return executor.execute(command);
|
102 | });
|
103 | }
|
104 |
|
105 |
|
106 | /**
|
107 | * Converts an object to its JSON representation in the WebDriver wire protocol.
|
108 | * When converting values of type object, the following steps will be taken:
|
109 | * <ol>
|
110 | * <li>if the object is a WebElement, the return value will be the element's
|
111 | * server ID
|
112 | * <li>if the object defines a {@link Symbols.serialize} method, this algorithm
|
113 | * will be recursively applied to the object's serialized representation
|
114 | * <li>if the object provides a "toJSON" function, this algorithm will
|
115 | * recursively be applied to the result of that function
|
116 | * <li>otherwise, the value of each key will be recursively converted according
|
117 | * to the rules above.
|
118 | * </ol>
|
119 | *
|
120 | * @param {*} obj The object to convert.
|
121 | * @return {!Promise<?>} A promise that will resolve to the input value's JSON
|
122 | * representation.
|
123 | */
|
124 | function toWireValue(obj) {
|
125 | if (promise.isPromise(obj)) {
|
126 | return Promise.resolve(obj).then(toWireValue);
|
127 | }
|
128 | return Promise.resolve(convertValue(obj));
|
129 | }
|
130 |
|
131 |
|
132 | function convertValue(value) {
|
133 | if (value === void 0 || value === null) {
|
134 | return value;
|
135 | }
|
136 |
|
137 | if (typeof value === 'boolean'
|
138 | || typeof value === 'number'
|
139 | || typeof value === 'string') {
|
140 | return value;
|
141 | }
|
142 |
|
143 | if (Array.isArray(value)) {
|
144 | return convertKeys(value);
|
145 | }
|
146 |
|
147 | if (typeof value === 'function') {
|
148 | return '' + value;
|
149 | }
|
150 |
|
151 | if (typeof value[Symbols.serialize] === 'function') {
|
152 | return toWireValue(value[Symbols.serialize]());
|
153 | } else if (typeof value.toJSON === 'function') {
|
154 | return toWireValue(value.toJSON());
|
155 | }
|
156 | return convertKeys(value);
|
157 | }
|
158 |
|
159 |
|
160 | function convertKeys(obj) {
|
161 | const isArray = Array.isArray(obj);
|
162 | const numKeys = isArray ? obj.length : Object.keys(obj).length;
|
163 | const ret = isArray ? new Array(numKeys) : {};
|
164 | if (!numKeys) {
|
165 | return Promise.resolve(ret);
|
166 | }
|
167 |
|
168 | let numResolved = 0;
|
169 |
|
170 | function forEachKey(obj, fn) {
|
171 | if (Array.isArray(obj)) {
|
172 | for (let i = 0, n = obj.length; i < n; i++) {
|
173 | fn(obj[i], i);
|
174 | }
|
175 | } else {
|
176 | for (let key in obj) {
|
177 | fn(obj[key], key);
|
178 | }
|
179 | }
|
180 | }
|
181 |
|
182 | return new Promise(function(done, reject) {
|
183 | forEachKey(obj, function(value, key) {
|
184 | if (promise.isPromise(value)) {
|
185 | value.then(toWireValue).then(setValue, reject);
|
186 | } else {
|
187 | value = convertValue(value);
|
188 | if (promise.isPromise(value)) {
|
189 | value.then(toWireValue).then(setValue, reject);
|
190 | } else {
|
191 | setValue(value);
|
192 | }
|
193 | }
|
194 |
|
195 | function setValue(value) {
|
196 | ret[key] = value;
|
197 | maybeFulfill();
|
198 | }
|
199 | });
|
200 |
|
201 | function maybeFulfill() {
|
202 | if (++numResolved === numKeys) {
|
203 | done(ret);
|
204 | }
|
205 | }
|
206 | });
|
207 | }
|
208 |
|
209 |
|
210 | /**
|
211 | * Converts a value from its JSON representation according to the WebDriver wire
|
212 | * protocol. Any JSON object that defines a WebElement ID will be decoded to a
|
213 | * {@link WebElement} object. All other values will be passed through as is.
|
214 | *
|
215 | * @param {!WebDriver} driver The driver to use as the parent of any unwrapped
|
216 | * {@link WebElement} values.
|
217 | * @param {*} value The value to convert.
|
218 | * @return {*} The converted value.
|
219 | */
|
220 | function fromWireValue(driver, value) {
|
221 | if (Array.isArray(value)) {
|
222 | value = value.map(v => fromWireValue(driver, v));
|
223 | } else if (WebElement.isId(value)) {
|
224 | let id = WebElement.extractId(value);
|
225 | value = new WebElement(driver, id);
|
226 | } else if (value && typeof value === 'object') {
|
227 | let result = {};
|
228 | for (let key in value) {
|
229 | if (value.hasOwnProperty(key)) {
|
230 | result[key] = fromWireValue(driver, value[key]);
|
231 | }
|
232 | }
|
233 | value = result;
|
234 | }
|
235 | return value;
|
236 | }
|
237 |
|
238 |
|
239 | /**
|
240 | * Structural interface for a WebDriver client.
|
241 | *
|
242 | * @record
|
243 | */
|
244 | class IWebDriver {
|
245 |
|
246 | /** @return {!promise.ControlFlow} The control flow used by this instance. */
|
247 | controlFlow() {}
|
248 |
|
249 | /**
|
250 | * Schedules a {@link command.Command} to be executed by this driver's
|
251 | * {@link command.Executor}.
|
252 | *
|
253 | * @param {!command.Command} command The command to schedule.
|
254 | * @param {string} description A description of the command for debugging.
|
255 | * @return {!promise.Thenable<T>} A promise that will be resolved
|
256 | * with the command result.
|
257 | * @template T
|
258 | */
|
259 | schedule(command, description) {}
|
260 |
|
261 | /**
|
262 | * Sets the {@linkplain input.FileDetector file detector} that should be
|
263 | * used with this instance.
|
264 | * @param {input.FileDetector} detector The detector to use or {@code null}.
|
265 | */
|
266 | setFileDetector(detector) {}
|
267 |
|
268 | /**
|
269 | * @return {!command.Executor} The command executor used by this instance.
|
270 | */
|
271 | getExecutor() {}
|
272 |
|
273 | /**
|
274 | * @return {!promise.Thenable<!Session>} A promise for this client's session.
|
275 | */
|
276 | getSession() {}
|
277 |
|
278 | /**
|
279 | * @return {!promise.Thenable<!Capabilities>} A promise that will resolve with
|
280 | * the this instance's capabilities.
|
281 | */
|
282 | getCapabilities() {}
|
283 |
|
284 | /**
|
285 | * Terminates the browser session. After calling quit, this instance will be
|
286 | * invalidated and may no longer be used to issue commands against the
|
287 | * browser.
|
288 | *
|
289 | * @return {!promise.Thenable<void>} A promise that will be resolved when the
|
290 | * command has completed.
|
291 | */
|
292 | quit() {}
|
293 |
|
294 | /**
|
295 | * Creates a new action sequence using this driver. The sequence will not be
|
296 | * scheduled for execution until {@link actions.ActionSequence#perform} is
|
297 | * called. Example:
|
298 | *
|
299 | * driver.actions().
|
300 | * mouseDown(element1).
|
301 | * mouseMove(element2).
|
302 | * mouseUp().
|
303 | * perform();
|
304 | *
|
305 | * @return {!actions.ActionSequence} A new action sequence for this instance.
|
306 | */
|
307 | actions() {}
|
308 |
|
309 | /**
|
310 | * Creates a new touch sequence using this driver. The sequence will not be
|
311 | * scheduled for execution until {@link actions.TouchSequence#perform} is
|
312 | * called. Example:
|
313 | *
|
314 | * driver.touchActions().
|
315 | * tap(element1).
|
316 | * doubleTap(element2).
|
317 | * perform();
|
318 | *
|
319 | * @return {!actions.TouchSequence} A new touch sequence for this instance.
|
320 | */
|
321 | touchActions() {}
|
322 |
|
323 | /**
|
324 | * Schedules a command to execute JavaScript in the context of the currently
|
325 | * selected frame or window. The script fragment will be executed as the body
|
326 | * of an anonymous function. If the script is provided as a function object,
|
327 | * that function will be converted to a string for injection into the target
|
328 | * window.
|
329 | *
|
330 | * Any arguments provided in addition to the script will be included as script
|
331 | * arguments and may be referenced using the {@code arguments} object.
|
332 | * Arguments may be a boolean, number, string, or {@linkplain WebElement}.
|
333 | * Arrays and objects may also be used as script arguments as long as each item
|
334 | * adheres to the types previously mentioned.
|
335 | *
|
336 | * The script may refer to any variables accessible from the current window.
|
337 | * Furthermore, the script will execute in the window's context, thus
|
338 | * {@code document} may be used to refer to the current document. Any local
|
339 | * variables will not be available once the script has finished executing,
|
340 | * though global variables will persist.
|
341 | *
|
342 | * If the script has a return value (i.e. if the script contains a return
|
343 | * statement), then the following steps will be taken for resolving this
|
344 | * functions return value:
|
345 | *
|
346 | * - For a HTML element, the value will resolve to a {@linkplain WebElement}
|
347 | * - Null and undefined return values will resolve to null</li>
|
348 | * - Booleans, numbers, and strings will resolve as is</li>
|
349 | * - Functions will resolve to their string representation</li>
|
350 | * - For arrays and objects, each member item will be converted according to
|
351 | * the rules above
|
352 | *
|
353 | * @param {!(string|Function)} script The script to execute.
|
354 | * @param {...*} var_args The arguments to pass to the script.
|
355 | * @return {!promise.Thenable<T>} A promise that will resolve to the
|
356 | * scripts return value.
|
357 | * @template T
|
358 | */
|
359 | executeScript(script, var_args) {}
|
360 |
|
361 | /**
|
362 | * Schedules a command to execute asynchronous JavaScript in the context of the
|
363 | * currently selected frame or window. The script fragment will be executed as
|
364 | * the body of an anonymous function. If the script is provided as a function
|
365 | * object, that function will be converted to a string for injection into the
|
366 | * target window.
|
367 | *
|
368 | * Any arguments provided in addition to the script will be included as script
|
369 | * arguments and may be referenced using the {@code arguments} object.
|
370 | * Arguments may be a boolean, number, string, or {@code WebElement}.
|
371 | * Arrays and objects may also be used as script arguments as long as each item
|
372 | * adheres to the types previously mentioned.
|
373 | *
|
374 | * Unlike executing synchronous JavaScript with {@link #executeScript},
|
375 | * scripts executed with this function must explicitly signal they are finished
|
376 | * by invoking the provided callback. This callback will always be injected
|
377 | * into the executed function as the last argument, and thus may be referenced
|
378 | * with {@code arguments[arguments.length - 1]}. The following steps will be
|
379 | * taken for resolving this functions return value against the first argument
|
380 | * to the script's callback function:
|
381 | *
|
382 | * - For a HTML element, the value will resolve to a
|
383 | * {@link WebElement}
|
384 | * - Null and undefined return values will resolve to null
|
385 | * - Booleans, numbers, and strings will resolve as is
|
386 | * - Functions will resolve to their string representation
|
387 | * - For arrays and objects, each member item will be converted according to
|
388 | * the rules above
|
389 | *
|
390 | * __Example #1:__ Performing a sleep that is synchronized with the currently
|
391 | * selected window:
|
392 | *
|
393 | * var start = new Date().getTime();
|
394 | * driver.executeAsyncScript(
|
395 | * 'window.setTimeout(arguments[arguments.length - 1], 500);').
|
396 | * then(function() {
|
397 | * console.log(
|
398 | * 'Elapsed time: ' + (new Date().getTime() - start) + ' ms');
|
399 | * });
|
400 | *
|
401 | * __Example #2:__ Synchronizing a test with an AJAX application:
|
402 | *
|
403 | * var button = driver.findElement(By.id('compose-button'));
|
404 | * button.click();
|
405 | * driver.executeAsyncScript(
|
406 | * 'var callback = arguments[arguments.length - 1];' +
|
407 | * 'mailClient.getComposeWindowWidget().onload(callback);');
|
408 | * driver.switchTo().frame('composeWidget');
|
409 | * driver.findElement(By.id('to')).sendKeys('dog@example.com');
|
410 | *
|
411 | * __Example #3:__ Injecting a XMLHttpRequest and waiting for the result. In
|
412 | * this example, the inject script is specified with a function literal. When
|
413 | * using this format, the function is converted to a string for injection, so it
|
414 | * should not reference any symbols not defined in the scope of the page under
|
415 | * test.
|
416 | *
|
417 | * driver.executeAsyncScript(function() {
|
418 | * var callback = arguments[arguments.length - 1];
|
419 | * var xhr = new XMLHttpRequest();
|
420 | * xhr.open("GET", "/resource/data.json", true);
|
421 | * xhr.onreadystatechange = function() {
|
422 | * if (xhr.readyState == 4) {
|
423 | * callback(xhr.responseText);
|
424 | * }
|
425 | * };
|
426 | * xhr.send('');
|
427 | * }).then(function(str) {
|
428 | * console.log(JSON.parse(str)['food']);
|
429 | * });
|
430 | *
|
431 | * @param {!(string|Function)} script The script to execute.
|
432 | * @param {...*} var_args The arguments to pass to the script.
|
433 | * @return {!promise.Thenable<T>} A promise that will resolve to the
|
434 | * scripts return value.
|
435 | * @template T
|
436 | */
|
437 | executeAsyncScript(script, var_args) {}
|
438 |
|
439 | /**
|
440 | * Schedules a command to execute a custom function.
|
441 | * @param {function(...): (T|IThenable<T>)} fn The function to execute.
|
442 | * @param {Object=} opt_scope The object in whose scope to execute the function.
|
443 | * @param {...*} var_args Any arguments to pass to the function.
|
444 | * @return {!promise.Thenable<T>} A promise that will be resolved'
|
445 | * with the function's result.
|
446 | * @template T
|
447 | */
|
448 | call(fn, opt_scope, var_args) {}
|
449 |
|
450 | /**
|
451 | * Schedules a command to wait for a condition to hold. The condition may be
|
452 | * specified by a {@link Condition}, as a custom function, or as any
|
453 | * promise-like thenable.
|
454 | *
|
455 | * For a {@link Condition} or function, the wait will repeatedly
|
456 | * evaluate the condition until it returns a truthy value. If any errors occur
|
457 | * while evaluating the condition, they will be allowed to propagate. In the
|
458 | * event a condition returns a {@link promise.Promise promise}, the polling
|
459 | * loop will wait for it to be resolved and use the resolved value for whether
|
460 | * the condition has been satisified. Note the resolution time for a promise
|
461 | * is factored into whether a wait has timed out.
|
462 | *
|
463 | * Note, if the provided condition is a {@link WebElementCondition}, then
|
464 | * the wait will return a {@link WebElementPromise} that will resolve to the
|
465 | * element that satisified the condition.
|
466 | *
|
467 | * _Example:_ waiting up to 10 seconds for an element to be present on the
|
468 | * page.
|
469 | *
|
470 | * var button = driver.wait(until.elementLocated(By.id('foo')), 10000);
|
471 | * button.click();
|
472 | *
|
473 | * This function may also be used to block the command flow on the resolution
|
474 | * of any thenable promise object. When given a promise, the command will
|
475 | * simply wait for its resolution before completing. A timeout may be provided
|
476 | * to fail the command if the promise does not resolve before the timeout
|
477 | * expires.
|
478 | *
|
479 | * _Example:_ Suppose you have a function, `startTestServer`, that returns a
|
480 | * promise for when a server is ready for requests. You can block a WebDriver
|
481 | * client on this promise with:
|
482 | *
|
483 | * var started = startTestServer();
|
484 | * driver.wait(started, 5 * 1000, 'Server should start within 5 seconds');
|
485 | * driver.get(getServerUrl());
|
486 | *
|
487 | * @param {!(IThenable<T>|
|
488 | * Condition<T>|
|
489 | * function(!WebDriver): T)} condition The condition to
|
490 | * wait on, defined as a promise, condition object, or a function to
|
491 | * evaluate as a condition.
|
492 | * @param {number=} opt_timeout How long to wait for the condition to be true.
|
493 | * @param {string=} opt_message An optional message to use if the wait times
|
494 | * out.
|
495 | * @return {!(promise.Thenable<T>|WebElementPromise)} A promise that will be
|
496 | * resolved with the first truthy value returned by the condition
|
497 | * function, or rejected if the condition times out. If the input
|
498 | * input condition is an instance of a {@link WebElementCondition},
|
499 | * the returned value will be a {@link WebElementPromise}.
|
500 | * @template T
|
501 | */
|
502 | wait(condition, opt_timeout, opt_message) {}
|
503 |
|
504 | /**
|
505 | * Schedules a command to make the driver sleep for the given amount of time.
|
506 | * @param {number} ms The amount of time, in milliseconds, to sleep.
|
507 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
508 | * when the sleep has finished.
|
509 | */
|
510 | sleep(ms) {}
|
511 |
|
512 | /**
|
513 | * Schedules a command to retrieve the current window handle.
|
514 | * @return {!promise.Thenable<string>} A promise that will be
|
515 | * resolved with the current window handle.
|
516 | */
|
517 | getWindowHandle() {}
|
518 |
|
519 | /**
|
520 | * Schedules a command to retrieve the current list of available window handles.
|
521 | * @return {!promise.Thenable<!Array<string>>} A promise that will
|
522 | * be resolved with an array of window handles.
|
523 | */
|
524 | getAllWindowHandles() {}
|
525 |
|
526 | /**
|
527 | * Schedules a command to retrieve the current page's source. The page source
|
528 | * returned is a representation of the underlying DOM: do not expect it to be
|
529 | * formatted or escaped in the same way as the response sent from the web
|
530 | * server.
|
531 | * @return {!promise.Thenable<string>} A promise that will be
|
532 | * resolved with the current page source.
|
533 | */
|
534 | getPageSource() {}
|
535 |
|
536 | /**
|
537 | * Schedules a command to close the current window.
|
538 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
539 | * when this command has completed.
|
540 | */
|
541 | close() {}
|
542 |
|
543 | /**
|
544 | * Schedules a command to navigate to the given URL.
|
545 | * @param {string} url The fully qualified URL to open.
|
546 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
547 | * when the document has finished loading.
|
548 | */
|
549 | get(url) {}
|
550 |
|
551 | /**
|
552 | * Schedules a command to retrieve the URL of the current page.
|
553 | * @return {!promise.Thenable<string>} A promise that will be
|
554 | * resolved with the current URL.
|
555 | */
|
556 | getCurrentUrl() {}
|
557 |
|
558 | /**
|
559 | * Schedules a command to retrieve the current page's title.
|
560 | * @return {!promise.Thenable<string>} A promise that will be
|
561 | * resolved with the current page's title.
|
562 | */
|
563 | getTitle() {}
|
564 |
|
565 | /**
|
566 | * Schedule a command to find an element on the page. If the element cannot be
|
567 | * found, a {@link bot.ErrorCode.NO_SUCH_ELEMENT} result will be returned
|
568 | * by the driver. Unlike other commands, this error cannot be suppressed. In
|
569 | * other words, scheduling a command to find an element doubles as an assert
|
570 | * that the element is present on the page. To test whether an element is
|
571 | * present on the page, use {@link #findElements}:
|
572 | *
|
573 | * driver.findElements(By.id('foo'))
|
574 | * .then(found => console.log('Element found? %s', !!found.length));
|
575 | *
|
576 | * The search criteria for an element may be defined using one of the
|
577 | * factories in the {@link webdriver.By} namespace, or as a short-hand
|
578 | * {@link webdriver.By.Hash} object. For example, the following two statements
|
579 | * are equivalent:
|
580 | *
|
581 | * var e1 = driver.findElement(By.id('foo'));
|
582 | * var e2 = driver.findElement({id:'foo'});
|
583 | *
|
584 | * You may also provide a custom locator function, which takes as input this
|
585 | * instance and returns a {@link WebElement}, or a promise that will resolve
|
586 | * to a WebElement. If the returned promise resolves to an array of
|
587 | * WebElements, WebDriver will use the first element. For example, to find the
|
588 | * first visible link on a page, you could write:
|
589 | *
|
590 | * var link = driver.findElement(firstVisibleLink);
|
591 | *
|
592 | * function firstVisibleLink(driver) {
|
593 | * var links = driver.findElements(By.tagName('a'));
|
594 | * return promise.filter(links, function(link) {
|
595 | * return link.isDisplayed();
|
596 | * });
|
597 | * }
|
598 | *
|
599 | * @param {!(by.By|Function)} locator The locator to use.
|
600 | * @return {!WebElementPromise} A WebElement that can be used to issue
|
601 | * commands against the located element. If the element is not found, the
|
602 | * element will be invalidated and all scheduled commands aborted.
|
603 | */
|
604 | findElement(locator) {}
|
605 |
|
606 | /**
|
607 | * Schedule a command to search for multiple elements on the page.
|
608 | *
|
609 | * @param {!(by.By|Function)} locator The locator to use.
|
610 | * @return {!promise.Thenable<!Array<!WebElement>>} A
|
611 | * promise that will resolve to an array of WebElements.
|
612 | */
|
613 | findElements(locator) {}
|
614 |
|
615 | /**
|
616 | * Schedule a command to take a screenshot. The driver makes a best effort to
|
617 | * return a screenshot of the following, in order of preference:
|
618 | *
|
619 | * 1. Entire page
|
620 | * 2. Current window
|
621 | * 3. Visible portion of the current frame
|
622 | * 4. The entire display containing the browser
|
623 | *
|
624 | * @return {!promise.Thenable<string>} A promise that will be
|
625 | * resolved to the screenshot as a base-64 encoded PNG.
|
626 | */
|
627 | takeScreenshot() {}
|
628 |
|
629 | /**
|
630 | * @return {!Options} The options interface for this instance.
|
631 | */
|
632 | manage() {}
|
633 |
|
634 | /**
|
635 | * @return {!Navigation} The navigation interface for this instance.
|
636 | */
|
637 | navigate() {}
|
638 |
|
639 | /**
|
640 | * @return {!TargetLocator} The target locator interface for this
|
641 | * instance.
|
642 | */
|
643 | switchTo() {}
|
644 | }
|
645 |
|
646 |
|
647 | /**
|
648 | * Each WebDriver instance provides automated control over a browser session.
|
649 | *
|
650 | * @implements {IWebDriver}
|
651 | */
|
652 | class WebDriver {
|
653 | /**
|
654 | * @param {!(Session|IThenable<!Session>)} session Either a known session or a
|
655 | * promise that will be resolved to a session.
|
656 | * @param {!command.Executor} executor The executor to use when sending
|
657 | * commands to the browser.
|
658 | * @param {promise.ControlFlow=} opt_flow The flow to
|
659 | * schedule commands through. Defaults to the active flow object.
|
660 | * @param {(function(this: void): ?)=} opt_onQuit A function to call, if any,
|
661 | * when the session is terminated.
|
662 | */
|
663 | constructor(session, executor, opt_flow, opt_onQuit) {
|
664 | /** @private {!promise.ControlFlow} */
|
665 | this.flow_ = opt_flow || promise.controlFlow();
|
666 |
|
667 | /** @private {!promise.Thenable<!Session>} */
|
668 | this.session_ = this.flow_.promise(resolve => resolve(session));
|
669 |
|
670 | /** @private {!command.Executor} */
|
671 | this.executor_ = executor;
|
672 |
|
673 | /** @private {input.FileDetector} */
|
674 | this.fileDetector_ = null;
|
675 |
|
676 | /** @private @const {(function(this: void): ?|undefined)} */
|
677 | this.onQuit_ = opt_onQuit;
|
678 | }
|
679 |
|
680 | /**
|
681 | * Creates a new WebDriver client for an existing session.
|
682 | * @param {!command.Executor} executor Command executor to use when querying
|
683 | * for session details.
|
684 | * @param {string} sessionId ID of the session to attach to.
|
685 | * @param {promise.ControlFlow=} opt_flow The control flow all
|
686 | * driver commands should execute under. Defaults to the
|
687 | * {@link promise.controlFlow() currently active} control flow.
|
688 | * @return {!WebDriver} A new client for the specified session.
|
689 | */
|
690 | static attachToSession(executor, sessionId, opt_flow) {
|
691 | let flow = opt_flow || promise.controlFlow();
|
692 | let cmd = new command.Command(command.Name.DESCRIBE_SESSION)
|
693 | .setParameter('sessionId', sessionId);
|
694 | let session = flow.execute(
|
695 | () => executeCommand(executor, cmd).catch(err => {
|
696 | // The DESCRIBE_SESSION command is not supported by the W3C spec, so
|
697 | // if we get back an unknown command, just return a session with
|
698 | // unknown capabilities.
|
699 | if (err instanceof error.UnknownCommandError) {
|
700 | return new Session(sessionId, new Capabilities);
|
701 | }
|
702 | throw err;
|
703 | }),
|
704 | 'WebDriver.attachToSession()');
|
705 | return new WebDriver(session, executor, flow);
|
706 | }
|
707 |
|
708 | /**
|
709 | * Creates a new WebDriver session.
|
710 | *
|
711 | * By default, the requested session `capabilities` are merely "desired" and
|
712 | * the remote end will still create a new session even if it cannot satisfy
|
713 | * all of the requested capabilities. You can query which capabilities a
|
714 | * session actually has using the
|
715 | * {@linkplain #getCapabilities() getCapabilities()} method on the returned
|
716 | * WebDriver instance.
|
717 | *
|
718 | * To define _required capabilities_, provide the `capabilities` as an object
|
719 | * literal with `required` and `desired` keys. The `desired` key may be
|
720 | * omitted if all capabilities are required, and vice versa. If the server
|
721 | * cannot create a session with all of the required capabilities, it will
|
722 | * return an {@linkplain error.SessionNotCreatedError}.
|
723 | *
|
724 | * let required = new Capabilities().set('browserName', 'firefox');
|
725 | * let desired = new Capabilities().set('version', '45');
|
726 | * let driver = WebDriver.createSession(executor, {required, desired});
|
727 | *
|
728 | * This function will always return a WebDriver instance. If there is an error
|
729 | * creating the session, such as the aforementioned SessionNotCreatedError,
|
730 | * the driver will have a rejected {@linkplain #getSession session} promise.
|
731 | * It is recommended that this promise is left _unhandled_ so it will
|
732 | * propagate through the {@linkplain promise.ControlFlow control flow} and
|
733 | * cause subsequent commands to fail.
|
734 | *
|
735 | * let required = Capabilities.firefox();
|
736 | * let driver = WebDriver.createSession(executor, {required});
|
737 | *
|
738 | * // If the createSession operation failed, then this command will also
|
739 | * // also fail, propagating the creation failure.
|
740 | * driver.get('http://www.google.com').catch(e => console.log(e));
|
741 | *
|
742 | * @param {!command.Executor} executor The executor to create the new session
|
743 | * with.
|
744 | * @param {(!Capabilities|
|
745 | * {desired: (Capabilities|undefined),
|
746 | * required: (Capabilities|undefined)})} capabilities The desired
|
747 | * capabilities for the new session.
|
748 | * @param {promise.ControlFlow=} opt_flow The control flow all driver
|
749 | * commands should execute under, including the initial session creation.
|
750 | * Defaults to the {@link promise.controlFlow() currently active}
|
751 | * control flow.
|
752 | * @param {(function(new: WebDriver,
|
753 | * !IThenable<!Session>,
|
754 | * !command.Executor,
|
755 | * promise.ControlFlow=))=} opt_ctor
|
756 | * A reference to the constructor of the specific type of WebDriver client
|
757 | * to instantiate. Will create a vanilla {@linkplain WebDriver} instance
|
758 | * if a constructor is not provided.
|
759 | * @param {(function(this: void): ?)=} opt_onQuit A callback to invoke when
|
760 | * the newly created session is terminated. This should be used to clean
|
761 | * up any resources associated with the session.
|
762 | * @return {!WebDriver} The driver for the newly created session.
|
763 | */
|
764 | static createSession(
|
765 | executor, capabilities, opt_flow, opt_ctor, opt_onQuit) {
|
766 | let flow = opt_flow || promise.controlFlow();
|
767 | let cmd = new command.Command(command.Name.NEW_SESSION);
|
768 |
|
769 | if (capabilities && (capabilities.desired || capabilities.required)) {
|
770 | cmd.setParameter('desiredCapabilities', capabilities.desired);
|
771 | cmd.setParameter('requiredCapabilities', capabilities.required);
|
772 | } else {
|
773 | cmd.setParameter('desiredCapabilities', capabilities);
|
774 | }
|
775 |
|
776 | let session = flow.execute(
|
777 | () => executeCommand(executor, cmd),
|
778 | 'WebDriver.createSession()');
|
779 | if (typeof opt_onQuit === 'function') {
|
780 | session = session.catch(err => {
|
781 | return Promise.resolve(opt_onQuit.call(void 0)).then(_ => {throw err});
|
782 | });
|
783 | }
|
784 | const ctor = opt_ctor || WebDriver;
|
785 | return new ctor(session, executor, flow, opt_onQuit);
|
786 | }
|
787 |
|
788 | /** @override */
|
789 | controlFlow() {
|
790 | return this.flow_;
|
791 | }
|
792 |
|
793 | /** @override */
|
794 | schedule(command, description) {
|
795 | command.setParameter('sessionId', this.session_);
|
796 |
|
797 | // If any of the command parameters are rejected promises, those
|
798 | // rejections may be reported as unhandled before the control flow
|
799 | // attempts to execute the command. To ensure parameters errors
|
800 | // propagate through the command itself, we resolve all of the
|
801 | // command parameters now, but suppress any errors until the ControlFlow
|
802 | // actually executes the command. This addresses scenarios like catching
|
803 | // an element not found error in:
|
804 | //
|
805 | // driver.findElement(By.id('foo')).click().catch(function(e) {
|
806 | // if (e instanceof NoSuchElementError) {
|
807 | // // Do something.
|
808 | // }
|
809 | // });
|
810 | var prepCommand = toWireValue(command.getParameters());
|
811 | prepCommand.catch(function() {});
|
812 |
|
813 | var flow = this.flow_;
|
814 | var executor = this.executor_;
|
815 | return flow.execute(() => {
|
816 | // Retrieve resolved command parameters; any previously suppressed errors
|
817 | // will now propagate up through the control flow as part of the command
|
818 | // execution.
|
819 | return prepCommand.then(function(parameters) {
|
820 | command.setParameters(parameters);
|
821 | return executor.execute(command);
|
822 | }).then(value => fromWireValue(this, value));
|
823 | }, description);
|
824 | }
|
825 |
|
826 | /** @override */
|
827 | setFileDetector(detector) {
|
828 | this.fileDetector_ = detector;
|
829 | }
|
830 |
|
831 | /** @override */
|
832 | getExecutor() {
|
833 | return this.executor_;
|
834 | }
|
835 |
|
836 | /** @override */
|
837 | getSession() {
|
838 | return this.session_;
|
839 | }
|
840 |
|
841 | /** @override */
|
842 | getCapabilities() {
|
843 | return this.session_.then(s => s.getCapabilities());
|
844 | }
|
845 |
|
846 | /** @override */
|
847 | quit() {
|
848 | var result = this.schedule(
|
849 | new command.Command(command.Name.QUIT),
|
850 | 'WebDriver.quit()');
|
851 | // Delete our session ID when the quit command finishes; this will allow us
|
852 | // to throw an error when attemnpting to use a driver post-quit.
|
853 | return /** @type {!promise.Thenable} */(promise.finally(result, () => {
|
854 | this.session_ = this.flow_.promise((_, reject) => {
|
855 | reject(new error.NoSuchSessionError(
|
856 | 'This driver instance does not have a valid session ID ' +
|
857 | '(did you call WebDriver.quit()?) and may no longer be used.'));
|
858 | });
|
859 |
|
860 | // Only want the session rejection to bubble if accessed.
|
861 | this.session_.catch(function() {});
|
862 |
|
863 | if (this.onQuit_) {
|
864 | return this.onQuit_.call(void 0);
|
865 | }
|
866 | }));
|
867 | }
|
868 |
|
869 | /** @override */
|
870 | actions() {
|
871 | return new actions.ActionSequence(this);
|
872 | }
|
873 |
|
874 | /** @override */
|
875 | touchActions() {
|
876 | return new actions.TouchSequence(this);
|
877 | }
|
878 |
|
879 | /** @override */
|
880 | executeScript(script, var_args) {
|
881 | if (typeof script === 'function') {
|
882 | script = 'return (' + script + ').apply(null, arguments);';
|
883 | }
|
884 | let args =
|
885 | arguments.length > 1 ? Array.prototype.slice.call(arguments, 1) : [];
|
886 | return this.schedule(
|
887 | new command.Command(command.Name.EXECUTE_SCRIPT).
|
888 | setParameter('script', script).
|
889 | setParameter('args', args),
|
890 | 'WebDriver.executeScript()');
|
891 | }
|
892 |
|
893 | /** @override */
|
894 | executeAsyncScript(script, var_args) {
|
895 | if (typeof script === 'function') {
|
896 | script = 'return (' + script + ').apply(null, arguments);';
|
897 | }
|
898 | let args = Array.prototype.slice.call(arguments, 1);
|
899 | return this.schedule(
|
900 | new command.Command(command.Name.EXECUTE_ASYNC_SCRIPT).
|
901 | setParameter('script', script).
|
902 | setParameter('args', args),
|
903 | 'WebDriver.executeScript()');
|
904 | }
|
905 |
|
906 | /** @override */
|
907 | call(fn, opt_scope, var_args) {
|
908 | let args = Array.prototype.slice.call(arguments, 2);
|
909 | return this.flow_.execute(function() {
|
910 | return promise.fullyResolved(args).then(function(args) {
|
911 | if (promise.isGenerator(fn)) {
|
912 | args.unshift(fn, opt_scope);
|
913 | return promise.consume.apply(null, args);
|
914 | }
|
915 | return fn.apply(opt_scope, args);
|
916 | });
|
917 | }, 'WebDriver.call(' + (fn.name || 'function') + ')');
|
918 | }
|
919 |
|
920 | /** @override */
|
921 | wait(condition, opt_timeout, opt_message) {
|
922 | if (promise.isPromise(condition)) {
|
923 | return this.flow_.wait(
|
924 | /** @type {!IThenable} */(condition),
|
925 | opt_timeout, opt_message);
|
926 | }
|
927 |
|
928 | var message = opt_message;
|
929 | var fn = /** @type {!Function} */(condition);
|
930 | if (condition instanceof Condition) {
|
931 | message = message || condition.description();
|
932 | fn = condition.fn;
|
933 | }
|
934 |
|
935 | var driver = this;
|
936 | var result = this.flow_.wait(function() {
|
937 | if (promise.isGenerator(fn)) {
|
938 | return promise.consume(fn, null, [driver]);
|
939 | }
|
940 | return fn(driver);
|
941 | }, opt_timeout, message);
|
942 |
|
943 | if (condition instanceof WebElementCondition) {
|
944 | result = new WebElementPromise(this, result.then(function(value) {
|
945 | if (!(value instanceof WebElement)) {
|
946 | throw TypeError(
|
947 | 'WebElementCondition did not resolve to a WebElement: '
|
948 | + Object.prototype.toString.call(value));
|
949 | }
|
950 | return value;
|
951 | }));
|
952 | }
|
953 | return result;
|
954 | }
|
955 |
|
956 | /** @override */
|
957 | sleep(ms) {
|
958 | return this.flow_.timeout(ms, 'WebDriver.sleep(' + ms + ')');
|
959 | }
|
960 |
|
961 | /** @override */
|
962 | getWindowHandle() {
|
963 | return this.schedule(
|
964 | new command.Command(command.Name.GET_CURRENT_WINDOW_HANDLE),
|
965 | 'WebDriver.getWindowHandle()');
|
966 | }
|
967 |
|
968 | /** @override */
|
969 | getAllWindowHandles() {
|
970 | return this.schedule(
|
971 | new command.Command(command.Name.GET_WINDOW_HANDLES),
|
972 | 'WebDriver.getAllWindowHandles()');
|
973 | }
|
974 |
|
975 | /** @override */
|
976 | getPageSource() {
|
977 | return this.schedule(
|
978 | new command.Command(command.Name.GET_PAGE_SOURCE),
|
979 | 'WebDriver.getPageSource()');
|
980 | }
|
981 |
|
982 | /** @override */
|
983 | close() {
|
984 | return this.schedule(new command.Command(command.Name.CLOSE),
|
985 | 'WebDriver.close()');
|
986 | }
|
987 |
|
988 | /** @override */
|
989 | get(url) {
|
990 | return this.navigate().to(url);
|
991 | }
|
992 |
|
993 | /** @override */
|
994 | getCurrentUrl() {
|
995 | return this.schedule(
|
996 | new command.Command(command.Name.GET_CURRENT_URL),
|
997 | 'WebDriver.getCurrentUrl()');
|
998 | }
|
999 |
|
1000 | /** @override */
|
1001 | getTitle() {
|
1002 | return this.schedule(new command.Command(command.Name.GET_TITLE),
|
1003 | 'WebDriver.getTitle()');
|
1004 | }
|
1005 |
|
1006 | /** @override */
|
1007 | findElement(locator) {
|
1008 | let id;
|
1009 | locator = by.checkedLocator(locator);
|
1010 | if (typeof locator === 'function') {
|
1011 | id = this.findElementInternal_(locator, this);
|
1012 | } else {
|
1013 | let cmd = new command.Command(command.Name.FIND_ELEMENT).
|
1014 | setParameter('using', locator.using).
|
1015 | setParameter('value', locator.value);
|
1016 | id = this.schedule(cmd, 'WebDriver.findElement(' + locator + ')');
|
1017 | }
|
1018 | return new WebElementPromise(this, id);
|
1019 | }
|
1020 |
|
1021 | /**
|
1022 | * @param {!Function} locatorFn The locator function to use.
|
1023 | * @param {!(WebDriver|WebElement)} context The search
|
1024 | * context.
|
1025 | * @return {!promise.Thenable<!WebElement>} A
|
1026 | * promise that will resolve to a list of WebElements.
|
1027 | * @private
|
1028 | */
|
1029 | findElementInternal_(locatorFn, context) {
|
1030 | return this.call(() => locatorFn(context)).then(function(result) {
|
1031 | if (Array.isArray(result)) {
|
1032 | result = result[0];
|
1033 | }
|
1034 | if (!(result instanceof WebElement)) {
|
1035 | throw new TypeError('Custom locator did not return a WebElement');
|
1036 | }
|
1037 | return result;
|
1038 | });
|
1039 | }
|
1040 |
|
1041 | /** @override */
|
1042 | findElements(locator) {
|
1043 | locator = by.checkedLocator(locator);
|
1044 | if (typeof locator === 'function') {
|
1045 | return this.findElementsInternal_(locator, this);
|
1046 | } else {
|
1047 | let cmd = new command.Command(command.Name.FIND_ELEMENTS).
|
1048 | setParameter('using', locator.using).
|
1049 | setParameter('value', locator.value);
|
1050 | let res = this.schedule(cmd, 'WebDriver.findElements(' + locator + ')');
|
1051 | return res.catch(function(e) {
|
1052 | if (e instanceof error.NoSuchElementError) {
|
1053 | return [];
|
1054 | }
|
1055 | throw e;
|
1056 | });
|
1057 | }
|
1058 | }
|
1059 |
|
1060 | /**
|
1061 | * @param {!Function} locatorFn The locator function to use.
|
1062 | * @param {!(WebDriver|WebElement)} context The search context.
|
1063 | * @return {!promise.Thenable<!Array<!WebElement>>} A promise that
|
1064 | * will resolve to an array of WebElements.
|
1065 | * @private
|
1066 | */
|
1067 | findElementsInternal_(locatorFn, context) {
|
1068 | return this.call(() => locatorFn(context)).then(function(result) {
|
1069 | if (result instanceof WebElement) {
|
1070 | return [result];
|
1071 | }
|
1072 |
|
1073 | if (!Array.isArray(result)) {
|
1074 | return [];
|
1075 | }
|
1076 |
|
1077 | return result.filter(function(item) {
|
1078 | return item instanceof WebElement;
|
1079 | });
|
1080 | });
|
1081 | }
|
1082 |
|
1083 | /** @override */
|
1084 | takeScreenshot() {
|
1085 | return this.schedule(new command.Command(command.Name.SCREENSHOT),
|
1086 | 'WebDriver.takeScreenshot()');
|
1087 | }
|
1088 |
|
1089 | /** @override */
|
1090 | manage() {
|
1091 | return new Options(this);
|
1092 | }
|
1093 |
|
1094 | /** @override */
|
1095 | navigate() {
|
1096 | return new Navigation(this);
|
1097 | }
|
1098 |
|
1099 | /** @override */
|
1100 | switchTo() {
|
1101 | return new TargetLocator(this);
|
1102 | }
|
1103 | }
|
1104 |
|
1105 |
|
1106 | /**
|
1107 | * Interface for navigating back and forth in the browser history.
|
1108 | *
|
1109 | * This class should never be instantiated directly. Instead, obtain an instance
|
1110 | * with
|
1111 | *
|
1112 | * webdriver.navigate()
|
1113 | *
|
1114 | * @see WebDriver#navigate()
|
1115 | */
|
1116 | class Navigation {
|
1117 | /**
|
1118 | * @param {!WebDriver} driver The parent driver.
|
1119 | * @private
|
1120 | */
|
1121 | constructor(driver) {
|
1122 | /** @private {!WebDriver} */
|
1123 | this.driver_ = driver;
|
1124 | }
|
1125 |
|
1126 | /**
|
1127 | * Schedules a command to navigate to a new URL.
|
1128 | * @param {string} url The URL to navigate to.
|
1129 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
1130 | * when the URL has been loaded.
|
1131 | */
|
1132 | to(url) {
|
1133 | return this.driver_.schedule(
|
1134 | new command.Command(command.Name.GET).
|
1135 | setParameter('url', url),
|
1136 | 'WebDriver.navigate().to(' + url + ')');
|
1137 | }
|
1138 |
|
1139 | /**
|
1140 | * Schedules a command to move backwards in the browser history.
|
1141 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
1142 | * when the navigation event has completed.
|
1143 | */
|
1144 | back() {
|
1145 | return this.driver_.schedule(
|
1146 | new command.Command(command.Name.GO_BACK),
|
1147 | 'WebDriver.navigate().back()');
|
1148 | }
|
1149 |
|
1150 | /**
|
1151 | * Schedules a command to move forwards in the browser history.
|
1152 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
1153 | * when the navigation event has completed.
|
1154 | */
|
1155 | forward() {
|
1156 | return this.driver_.schedule(
|
1157 | new command.Command(command.Name.GO_FORWARD),
|
1158 | 'WebDriver.navigate().forward()');
|
1159 | }
|
1160 |
|
1161 | /**
|
1162 | * Schedules a command to refresh the current page.
|
1163 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
1164 | * when the navigation event has completed.
|
1165 | */
|
1166 | refresh() {
|
1167 | return this.driver_.schedule(
|
1168 | new command.Command(command.Name.REFRESH),
|
1169 | 'WebDriver.navigate().refresh()');
|
1170 | }
|
1171 | }
|
1172 |
|
1173 |
|
1174 | /**
|
1175 | * Provides methods for managing browser and driver state.
|
1176 | *
|
1177 | * This class should never be instantiated directly. Insead, obtain an instance
|
1178 | * with {@linkplain WebDriver#manage() webdriver.manage()}.
|
1179 | */
|
1180 | class Options {
|
1181 | /**
|
1182 | * @param {!WebDriver} driver The parent driver.
|
1183 | * @private
|
1184 | */
|
1185 | constructor(driver) {
|
1186 | /** @private {!WebDriver} */
|
1187 | this.driver_ = driver;
|
1188 | }
|
1189 |
|
1190 | /**
|
1191 | * Schedules a command to add a cookie.
|
1192 | *
|
1193 | * __Sample Usage:__
|
1194 | *
|
1195 | * // Set a basic cookie.
|
1196 | * driver.options().addCookie({name: 'foo', value: 'bar'});
|
1197 | *
|
1198 | * // Set a cookie that expires in 10 minutes.
|
1199 | * let expiry = new Date(Date.now() + (10 * 60 * 1000));
|
1200 | * driver.options().addCookie({name: 'foo', value: 'bar', expiry});
|
1201 | *
|
1202 | * // The cookie expiration may also be specified in seconds since epoch.
|
1203 | * driver.options().addCookie({
|
1204 | * name: 'foo',
|
1205 | * value: 'bar',
|
1206 | * expiry: Math.floor(Date.now() / 1000)
|
1207 | * });
|
1208 | *
|
1209 | * @param {!Options.Cookie} spec Defines the cookie to add.
|
1210 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
1211 | * when the cookie has been added to the page.
|
1212 | * @throws {error.InvalidArgumentError} if any of the cookie parameters are
|
1213 | * invalid.
|
1214 | * @throws {TypeError} if `spec` is not a cookie object.
|
1215 | */
|
1216 | addCookie(spec) {
|
1217 | if (!spec || typeof spec !== 'object') {
|
1218 | throw TypeError('addCookie called with non-cookie parameter');
|
1219 | }
|
1220 |
|
1221 | // We do not allow '=' or ';' in the name.
|
1222 | let name = spec.name;
|
1223 | if (/[;=]/.test(name)) {
|
1224 | throw new error.InvalidArgumentError(
|
1225 | 'Invalid cookie name "' + name + '"');
|
1226 | }
|
1227 |
|
1228 | // We do not allow ';' in value.
|
1229 | let value = spec.value;
|
1230 | if (/;/.test(value)) {
|
1231 | throw new error.InvalidArgumentError(
|
1232 | 'Invalid cookie value "' + value + '"');
|
1233 | }
|
1234 |
|
1235 | let cookieString = name + '=' + value +
|
1236 | (spec.domain ? ';domain=' + spec.domain : '') +
|
1237 | (spec.path ? ';path=' + spec.path : '') +
|
1238 | (spec.secure ? ';secure' : '');
|
1239 |
|
1240 | let expiry;
|
1241 | if (typeof spec.expiry === 'number') {
|
1242 | expiry = Math.floor(spec.expiry);
|
1243 | cookieString += ';expires=' + new Date(spec.expiry * 1000).toUTCString();
|
1244 | } else if (spec.expiry instanceof Date) {
|
1245 | let date = /** @type {!Date} */(spec.expiry);
|
1246 | expiry = Math.floor(date.getTime() / 1000);
|
1247 | cookieString += ';expires=' + date.toUTCString();
|
1248 | }
|
1249 |
|
1250 | return this.driver_.schedule(
|
1251 | new command.Command(command.Name.ADD_COOKIE).
|
1252 | setParameter('cookie', {
|
1253 | 'name': name,
|
1254 | 'value': value,
|
1255 | 'path': spec.path,
|
1256 | 'domain': spec.domain,
|
1257 | 'secure': !!spec.secure,
|
1258 | 'expiry': expiry
|
1259 | }),
|
1260 | 'WebDriver.manage().addCookie(' + cookieString + ')');
|
1261 | }
|
1262 |
|
1263 | /**
|
1264 | * Schedules a command to delete all cookies visible to the current page.
|
1265 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
1266 | * when all cookies have been deleted.
|
1267 | */
|
1268 | deleteAllCookies() {
|
1269 | return this.driver_.schedule(
|
1270 | new command.Command(command.Name.DELETE_ALL_COOKIES),
|
1271 | 'WebDriver.manage().deleteAllCookies()');
|
1272 | }
|
1273 |
|
1274 | /**
|
1275 | * Schedules a command to delete the cookie with the given name. This command
|
1276 | * is a no-op if there is no cookie with the given name visible to the current
|
1277 | * page.
|
1278 | * @param {string} name The name of the cookie to delete.
|
1279 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
1280 | * when the cookie has been deleted.
|
1281 | */
|
1282 | deleteCookie(name) {
|
1283 | return this.driver_.schedule(
|
1284 | new command.Command(command.Name.DELETE_COOKIE).
|
1285 | setParameter('name', name),
|
1286 | 'WebDriver.manage().deleteCookie(' + name + ')');
|
1287 | }
|
1288 |
|
1289 | /**
|
1290 | * Schedules a command to retrieve all cookies visible to the current page.
|
1291 | * Each cookie will be returned as a JSON object as described by the WebDriver
|
1292 | * wire protocol.
|
1293 | * @return {!promise.Thenable<!Array<!Options.Cookie>>} A promise that will be
|
1294 | * resolved with the cookies visible to the current browsing context.
|
1295 | */
|
1296 | getCookies() {
|
1297 | return this.driver_.schedule(
|
1298 | new command.Command(command.Name.GET_ALL_COOKIES),
|
1299 | 'WebDriver.manage().getCookies()');
|
1300 | }
|
1301 |
|
1302 | /**
|
1303 | * Schedules a command to retrieve the cookie with the given name. Returns null
|
1304 | * if there is no such cookie. The cookie will be returned as a JSON object as
|
1305 | * described by the WebDriver wire protocol.
|
1306 | *
|
1307 | * @param {string} name The name of the cookie to retrieve.
|
1308 | * @return {!promise.Thenable<?Options.Cookie>} A promise that will be resolved
|
1309 | * with the named cookie, or `null` if there is no such cookie.
|
1310 | */
|
1311 | getCookie(name) {
|
1312 | return this.getCookies().then(function(cookies) {
|
1313 | for (let cookie of cookies) {
|
1314 | if (cookie && cookie['name'] === name) {
|
1315 | return cookie;
|
1316 | }
|
1317 | }
|
1318 | return null;
|
1319 | });
|
1320 | }
|
1321 |
|
1322 | /**
|
1323 | * @return {!Logs} The interface for managing driver
|
1324 | * logs.
|
1325 | */
|
1326 | logs() {
|
1327 | return new Logs(this.driver_);
|
1328 | }
|
1329 |
|
1330 | /**
|
1331 | * @return {!Timeouts} The interface for managing driver timeouts.
|
1332 | */
|
1333 | timeouts() {
|
1334 | return new Timeouts(this.driver_);
|
1335 | }
|
1336 |
|
1337 | /**
|
1338 | * @return {!Window} The interface for managing the current window.
|
1339 | */
|
1340 | window() {
|
1341 | return new Window(this.driver_);
|
1342 | }
|
1343 | }
|
1344 |
|
1345 |
|
1346 | /**
|
1347 | * A record object describing a browser cookie.
|
1348 | *
|
1349 | * @record
|
1350 | */
|
1351 | Options.Cookie = function() {};
|
1352 |
|
1353 |
|
1354 | /**
|
1355 | * The name of the cookie.
|
1356 | *
|
1357 | * @type {string}
|
1358 | */
|
1359 | Options.Cookie.prototype.name;
|
1360 |
|
1361 |
|
1362 | /**
|
1363 | * The cookie value.
|
1364 | *
|
1365 | * @type {string}
|
1366 | */
|
1367 | Options.Cookie.prototype.value;
|
1368 |
|
1369 |
|
1370 | /**
|
1371 | * The cookie path. Defaults to "/" when adding a cookie.
|
1372 | *
|
1373 | * @type {(string|undefined)}
|
1374 | */
|
1375 | Options.Cookie.prototype.path;
|
1376 |
|
1377 |
|
1378 | /**
|
1379 | * The domain the cookie is visible to. Defaults to the current browsing
|
1380 | * context's document's URL when adding a cookie.
|
1381 | *
|
1382 | * @type {(string|undefined)}
|
1383 | */
|
1384 | Options.Cookie.prototype.domain;
|
1385 |
|
1386 |
|
1387 | /**
|
1388 | * Whether the cookie is a secure cookie. Defaults to false when adding a new
|
1389 | * cookie.
|
1390 | *
|
1391 | * @type {(boolean|undefined)}
|
1392 | */
|
1393 | Options.Cookie.prototype.secure;
|
1394 |
|
1395 |
|
1396 | /**
|
1397 | * Whether the cookie is an HTTP only cookie. Defaults to false when adding a
|
1398 | * new cookie.
|
1399 | *
|
1400 | * @type {(boolean|undefined)}
|
1401 | */
|
1402 | Options.Cookie.prototype.httpOnly;
|
1403 |
|
1404 |
|
1405 | /**
|
1406 | * When the cookie expires.
|
1407 | *
|
1408 | * When {@linkplain Options#addCookie() adding a cookie}, this may be specified
|
1409 | * in _seconds_ since Unix epoch (January 1, 1970). The expiry will default to
|
1410 | * 20 years in the future if omitted.
|
1411 | *
|
1412 | * The expiry is always returned in seconds since epoch when
|
1413 | * {@linkplain Options#getCookies() retrieving cookies} from the browser.
|
1414 | *
|
1415 | * @type {(!Date|number|undefined)}
|
1416 | */
|
1417 | Options.Cookie.prototype.expiry;
|
1418 |
|
1419 |
|
1420 | /**
|
1421 | * An interface for managing timeout behavior for WebDriver instances.
|
1422 | *
|
1423 | * This class should never be instantiated directly. Insead, obtain an instance
|
1424 | * with
|
1425 | *
|
1426 | * webdriver.manage().timeouts()
|
1427 | *
|
1428 | * @see WebDriver#manage()
|
1429 | * @see Options#timeouts()
|
1430 | */
|
1431 | class Timeouts {
|
1432 | /**
|
1433 | * @param {!WebDriver} driver The parent driver.
|
1434 | * @private
|
1435 | */
|
1436 | constructor(driver) {
|
1437 | /** @private {!WebDriver} */
|
1438 | this.driver_ = driver;
|
1439 | }
|
1440 |
|
1441 | /**
|
1442 | * Specifies the amount of time the driver should wait when searching for an
|
1443 | * element if it is not immediately present.
|
1444 | *
|
1445 | * When searching for a single element, the driver should poll the page
|
1446 | * until the element has been found, or this timeout expires before failing
|
1447 | * with a {@link bot.ErrorCode.NO_SUCH_ELEMENT} error. When searching
|
1448 | * for multiple elements, the driver should poll the page until at least one
|
1449 | * element has been found or this timeout has expired.
|
1450 | *
|
1451 | * Setting the wait timeout to 0 (its default value), disables implicit
|
1452 | * waiting.
|
1453 | *
|
1454 | * Increasing the implicit wait timeout should be used judiciously as it
|
1455 | * will have an adverse effect on test run time, especially when used with
|
1456 | * slower location strategies like XPath.
|
1457 | *
|
1458 | * @param {number} ms The amount of time to wait, in milliseconds.
|
1459 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
1460 | * when the implicit wait timeout has been set.
|
1461 | */
|
1462 | implicitlyWait(ms) {
|
1463 | return this._scheduleCommand(ms, 'implicit', 'implicitlyWait');
|
1464 | }
|
1465 |
|
1466 | /**
|
1467 | * Sets the amount of time to wait, in milliseconds, for an asynchronous
|
1468 | * script to finish execution before returning an error. If the timeout is
|
1469 | * less than or equal to 0, the script will be allowed to run indefinitely.
|
1470 | *
|
1471 | * @param {number} ms The amount of time to wait, in milliseconds.
|
1472 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
1473 | * when the script timeout has been set.
|
1474 | */
|
1475 | setScriptTimeout(ms) {
|
1476 | return this._scheduleCommand(ms, 'script', 'setScriptTimeout');
|
1477 | }
|
1478 |
|
1479 | /**
|
1480 | * Sets the amount of time to wait for a page load to complete before
|
1481 | * returning an error. If the timeout is negative, page loads may be
|
1482 | * indefinite.
|
1483 | *
|
1484 | * @param {number} ms The amount of time to wait, in milliseconds.
|
1485 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
1486 | * when the timeout has been set.
|
1487 | */
|
1488 | pageLoadTimeout(ms) {
|
1489 | return this._scheduleCommand(ms, 'page load', 'pageLoadTimeout');
|
1490 | }
|
1491 |
|
1492 | _scheduleCommand(ms, timeoutIdentifier, timeoutName) {
|
1493 | return this.driver_.schedule(
|
1494 | new command.Command(command.Name.SET_TIMEOUT).
|
1495 | setParameter('type', timeoutIdentifier).
|
1496 | setParameter('ms', ms),
|
1497 | `WebDriver.manage().timeouts().${timeoutName}(${ms})`);
|
1498 | }
|
1499 | }
|
1500 |
|
1501 |
|
1502 | /**
|
1503 | * An interface for managing the current window.
|
1504 | *
|
1505 | * This class should never be instantiated directly. Instead, obtain an instance
|
1506 | * with
|
1507 | *
|
1508 | * webdriver.manage().window()
|
1509 | *
|
1510 | * @see WebDriver#manage()
|
1511 | * @see Options#window()
|
1512 | */
|
1513 | class Window {
|
1514 | /**
|
1515 | * @param {!WebDriver} driver The parent driver.
|
1516 | * @private
|
1517 | */
|
1518 | constructor(driver) {
|
1519 | /** @private {!WebDriver} */
|
1520 | this.driver_ = driver;
|
1521 | }
|
1522 |
|
1523 | /**
|
1524 | * Retrieves the window's current position, relative to the top left corner of
|
1525 | * the screen.
|
1526 | * @return {!promise.Thenable<{x: number, y: number}>} A promise
|
1527 | * that will be resolved with the window's position in the form of a
|
1528 | * {x:number, y:number} object literal.
|
1529 | */
|
1530 | getPosition() {
|
1531 | return this.driver_.schedule(
|
1532 | new command.Command(command.Name.GET_WINDOW_POSITION).
|
1533 | setParameter('windowHandle', 'current'),
|
1534 | 'WebDriver.manage().window().getPosition()');
|
1535 | }
|
1536 |
|
1537 | /**
|
1538 | * Repositions the current window.
|
1539 | * @param {number} x The desired horizontal position, relative to the left
|
1540 | * side of the screen.
|
1541 | * @param {number} y The desired vertical position, relative to the top of the
|
1542 | * of the screen.
|
1543 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
1544 | * when the command has completed.
|
1545 | */
|
1546 | setPosition(x, y) {
|
1547 | return this.driver_.schedule(
|
1548 | new command.Command(command.Name.SET_WINDOW_POSITION).
|
1549 | setParameter('windowHandle', 'current').
|
1550 | setParameter('x', x).
|
1551 | setParameter('y', y),
|
1552 | 'WebDriver.manage().window().setPosition(' + x + ', ' + y + ')');
|
1553 | }
|
1554 |
|
1555 | /**
|
1556 | * Retrieves the window's current size.
|
1557 | * @return {!promise.Thenable<{width: number, height: number}>} A
|
1558 | * promise that will be resolved with the window's size in the form of a
|
1559 | * {width:number, height:number} object literal.
|
1560 | */
|
1561 | getSize() {
|
1562 | return this.driver_.schedule(
|
1563 | new command.Command(command.Name.GET_WINDOW_SIZE).
|
1564 | setParameter('windowHandle', 'current'),
|
1565 | 'WebDriver.manage().window().getSize()');
|
1566 | }
|
1567 |
|
1568 | /**
|
1569 | * Resizes the current window.
|
1570 | * @param {number} width The desired window width.
|
1571 | * @param {number} height The desired window height.
|
1572 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
1573 | * when the command has completed.
|
1574 | */
|
1575 | setSize(width, height) {
|
1576 | return this.driver_.schedule(
|
1577 | new command.Command(command.Name.SET_WINDOW_SIZE).
|
1578 | setParameter('windowHandle', 'current').
|
1579 | setParameter('width', width).
|
1580 | setParameter('height', height),
|
1581 | 'WebDriver.manage().window().setSize(' + width + ', ' + height + ')');
|
1582 | }
|
1583 |
|
1584 | /**
|
1585 | * Maximizes the current window.
|
1586 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
1587 | * when the command has completed.
|
1588 | */
|
1589 | maximize() {
|
1590 | return this.driver_.schedule(
|
1591 | new command.Command(command.Name.MAXIMIZE_WINDOW).
|
1592 | setParameter('windowHandle', 'current'),
|
1593 | 'WebDriver.manage().window().maximize()');
|
1594 | }
|
1595 | }
|
1596 |
|
1597 |
|
1598 | /**
|
1599 | * Interface for managing WebDriver log records.
|
1600 | *
|
1601 | * This class should never be instantiated directly. Instead, obtain an
|
1602 | * instance with
|
1603 | *
|
1604 | * webdriver.manage().logs()
|
1605 | *
|
1606 | * @see WebDriver#manage()
|
1607 | * @see Options#logs()
|
1608 | */
|
1609 | class Logs {
|
1610 | /**
|
1611 | * @param {!WebDriver} driver The parent driver.
|
1612 | * @private
|
1613 | */
|
1614 | constructor(driver) {
|
1615 | /** @private {!WebDriver} */
|
1616 | this.driver_ = driver;
|
1617 | }
|
1618 |
|
1619 | /**
|
1620 | * Fetches available log entries for the given type.
|
1621 | *
|
1622 | * Note that log buffers are reset after each call, meaning that available
|
1623 | * log entries correspond to those entries not yet returned for a given log
|
1624 | * type. In practice, this means that this call will return the available log
|
1625 | * entries since the last call, or from the start of the session.
|
1626 | *
|
1627 | * @param {!logging.Type} type The desired log type.
|
1628 | * @return {!promise.Thenable<!Array.<!logging.Entry>>} A
|
1629 | * promise that will resolve to a list of log entries for the specified
|
1630 | * type.
|
1631 | */
|
1632 | get(type) {
|
1633 | let cmd = new command.Command(command.Name.GET_LOG).
|
1634 | setParameter('type', type);
|
1635 | return this.driver_.schedule(
|
1636 | cmd, 'WebDriver.manage().logs().get(' + type + ')').
|
1637 | then(function(entries) {
|
1638 | return entries.map(function(entry) {
|
1639 | if (!(entry instanceof logging.Entry)) {
|
1640 | return new logging.Entry(
|
1641 | entry['level'], entry['message'], entry['timestamp'],
|
1642 | entry['type']);
|
1643 | }
|
1644 | return entry;
|
1645 | });
|
1646 | });
|
1647 | }
|
1648 |
|
1649 | /**
|
1650 | * Retrieves the log types available to this driver.
|
1651 | * @return {!promise.Thenable<!Array<!logging.Type>>} A
|
1652 | * promise that will resolve to a list of available log types.
|
1653 | */
|
1654 | getAvailableLogTypes() {
|
1655 | return this.driver_.schedule(
|
1656 | new command.Command(command.Name.GET_AVAILABLE_LOG_TYPES),
|
1657 | 'WebDriver.manage().logs().getAvailableLogTypes()');
|
1658 | }
|
1659 | }
|
1660 |
|
1661 |
|
1662 | /**
|
1663 | * An interface for changing the focus of the driver to another frame or window.
|
1664 | *
|
1665 | * This class should never be instantiated directly. Instead, obtain an
|
1666 | * instance with
|
1667 | *
|
1668 | * webdriver.switchTo()
|
1669 | *
|
1670 | * @see WebDriver#switchTo()
|
1671 | */
|
1672 | class TargetLocator {
|
1673 | /**
|
1674 | * @param {!WebDriver} driver The parent driver.
|
1675 | * @private
|
1676 | */
|
1677 | constructor(driver) {
|
1678 | /** @private {!WebDriver} */
|
1679 | this.driver_ = driver;
|
1680 | }
|
1681 |
|
1682 | /**
|
1683 | * Schedules a command retrieve the {@code document.activeElement} element on
|
1684 | * the current document, or {@code document.body} if activeElement is not
|
1685 | * available.
|
1686 | * @return {!WebElementPromise} The active element.
|
1687 | */
|
1688 | activeElement() {
|
1689 | var id = this.driver_.schedule(
|
1690 | new command.Command(command.Name.GET_ACTIVE_ELEMENT),
|
1691 | 'WebDriver.switchTo().activeElement()');
|
1692 | return new WebElementPromise(this.driver_, id);
|
1693 | }
|
1694 |
|
1695 | /**
|
1696 | * Schedules a command to switch focus of all future commands to the topmost
|
1697 | * frame on the page.
|
1698 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
1699 | * when the driver has changed focus to the default content.
|
1700 | */
|
1701 | defaultContent() {
|
1702 | return this.driver_.schedule(
|
1703 | new command.Command(command.Name.SWITCH_TO_FRAME).
|
1704 | setParameter('id', null),
|
1705 | 'WebDriver.switchTo().defaultContent()');
|
1706 | }
|
1707 |
|
1708 | /**
|
1709 | * Schedules a command to switch the focus of all future commands to another
|
1710 | * frame on the page. The target frame may be specified as one of the
|
1711 | * following:
|
1712 | *
|
1713 | * - A number that specifies a (zero-based) index into [window.frames](
|
1714 | * https://developer.mozilla.org/en-US/docs/Web/API/Window.frames).
|
1715 | * - A {@link WebElement} reference, which correspond to a `frame` or `iframe`
|
1716 | * DOM element.
|
1717 | * - The `null` value, to select the topmost frame on the page. Passing `null`
|
1718 | * is the same as calling {@link #defaultContent defaultContent()}.
|
1719 | *
|
1720 | * If the specified frame can not be found, the returned promise will be
|
1721 | * rejected with a {@linkplain error.NoSuchFrameError}.
|
1722 | *
|
1723 | * @param {(number|WebElement|null)} id The frame locator.
|
1724 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
1725 | * when the driver has changed focus to the specified frame.
|
1726 | */
|
1727 | frame(id) {
|
1728 | return this.driver_.schedule(
|
1729 | new command.Command(command.Name.SWITCH_TO_FRAME).
|
1730 | setParameter('id', id),
|
1731 | 'WebDriver.switchTo().frame(' + id + ')');
|
1732 | }
|
1733 |
|
1734 | /**
|
1735 | * Schedules a command to switch the focus of all future commands to another
|
1736 | * window. Windows may be specified by their {@code window.name} attribute or
|
1737 | * by its handle (as returned by {@link WebDriver#getWindowHandles}).
|
1738 | *
|
1739 | * If the specified window cannot be found, the returned promise will be
|
1740 | * rejected with a {@linkplain error.NoSuchWindowError}.
|
1741 | *
|
1742 | * @param {string} nameOrHandle The name or window handle of the window to
|
1743 | * switch focus to.
|
1744 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
1745 | * when the driver has changed focus to the specified window.
|
1746 | */
|
1747 | window(nameOrHandle) {
|
1748 | return this.driver_.schedule(
|
1749 | new command.Command(command.Name.SWITCH_TO_WINDOW).
|
1750 | // "name" supports the legacy drivers. "handle" is the W3C
|
1751 | // compliant parameter.
|
1752 | setParameter('name', nameOrHandle).
|
1753 | setParameter('handle', nameOrHandle),
|
1754 | 'WebDriver.switchTo().window(' + nameOrHandle + ')');
|
1755 | }
|
1756 |
|
1757 | /**
|
1758 | * Schedules a command to change focus to the active modal dialog, such as
|
1759 | * those opened by `window.alert()`, `window.confirm()`, and
|
1760 | * `window.prompt()`. The returned promise will be rejected with a
|
1761 | * {@linkplain error.NoSuchAlertError} if there are no open alerts.
|
1762 | *
|
1763 | * @return {!AlertPromise} The open alert.
|
1764 | */
|
1765 | alert() {
|
1766 | var text = this.driver_.schedule(
|
1767 | new command.Command(command.Name.GET_ALERT_TEXT),
|
1768 | 'WebDriver.switchTo().alert()');
|
1769 | var driver = this.driver_;
|
1770 | return new AlertPromise(driver, text.then(function(text) {
|
1771 | return new Alert(driver, text);
|
1772 | }));
|
1773 | }
|
1774 | }
|
1775 |
|
1776 |
|
1777 | //////////////////////////////////////////////////////////////////////////////
|
1778 | //
|
1779 | // WebElement
|
1780 | //
|
1781 | //////////////////////////////////////////////////////////////////////////////
|
1782 |
|
1783 |
|
1784 | const LEGACY_ELEMENT_ID_KEY = 'ELEMENT';
|
1785 | const ELEMENT_ID_KEY = 'element-6066-11e4-a52e-4f735466cecf';
|
1786 |
|
1787 |
|
1788 | /**
|
1789 | * Represents a DOM element. WebElements can be found by searching from the
|
1790 | * document root using a {@link WebDriver} instance, or by searching
|
1791 | * under another WebElement:
|
1792 | *
|
1793 | * driver.get('http://www.google.com');
|
1794 | * var searchForm = driver.findElement(By.tagName('form'));
|
1795 | * var searchBox = searchForm.findElement(By.name('q'));
|
1796 | * searchBox.sendKeys('webdriver');
|
1797 | */
|
1798 | class WebElement {
|
1799 | /**
|
1800 | * @param {!WebDriver} driver the parent WebDriver instance for this element.
|
1801 | * @param {(!IThenable<string>|string)} id The server-assigned opaque ID for
|
1802 | * the underlying DOM element.
|
1803 | */
|
1804 | constructor(driver, id) {
|
1805 | /** @private {!WebDriver} */
|
1806 | this.driver_ = driver;
|
1807 |
|
1808 | /** @private {!promise.Thenable<string>} */
|
1809 | this.id_ = driver.controlFlow().promise(resolve => resolve(id));
|
1810 | }
|
1811 |
|
1812 | /**
|
1813 | * @param {string} id The raw ID.
|
1814 | * @param {boolean=} opt_noLegacy Whether to exclude the legacy element key.
|
1815 | * @return {!Object} The element ID for use with WebDriver's wire protocol.
|
1816 | */
|
1817 | static buildId(id, opt_noLegacy) {
|
1818 | return opt_noLegacy
|
1819 | ? {[ELEMENT_ID_KEY]: id}
|
1820 | : {[ELEMENT_ID_KEY]: id, [LEGACY_ELEMENT_ID_KEY]: id};
|
1821 | }
|
1822 |
|
1823 | /**
|
1824 | * Extracts the encoded WebElement ID from the object.
|
1825 | *
|
1826 | * @param {?} obj The object to extract the ID from.
|
1827 | * @return {string} the extracted ID.
|
1828 | * @throws {TypeError} if the object is not a valid encoded ID.
|
1829 | */
|
1830 | static extractId(obj) {
|
1831 | if (obj && typeof obj === 'object') {
|
1832 | if (typeof obj[ELEMENT_ID_KEY] === 'string') {
|
1833 | return obj[ELEMENT_ID_KEY];
|
1834 | } else if (typeof obj[LEGACY_ELEMENT_ID_KEY] === 'string') {
|
1835 | return obj[LEGACY_ELEMENT_ID_KEY];
|
1836 | }
|
1837 | }
|
1838 | throw new TypeError('object is not a WebElement ID');
|
1839 | }
|
1840 |
|
1841 | /**
|
1842 | * @param {?} obj the object to test.
|
1843 | * @return {boolean} whether the object is a valid encoded WebElement ID.
|
1844 | */
|
1845 | static isId(obj) {
|
1846 | return obj && typeof obj === 'object'
|
1847 | && (typeof obj[ELEMENT_ID_KEY] === 'string'
|
1848 | || typeof obj[LEGACY_ELEMENT_ID_KEY] === 'string');
|
1849 | }
|
1850 |
|
1851 | /**
|
1852 | * Compares two WebElements for equality.
|
1853 | *
|
1854 | * @param {!WebElement} a A WebElement.
|
1855 | * @param {!WebElement} b A WebElement.
|
1856 | * @return {!promise.Thenable<boolean>} A promise that will be
|
1857 | * resolved to whether the two WebElements are equal.
|
1858 | */
|
1859 | static equals(a, b) {
|
1860 | if (a === b) {
|
1861 | return a.driver_.controlFlow().promise(resolve => resolve(true));
|
1862 | }
|
1863 | let ids = [a.getId(), b.getId()];
|
1864 | return promise.all(ids).then(function(ids) {
|
1865 | // If the two element's have the same ID, they should be considered
|
1866 | // equal. Otherwise, they may still be equivalent, but we'll need to
|
1867 | // ask the server to check for us.
|
1868 | if (ids[0] === ids[1]) {
|
1869 | return true;
|
1870 | }
|
1871 |
|
1872 | let cmd = new command.Command(command.Name.ELEMENT_EQUALS);
|
1873 | cmd.setParameter('id', ids[0]);
|
1874 | cmd.setParameter('other', ids[1]);
|
1875 | return a.driver_.schedule(cmd, 'WebElement.equals()');
|
1876 | });
|
1877 | }
|
1878 |
|
1879 | /** @return {!WebDriver} The parent driver for this instance. */
|
1880 | getDriver() {
|
1881 | return this.driver_;
|
1882 | }
|
1883 |
|
1884 | /**
|
1885 | * @return {!promise.Thenable<string>} A promise that resolves to
|
1886 | * the server-assigned opaque ID assigned to this element.
|
1887 | */
|
1888 | getId() {
|
1889 | return this.id_;
|
1890 | }
|
1891 |
|
1892 | /**
|
1893 | * @return {!Object} Returns the serialized representation of this WebElement.
|
1894 | */
|
1895 | [Symbols.serialize]() {
|
1896 | return this.getId().then(WebElement.buildId);
|
1897 | }
|
1898 |
|
1899 | /**
|
1900 | * Schedules a command that targets this element with the parent WebDriver
|
1901 | * instance. Will ensure this element's ID is included in the command
|
1902 | * parameters under the "id" key.
|
1903 | *
|
1904 | * @param {!command.Command} command The command to schedule.
|
1905 | * @param {string} description A description of the command for debugging.
|
1906 | * @return {!promise.Thenable<T>} A promise that will be resolved
|
1907 | * with the command result.
|
1908 | * @template T
|
1909 | * @see WebDriver#schedule
|
1910 | * @private
|
1911 | */
|
1912 | schedule_(command, description) {
|
1913 | command.setParameter('id', this);
|
1914 | return this.driver_.schedule(command, description);
|
1915 | }
|
1916 |
|
1917 | /**
|
1918 | * Schedule a command to find a descendant of this element. If the element
|
1919 | * cannot be found, the returned promise will be rejected with a
|
1920 | * {@linkplain error.NoSuchElementError NoSuchElementError}.
|
1921 | *
|
1922 | * The search criteria for an element may be defined using one of the static
|
1923 | * factories on the {@link by.By} class, or as a short-hand
|
1924 | * {@link ./by.ByHash} object. For example, the following two statements
|
1925 | * are equivalent:
|
1926 | *
|
1927 | * var e1 = element.findElement(By.id('foo'));
|
1928 | * var e2 = element.findElement({id:'foo'});
|
1929 | *
|
1930 | * You may also provide a custom locator function, which takes as input this
|
1931 | * instance and returns a {@link WebElement}, or a promise that will resolve
|
1932 | * to a WebElement. If the returned promise resolves to an array of
|
1933 | * WebElements, WebDriver will use the first element. For example, to find the
|
1934 | * first visible link on a page, you could write:
|
1935 | *
|
1936 | * var link = element.findElement(firstVisibleLink);
|
1937 | *
|
1938 | * function firstVisibleLink(element) {
|
1939 | * var links = element.findElements(By.tagName('a'));
|
1940 | * return promise.filter(links, function(link) {
|
1941 | * return link.isDisplayed();
|
1942 | * });
|
1943 | * }
|
1944 | *
|
1945 | * @param {!(by.By|Function)} locator The locator strategy to use when
|
1946 | * searching for the element.
|
1947 | * @return {!WebElementPromise} A WebElement that can be used to issue
|
1948 | * commands against the located element. If the element is not found, the
|
1949 | * element will be invalidated and all scheduled commands aborted.
|
1950 | */
|
1951 | findElement(locator) {
|
1952 | locator = by.checkedLocator(locator);
|
1953 | let id;
|
1954 | if (typeof locator === 'function') {
|
1955 | id = this.driver_.findElementInternal_(locator, this);
|
1956 | } else {
|
1957 | let cmd = new command.Command(
|
1958 | command.Name.FIND_CHILD_ELEMENT).
|
1959 | setParameter('using', locator.using).
|
1960 | setParameter('value', locator.value);
|
1961 | id = this.schedule_(cmd, 'WebElement.findElement(' + locator + ')');
|
1962 | }
|
1963 | return new WebElementPromise(this.driver_, id);
|
1964 | }
|
1965 |
|
1966 | /**
|
1967 | * Schedules a command to find all of the descendants of this element that
|
1968 | * match the given search criteria.
|
1969 | *
|
1970 | * @param {!(by.By|Function)} locator The locator strategy to use when
|
1971 | * searching for the element.
|
1972 | * @return {!promise.Thenable<!Array<!WebElement>>} A
|
1973 | * promise that will resolve to an array of WebElements.
|
1974 | */
|
1975 | findElements(locator) {
|
1976 | locator = by.checkedLocator(locator);
|
1977 | let id;
|
1978 | if (typeof locator === 'function') {
|
1979 | return this.driver_.findElementsInternal_(locator, this);
|
1980 | } else {
|
1981 | var cmd = new command.Command(
|
1982 | command.Name.FIND_CHILD_ELEMENTS).
|
1983 | setParameter('using', locator.using).
|
1984 | setParameter('value', locator.value);
|
1985 | return this.schedule_(cmd, 'WebElement.findElements(' + locator + ')');
|
1986 | }
|
1987 | }
|
1988 |
|
1989 | /**
|
1990 | * Schedules a command to click on this element.
|
1991 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
1992 | * when the click command has completed.
|
1993 | */
|
1994 | click() {
|
1995 | return this.schedule_(
|
1996 | new command.Command(command.Name.CLICK_ELEMENT),
|
1997 | 'WebElement.click()');
|
1998 | }
|
1999 |
|
2000 | /**
|
2001 | * Schedules a command to type a sequence on the DOM element represented by
|
2002 | * this instance.
|
2003 | *
|
2004 | * Modifier keys (SHIFT, CONTROL, ALT, META) are stateful; once a modifier is
|
2005 | * processed in the keysequence, that key state is toggled until one of the
|
2006 | * following occurs:
|
2007 | *
|
2008 | * - The modifier key is encountered again in the sequence. At this point the
|
2009 | * state of the key is toggled (along with the appropriate keyup/down
|
2010 | * events).
|
2011 | * - The {@link input.Key.NULL} key is encountered in the sequence. When
|
2012 | * this key is encountered, all modifier keys current in the down state are
|
2013 | * released (with accompanying keyup events). The NULL key can be used to
|
2014 | * simulate common keyboard shortcuts:
|
2015 | *
|
2016 | * element.sendKeys("text was",
|
2017 | * Key.CONTROL, "a", Key.NULL,
|
2018 | * "now text is");
|
2019 | * // Alternatively:
|
2020 | * element.sendKeys("text was",
|
2021 | * Key.chord(Key.CONTROL, "a"),
|
2022 | * "now text is");
|
2023 | *
|
2024 | * - The end of the keysequence is encountered. When there are no more keys
|
2025 | * to type, all depressed modifier keys are released (with accompanying
|
2026 | * keyup events).
|
2027 | *
|
2028 | * If this element is a file input ({@code <input type="file">}), the
|
2029 | * specified key sequence should specify the path to the file to attach to
|
2030 | * the element. This is analgous to the user clicking "Browse..." and entering
|
2031 | * the path into the file select dialog.
|
2032 | *
|
2033 | * var form = driver.findElement(By.css('form'));
|
2034 | * var element = form.findElement(By.css('input[type=file]'));
|
2035 | * element.sendKeys('/path/to/file.txt');
|
2036 | * form.submit();
|
2037 | *
|
2038 | * For uploads to function correctly, the entered path must reference a file
|
2039 | * on the _browser's_ machine, not the local machine running this script. When
|
2040 | * running against a remote Selenium server, a {@link input.FileDetector}
|
2041 | * may be used to transparently copy files to the remote machine before
|
2042 | * attempting to upload them in the browser.
|
2043 | *
|
2044 | * __Note:__ On browsers where native keyboard events are not supported
|
2045 | * (e.g. Firefox on OS X), key events will be synthesized. Special
|
2046 | * punctionation keys will be synthesized according to a standard QWERTY en-us
|
2047 | * keyboard layout.
|
2048 | *
|
2049 | * @param {...(number|string|!IThenable<(number|string)>)} var_args The
|
2050 | * sequence of keys to type. Number keys may be referenced numerically or
|
2051 | * by string (1 or '1'). All arguments will be joined into a single
|
2052 | * sequence.
|
2053 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
2054 | * when all keys have been typed.
|
2055 | */
|
2056 | sendKeys(var_args) {
|
2057 | let keys = Promise.all(Array.prototype.slice.call(arguments, 0)).
|
2058 | then(keys => {
|
2059 | let ret = [];
|
2060 | keys.forEach(key => {
|
2061 | let type = typeof key;
|
2062 | if (type === 'number') {
|
2063 | key = String(key);
|
2064 | } else if (type !== 'string') {
|
2065 | throw TypeError(
|
2066 | 'each key must be a number of string; got ' + type);
|
2067 | }
|
2068 |
|
2069 | // The W3C protocol requires keys to be specified as an array where
|
2070 | // each element is a single key.
|
2071 | ret.push.apply(ret, key.split(''));
|
2072 | });
|
2073 | return ret;
|
2074 | });
|
2075 |
|
2076 | if (!this.driver_.fileDetector_) {
|
2077 | return this.schedule_(
|
2078 | new command.Command(command.Name.SEND_KEYS_TO_ELEMENT).
|
2079 | setParameter('value', keys),
|
2080 | 'WebElement.sendKeys()');
|
2081 | }
|
2082 |
|
2083 | // Suppress unhandled rejection errors until the flow executes the command.
|
2084 | keys.catch(function() {});
|
2085 |
|
2086 | var element = this;
|
2087 | return this.getDriver().controlFlow().execute(function() {
|
2088 | return keys.then(function(keys) {
|
2089 | return element.driver_.fileDetector_
|
2090 | .handleFile(element.driver_, keys.join(''));
|
2091 | }).then(function(keys) {
|
2092 | return element.schedule_(
|
2093 | new command.Command(command.Name.SEND_KEYS_TO_ELEMENT).
|
2094 | setParameter('value', keys.split('')),
|
2095 | 'WebElement.sendKeys()');
|
2096 | });
|
2097 | }, 'WebElement.sendKeys()');
|
2098 | }
|
2099 |
|
2100 | /**
|
2101 | * Schedules a command to query for the tag/node name of this element.
|
2102 | * @return {!promise.Thenable<string>} A promise that will be
|
2103 | * resolved with the element's tag name.
|
2104 | */
|
2105 | getTagName() {
|
2106 | return this.schedule_(
|
2107 | new command.Command(command.Name.GET_ELEMENT_TAG_NAME),
|
2108 | 'WebElement.getTagName()');
|
2109 | }
|
2110 |
|
2111 | /**
|
2112 | * Schedules a command to query for the computed style of the element
|
2113 | * represented by this instance. If the element inherits the named style from
|
2114 | * its parent, the parent will be queried for its value. Where possible, color
|
2115 | * values will be converted to their hex representation (e.g. #00ff00 instead
|
2116 | * of rgb(0, 255, 0)).
|
2117 | *
|
2118 | * _Warning:_ the value returned will be as the browser interprets it, so
|
2119 | * it may be tricky to form a proper assertion.
|
2120 | *
|
2121 | * @param {string} cssStyleProperty The name of the CSS style property to look
|
2122 | * up.
|
2123 | * @return {!promise.Thenable<string>} A promise that will be
|
2124 | * resolved with the requested CSS value.
|
2125 | */
|
2126 | getCssValue(cssStyleProperty) {
|
2127 | var name = command.Name.GET_ELEMENT_VALUE_OF_CSS_PROPERTY;
|
2128 | return this.schedule_(
|
2129 | new command.Command(name).
|
2130 | setParameter('propertyName', cssStyleProperty),
|
2131 | 'WebElement.getCssValue(' + cssStyleProperty + ')');
|
2132 | }
|
2133 |
|
2134 | /**
|
2135 | * Schedules a command to query for the value of the given attribute of the
|
2136 | * element. Will return the current value, even if it has been modified after
|
2137 | * the page has been loaded. More exactly, this method will return the value
|
2138 | * of the given attribute, unless that attribute is not present, in which case
|
2139 | * the value of the property with the same name is returned. If neither value
|
2140 | * is set, null is returned (for example, the "value" property of a textarea
|
2141 | * element). The "style" attribute is converted as best can be to a
|
2142 | * text representation with a trailing semi-colon. The following are deemed to
|
2143 | * be "boolean" attributes and will return either "true" or null:
|
2144 | *
|
2145 | * async, autofocus, autoplay, checked, compact, complete, controls, declare,
|
2146 | * defaultchecked, defaultselected, defer, disabled, draggable, ended,
|
2147 | * formnovalidate, hidden, indeterminate, iscontenteditable, ismap, itemscope,
|
2148 | * loop, multiple, muted, nohref, noresize, noshade, novalidate, nowrap, open,
|
2149 | * paused, pubdate, readonly, required, reversed, scoped, seamless, seeking,
|
2150 | * selected, spellcheck, truespeed, willvalidate
|
2151 | *
|
2152 | * Finally, the following commonly mis-capitalized attribute/property names
|
2153 | * are evaluated as expected:
|
2154 | *
|
2155 | * - "class"
|
2156 | * - "readonly"
|
2157 | *
|
2158 | * @param {string} attributeName The name of the attribute to query.
|
2159 | * @return {!promise.Thenable<?string>} A promise that will be
|
2160 | * resolved with the attribute's value. The returned value will always be
|
2161 | * either a string or null.
|
2162 | */
|
2163 | getAttribute(attributeName) {
|
2164 | return this.schedule_(
|
2165 | new command.Command(command.Name.GET_ELEMENT_ATTRIBUTE).
|
2166 | setParameter('name', attributeName),
|
2167 | 'WebElement.getAttribute(' + attributeName + ')');
|
2168 | }
|
2169 |
|
2170 | /**
|
2171 | * Get the visible (i.e. not hidden by CSS) innerText of this element,
|
2172 | * including sub-elements, without any leading or trailing whitespace.
|
2173 | *
|
2174 | * @return {!promise.Thenable<string>} A promise that will be
|
2175 | * resolved with the element's visible text.
|
2176 | */
|
2177 | getText() {
|
2178 | return this.schedule_(
|
2179 | new command.Command(command.Name.GET_ELEMENT_TEXT),
|
2180 | 'WebElement.getText()');
|
2181 | }
|
2182 |
|
2183 | /**
|
2184 | * Schedules a command to compute the size of this element's bounding box, in
|
2185 | * pixels.
|
2186 | * @return {!promise.Thenable<{width: number, height: number}>} A
|
2187 | * promise that will be resolved with the element's size as a
|
2188 | * {@code {width:number, height:number}} object.
|
2189 | */
|
2190 | getSize() {
|
2191 | return this.schedule_(
|
2192 | new command.Command(command.Name.GET_ELEMENT_SIZE),
|
2193 | 'WebElement.getSize()');
|
2194 | }
|
2195 |
|
2196 | /**
|
2197 | * Schedules a command to compute the location of this element in page space.
|
2198 | * @return {!promise.Thenable<{x: number, y: number}>} A promise that
|
2199 | * will be resolved to the element's location as a
|
2200 | * {@code {x:number, y:number}} object.
|
2201 | */
|
2202 | getLocation() {
|
2203 | return this.schedule_(
|
2204 | new command.Command(command.Name.GET_ELEMENT_LOCATION),
|
2205 | 'WebElement.getLocation()');
|
2206 | }
|
2207 |
|
2208 | /**
|
2209 | * Schedules a command to query whether the DOM element represented by this
|
2210 | * instance is enabled, as dicted by the {@code disabled} attribute.
|
2211 | * @return {!promise.Thenable<boolean>} A promise that will be
|
2212 | * resolved with whether this element is currently enabled.
|
2213 | */
|
2214 | isEnabled() {
|
2215 | return this.schedule_(
|
2216 | new command.Command(command.Name.IS_ELEMENT_ENABLED),
|
2217 | 'WebElement.isEnabled()');
|
2218 | }
|
2219 |
|
2220 | /**
|
2221 | * Schedules a command to query whether this element is selected.
|
2222 | * @return {!promise.Thenable<boolean>} A promise that will be
|
2223 | * resolved with whether this element is currently selected.
|
2224 | */
|
2225 | isSelected() {
|
2226 | return this.schedule_(
|
2227 | new command.Command(command.Name.IS_ELEMENT_SELECTED),
|
2228 | 'WebElement.isSelected()');
|
2229 | }
|
2230 |
|
2231 | /**
|
2232 | * Schedules a command to submit the form containing this element (or this
|
2233 | * element if it is a FORM element). This command is a no-op if the element is
|
2234 | * not contained in a form.
|
2235 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
2236 | * when the form has been submitted.
|
2237 | */
|
2238 | submit() {
|
2239 | return this.schedule_(
|
2240 | new command.Command(command.Name.SUBMIT_ELEMENT),
|
2241 | 'WebElement.submit()');
|
2242 | }
|
2243 |
|
2244 | /**
|
2245 | * Schedules a command to clear the `value` of this element. This command has
|
2246 | * no effect if the underlying DOM element is neither a text INPUT element
|
2247 | * nor a TEXTAREA element.
|
2248 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
2249 | * when the element has been cleared.
|
2250 | */
|
2251 | clear() {
|
2252 | return this.schedule_(
|
2253 | new command.Command(command.Name.CLEAR_ELEMENT),
|
2254 | 'WebElement.clear()');
|
2255 | }
|
2256 |
|
2257 | /**
|
2258 | * Schedules a command to test whether this element is currently displayed.
|
2259 | * @return {!promise.Thenable<boolean>} A promise that will be
|
2260 | * resolved with whether this element is currently visible on the page.
|
2261 | */
|
2262 | isDisplayed() {
|
2263 | return this.schedule_(
|
2264 | new command.Command(command.Name.IS_ELEMENT_DISPLAYED),
|
2265 | 'WebElement.isDisplayed()');
|
2266 | }
|
2267 |
|
2268 | /**
|
2269 | * Take a screenshot of the visible region encompassed by this element's
|
2270 | * bounding rectangle.
|
2271 | *
|
2272 | * @param {boolean=} opt_scroll Optional argument that indicates whether the
|
2273 | * element should be scrolled into view before taking a screenshot.
|
2274 | * Defaults to false.
|
2275 | * @return {!promise.Thenable<string>} A promise that will be
|
2276 | * resolved to the screenshot as a base-64 encoded PNG.
|
2277 | */
|
2278 | takeScreenshot(opt_scroll) {
|
2279 | var scroll = !!opt_scroll;
|
2280 | return this.schedule_(
|
2281 | new command.Command(command.Name.TAKE_ELEMENT_SCREENSHOT)
|
2282 | .setParameter('scroll', scroll),
|
2283 | 'WebElement.takeScreenshot(' + scroll + ')');
|
2284 | }
|
2285 | }
|
2286 |
|
2287 |
|
2288 | /**
|
2289 | * WebElementPromise is a promise that will be fulfilled with a WebElement.
|
2290 | * This serves as a forward proxy on WebElement, allowing calls to be
|
2291 | * scheduled without directly on this instance before the underlying
|
2292 | * WebElement has been fulfilled. In other words, the following two statements
|
2293 | * are equivalent:
|
2294 | *
|
2295 | * driver.findElement({id: 'my-button'}).click();
|
2296 | * driver.findElement({id: 'my-button'}).then(function(el) {
|
2297 | * return el.click();
|
2298 | * });
|
2299 | *
|
2300 | * @implements {promise.CancellableThenable<!WebElement>}
|
2301 | * @final
|
2302 | */
|
2303 | class WebElementPromise extends WebElement {
|
2304 | /**
|
2305 | * @param {!WebDriver} driver The parent WebDriver instance for this
|
2306 | * element.
|
2307 | * @param {!promise.Thenable<!WebElement>} el A promise
|
2308 | * that will resolve to the promised element.
|
2309 | */
|
2310 | constructor(driver, el) {
|
2311 | super(driver, 'unused');
|
2312 |
|
2313 | /**
|
2314 | * Cancel operation is only supported if the wrapped thenable is also
|
2315 | * cancellable.
|
2316 | * @param {(string|Error)=} opt_reason
|
2317 | * @override
|
2318 | */
|
2319 | this.cancel = function(opt_reason) {
|
2320 | if (promise.CancellableThenable.isImplementation(el)) {
|
2321 | /** @type {!promise.CancellableThenable} */(el).cancel(opt_reason);
|
2322 | }
|
2323 | }
|
2324 |
|
2325 | /** @override */
|
2326 | this.then = el.then.bind(el);
|
2327 |
|
2328 | /** @override */
|
2329 | this.catch = el.catch.bind(el);
|
2330 |
|
2331 | /**
|
2332 | * Defers returning the element ID until the wrapped WebElement has been
|
2333 | * resolved.
|
2334 | * @override
|
2335 | */
|
2336 | this.getId = function() {
|
2337 | return el.then(function(el) {
|
2338 | return el.getId();
|
2339 | });
|
2340 | };
|
2341 | }
|
2342 | }
|
2343 | promise.CancellableThenable.addImplementation(WebElementPromise);
|
2344 |
|
2345 |
|
2346 | //////////////////////////////////////////////////////////////////////////////
|
2347 | //
|
2348 | // Alert
|
2349 | //
|
2350 | //////////////////////////////////////////////////////////////////////////////
|
2351 |
|
2352 |
|
2353 | /**
|
2354 | * Represents a modal dialog such as {@code alert}, {@code confirm}, or
|
2355 | * {@code prompt}. Provides functions to retrieve the message displayed with
|
2356 | * the alert, accept or dismiss the alert, and set the response text (in the
|
2357 | * case of {@code prompt}).
|
2358 | */
|
2359 | class Alert {
|
2360 | /**
|
2361 | * @param {!WebDriver} driver The driver controlling the browser this alert
|
2362 | * is attached to.
|
2363 | * @param {string} text The message text displayed with this alert.
|
2364 | */
|
2365 | constructor(driver, text) {
|
2366 | /** @private {!WebDriver} */
|
2367 | this.driver_ = driver;
|
2368 |
|
2369 | /** @private {!promise.Thenable<string>} */
|
2370 | this.text_ = driver.controlFlow().promise(resolve => resolve(text));
|
2371 | }
|
2372 |
|
2373 | /**
|
2374 | * Retrieves the message text displayed with this alert. For instance, if the
|
2375 | * alert were opened with alert("hello"), then this would return "hello".
|
2376 | *
|
2377 | * @return {!promise.Thenable<string>} A promise that will be
|
2378 | * resolved to the text displayed with this alert.
|
2379 | */
|
2380 | getText() {
|
2381 | return this.text_;
|
2382 | }
|
2383 |
|
2384 | /**
|
2385 | * Sets the username and password in an alert prompting for credentials (such
|
2386 | * as a Basic HTTP Auth prompt). This method will implicitly
|
2387 | * {@linkplain #accept() submit} the dialog.
|
2388 | *
|
2389 | * @param {string} username The username to send.
|
2390 | * @param {string} password The password to send.
|
2391 | * @return {!promise.Thenable<void>} A promise that will be resolved when this
|
2392 | * command has completed.
|
2393 | */
|
2394 | authenticateAs(username, password) {
|
2395 | return this.driver_.schedule(
|
2396 | new command.Command(command.Name.SET_ALERT_CREDENTIALS),
|
2397 | 'WebDriver.switchTo().alert()'
|
2398 | + `.authenticateAs("${username}", "${password}")`);
|
2399 | }
|
2400 |
|
2401 | /**
|
2402 | * Accepts this alert.
|
2403 | *
|
2404 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
2405 | * when this command has completed.
|
2406 | */
|
2407 | accept() {
|
2408 | return this.driver_.schedule(
|
2409 | new command.Command(command.Name.ACCEPT_ALERT),
|
2410 | 'WebDriver.switchTo().alert().accept()');
|
2411 | }
|
2412 |
|
2413 | /**
|
2414 | * Dismisses this alert.
|
2415 | *
|
2416 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
2417 | * when this command has completed.
|
2418 | */
|
2419 | dismiss() {
|
2420 | return this.driver_.schedule(
|
2421 | new command.Command(command.Name.DISMISS_ALERT),
|
2422 | 'WebDriver.switchTo().alert().dismiss()');
|
2423 | }
|
2424 |
|
2425 | /**
|
2426 | * Sets the response text on this alert. This command will return an error if
|
2427 | * the underlying alert does not support response text (e.g. window.alert and
|
2428 | * window.confirm).
|
2429 | *
|
2430 | * @param {string} text The text to set.
|
2431 | * @return {!promise.Thenable<void>} A promise that will be resolved
|
2432 | * when this command has completed.
|
2433 | */
|
2434 | sendKeys(text) {
|
2435 | return this.driver_.schedule(
|
2436 | new command.Command(command.Name.SET_ALERT_TEXT).
|
2437 | setParameter('text', text),
|
2438 | 'WebDriver.switchTo().alert().sendKeys(' + text + ')');
|
2439 | }
|
2440 | }
|
2441 |
|
2442 |
|
2443 | /**
|
2444 | * AlertPromise is a promise that will be fulfilled with an Alert. This promise
|
2445 | * serves as a forward proxy on an Alert, allowing calls to be scheduled
|
2446 | * directly on this instance before the underlying Alert has been fulfilled. In
|
2447 | * other words, the following two statements are equivalent:
|
2448 | *
|
2449 | * driver.switchTo().alert().dismiss();
|
2450 | * driver.switchTo().alert().then(function(alert) {
|
2451 | * return alert.dismiss();
|
2452 | * });
|
2453 | *
|
2454 | * @implements {promise.CancellableThenable<!webdriver.Alert>}
|
2455 | * @final
|
2456 | */
|
2457 | class AlertPromise extends Alert {
|
2458 | /**
|
2459 | * @param {!WebDriver} driver The driver controlling the browser this
|
2460 | * alert is attached to.
|
2461 | * @param {!promise.Thenable<!Alert>} alert A thenable
|
2462 | * that will be fulfilled with the promised alert.
|
2463 | */
|
2464 | constructor(driver, alert) {
|
2465 | super(driver, 'unused');
|
2466 |
|
2467 | /**
|
2468 | * Cancel operation is only supported if the wrapped thenable is also
|
2469 | * cancellable.
|
2470 | * @param {(string|Error)=} opt_reason
|
2471 | * @override
|
2472 | */
|
2473 | this.cancel = function(opt_reason) {
|
2474 | if (promise.CancellableThenable.isImplementation(alert)) {
|
2475 | /** @type {!promise.CancellableThenable} */(alert).cancel(opt_reason);
|
2476 | }
|
2477 | };
|
2478 |
|
2479 | /** @override */
|
2480 | this.then = alert.then.bind(alert);
|
2481 |
|
2482 | /** @override */
|
2483 | this.catch = alert.catch.bind(alert);
|
2484 |
|
2485 | /**
|
2486 | * Defer returning text until the promised alert has been resolved.
|
2487 | * @override
|
2488 | */
|
2489 | this.getText = function() {
|
2490 | return alert.then(function(alert) {
|
2491 | return alert.getText();
|
2492 | });
|
2493 | };
|
2494 |
|
2495 | /**
|
2496 | * Defers action until the alert has been located.
|
2497 | * @override
|
2498 | */
|
2499 | this.authenticateAs = function(username, password) {
|
2500 | return alert.then(function(alert) {
|
2501 | return alert.authenticateAs(username, password);
|
2502 | });
|
2503 | };
|
2504 |
|
2505 | /**
|
2506 | * Defers action until the alert has been located.
|
2507 | * @override
|
2508 | */
|
2509 | this.accept = function() {
|
2510 | return alert.then(function(alert) {
|
2511 | return alert.accept();
|
2512 | });
|
2513 | };
|
2514 |
|
2515 | /**
|
2516 | * Defers action until the alert has been located.
|
2517 | * @override
|
2518 | */
|
2519 | this.dismiss = function() {
|
2520 | return alert.then(function(alert) {
|
2521 | return alert.dismiss();
|
2522 | });
|
2523 | };
|
2524 |
|
2525 | /**
|
2526 | * Defers action until the alert has been located.
|
2527 | * @override
|
2528 | */
|
2529 | this.sendKeys = function(text) {
|
2530 | return alert.then(function(alert) {
|
2531 | return alert.sendKeys(text);
|
2532 | });
|
2533 | };
|
2534 | }
|
2535 | }
|
2536 | promise.CancellableThenable.addImplementation(AlertPromise);
|
2537 |
|
2538 |
|
2539 | // PUBLIC API
|
2540 |
|
2541 |
|
2542 | module.exports = {
|
2543 | Alert: Alert,
|
2544 | AlertPromise: AlertPromise,
|
2545 | Condition: Condition,
|
2546 | Logs: Logs,
|
2547 | Navigation: Navigation,
|
2548 | Options: Options,
|
2549 | TargetLocator: TargetLocator,
|
2550 | Timeouts: Timeouts,
|
2551 | IWebDriver: IWebDriver,
|
2552 | WebDriver: WebDriver,
|
2553 | WebElement: WebElement,
|
2554 | WebElementCondition: WebElementCondition,
|
2555 | WebElementPromise: WebElementPromise,
|
2556 | Window: Window
|
2557 | };
|