UNPKG

91.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 by = require('./by')
25const { RelativeBy } = require('./by')
26const command = require('./command')
27const error = require('./error')
28const input = require('./input')
29const logging = require('./logging')
30const promise = require('./promise')
31const Symbols = require('./symbols')
32const cdpTargets = ['page', 'browser']
33const cdp = require('../devtools/CDPConnection')
34const WebSocket = require('ws')
35const http = require('../http/index')
36const fs = require('fs')
37const { Capabilities } = require('./capabilities')
38const path = require('path')
39const { NoSuchElementError } = require('./error')
40
41// Capability names that are defined in the W3C spec.
42const W3C_CAPABILITY_NAMES = new Set([
43 'acceptInsecureCerts',
44 'browserName',
45 'browserVersion',
46 'pageLoadStrategy',
47 'platformName',
48 'proxy',
49 'setWindowRect',
50 'strictFileInteractability',
51 'timeouts',
52 'unhandledPromptBehavior',
53 'webSocketUrl'
54])
55
56/**
57 * Defines a condition for use with WebDriver's {@linkplain WebDriver#wait wait
58 * command}.
59 *
60 * @template OUT
61 */
62class Condition {
63 /**
64 * @param {string} message A descriptive error message. Should complete the
65 * sentence "Waiting [...]"
66 * @param {function(!WebDriver): OUT} fn The condition function to
67 * evaluate on each iteration of the wait loop.
68 */
69 constructor(message, fn) {
70 /** @private {string} */
71 this.description_ = 'Waiting ' + message
72
73 /** @type {function(!WebDriver): OUT} */
74 this.fn = fn
75 }
76
77 /** @return {string} A description of this condition. */
78 description() {
79 return this.description_
80 }
81}
82
83/**
84 * Defines a condition that will result in a {@link WebElement}.
85 *
86 * @extends {Condition<!(WebElement|IThenable<!WebElement>)>}
87 */
88class WebElementCondition extends Condition {
89 /**
90 * @param {string} message A descriptive error message. Should complete the
91 * sentence "Waiting [...]"
92 * @param {function(!WebDriver): !(WebElement|IThenable<!WebElement>)}
93 * fn The condition function to evaluate on each iteration of the wait
94 * loop.
95 */
96 constructor(message, fn) {
97 super(message, fn)
98 }
99}
100
101//////////////////////////////////////////////////////////////////////////////
102//
103// WebDriver
104//
105//////////////////////////////////////////////////////////////////////////////
106
107/**
108 * Translates a command to its wire-protocol representation before passing it
109 * to the given `executor` for execution.
110 * @param {!command.Executor} executor The executor to use.
111 * @param {!command.Command} command The command to execute.
112 * @return {!Promise} A promise that will resolve with the command response.
113 */
114function executeCommand(executor, command) {
115 return toWireValue(command.getParameters()).then(function (parameters) {
116 command.setParameters(parameters)
117 return executor.execute(command)
118 })
119}
120
121/**
122 * Converts an object to its JSON representation in the WebDriver wire protocol.
123 * When converting values of type object, the following steps will be taken:
124 * <ol>
125 * <li>if the object is a WebElement, the return value will be the element's
126 * server ID
127 * <li>if the object defines a {@link Symbols.serialize} method, this algorithm
128 * will be recursively applied to the object's serialized representation
129 * <li>if the object provides a "toJSON" function, this algorithm will
130 * recursively be applied to the result of that function
131 * <li>otherwise, the value of each key will be recursively converted according
132 * to the rules above.
133 * </ol>
134 *
135 * @param {*} obj The object to convert.
136 * @return {!Promise<?>} A promise that will resolve to the input value's JSON
137 * representation.
138 */
139async function toWireValue(obj) {
140 let value = await Promise.resolve(obj)
141 if (value === void 0 || value === null) {
142 return value
143 }
144
145 if (
146 typeof value === 'boolean' ||
147 typeof value === 'number' ||
148 typeof value === 'string'
149 ) {
150 return value
151 }
152
153 if (Array.isArray(value)) {
154 return convertKeys(value)
155 }
156
157 if (typeof value === 'function') {
158 return '' + value
159 }
160
161 if (typeof value[Symbols.serialize] === 'function') {
162 return toWireValue(value[Symbols.serialize]())
163 } else if (typeof value.toJSON === 'function') {
164 return toWireValue(value.toJSON())
165 }
166 return convertKeys(value)
167}
168
169async function convertKeys(obj) {
170 const isArray = Array.isArray(obj)
171 const numKeys = isArray ? obj.length : Object.keys(obj).length
172 const ret = isArray ? new Array(numKeys) : {}
173 if (!numKeys) {
174 return ret
175 }
176
177 async function forEachKey(obj, fn) {
178 if (Array.isArray(obj)) {
179 for (let i = 0, n = obj.length; i < n; i++) {
180 await fn(obj[i], i)
181 }
182 } else {
183 for (let key in obj) {
184 await fn(obj[key], key)
185 }
186 }
187 }
188
189 await forEachKey(obj, async function (value, key) {
190 ret[key] = await toWireValue(value)
191 })
192
193 return ret
194}
195
196/**
197 * Converts a value from its JSON representation according to the WebDriver wire
198 * protocol. Any JSON object that defines a WebElement ID will be decoded to a
199 * {@link WebElement} object. All other values will be passed through as is.
200 *
201 * @param {!WebDriver} driver The driver to use as the parent of any unwrapped
202 * {@link WebElement} values.
203 * @param {*} value The value to convert.
204 * @return {*} The converted value.
205 */
206function fromWireValue(driver, value) {
207 if (Array.isArray(value)) {
208 value = value.map((v) => fromWireValue(driver, v))
209 } else if (WebElement.isId(value)) {
210 let id = WebElement.extractId(value)
211 value = new WebElement(driver, id)
212 } else if (value && typeof value === 'object') {
213 let result = {}
214 for (let key in value) {
215 if (Object.prototype.hasOwnProperty.call(value, key)) {
216 result[key] = fromWireValue(driver, value[key])
217 }
218 }
219 value = result
220 }
221 return value
222}
223
224/**
225 * Resolves a wait message from either a function or a string.
226 * @param {(string|Function)=} message An optional message to use if the wait times out.
227 * @return {string} The resolved message
228 */
229function resolveWaitMessage(message) {
230 return message
231 ? `${typeof message === 'function' ? message() : message}\n`
232 : ''
233}
234
235/**
236 * Structural interface for a WebDriver client.
237 *
238 * @record
239 */
240class IWebDriver {
241 /**
242 * Executes the provided {@link command.Command} using this driver's
243 * {@link command.Executor}.
244 *
245 * @param {!command.Command} command The command to schedule.
246 * @return {!Promise<T>} A promise that will be resolved with the command
247 * result.
248 * @template T
249 */
250 execute(command) { } // eslint-disable-line
251
252 /**
253 * Sets the {@linkplain input.FileDetector file detector} that should be
254 * used with this instance.
255 * @param {input.FileDetector} detector The detector to use or `null`.
256 */
257 setFileDetector(detector) { } // eslint-disable-line
258
259 /**
260 * @return {!command.Executor} The command executor used by this instance.
261 */
262 getExecutor() { }
263
264 /**
265 * @return {!Promise<!Session>} A promise for this client's session.
266 */
267 getSession() { }
268
269 /**
270 * @return {!Promise<!Capabilities>} A promise that will resolve with
271 * the this instance's capabilities.
272 */
273 getCapabilities() { }
274
275 /**
276 * Terminates the browser session. After calling quit, this instance will be
277 * invalidated and may no longer be used to issue commands against the
278 * browser.
279 *
280 * @return {!Promise<void>} A promise that will be resolved when the
281 * command has completed.
282 */
283 quit() { }
284
285 /**
286 * Creates a new action sequence using this driver. The sequence will not be
287 * submitted for execution until
288 * {@link ./input.Actions#perform Actions.perform()} is called.
289 *
290 * @param {{async: (boolean|undefined),
291 * bridge: (boolean|undefined)}=} options Configuration options for
292 * the action sequence (see {@link ./input.Actions Actions} documentation
293 * for details).
294 * @return {!input.Actions} A new action sequence for this instance.
295 */
296 actions(options) { } // eslint-disable-line
297
298 /**
299 * Executes a snippet of JavaScript in the context of the currently selected
300 * frame or window. The script fragment will be executed as the body of an
301 * anonymous function. If the script is provided as a function object, that
302 * function will be converted to a string for injection into the target
303 * window.
304 *
305 * Any arguments provided in addition to the script will be included as script
306 * arguments and may be referenced using the `arguments` object. Arguments may
307 * be a boolean, number, string, or {@linkplain WebElement}. Arrays and
308 * objects may also be used as script arguments as long as each item adheres
309 * to the types previously mentioned.
310 *
311 * The script may refer to any variables accessible from the current window.
312 * Furthermore, the script will execute in the window's context, thus
313 * `document` may be used to refer to the current document. Any local
314 * variables will not be available once the script has finished executing,
315 * though global variables will persist.
316 *
317 * If the script has a return value (i.e. if the script contains a return
318 * statement), then the following steps will be taken for resolving this
319 * functions return value:
320 *
321 * - For a HTML element, the value will resolve to a {@linkplain WebElement}
322 * - Null and undefined return values will resolve to null</li>
323 * - Booleans, numbers, and strings will resolve as is</li>
324 * - Functions will resolve to their string representation</li>
325 * - For arrays and objects, each member item will be converted according to
326 * the rules above
327 *
328 * @param {!(string|Function)} script The script to execute.
329 * @param {...*} args The arguments to pass to the script.
330 * @return {!IThenable<T>} A promise that will resolve to the
331 * scripts return value.
332 * @template T
333 */
334 executeScript(script, ...args) { } // eslint-disable-line
335
336 /**
337 * Executes a snippet of asynchronous JavaScript in the context of the
338 * currently selected frame or window. The script fragment will be executed as
339 * the body of an anonymous function. If the script is provided as a function
340 * object, that function will be converted to a string for injection into the
341 * target window.
342 *
343 * Any arguments provided in addition to the script will be included as script
344 * arguments and may be referenced using the `arguments` object. Arguments may
345 * be a boolean, number, string, or {@linkplain WebElement}. Arrays and
346 * objects may also be used as script arguments as long as each item adheres
347 * to the types previously mentioned.
348 *
349 * Unlike executing synchronous JavaScript with {@link #executeScript},
350 * scripts executed with this function must explicitly signal they are
351 * finished by invoking the provided callback. This callback will always be
352 * injected into the executed function as the last argument, and thus may be
353 * referenced with `arguments[arguments.length - 1]`. The following steps
354 * will be taken for resolving this functions return value against the first
355 * argument to the script's callback function:
356 *
357 * - For a HTML element, the value will resolve to a {@link WebElement}
358 * - Null and undefined return values will resolve to null
359 * - Booleans, numbers, and strings will resolve as is
360 * - Functions will resolve to their string representation
361 * - For arrays and objects, each member item will be converted according to
362 * the rules above
363 *
364 * __Example #1:__ Performing a sleep that is synchronized with the currently
365 * selected window:
366 *
367 * var start = new Date().getTime();
368 * driver.executeAsyncScript(
369 * 'window.setTimeout(arguments[arguments.length - 1], 500);').
370 * then(function() {
371 * console.log(
372 * 'Elapsed time: ' + (new Date().getTime() - start) + ' ms');
373 * });
374 *
375 * __Example #2:__ Synchronizing a test with an AJAX application:
376 *
377 * var button = driver.findElement(By.id('compose-button'));
378 * button.click();
379 * driver.executeAsyncScript(
380 * 'var callback = arguments[arguments.length - 1];' +
381 * 'mailClient.getComposeWindowWidget().onload(callback);');
382 * driver.switchTo().frame('composeWidget');
383 * driver.findElement(By.id('to')).sendKeys('dog@example.com');
384 *
385 * __Example #3:__ Injecting a XMLHttpRequest and waiting for the result. In
386 * this example, the inject script is specified with a function literal. When
387 * using this format, the function is converted to a string for injection, so
388 * it should not reference any symbols not defined in the scope of the page
389 * under test.
390 *
391 * driver.executeAsyncScript(function() {
392 * var callback = arguments[arguments.length - 1];
393 * var xhr = new XMLHttpRequest();
394 * xhr.open("GET", "/resource/data.json", true);
395 * xhr.onreadystatechange = function() {
396 * if (xhr.readyState == 4) {
397 * callback(xhr.responseText);
398 * }
399 * };
400 * xhr.send('');
401 * }).then(function(str) {
402 * console.log(JSON.parse(str)['food']);
403 * });
404 *
405 * @param {!(string|Function)} script The script to execute.
406 * @param {...*} args The arguments to pass to the script.
407 * @return {!IThenable<T>} A promise that will resolve to the scripts return
408 * value.
409 * @template T
410 */
411 executeAsyncScript(script, ...args) { } // eslint-disable-line
412
413 /**
414 * Waits for a condition to evaluate to a "truthy" value. The condition may be
415 * specified by a {@link Condition}, as a custom function, or as any
416 * promise-like thenable.
417 *
418 * For a {@link Condition} or function, the wait will repeatedly
419 * evaluate the condition until it returns a truthy value. If any errors occur
420 * while evaluating the condition, they will be allowed to propagate. In the
421 * event a condition returns a {@linkplain Promise}, the polling loop will
422 * wait for it to be resolved and use the resolved value for whether the
423 * condition has been satisfied. The resolution time for a promise is always
424 * factored into whether a wait has timed out.
425 *
426 * If the provided condition is a {@link WebElementCondition}, then
427 * the wait will return a {@link WebElementPromise} that will resolve to the
428 * element that satisfied the condition.
429 *
430 * _Example:_ waiting up to 10 seconds for an element to be present on the
431 * page.
432 *
433 * async function example() {
434 * let button =
435 * await driver.wait(until.elementLocated(By.id('foo')), 10000);
436 * await button.click();
437 * }
438 *
439 * @param {!(IThenable<T>|
440 * Condition<T>|
441 * function(!WebDriver): T)} condition The condition to
442 * wait on, defined as a promise, condition object, or a function to
443 * evaluate as a condition.
444 * @param {number=} timeout The duration in milliseconds, how long to wait
445 * for the condition to be true.
446 * @param {(string|Function)=} message An optional message to use if the wait times out.
447 * @param {number=} pollTimeout The duration in milliseconds, how long to
448 * wait between polling the condition.
449 * @return {!(IThenable<T>|WebElementPromise)} A promise that will be
450 * resolved with the first truthy value returned by the condition
451 * function, or rejected if the condition times out. If the input
452 * input condition is an instance of a {@link WebElementCondition},
453 * the returned value will be a {@link WebElementPromise}.
454 * @throws {TypeError} if the provided `condition` is not a valid type.
455 * @template T
456 */
457 wait(
458 condition, // eslint-disable-line
459 timeout = undefined, // eslint-disable-line
460 message = undefined, // eslint-disable-line
461 pollTimeout = undefined // eslint-disable-line
462 ) { }
463
464 /**
465 * Makes the driver sleep for the given amount of time.
466 *
467 * @param {number} ms The amount of time, in milliseconds, to sleep.
468 * @return {!Promise<void>} A promise that will be resolved when the sleep has
469 * finished.
470 */
471 sleep(ms) { } // eslint-disable-line
472
473 /**
474 * Retrieves the current window handle.
475 *
476 * @return {!Promise<string>} A promise that will be resolved with the current
477 * window handle.
478 */
479 getWindowHandle() { }
480
481 /**
482 * Retrieves a list of all available window handles.
483 *
484 * @return {!Promise<!Array<string>>} A promise that will be resolved with an
485 * array of window handles.
486 */
487 getAllWindowHandles() { }
488
489 /**
490 * Retrieves the current page's source. The returned source is a representation
491 * of the underlying DOM: do not expect it to be formatted or escaped in the
492 * same way as the raw response sent from the web server.
493 *
494 * @return {!Promise<string>} A promise that will be resolved with the current
495 * page source.
496 */
497 getPageSource() { }
498
499 /**
500 * Closes the current window.
501 *
502 * @return {!Promise<void>} A promise that will be resolved when this command
503 * has completed.
504 */
505 close() { }
506
507 /**
508 * Navigates to the given URL.
509 *
510 * @param {string} url The fully qualified URL to open.
511 * @return {!Promise<void>} A promise that will be resolved when the document
512 * has finished loading.
513 */
514 get(url) { } // eslint-disable-line
515
516 /**
517 * Retrieves the URL for the current page.
518 *
519 * @return {!Promise<string>} A promise that will be resolved with the
520 * current URL.
521 */
522 getCurrentUrl() { }
523
524 /**
525 * Retrieves the current page title.
526 *
527 * @return {!Promise<string>} A promise that will be resolved with the current
528 * page's title.
529 */
530 getTitle() { }
531
532 /**
533 * Locates an element on the page. If the element cannot be found, a
534 * {@link error.NoSuchElementError} will be returned by the driver.
535 *
536 * This function should not be used to test whether an element is present on
537 * the page. Rather, you should use {@link #findElements}:
538 *
539 * driver.findElements(By.id('foo'))
540 * .then(found => console.log('Element found? %s', !!found.length));
541 *
542 * The search criteria for an element may be defined using one of the
543 * factories in the {@link webdriver.By} namespace, or as a short-hand
544 * {@link webdriver.By.Hash} object. For example, the following two statements
545 * are equivalent:
546 *
547 * var e1 = driver.findElement(By.id('foo'));
548 * var e2 = driver.findElement({id:'foo'});
549 *
550 * You may also provide a custom locator function, which takes as input this
551 * instance and returns a {@link WebElement}, or a promise that will resolve
552 * to a WebElement. If the returned promise resolves to an array of
553 * WebElements, WebDriver will use the first element. For example, to find the
554 * first visible link on a page, you could write:
555 *
556 * var link = driver.findElement(firstVisibleLink);
557 *
558 * function firstVisibleLink(driver) {
559 * var links = driver.findElements(By.tagName('a'));
560 * return promise.filter(links, function(link) {
561 * return link.isDisplayed();
562 * });
563 * }
564 *
565 * @param {!(by.By|Function)} locator The locator to use.
566 * @return {!WebElementPromise} A WebElement that can be used to issue
567 * commands against the located element. If the element is not found, the
568 * element will be invalidated and all scheduled commands aborted.
569 */
570 findElement(locator) { } // eslint-disable-line
571
572 /**
573 * Search for multiple elements on the page. Refer to the documentation on
574 * {@link #findElement(by)} for information on element locator strategies.
575 *
576 * @param {!(by.By|Function)} locator The locator to use.
577 * @return {!Promise<!Array<!WebElement>>} A promise that will resolve to an
578 * array of WebElements.
579 */
580 findElements(locator) { } // eslint-disable-line
581
582 /**
583 * Takes a screenshot of the current page. The driver makes a best effort to
584 * return a screenshot of the following, in order of preference:
585 *
586 * 1. Entire page
587 * 2. Current window
588 * 3. Visible portion of the current frame
589 * 4. The entire display containing the browser
590 *
591 * @return {!Promise<string>} A promise that will be resolved to the
592 * screenshot as a base-64 encoded PNG.
593 */
594 takeScreenshot() { }
595
596 /**
597 * @return {!Options} The options interface for this instance.
598 */
599 manage() { }
600
601 /**
602 * @return {!Navigation} The navigation interface for this instance.
603 */
604 navigate() { }
605
606 /**
607 * @return {!TargetLocator} The target locator interface for this
608 * instance.
609 */
610 switchTo() { }
611
612 /**
613 *
614 * Takes a PDF of the current page. The driver makes a best effort to
615 * return a PDF based on the provided parameters.
616 *
617 * @param {{orientation: (string|undefined),
618 * scale: (number|undefined),
619 * background: (boolean|undefined)
620 * width: (number|undefined)
621 * height: (number|undefined)
622 * top: (number|undefined)
623 * bottom: (number|undefined)
624 * left: (number|undefined)
625 * right: (number|undefined)
626 * shrinkToFit: (boolean|undefined)
627 * pageRanges: (<Array>|undefined)}} options.
628 */
629 printPage(options) { } // eslint-disable-line
630}
631
632/**
633 * @param {!Capabilities} capabilities A capabilities object.
634 * @return {!Capabilities} A copy of the parameter capabilities, omitting
635 * capability names that are not valid W3C names.
636 */
637function filterNonW3CCaps(capabilities) {
638 let newCaps = new Capabilities(capabilities)
639 for (let k of newCaps.keys()) {
640 // Any key containing a colon is a vendor-prefixed capability.
641 if (!(W3C_CAPABILITY_NAMES.has(k) || k.indexOf(':') >= 0)) {
642 newCaps.delete(k)
643 }
644 }
645 return newCaps
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.Session|IThenable<!./session.Session>)} session Either
656 * a known session or a 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 {(function(this: void): ?)=} onQuit A function to call, if any,
660 * when the session is terminated.
661 */
662 constructor(session, executor, onQuit = undefined) {
663 /** @private {!Promise<!Session>} */
664 this.session_ = Promise.resolve(session)
665
666 // If session is a rejected promise, add a no-op rejection handler.
667 // This effectively hides setup errors until users attempt to interact
668 // with the session.
669 this.session_.catch(function () { })
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_ = onQuit
679 }
680
681 /**
682 * Creates a new WebDriver session.
683 *
684 * This function will always return a WebDriver instance. If there is an error
685 * creating the session, such as the aforementioned SessionNotCreatedError,
686 * the driver will have a rejected {@linkplain #getSession session} promise.
687 * This rejection will propagate through any subsequent commands scheduled
688 * on the returned WebDriver instance.
689 *
690 * let required = Capabilities.firefox();
691 * let driver = WebDriver.createSession(executor, {required});
692 *
693 * // If the createSession operation failed, then this command will also
694 * // also fail, propagating the creation failure.
695 * driver.get('http://www.google.com').catch(e => console.log(e));
696 *
697 * @param {!command.Executor} executor The executor to create the new session
698 * with.
699 * @param {!Capabilities} capabilities The desired capabilities for the new
700 * session.
701 * @param {(function(this: void): ?)=} onQuit A callback to invoke when
702 * the newly created session is terminated. This should be used to clean
703 * up any resources associated with the session.
704 * @return {!WebDriver} The driver for the newly created session.
705 */
706 static createSession(executor, capabilities, onQuit = undefined) {
707 let cmd = new command.Command(command.Name.NEW_SESSION)
708
709 // For OSS remote ends.
710 cmd.setParameter('desiredCapabilities', capabilities)
711 // For W3C remote ends.
712 cmd.setParameter('capabilities', {
713 alwaysMatch: filterNonW3CCaps(capabilities),
714 })
715
716 let session = executeCommand(executor, cmd)
717 if (typeof onQuit === 'function') {
718 session = session.catch((err) => {
719 return Promise.resolve(onQuit.call(void 0)).then((_) => {
720 throw err
721 })
722 })
723 }
724 return new this(session, executor, onQuit)
725 }
726
727 /** @override */
728 async execute(command) {
729 command.setParameter('sessionId', this.session_)
730 let parameters = await toWireValue(command.getParameters())
731 command.setParameters(parameters)
732 let value = await this.executor_.execute(command)
733 return fromWireValue(this, value)
734 }
735
736 /** @override */
737 setFileDetector(detector) {
738 this.fileDetector_ = detector
739 }
740
741 /** @override */
742 getExecutor() {
743 return this.executor_
744 }
745
746 /** @override */
747 getSession() {
748 return this.session_
749 }
750
751 /** @override */
752 getCapabilities() {
753 return this.session_.then((s) => s.getCapabilities())
754 }
755
756 /** @override */
757 quit() {
758 let result = this.execute(new command.Command(command.Name.QUIT))
759 // Delete our session ID when the quit command finishes; this will allow us
760 // to throw an error when attempting to use a driver post-quit.
761 return promise.finally(result, () => {
762 this.session_ = Promise.reject(
763 new error.NoSuchSessionError(
764 'This driver instance does not have a valid session ID ' +
765 '(did you call WebDriver.quit()?) and may no longer be used.'
766 )
767 )
768
769 // Only want the session rejection to bubble if accessed.
770 this.session_.catch(function () { })
771
772 if (this.onQuit_) {
773 return this.onQuit_.call(void 0)
774 }
775 })
776 }
777
778 /** @override */
779 actions(options) {
780 return new input.Actions(this, options || undefined)
781 }
782
783 /** @override */
784 executeScript(script, ...args) {
785 if (typeof script === 'function') {
786 script = 'return (' + script + ').apply(null, arguments);'
787 }
788 return this.execute(
789 new command.Command(command.Name.EXECUTE_SCRIPT)
790 .setParameter('script', script)
791 .setParameter('args', args)
792 )
793 }
794
795 /** @override */
796 executeAsyncScript(script, ...args) {
797 if (typeof script === 'function') {
798 script = 'return (' + script + ').apply(null, arguments);'
799 }
800 return this.execute(
801 new command.Command(command.Name.EXECUTE_ASYNC_SCRIPT)
802 .setParameter('script', script)
803 .setParameter('args', args)
804 )
805 }
806
807 /** @override */
808 wait(condition, timeout = 0, message = undefined, pollTimeout = 200) {
809 if (typeof timeout !== 'number' || timeout < 0) {
810 throw TypeError('timeout must be a number >= 0: ' + timeout)
811 }
812
813 if (typeof pollTimeout !== 'number' || pollTimeout < 0) {
814 throw TypeError('pollTimeout must be a number >= 0: ' + pollTimeout)
815 }
816
817 if (promise.isPromise(condition)) {
818 return new Promise((resolve, reject) => {
819 if (!timeout) {
820 resolve(condition)
821 return
822 }
823
824 let start = Date.now()
825 let timer = setTimeout(function () {
826 timer = null
827 try {
828 let timeoutMessage = resolveWaitMessage(message)
829 reject(
830 new error.TimeoutError(
831 `${timeoutMessage}Timed out waiting for promise to resolve after ${Date.now() - start
832 }ms`
833 )
834 )
835 } catch (ex) {
836 reject(
837 new error.TimeoutError(
838 `${ex.message
839 }\nTimed out waiting for promise to resolve after ${Date.now() - start
840 }ms`
841 )
842 )
843 }
844 }, timeout)
845 const clearTimer = () => timer && clearTimeout(timer)
846
847 /** @type {!IThenable} */ condition.then(
848 function (value) {
849 clearTimer()
850 resolve(value)
851 },
852 function (error) {
853 clearTimer()
854 reject(error)
855 }
856 )
857 })
858 }
859
860 let fn = /** @type {!Function} */ (condition)
861 if (condition instanceof Condition) {
862 message = message || condition.description()
863 fn = condition.fn
864 }
865
866 if (typeof fn !== 'function') {
867 throw TypeError(
868 'Wait condition must be a promise-like object, function, or a ' +
869 'Condition object'
870 )
871 }
872
873 const driver = this
874 function evaluateCondition() {
875 return new Promise((resolve, reject) => {
876 try {
877 resolve(fn(driver))
878 } catch (ex) {
879 reject(ex)
880 }
881 })
882 }
883
884 let result = new Promise((resolve, reject) => {
885 const startTime = Date.now()
886 const pollCondition = async () => {
887 evaluateCondition().then(function (value) {
888 const elapsed = Date.now() - startTime
889 if (value) {
890 resolve(value)
891 } else if (timeout && elapsed >= timeout) {
892 try {
893 let timeoutMessage = resolveWaitMessage(message)
894 reject(
895 new error.TimeoutError(
896 `${timeoutMessage}Wait timed out after ${elapsed}ms`
897 )
898 )
899 } catch (ex) {
900 reject(
901 new error.TimeoutError(
902 `${ex.message}\nWait timed out after ${elapsed}ms`
903 )
904 )
905 }
906 } else {
907 setTimeout(pollCondition, pollTimeout)
908 }
909 }, reject)
910 }
911 pollCondition()
912 })
913
914 if (condition instanceof WebElementCondition) {
915 result = new WebElementPromise(
916 this,
917 result.then(function (value) {
918 if (!(value instanceof WebElement)) {
919 throw TypeError(
920 'WebElementCondition did not resolve to a WebElement: ' +
921 Object.prototype.toString.call(value)
922 )
923 }
924 return value
925 })
926 )
927 }
928 return result
929 }
930
931 /** @override */
932 sleep(ms) {
933 return new Promise((resolve) => setTimeout(resolve, ms))
934 }
935
936 /** @override */
937 getWindowHandle() {
938 return this.execute(
939 new command.Command(command.Name.GET_CURRENT_WINDOW_HANDLE)
940 )
941 }
942
943 /** @override */
944 getAllWindowHandles() {
945 return this.execute(new command.Command(command.Name.GET_WINDOW_HANDLES))
946 }
947
948 /** @override */
949 getPageSource() {
950 return this.execute(new command.Command(command.Name.GET_PAGE_SOURCE))
951 }
952
953 /** @override */
954 close() {
955 return this.execute(new command.Command(command.Name.CLOSE))
956 }
957
958 /** @override */
959 get(url) {
960 return this.navigate().to(url)
961 }
962
963 /** @override */
964 getCurrentUrl() {
965 return this.execute(new command.Command(command.Name.GET_CURRENT_URL))
966 }
967
968 /** @override */
969 getTitle() {
970 return this.execute(new command.Command(command.Name.GET_TITLE))
971 }
972
973 /** @override */
974 findElement(locator) {
975 let id
976 let cmd = null
977
978 if (locator instanceof RelativeBy) {
979 cmd = new command.Command(
980 command.Name.FIND_ELEMENTS_RELATIVE
981 ).setParameter('args', locator.marshall())
982 } else {
983 locator = by.checkedLocator(locator)
984 }
985
986 if (typeof locator === 'function') {
987 id = this.findElementInternal_(locator, this)
988 return new WebElementPromise(this, id)
989 } else if (cmd === null) {
990 cmd = new command.Command(command.Name.FIND_ELEMENT)
991 .setParameter('using', locator.using)
992 .setParameter('value', locator.value)
993 }
994
995 id = this.execute(cmd)
996 if (locator instanceof RelativeBy) {
997 return this.normalize_(id)
998 } else {
999 return new WebElementPromise(this, id)
1000 }
1001 }
1002
1003 /**
1004 * @param {!Function} webElementPromise The webElement in unresolved state
1005 * @return {!Promise<!WebElement>} First single WebElement from array of resolved promises
1006 */
1007 async normalize_(webElementPromise) {
1008 let result = await webElementPromise
1009 if (result.length === 0) {
1010 throw new NoSuchElementError(
1011 'Cannot locate an element with provided parameters'
1012 )
1013 } else {
1014 return result[0]
1015 }
1016 }
1017
1018 /**
1019 * @param {!Function} locatorFn The locator function to use.
1020 * @param {!(WebDriver|WebElement)} context The search context.
1021 * @return {!Promise<!WebElement>} A promise that will resolve to a list of
1022 * WebElements.
1023 * @private
1024 */
1025 async findElementInternal_(locatorFn, context) {
1026 let result = await locatorFn(context)
1027 if (Array.isArray(result)) {
1028 result = result[0]
1029 }
1030 if (!(result instanceof WebElement)) {
1031 throw new TypeError('Custom locator did not return a WebElement')
1032 }
1033 return result
1034 }
1035
1036 /** @override */
1037 async findElements(locator) {
1038 let cmd = null
1039 if (locator instanceof RelativeBy) {
1040 cmd = new command.Command(
1041 command.Name.FIND_ELEMENTS_RELATIVE
1042 ).setParameter('args', locator.marshall())
1043 } else {
1044 locator = by.checkedLocator(locator)
1045 }
1046
1047 if (typeof locator === 'function') {
1048 return this.findElementsInternal_(locator, this)
1049 } else if (cmd === null) {
1050 cmd = new command.Command(command.Name.FIND_ELEMENTS)
1051 .setParameter('using', locator.using)
1052 .setParameter('value', locator.value)
1053 }
1054 try {
1055 let res = await this.execute(cmd)
1056 return Array.isArray(res) ? res : []
1057 } catch (ex) {
1058 if (ex instanceof error.NoSuchElementError) {
1059 return []
1060 }
1061 throw ex
1062 }
1063 }
1064
1065 /**
1066 * @param {!Function} locatorFn The locator function to use.
1067 * @param {!(WebDriver|WebElement)} context The search context.
1068 * @return {!Promise<!Array<!WebElement>>} A promise that will resolve to an
1069 * array of WebElements.
1070 * @private
1071 */
1072 async findElementsInternal_(locatorFn, context) {
1073 const result = await locatorFn(context)
1074 if (result instanceof WebElement) {
1075 return [result]
1076 }
1077
1078 if (!Array.isArray(result)) {
1079 return []
1080 }
1081
1082 return result.filter(function (item) {
1083 return item instanceof WebElement
1084 })
1085 }
1086
1087 /** @override */
1088 takeScreenshot() {
1089 return this.execute(new command.Command(command.Name.SCREENSHOT))
1090 }
1091
1092 /** @override */
1093 manage() {
1094 return new Options(this)
1095 }
1096
1097 /** @override */
1098 navigate() {
1099 return new Navigation(this)
1100 }
1101
1102 /** @override */
1103 switchTo() {
1104 return new TargetLocator(this)
1105 }
1106
1107 validatePrintPageParams(keys, object) {
1108 let page = {}
1109 let margin = {}
1110 let data
1111 Object.keys(keys).forEach(function (key) {
1112 data = keys[key]
1113 let obj = {
1114 orientation: function () {
1115 object.orientation = data
1116 },
1117
1118 scale: function () {
1119 object.scale = data
1120 },
1121
1122 background: function () {
1123 object.background = data
1124 },
1125
1126 width: function () {
1127 page.width = data
1128 object.page = page
1129 },
1130
1131 height: function () {
1132 page.height = data
1133 object.page = page
1134 },
1135
1136 top: function () {
1137 margin.top = data
1138 object.margin = margin
1139 },
1140
1141 left: function () {
1142 margin.left = data
1143 object.margin = margin
1144 },
1145
1146 bottom: function () {
1147 margin.bottom = data
1148 object.margin = margin
1149 },
1150
1151 right: function () {
1152 margin.right = data
1153 object.margin = margin
1154 },
1155
1156 shrinkToFit: function () {
1157 object.shrinkToFit = data
1158 },
1159
1160 pageRanges: function () {
1161 object.pageRanges = data
1162 },
1163 }
1164
1165 if (!Object.prototype.hasOwnProperty.call(obj, key)) {
1166 throw new error.InvalidArgumentError(`Invalid Argument '${key}'`)
1167 } else {
1168 obj[key]()
1169 }
1170 })
1171
1172 return object
1173 }
1174
1175 /** @override */
1176 printPage(options = {}) {
1177 let keys = options
1178 let params = {}
1179 let resultObj
1180
1181 let self = this
1182 resultObj = self.validatePrintPageParams(keys, params)
1183
1184 return this.execute(
1185 new command.Command(command.Name.PRINT_PAGE).setParameters(resultObj)
1186 )
1187 }
1188
1189 /**
1190 * Creates a new WebSocket connection.
1191 * @return {!Promise<resolved>} A new CDP instance.
1192 */
1193 async createCDPConnection(target) {
1194 const caps = await this.getCapabilities()
1195 const seCdp = caps['map_'].get('se:cdp')
1196 const vendorInfo =
1197 caps['map_'].get(this.VENDOR_COMMAND_PREFIX + ':chromeOptions') ||
1198 caps['map_'].get(this.VENDOR_CAPABILITY_PREFIX + ':edgeOptions') ||
1199 caps['map_'].get('moz:debuggerAddress') ||
1200 new Map()
1201 const debuggerUrl = seCdp || vendorInfo['debuggerAddress'] || vendorInfo
1202 this._wsUrl = await this.getWsUrl(debuggerUrl, target)
1203
1204 return new Promise((resolve, reject) => {
1205 try {
1206 this._wsConnection = new WebSocket(this._wsUrl)
1207 } catch (err) {
1208 reject(err)
1209 return
1210 }
1211
1212 this._wsConnection.on('open', () => {
1213 this._cdpConnection = new cdp.CdpConnection(this._wsConnection)
1214 resolve(this._cdpConnection)
1215 })
1216
1217 this._wsConnection.on('error', (error) => {
1218 reject(error)
1219 })
1220 })
1221 }
1222
1223 /**
1224 * Retrieves 'webSocketDebuggerUrl' by sending a http request using debugger address
1225 * @param {string} debuggerAddress
1226 * @param {string} target
1227 * @return {string} Returns parsed webSocketDebuggerUrl obtained from the http request
1228 */
1229 async getWsUrl(debuggerAddress, target) {
1230 if (target && cdpTargets.indexOf(target.toLowerCase()) === -1) {
1231 throw new error.InvalidArgumentError('invalid target value')
1232 }
1233
1234 if (debuggerAddress.match(/\/se\/cdp/)) {
1235 if (debuggerAddress.match("ws:\/\/", "http:\/\/")) {
1236 return debuggerAddress.replace("ws:\/\/", "http:\/\/")
1237 }
1238 else if (debuggerAddress.match("wss:\/\/", "https:\/\/")) {
1239 return debuggerAddress.replace("wss:\/\/", "https:\/\/")
1240 }
1241 else {
1242 return debuggerAddress
1243 }
1244 }
1245
1246 const path = '/json/version'
1247 let request = new http.Request('GET', path)
1248 let client = new http.HttpClient('http://' + debuggerAddress)
1249 let response = await client.send(request)
1250
1251 return JSON.parse(response.body)['webSocketDebuggerUrl']
1252 }
1253
1254 /**
1255 * Sets a listener for Fetch.authRequired event from CDP
1256 * If event is triggered, it enter username and password
1257 * and allows the test to move forward
1258 * @param {string} username
1259 * @param {string} password
1260 * @param connection CDP Connection
1261 */
1262 async register(username, password, connection) {
1263
1264 this._wsConnection.on('message', (message) => {
1265 const params = JSON.parse(message)
1266
1267 if (params.method === 'Fetch.authRequired') {
1268 const requestParams = params['params']
1269 connection.execute(
1270 'Fetch.continueWithAuth',
1271 this.getRandomNumber(1, 10),
1272 {
1273 requestId: requestParams['requestId'],
1274 authChallengeResponse: {
1275 response: 'ProvideCredentials',
1276 username: username,
1277 password: password,
1278 },
1279 }
1280 )
1281 } else if (params.method === 'Fetch.requestPaused') {
1282 const requestPausedParams = params['params']
1283 connection.execute(
1284 'Fetch.continueRequest',
1285 this.getRandomNumber(1, 10),
1286 {
1287 requestId: requestPausedParams['requestId'],
1288 }
1289 )
1290 }
1291 })
1292
1293 await connection.execute(
1294 'Fetch.enable',
1295 1,
1296 {
1297 handleAuthRequests: true,
1298 },
1299 null
1300 )
1301 await connection.execute(
1302 'Network.setCacheDisabled',
1303 this.getRandomNumber(1, 10),
1304 {
1305 cacheDisabled: true,
1306 },
1307 null
1308 )
1309 }
1310
1311 /**
1312 * Handle Network interception requests
1313 * @param connection WebSocket connection to the browser
1314 * @param httpResponse Object representing what we are intercepting
1315 * as well as what should be returned.
1316 * @param callback callback called when we intercept requests.
1317 */
1318 async onIntercept(connection, httpResponse, callback) {
1319
1320 this._wsConnection.on('message', (message) => {
1321 const params = JSON.parse(message)
1322 if (params.method === 'Fetch.requestPaused') {
1323 const requestPausedParams = params['params']
1324 if (requestPausedParams.request.url == httpResponse.urlToIntercept) {
1325 connection.execute(
1326 'Fetch.continueRequest',
1327 this.getRandomNumber(1, 10),
1328 {
1329 requestId: requestPausedParams['requestId'],
1330 url: httpResponse.urlToIntercept,
1331 method: httpResponse.method,
1332 headers: httpResponse.headers,
1333 postData: httpResponse.body
1334 }
1335 )
1336 callback()
1337 } else {
1338 connection.execute(
1339 'Fetch.continueRequest',
1340 this.getRandomNumber(1, 10),
1341 {
1342 requestId: requestPausedParams['requestId'],
1343 }
1344 )
1345 }
1346 }
1347 })
1348
1349 await connection.execute(
1350 'Fetch.enable',
1351 1,
1352 {},
1353 null
1354 )
1355 await connection.execute(
1356 'Network.setCacheDisabled',
1357 this.getRandomNumber(1, 10),
1358 {
1359 cacheDisabled: true,
1360 },
1361 null
1362 )
1363 }
1364 /**
1365 *
1366 * @param connection
1367 * @param callback
1368 * @returns {Promise<void>}
1369 */
1370 async onLogEvent(connection, callback) {
1371
1372
1373 this._wsConnection.on('message', (message) => {
1374 const params = JSON.parse(message)
1375
1376 if (params.method === 'Runtime.consoleAPICalled') {
1377 const consoleEventParams = params['params']
1378 let event = {
1379 type: consoleEventParams['type'],
1380 timestamp: new Date(consoleEventParams['timestamp']),
1381 args: consoleEventParams['args'],
1382 }
1383
1384 callback(event)
1385 }
1386 })
1387 await connection.execute(
1388 'Runtime.enable',
1389 this.getRandomNumber(1, 10),
1390 {},
1391 null
1392 )
1393 }
1394
1395 /**
1396 *
1397 * @param connection
1398 * @param callback
1399 * @returns {Promise<void>}
1400 */
1401 async onLogException(connection, callback) {
1402 await connection.execute(
1403 'Runtime.enable',
1404 this.getRandomNumber(1, 10),
1405 {},
1406 null
1407 )
1408
1409 this._wsConnection.on('message', (message) => {
1410 const params = JSON.parse(message)
1411
1412 if (params.method === 'Runtime.exceptionThrown') {
1413 const exceptionEventParams = params['params']
1414 let event = {
1415 exceptionDetails: exceptionEventParams['exceptionDetails'],
1416 timestamp: new Date(exceptionEventParams['timestamp']),
1417 }
1418
1419 callback(event)
1420 }
1421 })
1422 }
1423
1424 /**
1425 * @param connection
1426 * @param callback
1427 * @returns {Promise<void>}
1428 */
1429 async logMutationEvents(connection, callback) {
1430 await connection.execute(
1431 'Runtime.enable',
1432 this.getRandomNumber(1, 10),
1433 {},
1434 null
1435 )
1436 await connection.execute(
1437 'Page.enable',
1438 this.getRandomNumber(1, 10),
1439 {},
1440 null
1441 )
1442
1443 await connection.execute(
1444 'Runtime.addBinding',
1445 this.getRandomNumber(1, 10),
1446 {
1447 name: '__webdriver_attribute',
1448 },
1449 null
1450 )
1451
1452 let mutationListener = ''
1453 try {
1454 // Depending on what is running the code it could appear in 2 different places which is why we try
1455 // here and then the other location
1456 mutationListener = fs
1457 .readFileSync(
1458 './javascript/node/selenium-webdriver/lib/atoms/mutation-listener.js',
1459 'utf-8'
1460 )
1461 .toString()
1462 } catch {
1463 mutationListener = fs
1464 .readFileSync(
1465 path.resolve(__dirname, './atoms/mutation-listener.js'),
1466 'utf-8'
1467 )
1468 .toString()
1469 }
1470
1471 this.executeScript(mutationListener)
1472
1473 await connection.execute(
1474 'Page.addScriptToEvaluateOnNewDocument',
1475 this.getRandomNumber(1, 10),
1476 {
1477 source: mutationListener,
1478 },
1479 null
1480 )
1481
1482 this._wsConnection.on('message', async (message) => {
1483 const params = JSON.parse(message)
1484 if (params.method === 'Runtime.bindingCalled') {
1485 let payload = JSON.parse(params['params']['payload'])
1486 let elements = await this.findElements({
1487 css: '*[data-__webdriver_id=' + payload['target'],
1488 })
1489
1490 if (elements.length === 0) {
1491 return
1492 }
1493
1494 let event = {
1495 element: elements[0],
1496 attribute_name: payload['name'],
1497 current_value: payload['value'],
1498 old_value: payload['oldValue'],
1499 }
1500 callback(event)
1501 }
1502 })
1503 }
1504
1505 getRandomNumber(min, max) {
1506 return Math.floor(Math.random() * (max - min + 1) + min)
1507 }
1508}
1509
1510/**
1511 * Interface for navigating back and forth in the browser history.
1512 *
1513 * This class should never be instantiated directly. Instead, obtain an instance
1514 * with
1515 *
1516 * webdriver.navigate()
1517 *
1518 * @see WebDriver#navigate()
1519 */
1520class Navigation {
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 * Navigates to a new URL.
1532 *
1533 * @param {string} url The URL to navigate to.
1534 * @return {!Promise<void>} A promise that will be resolved when the URL
1535 * has been loaded.
1536 */
1537 to(url) {
1538 return this.driver_.execute(
1539 new command.Command(command.Name.GET).setParameter('url', url)
1540 )
1541 }
1542
1543 /**
1544 * Moves backwards in the browser history.
1545 *
1546 * @return {!Promise<void>} A promise that will be resolved when the
1547 * navigation event has completed.
1548 */
1549 back() {
1550 return this.driver_.execute(new command.Command(command.Name.GO_BACK))
1551 }
1552
1553 /**
1554 * Moves forwards in the browser history.
1555 *
1556 * @return {!Promise<void>} A promise that will be resolved when the
1557 * navigation event has completed.
1558 */
1559 forward() {
1560 return this.driver_.execute(new command.Command(command.Name.GO_FORWARD))
1561 }
1562
1563 /**
1564 * Refreshes the current page.
1565 *
1566 * @return {!Promise<void>} A promise that will be resolved when the
1567 * navigation event has completed.
1568 */
1569 refresh() {
1570 return this.driver_.execute(new command.Command(command.Name.REFRESH))
1571 }
1572}
1573
1574/**
1575 * Provides methods for managing browser and driver state.
1576 *
1577 * This class should never be instantiated directly. Instead, obtain an instance
1578 * with {@linkplain WebDriver#manage() webdriver.manage()}.
1579 */
1580class Options {
1581 /**
1582 * @param {!WebDriver} driver The parent driver.
1583 * @private
1584 */
1585 constructor(driver) {
1586 /** @private {!WebDriver} */
1587 this.driver_ = driver
1588 }
1589
1590 /**
1591 * Adds a cookie.
1592 *
1593 * __Sample Usage:__
1594 *
1595 * // Set a basic cookie.
1596 * driver.manage().addCookie({name: 'foo', value: 'bar'});
1597 *
1598 * // Set a cookie that expires in 10 minutes.
1599 * let expiry = new Date(Date.now() + (10 * 60 * 1000));
1600 * driver.manage().addCookie({name: 'foo', value: 'bar', expiry});
1601 *
1602 * // The cookie expiration may also be specified in seconds since epoch.
1603 * driver.manage().addCookie({
1604 * name: 'foo',
1605 * value: 'bar',
1606 * expiry: Math.floor(Date.now() / 1000)
1607 * });
1608 *
1609 * @param {!Options.Cookie} spec Defines the cookie to add.
1610 * @return {!Promise<void>} A promise that will be resolved
1611 * when the cookie has been added to the page.
1612 * @throws {error.InvalidArgumentError} if any of the cookie parameters are
1613 * invalid.
1614 * @throws {TypeError} if `spec` is not a cookie object.
1615 */
1616 addCookie({ name, value, path, domain, secure, httpOnly, expiry, sameSite }) {
1617 // We do not allow '=' or ';' in the name.
1618 if (/[;=]/.test(name)) {
1619 throw new error.InvalidArgumentError('Invalid cookie name "' + name + '"')
1620 }
1621
1622 // We do not allow ';' in value.
1623 if (/;/.test(value)) {
1624 throw new error.InvalidArgumentError(
1625 'Invalid cookie value "' + value + '"'
1626 )
1627 }
1628
1629 if (typeof expiry === 'number') {
1630 expiry = Math.floor(expiry)
1631 } else if (expiry instanceof Date) {
1632 let date = /** @type {!Date} */ (expiry)
1633 expiry = Math.floor(date.getTime() / 1000)
1634 }
1635
1636 if (sameSite && !['Strict', 'Lax', 'None'].includes(sameSite)) {
1637 throw new error.InvalidArgumentError(
1638 `Invalid sameSite cookie value '${sameSite}'. It should be one of "Lax", "Strict" or "None"`
1639 )
1640 }
1641
1642 if (sameSite === 'None' && !secure) {
1643 throw new error.InvalidArgumentError(
1644 'Invalid cookie configuration: SameSite=None must be Secure'
1645 )
1646 }
1647
1648 return this.driver_.execute(
1649 new command.Command(command.Name.ADD_COOKIE).setParameter('cookie', {
1650 name: name,
1651 value: value,
1652 path: path,
1653 domain: domain,
1654 secure: !!secure,
1655 httpOnly: !!httpOnly,
1656 expiry: expiry,
1657 sameSite: sameSite,
1658 })
1659 )
1660 }
1661
1662 /**
1663 * Deletes all cookies visible to the current page.
1664 *
1665 * @return {!Promise<void>} A promise that will be resolved
1666 * when all cookies have been deleted.
1667 */
1668 deleteAllCookies() {
1669 return this.driver_.execute(
1670 new command.Command(command.Name.DELETE_ALL_COOKIES)
1671 )
1672 }
1673
1674 /**
1675 * Deletes the cookie with the given name. This command is a no-op if there is
1676 * no cookie with the given name visible to the current page.
1677 *
1678 * @param {string} name The name of the cookie to delete.
1679 * @return {!Promise<void>} A promise that will be resolved
1680 * when the cookie has been deleted.
1681 */
1682 deleteCookie(name) {
1683 return this.driver_.execute(
1684 new command.Command(command.Name.DELETE_COOKIE).setParameter('name', name)
1685 )
1686 }
1687
1688 /**
1689 * Retrieves all cookies visible to the current page. Each cookie will be
1690 * returned as a JSON object as described by the WebDriver wire protocol.
1691 *
1692 * @return {!Promise<!Array<!Options.Cookie>>} A promise that will be
1693 * resolved with the cookies visible to the current browsing context.
1694 */
1695 getCookies() {
1696 return this.driver_.execute(
1697 new command.Command(command.Name.GET_ALL_COOKIES)
1698 )
1699 }
1700
1701 /**
1702 * Retrieves the cookie with the given name. Returns null if there is no such
1703 * cookie. The cookie will be returned as a JSON object as described by the
1704 * WebDriver wire protocol.
1705 *
1706 * @param {string} name The name of the cookie to retrieve.
1707 * @return {!Promise<?Options.Cookie>} A promise that will be resolved
1708 * with the named cookie
1709 * @throws {error.NoSuchCookieError} if there is no such cookie.
1710 */
1711 async getCookie(name) {
1712 try {
1713 const cookie = await this.driver_.execute(
1714 new command.Command(command.Name.GET_COOKIE).setParameter('name', name)
1715 )
1716 return cookie
1717 } catch (err) {
1718 if (
1719 !(err instanceof error.UnknownCommandError) &&
1720 !(err instanceof error.UnsupportedOperationError)
1721 ) {
1722 throw err
1723 }
1724
1725 const cookies = await this.getCookies()
1726 for (let cookie of cookies) {
1727 if (cookie && cookie['name'] === name) {
1728 return cookie
1729 }
1730 }
1731 return null
1732 }
1733 }
1734
1735 /**
1736 * Fetches the timeouts currently configured for the current session.
1737 *
1738 * @return {!Promise<{script: number,
1739 * pageLoad: number,
1740 * implicit: number}>} A promise that will be
1741 * resolved with the timeouts currently configured for the current
1742 * session.
1743 * @see #setTimeouts()
1744 */
1745 getTimeouts() {
1746 return this.driver_.execute(new command.Command(command.Name.GET_TIMEOUT))
1747 }
1748
1749 /**
1750 * Sets the timeout durations associated with the current session.
1751 *
1752 * The following timeouts are supported (all timeouts are specified in
1753 * milliseconds):
1754 *
1755 * - `implicit` specifies the maximum amount of time to wait for an element
1756 * locator to succeed when {@linkplain WebDriver#findElement locating}
1757 * {@linkplain WebDriver#findElements elements} on the page.
1758 * Defaults to 0 milliseconds.
1759 *
1760 * - `pageLoad` specifies the maximum amount of time to wait for a page to
1761 * finishing loading. Defaults to 300000 milliseconds.
1762 *
1763 * - `script` specifies the maximum amount of time to wait for an
1764 * {@linkplain WebDriver#executeScript evaluated script} to run. If set to
1765 * `null`, the script timeout will be indefinite.
1766 * Defaults to 30000 milliseconds.
1767 *
1768 * @param {{script: (number|null|undefined),
1769 * pageLoad: (number|null|undefined),
1770 * implicit: (number|null|undefined)}} conf
1771 * The desired timeout configuration.
1772 * @return {!Promise<void>} A promise that will be resolved when the timeouts
1773 * have been set.
1774 * @throws {!TypeError} if an invalid options object is provided.
1775 * @see #getTimeouts()
1776 * @see <https://w3c.github.io/webdriver/webdriver-spec.html#dfn-set-timeouts>
1777 */
1778 setTimeouts({ script, pageLoad, implicit } = {}) {
1779 let cmd = new command.Command(command.Name.SET_TIMEOUT)
1780
1781 let valid = false
1782 function setParam(key, value) {
1783 if (value === null || typeof value === 'number') {
1784 valid = true
1785 cmd.setParameter(key, value)
1786 } else if (typeof value !== 'undefined') {
1787 throw TypeError(
1788 'invalid timeouts configuration:' +
1789 ` expected "${key}" to be a number, got ${typeof value}`
1790 )
1791 }
1792 }
1793 setParam('implicit', implicit)
1794 setParam('pageLoad', pageLoad)
1795 setParam('script', script)
1796
1797 if (valid) {
1798 return this.driver_.execute(cmd).catch(() => {
1799 // Fallback to the legacy method.
1800 let cmds = []
1801 if (typeof script === 'number') {
1802 cmds.push(legacyTimeout(this.driver_, 'script', script))
1803 }
1804 if (typeof implicit === 'number') {
1805 cmds.push(legacyTimeout(this.driver_, 'implicit', implicit))
1806 }
1807 if (typeof pageLoad === 'number') {
1808 cmds.push(legacyTimeout(this.driver_, 'page load', pageLoad))
1809 }
1810 return Promise.all(cmds)
1811 })
1812 }
1813 throw TypeError('no timeouts specified')
1814 }
1815
1816 /**
1817 * @return {!Logs} The interface for managing driver logs.
1818 */
1819 logs() {
1820 return new Logs(this.driver_)
1821 }
1822
1823 /**
1824 * @return {!Window} The interface for managing the current window.
1825 */
1826 window() {
1827 return new Window(this.driver_)
1828 }
1829}
1830
1831/**
1832 * @param {!WebDriver} driver
1833 * @param {string} type
1834 * @param {number} ms
1835 * @return {!Promise<void>}
1836 */
1837function legacyTimeout(driver, type, ms) {
1838 return driver.execute(
1839 new command.Command(command.Name.SET_TIMEOUT)
1840 .setParameter('type', type)
1841 .setParameter('ms', ms)
1842 )
1843}
1844
1845/**
1846 * A record object describing a browser cookie.
1847 *
1848 * @record
1849 */
1850Options.Cookie = function () { }
1851
1852/**
1853 * The name of the cookie.
1854 *
1855 * @type {string}
1856 */
1857Options.Cookie.prototype.name
1858
1859/**
1860 * The cookie value.
1861 *
1862 * @type {string}
1863 */
1864Options.Cookie.prototype.value
1865
1866/**
1867 * The cookie path. Defaults to "/" when adding a cookie.
1868 *
1869 * @type {(string|undefined)}
1870 */
1871Options.Cookie.prototype.path
1872
1873/**
1874 * The domain the cookie is visible to. Defaults to the current browsing
1875 * context's document's URL when adding a cookie.
1876 *
1877 * @type {(string|undefined)}
1878 */
1879Options.Cookie.prototype.domain
1880
1881/**
1882 * Whether the cookie is a secure cookie. Defaults to false when adding a new
1883 * cookie.
1884 *
1885 * @type {(boolean|undefined)}
1886 */
1887Options.Cookie.prototype.secure
1888
1889/**
1890 * Whether the cookie is an HTTP only cookie. Defaults to false when adding a
1891 * new cookie.
1892 *
1893 * @type {(boolean|undefined)}
1894 */
1895Options.Cookie.prototype.httpOnly
1896
1897/**
1898 * When the cookie expires.
1899 *
1900 * When {@linkplain Options#addCookie() adding a cookie}, this may be specified
1901 * as a {@link Date} object, or in _seconds_ since Unix epoch (January 1, 1970).
1902 *
1903 * The expiry is always returned in seconds since epoch when
1904 * {@linkplain Options#getCookies() retrieving cookies} from the browser.
1905 *
1906 * @type {(!Date|number|undefined)}
1907 */
1908Options.Cookie.prototype.expiry
1909
1910/**
1911 * When the cookie applies to a SameSite policy.
1912 *
1913 * When {@linkplain Options#addCookie() adding a cookie}, this may be specified
1914 * as a {@link string} object which is one of 'Lax', 'Strict' or 'None'.
1915 *
1916 *
1917 * @type {(string|undefined)}
1918 */
1919Options.Cookie.prototype.sameSite
1920
1921/**
1922 * An interface for managing the current window.
1923 *
1924 * This class should never be instantiated directly. Instead, obtain an instance
1925 * with
1926 *
1927 * webdriver.manage().window()
1928 *
1929 * @see WebDriver#manage()
1930 * @see Options#window()
1931 */
1932class Window {
1933 /**
1934 * @param {!WebDriver} driver The parent driver.
1935 * @private
1936 */
1937 constructor(driver) {
1938 /** @private {!WebDriver} */
1939 this.driver_ = driver
1940 }
1941
1942 /**
1943 * Retrieves the a rect describing the current top-level window's size and
1944 * position.
1945 *
1946 * @return {!Promise<{x: number, y: number, width: number, height: number}>}
1947 * A promise that will resolve to the window rect of the current window.
1948 */
1949 async getRect() {
1950 try {
1951 return await this.driver_.execute(
1952 new command.Command(command.Name.GET_WINDOW_RECT)
1953 )
1954 } catch (ex) {
1955 if (ex instanceof error.UnknownCommandError) {
1956 let { width, height } = await this.driver_.execute(
1957 new command.Command(command.Name.GET_WINDOW_SIZE).setParameter(
1958 'windowHandle',
1959 'current'
1960 )
1961 )
1962 let { x, y } = await this.driver_.execute(
1963 new command.Command(command.Name.GET_WINDOW_POSITION).setParameter(
1964 'windowHandle',
1965 'current'
1966 )
1967 )
1968 return { x, y, width, height }
1969 }
1970 throw ex
1971 }
1972 }
1973
1974 /**
1975 * Sets the current top-level window's size and position. You may update just
1976 * the size by omitting `x` & `y`, or just the position by omitting
1977 * `width` & `height` options.
1978 *
1979 * @param {{x: (number|undefined),
1980 * y: (number|undefined),
1981 * width: (number|undefined),
1982 * height: (number|undefined)}} options
1983 * The desired window size and position.
1984 * @return {!Promise<{x: number, y: number, width: number, height: number}>}
1985 * A promise that will resolve to the current window's updated window
1986 * rect.
1987 */
1988 async setRect({ x, y, width, height }) {
1989 try {
1990 return await this.driver_.execute(
1991 new command.Command(command.Name.SET_WINDOW_RECT).setParameters({
1992 x,
1993 y,
1994 width,
1995 height,
1996 })
1997 )
1998 } catch (ex) {
1999 if (ex instanceof error.UnknownCommandError) {
2000 if (typeof x === 'number' && typeof y === 'number') {
2001 await this.driver_.execute(
2002 new command.Command(command.Name.SET_WINDOW_POSITION)
2003 .setParameter('windowHandle', 'current')
2004 .setParameter('x', x)
2005 .setParameter('y', y)
2006 )
2007 }
2008
2009 if (typeof width === 'number' && typeof height === 'number') {
2010 await this.driver_.execute(
2011 new command.Command(command.Name.SET_WINDOW_SIZE)
2012 .setParameter('windowHandle', 'current')
2013 .setParameter('width', width)
2014 .setParameter('height', height)
2015 )
2016 }
2017 return this.getRect()
2018 }
2019 throw ex
2020 }
2021 }
2022
2023 /**
2024 * Maximizes the current window. The exact behavior of this command is
2025 * specific to individual window managers, but typically involves increasing
2026 * the window to the maximum available size without going full-screen.
2027 *
2028 * @return {!Promise<void>} A promise that will be resolved when the command
2029 * has completed.
2030 */
2031 maximize() {
2032 return this.driver_.execute(
2033 new command.Command(command.Name.MAXIMIZE_WINDOW).setParameter(
2034 'windowHandle',
2035 'current'
2036 )
2037 )
2038 }
2039
2040 /**
2041 * Minimizes the current window. The exact behavior of this command is
2042 * specific to individual window managers, but typically involves hiding
2043 * the window in the system tray.
2044 *
2045 * @return {!Promise<void>} A promise that will be resolved when the command
2046 * has completed.
2047 */
2048 minimize() {
2049 return this.driver_.execute(
2050 new command.Command(command.Name.MINIMIZE_WINDOW)
2051 )
2052 }
2053
2054 /**
2055 * Invokes the "full screen" operation on the current window. The exact
2056 * behavior of this command is specific to individual window managers, but
2057 * this will typically increase the window size to the size of the physical
2058 * display and hide the browser chrome.
2059 *
2060 * @return {!Promise<void>} A promise that will be resolved when the command
2061 * has completed.
2062 * @see <https://fullscreen.spec.whatwg.org/#fullscreen-an-element>
2063 */
2064 fullscreen() {
2065 return this.driver_.execute(
2066 new command.Command(command.Name.FULLSCREEN_WINDOW)
2067 )
2068 }
2069}
2070
2071/**
2072 * Interface for managing WebDriver log records.
2073 *
2074 * This class should never be instantiated directly. Instead, obtain an
2075 * instance with
2076 *
2077 * webdriver.manage().logs()
2078 *
2079 * @see WebDriver#manage()
2080 * @see Options#logs()
2081 */
2082class Logs {
2083 /**
2084 * @param {!WebDriver} driver The parent driver.
2085 * @private
2086 */
2087 constructor(driver) {
2088 /** @private {!WebDriver} */
2089 this.driver_ = driver
2090 }
2091
2092 /**
2093 * Fetches available log entries for the given type.
2094 *
2095 * Note that log buffers are reset after each call, meaning that available
2096 * log entries correspond to those entries not yet returned for a given log
2097 * type. In practice, this means that this call will return the available log
2098 * entries since the last call, or from the start of the session.
2099 *
2100 * @param {!logging.Type} type The desired log type.
2101 * @return {!Promise<!Array.<!logging.Entry>>} A
2102 * promise that will resolve to a list of log entries for the specified
2103 * type.
2104 */
2105 get(type) {
2106 let cmd = new command.Command(command.Name.GET_LOG).setParameter(
2107 'type',
2108 type
2109 )
2110 return this.driver_.execute(cmd).then(function (entries) {
2111 return entries.map(function (entry) {
2112 if (!(entry instanceof logging.Entry)) {
2113 return new logging.Entry(
2114 entry['level'],
2115 entry['message'],
2116 entry['timestamp'],
2117 entry['type']
2118 )
2119 }
2120 return entry
2121 })
2122 })
2123 }
2124
2125 /**
2126 * Retrieves the log types available to this driver.
2127 * @return {!Promise<!Array<!logging.Type>>} A
2128 * promise that will resolve to a list of available log types.
2129 */
2130 getAvailableLogTypes() {
2131 return this.driver_.execute(
2132 new command.Command(command.Name.GET_AVAILABLE_LOG_TYPES)
2133 )
2134 }
2135}
2136
2137/**
2138 * An interface for changing the focus of the driver to another frame or window.
2139 *
2140 * This class should never be instantiated directly. Instead, obtain an
2141 * instance with
2142 *
2143 * webdriver.switchTo()
2144 *
2145 * @see WebDriver#switchTo()
2146 */
2147class TargetLocator {
2148 /**
2149 * @param {!WebDriver} driver The parent driver.
2150 * @private
2151 */
2152 constructor(driver) {
2153 /** @private {!WebDriver} */
2154 this.driver_ = driver
2155 }
2156
2157 /**
2158 * Locates the DOM element on the current page that corresponds to
2159 * `document.activeElement` or `document.body` if the active element is not
2160 * available.
2161 *
2162 * @return {!WebElementPromise} The active element.
2163 */
2164 activeElement() {
2165 var id = this.driver_.execute(
2166 new command.Command(command.Name.GET_ACTIVE_ELEMENT)
2167 )
2168 return new WebElementPromise(this.driver_, id)
2169 }
2170
2171 /**
2172 * Switches focus of all future commands to the topmost frame in the current
2173 * window.
2174 *
2175 * @return {!Promise<void>} A promise that will be resolved
2176 * when the driver has changed focus to the default content.
2177 */
2178 defaultContent() {
2179 return this.driver_.execute(
2180 new command.Command(command.Name.SWITCH_TO_FRAME).setParameter('id', null)
2181 )
2182 }
2183
2184 /**
2185 * Changes the focus of all future commands to another frame on the page. The
2186 * target frame may be specified as one of the following:
2187 *
2188 * - A number that specifies a (zero-based) index into [window.frames](
2189 * https://developer.mozilla.org/en-US/docs/Web/API/Window.frames).
2190 * - A {@link WebElement} reference, which correspond to a `frame` or `iframe`
2191 * DOM element.
2192 * - The `null` value, to select the topmost frame on the page. Passing `null`
2193 * is the same as calling {@link #defaultContent defaultContent()}.
2194 *
2195 * If the specified frame can not be found, the returned promise will be
2196 * rejected with a {@linkplain error.NoSuchFrameError}.
2197 *
2198 * @param {(number|WebElement|null)} id The frame locator.
2199 * @return {!Promise<void>} A promise that will be resolved
2200 * when the driver has changed focus to the specified frame.
2201 */
2202 frame(id) {
2203 return this.driver_.execute(
2204 new command.Command(command.Name.SWITCH_TO_FRAME).setParameter('id', id)
2205 )
2206 }
2207
2208 /**
2209 * Changes the focus of all future commands to the parent frame of the
2210 * currently selected frame. This command has no effect if the driver is
2211 * already focused on the top-level browsing context.
2212 *
2213 * @return {!Promise<void>} A promise that will be resolved when the command
2214 * has completed.
2215 */
2216 parentFrame() {
2217 return this.driver_.execute(
2218 new command.Command(command.Name.SWITCH_TO_FRAME_PARENT)
2219 )
2220 }
2221
2222 /**
2223 * Changes the focus of all future commands to another window. Windows may be
2224 * specified by their {@code window.name} attribute or by its handle
2225 * (as returned by {@link WebDriver#getWindowHandles}).
2226 *
2227 * If the specified window cannot be found, the returned promise will be
2228 * rejected with a {@linkplain error.NoSuchWindowError}.
2229 *
2230 * @param {string} nameOrHandle The name or window handle of the window to
2231 * switch focus to.
2232 * @return {!Promise<void>} A promise that will be resolved
2233 * when the driver has changed focus to the specified window.
2234 */
2235 window(nameOrHandle) {
2236 return this.driver_.execute(
2237 new command.Command(command.Name.SWITCH_TO_WINDOW)
2238 // "name" supports the legacy drivers. "handle" is the W3C
2239 // compliant parameter.
2240 .setParameter('name', nameOrHandle)
2241 .setParameter('handle', nameOrHandle)
2242 )
2243 }
2244
2245 /**
2246 * Creates a new browser window and switches the focus for future
2247 * commands of this driver to the new window.
2248 *
2249 * @param {string} typeHint 'window' or 'tab'. The created window is not
2250 * guaranteed to be of the requested type; if the driver does not support
2251 * the requested type, a new browser window will be created of whatever type
2252 * the driver does support.
2253 * @return {!Promise<void>} A promise that will be resolved
2254 * when the driver has changed focus to the new window.
2255 */
2256 newWindow(typeHint) {
2257 var driver = this.driver_
2258 return this.driver_
2259 .execute(
2260 new command.Command(command.Name.SWITCH_TO_NEW_WINDOW).setParameter(
2261 'type',
2262 typeHint
2263 )
2264 )
2265 .then(function (response) {
2266 return driver.switchTo().window(response.handle)
2267 })
2268 }
2269
2270 /**
2271 * Changes focus to the active modal dialog, such as those opened by
2272 * `window.alert()`, `window.confirm()`, and `window.prompt()`. The returned
2273 * promise will be rejected with a
2274 * {@linkplain error.NoSuchAlertError} if there are no open alerts.
2275 *
2276 * @return {!AlertPromise} The open alert.
2277 */
2278 alert() {
2279 var text = this.driver_.execute(
2280 new command.Command(command.Name.GET_ALERT_TEXT)
2281 )
2282 var driver = this.driver_
2283 return new AlertPromise(
2284 driver,
2285 text.then(function (text) {
2286 return new Alert(driver, text)
2287 })
2288 )
2289 }
2290}
2291
2292//////////////////////////////////////////////////////////////////////////////
2293//
2294// WebElement
2295//
2296//////////////////////////////////////////////////////////////////////////////
2297
2298const LEGACY_ELEMENT_ID_KEY = 'ELEMENT'
2299const ELEMENT_ID_KEY = 'element-6066-11e4-a52e-4f735466cecf'
2300
2301/**
2302 * Represents a DOM element. WebElements can be found by searching from the
2303 * document root using a {@link WebDriver} instance, or by searching
2304 * under another WebElement:
2305 *
2306 * driver.get('http://www.google.com');
2307 * var searchForm = driver.findElement(By.tagName('form'));
2308 * var searchBox = searchForm.findElement(By.name('q'));
2309 * searchBox.sendKeys('webdriver');
2310 */
2311class WebElement {
2312 /**
2313 * @param {!WebDriver} driver the parent WebDriver instance for this element.
2314 * @param {(!IThenable<string>|string)} id The server-assigned opaque ID for
2315 * the underlying DOM element.
2316 */
2317 constructor(driver, id) {
2318 /** @private {!WebDriver} */
2319 this.driver_ = driver
2320
2321 /** @private {!Promise<string>} */
2322 this.id_ = Promise.resolve(id)
2323 }
2324
2325 /**
2326 * @param {string} id The raw ID.
2327 * @param {boolean=} noLegacy Whether to exclude the legacy element key.
2328 * @return {!Object} The element ID for use with WebDriver's wire protocol.
2329 */
2330 static buildId(id, noLegacy = false) {
2331 return noLegacy
2332 ? { [ELEMENT_ID_KEY]: id }
2333 : { [ELEMENT_ID_KEY]: id, [LEGACY_ELEMENT_ID_KEY]: id }
2334 }
2335
2336 /**
2337 * Extracts the encoded WebElement ID from the object.
2338 *
2339 * @param {?} obj The object to extract the ID from.
2340 * @return {string} the extracted ID.
2341 * @throws {TypeError} if the object is not a valid encoded ID.
2342 */
2343 static extractId(obj) {
2344 if (obj && typeof obj === 'object') {
2345 if (typeof obj[ELEMENT_ID_KEY] === 'string') {
2346 return obj[ELEMENT_ID_KEY]
2347 } else if (typeof obj[LEGACY_ELEMENT_ID_KEY] === 'string') {
2348 return obj[LEGACY_ELEMENT_ID_KEY]
2349 }
2350 }
2351 throw new TypeError('object is not a WebElement ID')
2352 }
2353
2354 /**
2355 * @param {?} obj the object to test.
2356 * @return {boolean} whether the object is a valid encoded WebElement ID.
2357 */
2358 static isId(obj) {
2359 return (
2360 obj &&
2361 typeof obj === 'object' &&
2362 (typeof obj[ELEMENT_ID_KEY] === 'string' ||
2363 typeof obj[LEGACY_ELEMENT_ID_KEY] === 'string')
2364 )
2365 }
2366
2367 /**
2368 * Compares two WebElements for equality.
2369 *
2370 * @param {!WebElement} a A WebElement.
2371 * @param {!WebElement} b A WebElement.
2372 * @return {!Promise<boolean>} A promise that will be
2373 * resolved to whether the two WebElements are equal.
2374 */
2375 static async equals(a, b) {
2376 if (a === b) {
2377 return true
2378 }
2379 return a.driver_.executeScript('return arguments[0] === arguments[1]', a, b)
2380 }
2381
2382 /** @return {!WebDriver} The parent driver for this instance. */
2383 getDriver() {
2384 return this.driver_
2385 }
2386
2387 /**
2388 * @return {!Promise<string>} A promise that resolves to
2389 * the server-assigned opaque ID assigned to this element.
2390 */
2391 getId() {
2392 return this.id_
2393 }
2394
2395 /**
2396 * @return {!Object} Returns the serialized representation of this WebElement.
2397 */
2398 [Symbols.serialize]() {
2399 return this.getId().then(WebElement.buildId)
2400 }
2401
2402 /**
2403 * Schedules a command that targets this element with the parent WebDriver
2404 * instance. Will ensure this element's ID is included in the command
2405 * parameters under the "id" key.
2406 *
2407 * @param {!command.Command} command The command to schedule.
2408 * @return {!Promise<T>} A promise that will be resolved with the result.
2409 * @template T
2410 * @see WebDriver#schedule
2411 * @private
2412 */
2413 execute_(command) {
2414 command.setParameter('id', this)
2415 return this.driver_.execute(command)
2416 }
2417
2418 /**
2419 * Schedule a command to find a descendant of this element. If the element
2420 * cannot be found, the returned promise will be rejected with a
2421 * {@linkplain error.NoSuchElementError NoSuchElementError}.
2422 *
2423 * The search criteria for an element may be defined using one of the static
2424 * factories on the {@link by.By} class, or as a short-hand
2425 * {@link ./by.ByHash} object. For example, the following two statements
2426 * are equivalent:
2427 *
2428 * var e1 = element.findElement(By.id('foo'));
2429 * var e2 = element.findElement({id:'foo'});
2430 *
2431 * You may also provide a custom locator function, which takes as input this
2432 * instance and returns a {@link WebElement}, or a promise that will resolve
2433 * to a WebElement. If the returned promise resolves to an array of
2434 * WebElements, WebDriver will use the first element. For example, to find the
2435 * first visible link on a page, you could write:
2436 *
2437 * var link = element.findElement(firstVisibleLink);
2438 *
2439 * function firstVisibleLink(element) {
2440 * var links = element.findElements(By.tagName('a'));
2441 * return promise.filter(links, function(link) {
2442 * return link.isDisplayed();
2443 * });
2444 * }
2445 *
2446 * @param {!(by.By|Function)} locator The locator strategy to use when
2447 * searching for the element.
2448 * @return {!WebElementPromise} A WebElement that can be used to issue
2449 * commands against the located element. If the element is not found, the
2450 * element will be invalidated and all scheduled commands aborted.
2451 */
2452 findElement(locator) {
2453 locator = by.checkedLocator(locator)
2454 let id
2455 if (typeof locator === 'function') {
2456 id = this.driver_.findElementInternal_(locator, this)
2457 } else {
2458 let cmd = new command.Command(command.Name.FIND_CHILD_ELEMENT)
2459 .setParameter('using', locator.using)
2460 .setParameter('value', locator.value)
2461 id = this.execute_(cmd)
2462 }
2463 return new WebElementPromise(this.driver_, id)
2464 }
2465
2466 /**
2467 * Locates all of the descendants of this element that match the given search
2468 * criteria.
2469 *
2470 * @param {!(by.By|Function)} locator The locator strategy to use when
2471 * searching for the element.
2472 * @return {!Promise<!Array<!WebElement>>} A promise that will resolve to an
2473 * array of WebElements.
2474 */
2475 async findElements(locator) {
2476 locator = by.checkedLocator(locator)
2477 if (typeof locator === 'function') {
2478 return this.driver_.findElementsInternal_(locator, this)
2479 } else {
2480 let cmd = new command.Command(command.Name.FIND_CHILD_ELEMENTS)
2481 .setParameter('using', locator.using)
2482 .setParameter('value', locator.value)
2483 let result = await this.execute_(cmd)
2484 return Array.isArray(result) ? result : []
2485 }
2486 }
2487
2488 /**
2489 * Clicks on this element.
2490 *
2491 * @return {!Promise<void>} A promise that will be resolved when the click
2492 * command has completed.
2493 */
2494 click() {
2495 return this.execute_(new command.Command(command.Name.CLICK_ELEMENT))
2496 }
2497
2498 /**
2499 * Types a key sequence on the DOM element represented by this instance.
2500 *
2501 * Modifier keys (SHIFT, CONTROL, ALT, META) are stateful; once a modifier is
2502 * processed in the key sequence, that key state is toggled until one of the
2503 * following occurs:
2504 *
2505 * - The modifier key is encountered again in the sequence. At this point the
2506 * state of the key is toggled (along with the appropriate keyup/down
2507 * events).
2508 * - The {@link input.Key.NULL} key is encountered in the sequence. When
2509 * this key is encountered, all modifier keys current in the down state are
2510 * released (with accompanying keyup events). The NULL key can be used to
2511 * simulate common keyboard shortcuts:
2512 *
2513 * element.sendKeys("text was",
2514 * Key.CONTROL, "a", Key.NULL,
2515 * "now text is");
2516 * // Alternatively:
2517 * element.sendKeys("text was",
2518 * Key.chord(Key.CONTROL, "a"),
2519 * "now text is");
2520 *
2521 * - The end of the key sequence is encountered. When there are no more keys
2522 * to type, all depressed modifier keys are released (with accompanying
2523 * keyup events).
2524 *
2525 * If this element is a file input ({@code <input type="file">}), the
2526 * specified key sequence should specify the path to the file to attach to
2527 * the element. This is analogous to the user clicking "Browse..." and entering
2528 * the path into the file select dialog.
2529 *
2530 * var form = driver.findElement(By.css('form'));
2531 * var element = form.findElement(By.css('input[type=file]'));
2532 * element.sendKeys('/path/to/file.txt');
2533 * form.submit();
2534 *
2535 * For uploads to function correctly, the entered path must reference a file
2536 * on the _browser's_ machine, not the local machine running this script. When
2537 * running against a remote Selenium server, a {@link input.FileDetector}
2538 * may be used to transparently copy files to the remote machine before
2539 * attempting to upload them in the browser.
2540 *
2541 * __Note:__ On browsers where native keyboard events are not supported
2542 * (e.g. Firefox on OS X), key events will be synthesized. Special
2543 * punctuation keys will be synthesized according to a standard QWERTY en-us
2544 * keyboard layout.
2545 *
2546 * @param {...(number|string|!IThenable<(number|string)>)} args The
2547 * sequence of keys to type. Number keys may be referenced numerically or
2548 * by string (1 or '1'). All arguments will be joined into a single
2549 * sequence.
2550 * @return {!Promise<void>} A promise that will be resolved when all keys
2551 * have been typed.
2552 */
2553 async sendKeys(...args) {
2554 let keys = []
2555 ; (await Promise.all(args)).forEach((key) => {
2556 let type = typeof key
2557 if (type === 'number') {
2558 key = String(key)
2559 } else if (type !== 'string') {
2560 throw TypeError('each key must be a number of string; got ' + type)
2561 }
2562
2563 // The W3C protocol requires keys to be specified as an array where
2564 // each element is a single key.
2565 keys.push(...key.split(''))
2566 })
2567
2568 if (!this.driver_.fileDetector_) {
2569 return this.execute_(
2570 new command.Command(command.Name.SEND_KEYS_TO_ELEMENT)
2571 .setParameter('text', keys.join(''))
2572 .setParameter('value', keys)
2573 )
2574 }
2575
2576 keys = await this.driver_.fileDetector_.handleFile(
2577 this.driver_,
2578 keys.join('')
2579 )
2580 return this.execute_(
2581 new command.Command(command.Name.SEND_KEYS_TO_ELEMENT)
2582 .setParameter('text', keys)
2583 .setParameter('value', keys.split(''))
2584 )
2585 }
2586
2587 /**
2588 * Retrieves the element's tag name.
2589 *
2590 * @return {!Promise<string>} A promise that will be resolved with the
2591 * element's tag name.
2592 */
2593 getTagName() {
2594 return this.execute_(new command.Command(command.Name.GET_ELEMENT_TAG_NAME))
2595 }
2596
2597 /**
2598 * Retrieves the value of a computed style property for this instance. If
2599 * the element inherits the named style from its parent, the parent will be
2600 * queried for its value. Where possible, color values will be converted to
2601 * their hex representation (e.g. #00ff00 instead of rgb(0, 255, 0)).
2602 *
2603 * _Warning:_ the value returned will be as the browser interprets it, so
2604 * it may be tricky to form a proper assertion.
2605 *
2606 * @param {string} cssStyleProperty The name of the CSS style property to look
2607 * up.
2608 * @return {!Promise<string>} A promise that will be resolved with the
2609 * requested CSS value.
2610 */
2611 getCssValue(cssStyleProperty) {
2612 var name = command.Name.GET_ELEMENT_VALUE_OF_CSS_PROPERTY
2613 return this.execute_(
2614 new command.Command(name).setParameter('propertyName', cssStyleProperty)
2615 )
2616 }
2617
2618 /**
2619 * Retrieves the current value of the given attribute of this element.
2620 * Will return the current value, even if it has been modified after the page
2621 * has been loaded. More exactly, this method will return the value
2622 * of the given attribute, unless that attribute is not present, in which case
2623 * the value of the property with the same name is returned. If neither value
2624 * is set, null is returned (for example, the "value" property of a textarea
2625 * element). The "style" attribute is converted as best can be to a
2626 * text representation with a trailing semi-colon. The following are deemed to
2627 * be "boolean" attributes and will return either "true" or null:
2628 *
2629 * async, autofocus, autoplay, checked, compact, complete, controls, declare,
2630 * defaultchecked, defaultselected, defer, disabled, draggable, ended,
2631 * formnovalidate, hidden, indeterminate, iscontenteditable, ismap, itemscope,
2632 * loop, multiple, muted, nohref, noresize, noshade, novalidate, nowrap, open,
2633 * paused, pubdate, readonly, required, reversed, scoped, seamless, seeking,
2634 * selected, spellcheck, truespeed, willvalidate
2635 *
2636 * Finally, the following commonly mis-capitalized attribute/property names
2637 * are evaluated as expected:
2638 *
2639 * - "class"
2640 * - "readonly"
2641 *
2642 * @param {string} attributeName The name of the attribute to query.
2643 * @return {!Promise<?string>} A promise that will be
2644 * resolved with the attribute's value. The returned value will always be
2645 * either a string or null.
2646 */
2647 getAttribute(attributeName) {
2648 return this.execute_(
2649 new command.Command(command.Name.GET_ELEMENT_ATTRIBUTE).setParameter(
2650 'name',
2651 attributeName
2652 )
2653 )
2654 }
2655
2656 /**
2657 * Get the given property of the referenced web element
2658 * @param {string} propertyName The name of the attribute to query.
2659 * @return {!Promise<string>} A promise that will be
2660 * resolved with the element's property value
2661 */
2662 getProperty(propertyName) {
2663 return this.execute_(
2664 new command.Command(command.Name.GET_ELEMENT_PROPERTY).setParameter(
2665 'name',
2666 propertyName
2667 )
2668 )
2669 }
2670
2671 /**
2672 * Get the visible (i.e. not hidden by CSS) innerText of this element,
2673 * including sub-elements, without any leading or trailing whitespace.
2674 *
2675 * @return {!Promise<string>} A promise that will be
2676 * resolved with the element's visible text.
2677 */
2678 getText() {
2679 return this.execute_(new command.Command(command.Name.GET_ELEMENT_TEXT))
2680 }
2681
2682 /**
2683 * Get the computed WAI-ARIA role of element.
2684 *
2685 * @return {!Promise<string>} A promise that will be
2686 * resolved with the element's computed role.
2687 */
2688 getAriaRole() {
2689 return this.execute_(new command.Command(command.Name.GET_COMPUTED_ROLE))
2690 }
2691
2692 /**
2693 * Get the computed WAI-ARIA label of element.
2694 *
2695 * @return {!Promise<string>} A promise that will be
2696 * resolved with the element's computed label.
2697 */
2698 getAccessibleName() {
2699 return this.execute_(new command.Command(command.Name.GET_COMPUTED_LABEL))
2700 }
2701 /**
2702 * Returns an object describing an element's location, in pixels relative to
2703 * the document element, and the element's size in pixels.
2704 *
2705 * @return {!Promise<{width: number, height: number, x: number, y: number}>}
2706 * A promise that will resolve with the element's rect.
2707 */
2708 async getRect() {
2709 try {
2710 return await this.execute_(
2711 new command.Command(command.Name.GET_ELEMENT_RECT)
2712 )
2713 } catch (err) {
2714 if (err instanceof error.UnknownCommandError) {
2715 const { width, height } = await this.execute_(
2716 new command.Command(command.Name.GET_ELEMENT_SIZE)
2717 )
2718 const { x, y } = await this.execute_(
2719 new command.Command(command.Name.GET_ELEMENT_LOCATION)
2720 )
2721 return { x, y, width, height }
2722 }
2723 }
2724 }
2725
2726 /**
2727 * Tests whether this element is enabled, as dictated by the `disabled`
2728 * attribute.
2729 *
2730 * @return {!Promise<boolean>} A promise that will be
2731 * resolved with whether this element is currently enabled.
2732 */
2733 isEnabled() {
2734 return this.execute_(new command.Command(command.Name.IS_ELEMENT_ENABLED))
2735 }
2736
2737 /**
2738 * Tests whether this element is selected.
2739 *
2740 * @return {!Promise<boolean>} A promise that will be
2741 * resolved with whether this element is currently selected.
2742 */
2743 isSelected() {
2744 return this.execute_(new command.Command(command.Name.IS_ELEMENT_SELECTED))
2745 }
2746
2747 /**
2748 * Submits the form containing this element (or this element if it is itself
2749 * a FORM element). his command is a no-op if the element is not contained in
2750 * a form.
2751 *
2752 * @return {!Promise<void>} A promise that will be resolved
2753 * when the form has been submitted.
2754 */
2755 submit() {
2756 return this.execute_(new command.Command(command.Name.SUBMIT_ELEMENT))
2757 }
2758
2759 /**
2760 * Clear the `value` of this element. This command has no effect if the
2761 * underlying DOM element is neither a text INPUT element nor a TEXTAREA
2762 * element.
2763 *
2764 * @return {!Promise<void>} A promise that will be resolved
2765 * when the element has been cleared.
2766 */
2767 clear() {
2768 return this.execute_(new command.Command(command.Name.CLEAR_ELEMENT))
2769 }
2770
2771 /**
2772 * Test whether this element is currently displayed.
2773 *
2774 * @return {!Promise<boolean>} A promise that will be
2775 * resolved with whether this element is currently visible on the page.
2776 */
2777 isDisplayed() {
2778 return this.execute_(new command.Command(command.Name.IS_ELEMENT_DISPLAYED))
2779 }
2780
2781 /**
2782 * Take a screenshot of the visible region encompassed by this element's
2783 * bounding rectangle.
2784 *
2785 * @return {!Promise<string>} A promise that will be
2786 * resolved to the screenshot as a base-64 encoded PNG.
2787 */
2788 takeScreenshot() {
2789 return this.execute_(
2790 new command.Command(command.Name.TAKE_ELEMENT_SCREENSHOT)
2791 )
2792 }
2793}
2794
2795/**
2796 * WebElementPromise is a promise that will be fulfilled with a WebElement.
2797 * This serves as a forward proxy on WebElement, allowing calls to be
2798 * scheduled without directly on this instance before the underlying
2799 * WebElement has been fulfilled. In other words, the following two statements
2800 * are equivalent:
2801 *
2802 * driver.findElement({id: 'my-button'}).click();
2803 * driver.findElement({id: 'my-button'}).then(function(el) {
2804 * return el.click();
2805 * });
2806 *
2807 * @implements {IThenable<!WebElement>}
2808 * @final
2809 */
2810class WebElementPromise extends WebElement {
2811 /**
2812 * @param {!WebDriver} driver The parent WebDriver instance for this
2813 * element.
2814 * @param {!Promise<!WebElement>} el A promise
2815 * that will resolve to the promised element.
2816 */
2817 constructor(driver, el) {
2818 super(driver, 'unused')
2819
2820 /** @override */
2821 this.then = el.then.bind(el)
2822
2823 /** @override */
2824 this.catch = el.catch.bind(el)
2825
2826 /**
2827 * Defers returning the element ID until the wrapped WebElement has been
2828 * resolved.
2829 * @override
2830 */
2831 this.getId = function () {
2832 return el.then(function (el) {
2833 return el.getId()
2834 })
2835 }
2836 }
2837}
2838
2839//////////////////////////////////////////////////////////////////////////////
2840//
2841// Alert
2842//
2843//////////////////////////////////////////////////////////////////////////////
2844
2845/**
2846 * Represents a modal dialog such as {@code alert}, {@code confirm}, or
2847 * {@code prompt}. Provides functions to retrieve the message displayed with
2848 * the alert, accept or dismiss the alert, and set the response text (in the
2849 * case of {@code prompt}).
2850 */
2851class Alert {
2852 /**
2853 * @param {!WebDriver} driver The driver controlling the browser this alert
2854 * is attached to.
2855 * @param {string} text The message text displayed with this alert.
2856 */
2857 constructor(driver, text) {
2858 /** @private {!WebDriver} */
2859 this.driver_ = driver
2860
2861 /** @private {!Promise<string>} */
2862 this.text_ = Promise.resolve(text)
2863 }
2864
2865 /**
2866 * Retrieves the message text displayed with this alert. For instance, if the
2867 * alert were opened with alert("hello"), then this would return "hello".
2868 *
2869 * @return {!Promise<string>} A promise that will be
2870 * resolved to the text displayed with this alert.
2871 */
2872 getText() {
2873 return this.text_
2874 }
2875
2876 /**
2877 * Accepts this alert.
2878 *
2879 * @return {!Promise<void>} A promise that will be resolved
2880 * when this command has completed.
2881 */
2882 accept() {
2883 return this.driver_.execute(new command.Command(command.Name.ACCEPT_ALERT))
2884 }
2885
2886 /**
2887 * Dismisses this alert.
2888 *
2889 * @return {!Promise<void>} A promise that will be resolved
2890 * when this command has completed.
2891 */
2892 dismiss() {
2893 return this.driver_.execute(new command.Command(command.Name.DISMISS_ALERT))
2894 }
2895
2896 /**
2897 * Sets the response text on this alert. This command will return an error if
2898 * the underlying alert does not support response text (e.g. window.alert and
2899 * window.confirm).
2900 *
2901 * @param {string} text The text to set.
2902 * @return {!Promise<void>} A promise that will be resolved
2903 * when this command has completed.
2904 */
2905 sendKeys(text) {
2906 return this.driver_.execute(
2907 new command.Command(command.Name.SET_ALERT_TEXT).setParameter(
2908 'text',
2909 text
2910 )
2911 )
2912 }
2913}
2914
2915/**
2916 * AlertPromise is a promise that will be fulfilled with an Alert. This promise
2917 * serves as a forward proxy on an Alert, allowing calls to be scheduled
2918 * directly on this instance before the underlying Alert has been fulfilled. In
2919 * other words, the following two statements are equivalent:
2920 *
2921 * driver.switchTo().alert().dismiss();
2922 * driver.switchTo().alert().then(function(alert) {
2923 * return alert.dismiss();
2924 * });
2925 *
2926 * @implements {IThenable<!Alert>}
2927 * @final
2928 */
2929class AlertPromise extends Alert {
2930 /**
2931 * @param {!WebDriver} driver The driver controlling the browser this
2932 * alert is attached to.
2933 * @param {!Promise<!Alert>} alert A thenable
2934 * that will be fulfilled with the promised alert.
2935 */
2936 constructor(driver, alert) {
2937 super(driver, 'unused')
2938
2939 /** @override */
2940 this.then = alert.then.bind(alert)
2941
2942 /** @override */
2943 this.catch = alert.catch.bind(alert)
2944
2945 /**
2946 * Defer returning text until the promised alert has been resolved.
2947 * @override
2948 */
2949 this.getText = function () {
2950 return alert.then(function (alert) {
2951 return alert.getText()
2952 })
2953 }
2954
2955 /**
2956 * Defers action until the alert has been located.
2957 * @override
2958 */
2959 this.accept = function () {
2960 return alert.then(function (alert) {
2961 return alert.accept()
2962 })
2963 }
2964
2965 /**
2966 * Defers action until the alert has been located.
2967 * @override
2968 */
2969 this.dismiss = function () {
2970 return alert.then(function (alert) {
2971 return alert.dismiss()
2972 })
2973 }
2974
2975 /**
2976 * Defers action until the alert has been located.
2977 * @override
2978 */
2979 this.sendKeys = function (text) {
2980 return alert.then(function (alert) {
2981 return alert.sendKeys(text)
2982 })
2983 }
2984 }
2985}
2986
2987// PUBLIC API
2988
2989module.exports = {
2990 Alert,
2991 AlertPromise,
2992 Condition,
2993 Logs,
2994 Navigation,
2995 Options,
2996 TargetLocator,
2997 IWebDriver,
2998 WebDriver,
2999 WebElement,
3000 WebElementCondition,
3001 WebElementPromise,
3002 Window,
3003}