UNPKG

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