UNPKG

85.6 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 * @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 */
652class 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 */
1116class 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 */
1180class 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 */
1351Options.Cookie = function() {};
1352
1353
1354/**
1355 * The name of the cookie.
1356 *
1357 * @type {string}
1358 */
1359Options.Cookie.prototype.name;
1360
1361
1362/**
1363 * The cookie value.
1364 *
1365 * @type {string}
1366 */
1367Options.Cookie.prototype.value;
1368
1369
1370/**
1371 * The cookie path. Defaults to "/" when adding a cookie.
1372 *
1373 * @type {(string|undefined)}
1374 */
1375Options.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 */
1384Options.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 */
1393Options.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 */
1402Options.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 */
1417Options.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 */
1431class 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 */
1513class 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 */
1609class 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 */
1672class 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
1784const LEGACY_ELEMENT_ID_KEY = 'ELEMENT';
1785const 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 */
1798class 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 */
2303class 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}
2343promise.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 */
2359class 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 */
2457class 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}
2536promise.CancellableThenable.addImplementation(AlertPromise);
2537
2538
2539// PUBLIC API
2540
2541
2542module.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};