Added files for Juan

This commit is contained in:
2024-08-07 11:38:48 -07:00
parent f7878e3e60
commit 795d1d72ce
2871 changed files with 621552 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2024 Software Freedom Conservancy (SFC)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,2 @@
Copyright 2011-2024 Software Freedom Conservancy
Copyright 2004-2011 Selenium committers

View File

@@ -0,0 +1,230 @@
# selenium-webdriver
Selenium is a browser automation library. Most often used for testing
web-applications, Selenium may be used for any task that requires automating
interaction with the browser.
## Installation
Selenium may be installed via npm with
npm install selenium-webdriver
You will need to download additional components to work with each of the major
browsers. The drivers for Chrome, Firefox, and Microsoft's IE and Edge web
browsers are all standalone executables that should be placed on your system
[PATH]. Apple's safaridriver (v10 and above) can be found at the
following path – /usr/bin/safaridriver. To enable automation on safari,
you need to run command `safaridriver --enable`.
| Browser | Component |
| :---------------- | :------------------------------- |
| Chrome | [chromedriver(.exe)][chrome] |
| Internet Explorer | [IEDriverServer.exe][release] |
| Edge | [MicrosoftWebDriver.msi][edge] |
| Firefox | [geckodriver(.exe)][geckodriver] |
| Opera | [operadriver(.exe)][operadriver] |
| Safari | [safaridriver] |
## Usage
The sample below and others are included in the `example` directory. You may
also find the tests for selenium-webdriver informative.
```javascript
const { Builder, Browser, By, Key, until } = require('selenium-webdriver')
;(async function example() {
let driver = await new Builder().forBrowser(Browser.FIREFOX).build()
try {
await driver.get('https://www.google.com/ncr')
await driver.findElement(By.name('q')).sendKeys('webdriver', Key.RETURN)
await driver.wait(until.titleIs('webdriver - Google Search'), 1000)
} finally {
await driver.quit()
}
})()
```
### Using the Builder API
The `Builder` class is your one-stop shop for configuring new WebDriver
instances. Rather than clutter your code with branches for the various browsers,
the builder lets you set all options in one flow. When you call
`Builder#build()`, all options irrelevant to the selected browser are dropped:
```javascript
const webdriver = require('selenium-webdriver')
const chrome = require('selenium-webdriver/chrome')
const firefox = require('selenium-webdriver/firefox')
let driver = new webdriver.Builder()
.forBrowser(webdriver.Browser.FIREFOX)
.setChromeOptions(/* ... */)
.setFirefoxOptions(/* ... */)
.build()
```
Why would you want to configure options irrelevant to the target browser? The
`Builder`'s API defines your _default_ configuration. You can change the target
browser at runtime through the `SELENIUM_BROWSER` environment variable. For
example, the `example/google_search.js` script is configured to run against
Firefox. You can run the example against other browsers just by changing the
runtime environment
# cd node_modules/selenium-webdriver
node example/google_search
SELENIUM_BROWSER=chrome node example/google_search
SELENIUM_BROWSER=safari node example/google_search
### The Standalone Selenium Server
The standalone Selenium Server acts as a proxy between your script and the
browser-specific drivers. The server may be used when running locally, but it's
not recommend as it introduces an extra hop for each request and will slow
things down. The server is required, however, to use a browser on a remote host
(most browser drivers, like the IEDriverServer, do not accept remote
connections).
To use the Selenium Server, you will need to install the
[JDK](http://www.oracle.com/technetwork/java/javase/downloads/index.html) and
download the latest server from [Selenium][release]. Once downloaded, run the
server with
java -jar selenium-server-4.4.0.jar standalone
You may configure your tests to run against a remote server through the Builder
API:
```javascript
let driver = new webdriver.Builder()
.forBrowser(webdriver.Browser.FIREFOX)
.usingServer('http://localhost:4444/wd/hub')
.build()
```
Or change the Builder's configuration at runtime with the `SELENIUM_REMOTE_URL`
environment variable:
SELENIUM_REMOTE_URL="http://localhost:4444/wd/hub" node script.js
You can experiment with these options using the `example/google_search.js`
script provided with `selenium-webdriver`.
## Documentation
API documentation is available online from the [Selenium project][api].
Additional resources include
- the #selenium channel on freenode IRC
- the [selenium-users@googlegroups.com][users] list
- [SeleniumHQ](https://selenium.dev/documentation/) documentation
## Contributing
Contributions are accepted either through [GitHub][gh] pull requests or patches
via the [Selenium issue tracker][issues].
## Node Support Policy
Each version of selenium-webdriver will support the latest _semver-minor_
version of the [LTS] and stable Node releases. All _semver-major_ &
_semver-minor_ versions between the LTS and stable release will have "best
effort" support. Following a Selenium release, any _semver-minor_ Node releases
will also have "best effort" support. Releases older than the latest LTS,
_semver-major_ releases, and all unstable release branches (e.g. "v.Next")
are considered strictly unsupported.
For example, suppose the current LTS and stable releases are v14.20.0 and
v18.8.0,
respectively. Then a Selenium release would have the following support levels:
| Version | Support |
| :-------: | :-----------: |
| <= 14.19 | _unsupported_ |
| 14.20.0 | supported |
| 18.0-7 | best effort |
| 18.8.0 | supported |
| >= 18.8.0 | best effort |
| v.Next | _unsupported_ |
### Support Level Definitions
- _supported:_ A selenium-webdriver release will be API compatible with the
platform API, without the use of runtime flags.
- _best effort:_ Bugs will be investigated as time permits. API compatibility is
only guaranteed where required by a _supported_ release. This effectively
means the adoption of new JS features, such as ES2015 modules, will depend
on what is supported in Node's LTS.
- _unsupported:_ Bug submissions will be closed as will-not-fix and API
compatibility is not guaranteed.
### Projected Support Schedule
If Node releases a new [LTS] each October and a new major version every 6
months, the support window for selenium-webdriver will be roughly:
| Release | Status | END-OF-LIFE |
| :-----: | :-------------: | :---------: |
| v14.x | Maintenance LTS | 2023-04-30 |
| v16.x | Active LTS | 2023-09-11 |
| v18.x | Current | 2025-04-30 |
| v19.x | Pending | 2023-06-01 |
| v20 | Pending | 2026-04-30 |
## Issues
Please report any issues using the [Selenium issue tracker][issues]. When using
the issue tracker
- **Do** include a detailed description of the problem.
- **Do** include a link to a [gist](http://gist.github.com/) with any
interesting stack traces/logs (you may also attach these directly to the bug
report).
- **Do** include a [reduced test case][reduction]. Reporting "unable to find
element on the page" is _not_ a valid report - there's nothing for us to
look into. Expect your bug report to be closed if you do not provide enough
information for us to investigate.
- **Do not** use the issue tracker to submit basic help requests. All help
inquiries should be directed to the [user forum][users] or #selenium IRC
channel.
- **Do not** post empty "I see this too" or "Any updates?" comments. These
provide no additional information and clutter the log.
- **Do not** report regressions on closed bugs as they are not actively
monitored for updates (especially bugs that are >6 months old). Please open a
new issue and reference the original bug in your report.
## License
Licensed to the Software Freedom Conservancy (SFC) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The SFC licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
[LTS]: https://github.com/nodejs/LTS
[PATH]: http://en.wikipedia.org/wiki/PATH_%28variable%29
[api]: https://www.selenium.dev/selenium/docs/api/javascript/
[chrome]: https://googlechromelabs.github.io/chrome-for-testing/#stable
[gh]: https://github.com/SeleniumHQ/selenium/
[issues]: https://github.com/SeleniumHQ/selenium/issues
[edge]: http://go.microsoft.com/fwlink/?LinkId=619687
[geckodriver]: https://github.com/mozilla/geckodriver/releases/
[reduction]: http://www.webkit.org/quality/reduction.html
[release]: https://www.selenium.dev/downloads/
[users]: https://groups.google.com/forum/#!forum/selenium-users
[safaridriver]: https://developer.apple.com/library/prerelease/content/releasenotes/General/WhatsNewInSafari/Articles/Safari_10_0.html#//apple_ref/doc/uid/TP40014305-CH11-DontLinkElementID_28
[operadriver]: https://github.com/operasoftware/operachromiumdriver/releases

View File

@@ -0,0 +1,106 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const { UrlPattern } = require('./urlPattern')
class AddInterceptParameters {
#phases = []
#urlPatterns = []
constructor(phases) {
if (phases instanceof Array) {
phases.forEach((phase) => this.#phases.push(phase))
} else {
this.#phases.push(phases)
}
}
/**
* Adds a URL pattern to intercept.
*
* @param {UrlPattern} pattern - The URL pattern to add.
* @returns {AddInterceptParameters} - Returns the current instance of the class AddInterceptParameters for chaining.
* @throws {Error} - Throws an error if the pattern is not an instance of UrlPattern.
*/
urlPattern(pattern) {
if (!(pattern instanceof UrlPattern)) {
throw new Error(`Pattern must be an instance of UrlPattern. Received: '${pattern})'`)
}
this.#urlPatterns.push(Object.fromEntries(pattern.asMap()))
return this
}
/**
* Adds array of URL patterns to intercept.
*
* @param {UrlPattern[]} patterns - An array of UrlPattern instances representing the URL patterns to intercept.
* @returns {AddInterceptParameters} - Returns the instance of AddInterceptParameters for chaining.
* @throws {Error} - Throws an error if the pattern is not an instance of UrlPattern.
*/
urlPatterns(patterns) {
patterns.forEach((pattern) => {
if (!(pattern instanceof UrlPattern)) {
throw new Error(`Pattern must be an instance of UrlPattern. Received:'${pattern}'`)
}
this.#urlPatterns.push(Object.fromEntries(pattern.asMap()))
})
return this
}
/**
* Adds string URL to intercept.
*
* @param {string} pattern - The URL pattern to be added.
* @returns {AddInterceptParameters} - Returns the instance of AddInterceptParameters for chaining..
* @throws {Error} - If the pattern is not an instance of String.
*/
urlStringPattern(pattern) {
if (typeof pattern !== 'string') {
throw new Error(`Pattern must be an instance of String. Received:'${pattern}'`)
}
this.#urlPatterns.push({ type: 'string', pattern: pattern })
return this
}
/**
* Adds array of string URLs to intercept.
* @param {string[]} patterns - An array of URL string patterns.
* @returns {this} - Returns the instance of AddInterceptParameters for chaining.
*/
urlStringPatterns(patterns) {
patterns.forEach((pattern) => {
if (typeof pattern !== 'string') {
throw new Error(`Pattern must be an instance of String. Received:'${pattern}'`)
}
this.#urlPatterns.push({ type: 'string', pattern: pattern })
})
return this
}
asMap() {
const map = new Map()
map.set('phases', this.#phases)
if (this.#urlPatterns.length > 0) {
map.set('urlPatterns', this.#urlPatterns)
}
return map
}
}
module.exports = { AddInterceptParameters }

View File

@@ -0,0 +1,35 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const { LocalValue } = require('./protocolValue')
class ArgumentValue {
constructor(value) {
this.value = value
}
asMap() {
if (this.value instanceof LocalValue) {
return this.value.toJson()
} else {
// ReferenceValue
return this.value.asMap()
}
}
}
module.exports = { ArgumentValue }

View File

@@ -0,0 +1,94 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* Represents the commands and events under Browser Module.
* Described in https://w3c.github.io/webdriver-bidi/#module-browser
*/
class Browser {
constructor(driver) {
this._driver = driver
}
async init() {
if (!(await this._driver.getCapabilities()).get('webSocketUrl')) {
throw Error('WebDriver instance must support BiDi protocol')
}
this.bidi = await this._driver.getBidi()
}
/**
* Creates a new user context.
* @returns {Promise<string>} A promise that resolves to the user context id.
*/
async createUserContext() {
const command = {
method: 'browser.createUserContext',
params: {},
}
let response = await this.bidi.send(command)
return response.result.userContext
}
/**
* Gets the list of all user contexts.
* @returns {Promise<string[]>} A promise that resolves to an array of user context ids.
*/
async getUserContexts() {
const command = {
method: 'browser.getUserContexts',
params: {},
}
let response = await this.bidi.send(command)
let userContexts = []
let userContextsArray = response.result.userContexts
for (let userContextJson of userContextsArray) {
userContexts.push(userContextJson.userContext)
}
return userContexts
}
/**
* Removes a user context.
* @param {string} userContext The user context id to be removed.
* @returns {Promise<void>}
*/
async removeUserContext(userContext) {
const command = {
method: 'browser.removeUserContext',
params: { userContext: userContext },
}
await this.bidi.send(command)
}
}
async function getBrowserInstance(driver) {
let instance = new Browser(driver)
await instance.init()
return instance
}
module.exports = getBrowserInstance

View File

@@ -0,0 +1,656 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const { InvalidArgumentError, NoSuchFrameError } = require('../lib/error')
const { BrowsingContextInfo } = require('./browsingContextTypes')
const { SerializationOptions, ReferenceValue, RemoteValue } = require('./protocolValue')
const { WebElement } = require('../lib/webdriver')
const { CaptureScreenshotParameters } = require('./captureScreenshotParameters')
const { CreateContextParameters } = require('./createContextParameters')
/**
* Represents the locator to locate nodes in the browsing context.
* Described in https://w3c.github.io/webdriver-bidi/#type-browsingContext-Locator.
*/
class Locator {
static Type = Object.freeze({
CSS: 'css',
INNER_TEXT: 'innerText',
XPATH: 'xpath',
})
#type
#value
#ignoreCase
#matchType
#maxDepth
constructor(type, value, ignoreCase = undefined, matchType = undefined, maxDepth = undefined) {
this.#type = type
this.#value = value
this.#ignoreCase = ignoreCase
this.#matchType = matchType
this.#maxDepth = maxDepth
}
/**
* Creates a new Locator object with CSS selector type.
*
* @param {string} value - The CSS selector value.
* @returns {Locator} A new Locator object with CSS selector type.
*/
static css(value) {
return new Locator(Locator.Type.CSS, value)
}
/**
* Creates a new Locator object with the given XPath value.
*
* @param {string} value - The XPath value.
* @returns {Locator} A new Locator object.
*/
static xpath(value) {
return new Locator(Locator.Type.XPATH, value)
}
/**
* Creates a new Locator object with the specified inner text value.
*
* @param {string} value - The inner text value to locate.
* @param {boolean|undefined} [ignoreCase] - Whether to ignore the case when matching the inner text value.
* @param {string|undefined} [matchType] - The type of matching to perform (full or partial).
* @param {number|undefined} [maxDepth] - The maximum depth to search for the inner text value.
* @returns {Locator} A new Locator object with the specified inner text value.
*/
static innerText(value, ignoreCase = undefined, matchType = undefined, maxDepth = undefined) {
return new Locator(Locator.Type.INNER_TEXT, value, ignoreCase, matchType, maxDepth)
}
toMap() {
const map = new Map()
map.set('type', this.#type.toString())
map.set('value', this.#value)
map.set('ignoreCase', this.#ignoreCase)
map.set('matchType', this.#matchType)
map.set('maxDepth', this.#maxDepth)
return map
}
}
/**
* Represents the contains under BrowsingContext module commands.
* Described in https://w3c.github.io/webdriver-bidi/#module-browsingContext
* Each browsing context command requires a browsing context id.
* Hence, this class represent browsing context lifecycle.
*/
class BrowsingContext {
constructor(driver) {
this._driver = driver
}
/**
* @returns id
*/
get id() {
return this._id
}
async init({ browsingContextId = undefined, type = undefined, createParameters = undefined }) {
if (!(await this._driver.getCapabilities()).get('webSocketUrl')) {
throw Error('WebDriver instance must support BiDi protocol')
}
if (browsingContextId === undefined && type === undefined && createParameters === undefined) {
throw Error('Either BrowsingContextId or Type or CreateParameters must be provided')
}
if (type === undefined && createParameters !== undefined) {
throw Error('Type must be provided with CreateParameters')
}
if (type !== undefined && !['window', 'tab'].includes(type)) {
throw Error(`Valid types are 'window' & 'tab'. Received: ${type}`)
}
this.bidi = await this._driver.getBidi()
this._id =
browsingContextId === undefined
? (await this.create(type, createParameters))['result']['context']
: browsingContextId
}
/**
* Creates a browsing context for the given type with the given parameters
*/
async create(type, createParameters = undefined) {
if (createParameters !== undefined && (!createParameters) instanceof CreateContextParameters) {
throw Error(`Pass in the instance of CreateContextParameters. Received: ${createParameters}`)
}
let parameters = new Map()
parameters.set('type', type)
if (createParameters !== undefined) {
createParameters.asMap().forEach((value, key) => {
parameters.set(key, value)
})
}
const params = {
method: 'browsingContext.create',
params: Object.fromEntries(parameters),
}
const res = await this.bidi.send(params)
return res
}
/**
* @param url the url to navigate to
* @param readinessState type of readiness state: "none" / "interactive" / "complete"
* @returns NavigateResult object
*/
async navigate(url, readinessState = undefined) {
if (readinessState !== undefined && !['none', 'interactive', 'complete'].includes(readinessState)) {
throw Error(`Valid readiness states are 'none', 'interactive' & 'complete'. Received: ${readinessState}`)
}
const params = {
method: 'browsingContext.navigate',
params: {
context: this._id,
url: url,
wait: readinessState,
},
}
const navigateResult = (await this.bidi.send(params))['result']
return new NavigateResult(navigateResult['url'], navigateResult['navigation'])
}
/**
* @param maxDepth the max depth of the descendents of browsing context tree
* @returns BrowsingContextInfo object
*/
async getTree(maxDepth = undefined) {
const params = {
method: 'browsingContext.getTree',
params: {
root: this._id,
maxDepth: maxDepth,
},
}
let result = await this.bidi.send(params)
if ('error' in result) {
throw Error(result['error'])
}
result = result['result']['contexts'][0]
return new BrowsingContextInfo(result['context'], result['url'], result['children'], result['parent'])
}
/**
* @returns {Promise<Array<BrowsingContextInfo>>} A Promise that resolves to an array of BrowsingContextInfo objects representing the top-level browsing contexts.
*/
async getTopLevelContexts() {
const params = {
method: 'browsingContext.getTree',
params: {},
}
let result = await this.bidi.send(params)
if ('error' in result) {
throw Error(result['error'])
}
const contexts = result['result']['contexts']
const browsingContexts = contexts.map((context) => {
return new BrowsingContextInfo(context['id'], context['url'], context['children'], context['parent'])
})
return browsingContexts
}
/**
* Closes the browsing context
* @returns {Promise<void>}
*/
async close() {
const params = {
method: 'browsingContext.close',
params: {
context: this._id,
},
}
await this.bidi.send(params)
}
/**
* Prints PDF of the webpage
* @param options print options given by the user
* @returns PrintResult object
*/
async printPage(options = {}) {
let params = {
method: 'browsingContext.print',
// Setting default values for parameters
params: {
context: this._id,
background: false,
margin: {
bottom: 1.0,
left: 1.0,
right: 1.0,
top: 1.0,
},
orientation: 'portrait',
page: {
height: 27.94,
width: 21.59,
},
pageRanges: [],
scale: 1.0,
shrinkToFit: true,
},
}
// Updating parameter values based on the options passed
params.params = this._driver.validatePrintPageParams(options, params.params)
const response = await this.bidi.send(params)
return new PrintResult(response.result.data)
}
/**
* Captures a screenshot of the browsing context.
*
* @param {CaptureScreenshotParameters|undefined} [captureScreenshotParameters] - Optional parameters for capturing the screenshot.
* @returns {Promise<string>} - A promise that resolves to the base64-encoded string representation of the captured screenshot.
* @throws {InvalidArgumentError} - If the provided captureScreenshotParameters is not an instance of CaptureScreenshotParameters.
*/
async captureScreenshot(captureScreenshotParameters = undefined) {
if (
captureScreenshotParameters !== undefined &&
!(captureScreenshotParameters instanceof CaptureScreenshotParameters)
) {
throw new InvalidArgumentError(
`Pass in a CaptureScreenshotParameters object. Received: ${captureScreenshotParameters}`,
)
}
const screenshotParams = new Map()
screenshotParams.set('context', this._id)
if (captureScreenshotParameters !== undefined) {
captureScreenshotParameters.asMap().forEach((value, key) => {
screenshotParams.set(key, value)
})
}
let params = {
method: 'browsingContext.captureScreenshot',
params: Object.fromEntries(screenshotParams),
}
const response = await this.bidi.send(params)
this.checkErrorInScreenshot(response)
return response['result']['data']
}
async captureBoxScreenshot(x, y, width, height) {
let params = {
method: 'browsingContext.captureScreenshot',
params: {
context: this._id,
clip: {
type: 'box',
x: x,
y: y,
width: width,
height: height,
},
},
}
const response = await this.bidi.send(params)
this.checkErrorInScreenshot(response)
return response['result']['data']
}
/**
* Captures a screenshot of a specific element within the browsing context.
* @param {string} sharedId - The shared ID of the element to capture.
* @param {string} [handle] - The handle of the element to capture (optional).
* @returns {Promise<string>} A promise that resolves to the base64-encoded screenshot data.
*/
async captureElementScreenshot(sharedId, handle = undefined) {
let params = {
method: 'browsingContext.captureScreenshot',
params: {
context: this._id,
clip: {
type: 'element',
element: {
sharedId: sharedId,
handle: handle,
},
},
},
}
const response = await this.bidi.send(params)
this.checkErrorInScreenshot(response)
return response['result']['data']
}
checkErrorInScreenshot(response) {
if ('error' in response) {
const { error, msg } = response
switch (error) {
case 'invalid argument':
throw new InvalidArgumentError(msg)
case 'no such frame':
throw new NoSuchFrameError(msg)
}
}
}
/**
* Activates and focuses the top-level browsing context.
* @returns {Promise<void>} A promise that resolves when the browsing context is activated.
* @throws {Error} If there is an error while activating the browsing context.
*/
async activate() {
const params = {
method: 'browsingContext.activate',
params: {
context: this._id,
},
}
let result = await this.bidi.send(params)
if ('error' in result) {
throw Error(result['error'])
}
}
/**
* Handles a user prompt in the browsing context.
*
* @param {boolean} [accept] - Optional. Indicates whether to accept or dismiss the prompt.
* @param {string} [userText] - Optional. The text to enter.
* @throws {Error} If an error occurs while handling the user prompt.
*/
async handleUserPrompt(accept = undefined, userText = undefined) {
const params = {
method: 'browsingContext.handleUserPrompt',
params: {
context: this._id,
accept: accept,
userText: userText,
},
}
let result = await this.bidi.send(params)
if ('error' in result) {
throw Error(result['error'])
}
}
/**
* Reloads the current browsing context.
*
* @param {boolean} [ignoreCache] - Whether to ignore the cache when reloading.
* @param {string} [readinessState] - The readiness state to wait for before returning.
* Valid readiness states are 'none', 'interactive', and 'complete'.
* @returns {Promise<NavigateResult>} - A promise that resolves to the result of the reload operation.
* @throws {Error} - If an invalid readiness state is provided.
*/
async reload(ignoreCache = undefined, readinessState = undefined) {
if (readinessState !== undefined && !['none', 'interactive', 'complete'].includes(readinessState)) {
throw Error(`Valid readiness states are 'none', 'interactive' & 'complete'. Received: ${readinessState}`)
}
const params = {
method: 'browsingContext.reload',
params: {
context: this._id,
ignoreCache: ignoreCache,
wait: readinessState,
},
}
const navigateResult = (await this.bidi.send(params))['result']
return new NavigateResult(navigateResult['url'], navigateResult['navigation'])
}
/**
* Sets the viewport size and device pixel ratio for the browsing context.
* @param {number} width - The width of the viewport.
* @param {number} height - The height of the viewport.
* @param {number} [devicePixelRatio] - The device pixel ratio (optional)
* @throws {Error} If an error occurs while setting the viewport.
*/
async setViewport(width, height, devicePixelRatio = undefined) {
const params = {
method: 'browsingContext.setViewport',
params: {
context: this._id,
viewport: { width: width, height: height },
devicePixelRatio: devicePixelRatio,
},
}
let result = await this.bidi.send(params)
if ('error' in result) {
throw Error(result['error'])
}
}
/**
* Traverses the browsing context history by a given delta.
*
* @param {number} delta - The delta value to traverse the history. A positive value moves forward, while a negative value moves backward.
* @returns {Promise<void>} - A promise that resolves when the history traversal is complete.
*/
async traverseHistory(delta) {
const params = {
method: 'browsingContext.traverseHistory',
params: {
context: this._id,
delta: delta,
},
}
await this.bidi.send(params)
}
/**
* Moves the browsing context forward by one step in the history.
* @returns {Promise<void>} A promise that resolves when the browsing context has moved forward.
*/
async forward() {
await this.traverseHistory(1)
}
/**
* Navigates the browsing context to the previous page in the history.
* @returns {Promise<void>} A promise that resolves when the navigation is complete.
*/
async back() {
await this.traverseHistory(-1)
}
/**
* Locates nodes in the browsing context.
*
* @param {Locator} locator - The locator object used to locate the nodes.
* @param {number} [maxNodeCount] - The maximum number of nodes to locate (optional).
* @param {string} [sandbox] - The sandbox name for locating nodes (optional).
* @param {SerializationOptions} [serializationOptions] - The serialization options for locating nodes (optional).
* @param {ReferenceValue[]} [startNodes] - The array of start nodes for locating nodes (optional).
* @returns {Promise<RemoteValue[]>} - A promise that resolves to the arrays of located nodes.
* @throws {Error} - If the locator is not an instance of Locator.
* @throws {Error} - If the serializationOptions is provided but not an instance of SerializationOptions.
* @throws {Error} - If the startNodes is provided but not an array of ReferenceValue objects.
* @throws {Error} - If any of the startNodes is not an instance of ReferenceValue.
*/
async locateNodes(
locator,
maxNodeCount = undefined,
sandbox = undefined,
serializationOptions = undefined,
startNodes = undefined,
) {
if (!(locator instanceof Locator)) {
throw Error(`Pass in a Locator object. Received: ${locator}`)
}
if (serializationOptions !== undefined && !(serializationOptions instanceof SerializationOptions)) {
throw Error(`Pass in SerializationOptions object. Received: ${serializationOptions} `)
}
if (startNodes !== undefined && !Array.isArray(startNodes)) {
throw Error(`Pass in an array of ReferenceValue objects. Received: ${startNodes}`)
}
if (startNodes !== undefined && Array.isArray(startNodes)) {
startNodes.forEach((node) => {
if (!(node instanceof ReferenceValue)) {
throw Error(`Pass in a ReferenceValue object. Received: ${node}`)
}
})
}
const params = {
method: 'browsingContext.locateNodes',
params: {
context: this._id,
locator: Object.fromEntries(locator.toMap()),
maxNodeCount: maxNodeCount,
sandbox: sandbox,
serializationOptions: serializationOptions,
startNodes: startNodes,
},
}
let response = await this.bidi.send(params)
if ('error' in response) {
throw Error(response['error'])
}
const nodes = response.result.nodes
const remoteValues = []
nodes.forEach((node) => {
remoteValues.push(new RemoteValue(node))
})
return remoteValues
}
/**
* Locates a single node in the browsing context.
*
* @param {Locator} locator - The locator used to find the node.
* @param {string} [sandbox] - The sandbox of the node (optional).
* @param {SerializationOptions} [serializationOptions] - The serialization options for the node (optional).
* @param {Array} [startNodes] - The starting nodes for the search (optional).
* @returns {Promise<RemoteValue>} - A promise that resolves to the located node.
*/
async locateNode(locator, sandbox = undefined, serializationOptions = undefined, startNodes = undefined) {
const elements = await this.locateNodes(locator, 1, sandbox, serializationOptions, startNodes)
return elements[0]
}
async locateElement(locator) {
const elements = await this.locateNodes(locator, 1)
return new WebElement(this._driver, elements[0].sharedId)
}
async locateElements(locator) {
const elements = await this.locateNodes(locator)
let webElements = []
elements.forEach((element) => {
webElements.push(new WebElement(this._driver, element.sharedId))
})
return webElements
}
}
/**
* Represents the result of a navigation operation.
*/
class NavigateResult {
constructor(url, navigationId) {
this._url = url
this._navigationId = navigationId
}
/**
* Gets the URL of the navigated page.
* @returns {string} The URL of the navigated page.
*/
get url() {
return this._url
}
/**
* Gets the ID of the navigation operation.
* @returns {number} The ID of the navigation operation.
*/
get navigationId() {
return this._navigationId
}
}
/**
* Represents a print result.
*/
class PrintResult {
constructor(data) {
this._data = data
}
/**
* Gets the data associated with the print result.
* @returns {any} The data associated with the print result.
*/
get data() {
return this._data
}
}
/**
* initiate browsing context instance and return
* @param driver
* @param browsingContextId The browsing context of current window/tab
* @param type "window" or "tab"
* @param createParameters The parameters for creating a new browsing context
* @returns {Promise<BrowsingContext>}
*/
async function getBrowsingContextInstance(
driver,
{ browsingContextId = undefined, type = undefined, createParameters = undefined },
) {
let instance = new BrowsingContext(driver)
await instance.init({ browsingContextId, type, createParameters })
return instance
}
module.exports = getBrowsingContextInstance
module.exports.Locator = Locator

View File

@@ -0,0 +1,149 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const { BrowsingContextInfo, NavigationInfo, UserPromptOpened, UserPromptClosed } = require('./browsingContextTypes')
/**
* Represents a browsing context related events.
* Described in https://w3c.github.io/webdriver-bidi/#module-contexts-events.
* While BrowsingContext class represents a browsing context lifecycle and related commands.
* This class is specific to listening to events. Events can be subscribed to multiple browsing contexts or all of them.
*/
class BrowsingContextInspector {
constructor(driver, browsingContextIds) {
this._driver = driver
this._browsingContextIds = browsingContextIds
}
async init() {
this.bidi = await this._driver.getBidi()
}
/**
* Subscribes to the 'browsingContext.contextCreated' event.
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the event is emitted.
*/
async onBrowsingContextCreated(callback) {
await this.subscribeAndHandleEvent('browsingContext.contextCreated', callback)
}
/**
* Subscribes to the 'browsingContext.contextDestroyed' event.
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the event is emitted.
*/
async onBrowsingContextDestroyed(callback) {
await this.subscribeAndHandleEvent('browsingContext.contextDestroyed', callback)
}
/**
* Subscribe to the 'browsingContext.navigationStarted' event.
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the event is emitted.
*/
async onNavigationStarted(callback) {
await this.subscribeAndHandleEvent('browsingContext.navigationStarted', callback)
}
/**
* Subscribes to the 'browsingContext.fragmentNavigated' event.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the event is emitted.
*/
async onFragmentNavigated(callback) {
await this.subscribeAndHandleEvent('browsingContext.fragmentNavigated', callback)
}
/**
* Subscribes to the 'browsingContext.userPromptClosed' event.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the event is emitted.
*/
async onUserPromptClosed(callback) {
await this.subscribeAndHandleEvent('browsingContext.userPromptClosed', callback)
}
/**
* Subscribes to the 'browsingContext.userPromptOpened' event.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the event is emitted.
*/
async onUserPromptOpened(callback) {
await this.subscribeAndHandleEvent('browsingContext.userPromptOpened', callback)
}
/**
* Subscribes to the 'browsingContext.domContentLoaded' event.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the event is emitted.
*/
async onDomContentLoaded(callback) {
await this.subscribeAndHandleEvent('browsingContext.domContentLoaded', callback)
}
/**
* Subscribes to the 'browsingContext.load' event.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the event is emitted.
*/
async onBrowsingContextLoaded(callback) {
await this.subscribeAndHandleEvent('browsingContext.load', callback)
}
async subscribeAndHandleEvent(eventType, callback) {
if (this._browsingContextIds != null) {
await this.bidi.subscribe(eventType, this._browsingContextIds)
} else {
await this.bidi.subscribe(eventType)
}
await this._on(callback)
}
async _on(callback) {
this.ws = await this.bidi.socket
this.ws.on('message', (event) => {
const { params } = JSON.parse(Buffer.from(event.toString()))
if (params) {
let response = null
if ('navigation' in params) {
response = new NavigationInfo(params.context, params.navigation, params.timestamp, params.url)
} else if ('type' in params) {
response = new UserPromptOpened(params.context, params.type, params.message)
} else if ('accepted' in params) {
response = new UserPromptClosed(params.context, params.accepted, params.userText)
} else {
response = new BrowsingContextInfo(params.context, params.url, params.children, params.parent)
}
callback(response)
}
})
}
}
async function getBrowsingContextInstance(driver, browsingContextIds = null) {
let instance = new BrowsingContextInspector(driver, browsingContextIds)
await instance.init()
return instance
}
module.exports = getBrowsingContextInstance

View File

@@ -0,0 +1,99 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* Represents information about a browsing context.
* Described in https://w3c.github.io/webdriver-bidi/#type-browsingContext-Info
*/
class BrowsingContextInfo {
constructor(id, url, children, parentBrowsingContext) {
this._id = id
this._url = url
this._children = children
this._parentBrowsingContext = parentBrowsingContext
}
/**
* Get the ID of the browsing context.
* @returns {string} The ID of the browsing context.
*/
get id() {
return this._id
}
/**
* Get the URL of the browsing context.
* @returns {string} The URL of the browsing context.
*/
get url() {
return this._url
}
/**
* Get the children of the browsing context.
* @returns {Array<BrowsingContextInfo>} The children of the browsing context.
*/
get children() {
return this._children
}
/**
* Get the parent browsing context.
* @returns {BrowsingContextInfo} The parent browsing context.
*/
get parentBrowsingContext() {
return this._parentBrowsingContext
}
}
/**
* Represents information about a navigation.
* Described in https://w3c.github.io/webdriver-bidi/#type-browsingContext-NavigationInfo.
*/
class NavigationInfo {
/**
* Constructs a new NavigationInfo object.
* @param {string} browsingContextId - The ID of the browsing context.
* @param {string} navigationId - The ID of the navigation.
* @param {number} timestamp - The timestamp of the navigation.
* @param {string} url - The URL of the page navigated to.
*/
constructor(browsingContextId, navigationId, timestamp, url) {
this.browsingContextId = browsingContextId
this.navigationId = navigationId
this.timestamp = timestamp
this.url = url
}
}
class UserPromptOpened {
constructor(browsingContextId, type, message) {
this.browsingContextId = browsingContextId
this.type = type
this.message = message
}
}
class UserPromptClosed {
constructor(browsingContextId, accepted, userText = undefined) {
this.browsingContextId = browsingContextId
this.accepted = accepted
this.userText = userText
}
}
module.exports = { BrowsingContextInfo, NavigationInfo, UserPromptOpened, UserPromptClosed }

View File

@@ -0,0 +1,96 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const { BoxClipRectangle, ElementClipRectangle } = require('./clipRectangle')
/**
* Defines the reference point from which to compute offsets for capturing screenshot.
*
* @enum {string}
*/
const Origin = {
VIEWPORT: 'viewport',
DOCUMENT: 'document',
}
/**
* Represents the optional parameters for capturing a screenshot.
* Described in https://w3c.github.io/webdriver-bidi/#command-browsingContext-captureScreenshot.
*/
class CaptureScreenshotParameters {
#map = new Map()
/**
* Sets the origin for capturing the screenshot.
*
* @param {Origin} origin - The origin for capturing the screenshot. Must be one of `Origin.VIEWPORT` or `Origin.DOCUMENT`.
* @returns {CaptureScreenshotParameters} - The current instance of the CaptureScreenshotParameters for chaining.
* @throws {Error} - If the provided origin is not valid.
*/
origin(origin) {
if (origin !== Origin.VIEWPORT && origin !== Origin.DOCUMENT) {
throw new Error(`Origin must be one of ${Object.values(Origin)}. Received:'${origin}'`)
}
this.#map.set('origin', origin)
return this
}
/**
* Sets the image format and quality for capturing a screenshot.
*
* @param {string} type - The image format type.
* @param {number} [quality] - The image quality (optional).
* @throws {Error} If the type is not a string or if the quality is not a number.
* @returns {CaptureScreenshotParameters} - The current instance of the CaptureScreenshotParameters for chaining.
*/
imageFormat(type, quality = undefined) {
if (typeof type !== 'string') {
throw new Error(`Type must be an instance of String. Received:'${type}'`)
}
this.#map.set('type', type)
if (quality !== undefined) {
if (typeof quality !== 'number') {
throw new Error(`Quality must be a number. Received:'${quality}'`)
}
this.#map.set('quality', quality)
}
return this
}
/**
* Sets the clip rectangle for capturing a screenshot.
*
* @param {BoxClipRectangle|ElementClipRectangle} clipRectangle - The clip rectangle to set.
* @throws {Error} If the clipRectangle is not an instance of ClipRectangle.
* @returns {CaptureScreenshotParameters} - The current instance of the CaptureScreenshotParameters for chaining.
*/
clipRectangle(clipRectangle) {
if (!(clipRectangle instanceof BoxClipRectangle || clipRectangle instanceof ElementClipRectangle)) {
throw new Error(`ClipRectangle must be an instance of ClipRectangle. Received:'${clipRectangle}'`)
}
this.#map.set('clip', Object.fromEntries(clipRectangle.asMap()))
return this
}
asMap() {
return this.#map
}
}
module.exports = { CaptureScreenshotParameters, Origin }

View File

@@ -0,0 +1,127 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* Represents a clip rectangle.
* Described in https://w3c.github.io/webdriver-bidi/#command-browsingContext-captureScreenshot.
*/
class ClipRectangle {
clipType
/**
* Constructs a new ClipRectangle object.
* @param {string} type - The type of the clip rectangle.
*/
constructor(type) {
this.clipType = type
}
/**
* Gets the type of the clip rectangle.
* @returns {string} The type of the clip rectangle.
*/
get type() {
return this.clipType
}
asMap() {}
}
/**
* Represents a clip rectangle for an element.
* @extends ClipRectangle
*/
class ElementClipRectangle extends ClipRectangle {
#sharedId
#handleId
/**
* Constructs a new ElementClipRectangle instance.
* @param {string} sharedId - The shared ID of the element.
* @param {string} [handleId] - The handle ID of the element (optional).
*/
constructor(sharedId, handleId = undefined) {
super('element')
this.#sharedId = sharedId
if (handleId !== undefined) {
this.#handleId = handleId
}
}
/**
* Converts the ElementClipRectangle instance to a map.
* @returns {Map} - The converted map.
*/
asMap() {
const map = new Map()
map.set('type', super.type)
const sharedReference = new Map()
sharedReference.set('sharedId', this.#sharedId)
if (this.#handleId !== undefined) {
sharedReference.set('handleId', this.#handleId)
}
map.set('element', Object.fromEntries(sharedReference))
return map
}
}
/**
* Represents a box-shaped clip rectangle.
* @extends ClipRectangle
*/
class BoxClipRectangle extends ClipRectangle {
#x
#y
#width
#height
/**
* Constructs a new BoxClipRectangle object.
* @param {number} x - The x-coordinate of the top-left corner of the rectangle.
* @param {number} y - The y-coordinate of the top-left corner of the rectangle.
* @param {number} width - The width of the rectangle.
* @param {number} height - The height of the rectangle.
*/
constructor(x, y, width, height) {
super('box')
this.#x = x
this.#y = y
this.#width = width
this.#height = height
}
/**
* Converts the BoxClipRectangle object to a Map.
* @returns {Map<string, any>} - The Map representation of the BoxClipRectangle object.
*/
asMap() {
const map = new Map()
map.set('type', super.type)
map.set('x', this.#x)
map.set('y', this.#y)
map.set('width', this.#width)
map.set('height', this.#height)
return map
}
}
module.exports = { BoxClipRectangle, ElementClipRectangle }

View File

@@ -0,0 +1,122 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const { BytesValue, Header } = require('./networkTypes')
/**
* Represents the parameters for a continue request command.
* Described in https://w3c.github.io/webdriver-bidi/#command-network-continueRequest.
*/
class ContinueRequestParameters {
#map = new Map()
constructor(request) {
this.#map.set('request', request)
}
/**
* Sets the body value for the request.
*
* @param {BytesValue} value - The value to set as the body. Must be an instance of BytesValue.
* @returns {ContinueRequestParameters} - The current instance of the ContinueRequestParameters for chaining.
* @throws {Error} - If the value is not an instance of BytesValue.
*/
body(value) {
if (!(value instanceof BytesValue)) {
throw new Error(`Value must be an instance of BytesValue. Received: '${value})'`)
}
this.#map.set('body', Object.fromEntries(value.asMap()))
return this
}
/**
* Sets the cookies for the request.
*
* @param {Header[]} cookieHeaders - An array of cookie headers.
* @returns {continueRequestParameters} - The current instance of the ContinueRequestParameters for chaining.
* @throws {Error} - If a cookie header is not an instance of Header.
*/
cookies(cookieHeaders) {
const cookies = []
cookieHeaders.forEach((header) => {
if (!(header instanceof Header)) {
throw new Error(`CookieHeader must be an instance of Header. Received:'${header}'`)
}
cookies.push(Object.fromEntries(header.asMap()))
})
this.#map.set('cookies', cookies)
return this
}
/**
* Sets the headers for the request.
*
* @param {Header[]} headers - An array of Header objects.
* @returns {ContinueRequestParameters} - The current instance of the ContinueRequestParameters for chaining.
* @throws {Error} - If the header value is not an instance of Header.
*/
headers(headers) {
const headerList = []
headers.forEach((header) => {
if (!(header instanceof Header)) {
throw new Error(`Header value must be an instance of Header. Received:'${header}'`)
}
headerList.push(Object.fromEntries(header.asMap()))
})
this.#map.set('headers', headerList)
return this
}
/**
* Sets the HTTP method for the request.
*
* @param {string} method - The HTTP method to be set.
* @returns {ContinueRequestParameters} - The updated `continueRequestParameters` object.
* @throws {Error} - If the method parameter is not a string.
*/
method(method) {
if (typeof method !== 'string') {
throw new Error(`Http method must be a string. Received: '${method})'`)
}
this.#map.set('method', method)
return this
}
/**
* Sets the URL for the request.
*
* @param {string} url - The URL to set for the request.
* @returns {ContinueRequestParameters} - The current instance of the ContinueRequestParameters for chaining.
* @throws {Error} - If the url parameter is not a string.
*/
url(url) {
if (typeof url !== 'string') {
throw new Error(`Url must be a string. Received:'${url}'`)
}
this.#map.set('url', url)
return this
}
asMap() {
return this.#map
}
}
module.exports = { ContinueRequestParameters }

View File

@@ -0,0 +1,129 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const { Header } = require('./networkTypes')
/**
* Represents the parameters for a continue response.
* Described in https://w3c.github.io/webdriver-bidi/#command-network-continueResponse.
*/
class ContinueResponseParameters {
#map = new Map()
constructor(request) {
this.#map.set('request', request)
}
/**
* Sets the cookies for the response.
*
* @param {Header[]} cookieHeaders - The array of cookie headers.
* @returns {ContinueResponseParameters} - The current instance of the ContinueResponseParameters for chaining.
* @throws {Error} - If the cookieHeader is not an instance of Header.
*/
cookies(cookieHeaders) {
const cookies = []
cookieHeaders.forEach((header) => {
if (!(header instanceof Header)) {
throw new Error(`CookieHeader must be an instance of Header. Received:'${header}'`)
}
cookies.push(Object.fromEntries(header.asMap()))
})
this.#map.set('cookies', cookies)
return this
}
/**
* Sets the credentials for authentication.
*
* @param {string} username - The username for authentication.
* @param {string} password - The password for authentication.
* @returns {ContinueResponseParameters} The current instance of the ContinueResponseParameters for chaining.
* @throws {Error} If username or password is not a string.
*/
credentials(username, password) {
if (typeof username !== 'string') {
throw new Error(`Username must be a string. Received:'${username}'`)
}
if (typeof password !== 'string') {
throw new Error(`Password must be a string. Received:'${password}'`)
}
this.#map.set('credentials', { type: 'password', username: username, password: password })
return this
}
/**
* Sets the headers for the response.
*
* @param {Header[]} headers - An array of Header objects representing the headers.
* @returns {ContinueResponseParameters} - The current instance of the ContinueResponseParameters for chaining.
* @throws {Error} - If the header value is not an instance of Header.
*/
headers(headers) {
const headerList = []
headers.forEach((header) => {
if (!(header instanceof Header)) {
throw new Error(`Header value must be an instance of Header. Received:'${header}'`)
}
headerList.push(Object.fromEntries(header.asMap()))
})
this.#map.set('headers', headerList)
return this
}
/**
* Sets the reason phrase for the response.
*
* @param {string} reasonPhrase - The reason phrase for the response.
* @returns {ContinueResponseParameters} - The current instance of the ContinueResponseParameters for chaining.
* @throws {Error} - If the reason phrase is not a string.
*/
reasonPhrase(reasonPhrase) {
if (typeof reasonPhrase !== 'string') {
throw new Error(`Reason phrase must be a string. Received: '${reasonPhrase})'`)
}
this.#map.set('reasonPhrase', reasonPhrase)
return this
}
/**
* Sets the status code for the response.
*
* @param {number} statusCode - The status code to set.
* @returns {ContinueResponseParameters} - The current instance of the ContinueResponseParameters for chaining.
* @throws {Error} - If the `statusCode` parameter is not an integer.
*/
statusCode(statusCode) {
if (!Number.isInteger(statusCode)) {
throw new Error(`Status must be an integer. Received:'${statusCode}'`)
}
this.#map.set('statusCode', statusCode)
return this
}
asMap() {
return this.#map
}
}
module.exports = { ContinueResponseParameters }

View File

@@ -0,0 +1,139 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const { SameSite, BytesValue } = require('./networkTypes')
/**
* Represents a filter for fetching cookies.
* Described in https://w3c.github.io/webdriver-bidi/#command-storage-getCookies
*/
class CookieFilter {
#map = new Map()
/**
* Sets the name of the cookie.
*
* @param {string} name - The name of the cookie.
* @returns {CookieFilter} - The updated CookieFilter instance for chaining.
*/
name(name) {
this.#map.set('name', name)
return this
}
/**
* Sets the value of the cookie.
*
* @param {BytesValue} value - The value to be set. Must be an instance of BytesValue.
* @returns {CookieFilter} - The updated CookieFilter instance for chaining.
* @throws {Error} - If the value is not an instance of BytesValue.
*/
value(value) {
if (!(value instanceof BytesValue)) {
throw new Error(`Value must be an instance of BytesValue. Received:'${value}'`)
}
this.#map.set('value', Object.fromEntries(value.asMap()))
return this
}
/**
* Sets the domain for the cookie.
*
* @param {string} domain - The domain to set.
* @returns {CookieFilter} - The updated CookieFilter instance for chaining.
*/
domain(domain) {
this.#map.set('domain', domain)
return this
}
/**
* Sets the url path for the cookie to be fetched.
*
* @param {string} path - The url path for the cookie to be fetched.
* @returns {CookieFilter} - The updated CookieFilter instance for chaining.
*/
path(path) {
this.#map.set('path', path)
return this
}
/**
* Sets the size of the cookie to be fetched.
*
* @param {number} size - The size of the cookie.
* @returns {CookieFilter} - The updated CookieFilter instance for chaining.
*/
size(size) {
this.#map.set('size', size)
return this
}
/**
* Sets the `httpOnly` flag for the cookie filter.
*
* @param {boolean} httpOnly - The value to set for the `httpOnly` flag.
* @returns {CookieFilter} - The updated CookieFilter instance for chaining.
*/
httpOnly(httpOnly) {
this.#map.set('httpOnly', httpOnly)
return this
}
/**
* Sets the flag to fetch secure cookies.
*
* @param {boolean} secure - Whether the cookie fetched should be secure only or not.
* @returns {CookieFilter} - The updated CookieFilter instance for chaining.
*/
secure(secure) {
this.#map.set('secure', secure)
return this
}
/**
* Sets the SameSite attribute for the cookie.
*
* @param {SameSite} sameSite - The SameSite value to be set for the cookie.
* @returns {CookieFilter} - The updated CookieFilter instance for chaining.
* @throws {Error} - If the provided sameSite value is not an instance of SameSite.
*/
sameSite(sameSite) {
if (!(sameSite instanceof SameSite)) {
throw new Error(`Params must be a value in SameSite. Received:'${sameSite}'`)
}
this.#map.set('sameSite', sameSite)
return this
}
/**
* Sets the expiry value.
*
* @param {number} expiry - The expiry value.
* @returns {CookieFilter} - The updated CookieFilter instance for chaining.
*/
expiry(expiry) {
this.#map.set('expiry', expiry)
return this
}
asMap() {
return this.#map
}
}
module.exports = { CookieFilter }

View File

@@ -0,0 +1,73 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* Represents a set of parameters for creating a context.
* Described in https://w3c.github.io/webdriver-bidi/#command-browsingContext-create.
*/
class CreateContextParameters {
#map = new Map()
/**
* Sets the reference context.
* @param {string} id - The ID of the reference context.
* @returns {CreateContextParameters} - The updated instance of CreateContextParameters for chaining.
* @throws {Error} - If the provided ID is not a string.
*/
referenceContext(id) {
if (typeof id !== 'string') {
throw new Error(`ReferenceContext must be string. Received:'${id}'`)
}
this.#map.set('referenceContext', id)
return this
}
/**
* Sets the background parameter.
*
* @param {boolean} background - The background value to set.
* @returns {CreateContextParameters} - The updated instance of CreateContextParameters for chaining.
* @throws {Error} - If the background parameter is not a boolean.
*/
background(background) {
if (typeof background !== 'boolean') {
throw new Error(`Background must be boolean. Received:'${background}'`)
}
this.#map.set('background', background)
return this
}
/**
* Sets the user context.
* @param {string} userContext - The user context to set.
* @returns {CreateContextParameters} - The updated instance of CreateContextParameters for chaining.
* @throws {Error} - If the userContext parameter is not a string.
*/
userContext(userContext) {
if (typeof userContext !== 'string') {
throw new Error(`UserContext must be string. Received:'${userContext}'`)
}
this.#map.set('userContext', userContext)
return this
}
asMap() {
return this.#map
}
}
module.exports = { CreateContextParameters }

View File

@@ -0,0 +1,71 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* Represents the type of script evaluation result.
* Described in https://w3c.github.io/webdriver-bidi/#type-script-EvaluateResult.
* @enum {string}
*/
const EvaluateResultType = {
SUCCESS: 'success',
EXCEPTION: 'exception',
}
/**
* Represents a successful evaluation result.
* @class
*/
class EvaluateResultSuccess {
constructor(realmId, value) {
this.resultType = EvaluateResultType.SUCCESS
this.realmId = realmId
this.result = value
}
}
/**
* Represents an exception that occurred during evaluation of a result.
* @class
*/
class EvaluateResultException {
constructor(realmId, exceptionDetails) {
this.resultType = EvaluateResultType.EXCEPTION
this.realmId = realmId
this.exceptionDetails = exceptionDetails
}
}
/**
* Represents details of an exception.
* @class
*/
class ExceptionDetails {
constructor(exceptionDetails) {
this.columnNumber = 'columnNumber' in exceptionDetails ? exceptionDetails['columnNumber'] : null
this.exception = 'exception' in exceptionDetails ? exceptionDetails['exception'] : null
this.lineNumber = 'lineNumber' in exceptionDetails ? exceptionDetails['lineNumber'] : null
this.stackTrace = 'stackTrace' in exceptionDetails ? exceptionDetails['stackTrace'] : null
this.text = 'text' in exceptionDetails ? exceptionDetails['text'] : null
}
}
module.exports = {
EvaluateResultType,
EvaluateResultSuccess,
EvaluateResultException,
ExceptionDetails,
}

View File

@@ -0,0 +1,42 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
class FilterBy {
constructor(level) {
this.level_ = level
}
static logLevel(level) {
if (level === undefined || (level != undefined && !['debug', 'error', 'info', 'warning'].includes(level))) {
throw Error(
`Please pass valid log level. Valid log levels are 'debug', 'error', 'info' and 'warning'. Received: ${level}`,
)
}
return new FilterBy(level)
}
getLevel() {
return this.level_
}
}
// PUBLIC API
module.exports = {
FilterBy,
}

View File

@@ -0,0 +1,225 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const { EventEmitter } = require('node:events')
const WebSocket = require('ws')
const RESPONSE_TIMEOUT = 1000 * 30
class Index extends EventEmitter {
id = 0
connected = false
events = []
browsingContexts = []
/**
* Create a new websocket connection
* @param _webSocketUrl
*/
constructor(_webSocketUrl) {
super()
this.connected = false
this._ws = new WebSocket(_webSocketUrl)
this._ws.on('open', () => {
this.connected = true
})
}
/**
* @returns {WebSocket}
*/
get socket() {
return this._ws
}
/**
* @returns {boolean|*}
*/
get isConnected() {
return this.connected
}
/**
* Get Bidi Status
* @returns {Promise<*>}
*/
get status() {
return this.send({
method: 'session.status',
params: {},
})
}
/**
* Resolve connection
* @returns {Promise<unknown>}
*/
async waitForConnection() {
return new Promise((resolve) => {
if (this.connected) {
resolve()
} else {
this._ws.once('open', () => {
resolve()
})
}
})
}
/**
* Sends a bidi request
* @param params
* @returns {Promise<unknown>}
*/
async send(params) {
if (!this.connected) {
await this.waitForConnection()
}
const id = ++this.id
this._ws.send(JSON.stringify({ id, ...params }))
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error(`Request with id ${id} timed out`))
handler.off('message', listener)
}, RESPONSE_TIMEOUT)
const listener = (data) => {
try {
const payload = JSON.parse(data.toString())
if (payload.id === id) {
clearTimeout(timeoutId)
handler.off('message', listener)
resolve(payload)
}
} catch (err) {
// eslint-disable-next-line no-undef
log.error(`Failed parse message: ${err.message}`)
}
}
const handler = this._ws.on('message', listener)
})
}
/**
* Subscribe to events
* @param events
* @param browsingContexts
* @returns {Promise<void>}
*/
async subscribe(events, browsingContexts) {
function toArray(arg) {
if (arg === undefined) {
return []
}
return Array.isArray(arg) ? [...arg] : [arg]
}
const eventsArray = toArray(events)
const contextsArray = toArray(browsingContexts)
const params = {
method: 'session.subscribe',
params: {},
}
if (eventsArray.length && eventsArray.some((event) => typeof event !== 'string')) {
throw new TypeError('events should be string or string array')
}
if (contextsArray.length && contextsArray.some((context) => typeof context !== 'string')) {
throw new TypeError('browsingContexts should be string or string array')
}
if (eventsArray.length) {
params.params.events = eventsArray
}
if (contextsArray.length) {
params.params.contexts = contextsArray
}
await this.send(params)
}
/**
* Unsubscribe to events
* @param events
* @param browsingContexts
* @returns {Promise<void>}
*/
async unsubscribe(events, browsingContexts) {
if (typeof events === 'string') {
this.events = this.events.filter((event) => event !== events)
} else if (Array.isArray(events)) {
this.events = this.events.filter((event) => !events.includes(event))
}
if (typeof browsingContexts === 'string') {
this.browsingContexts.pop()
} else if (Array.isArray(browsingContexts)) {
this.browsingContexts = this.browsingContexts.filter((id) => !browsingContexts.includes(id))
}
const params = {
method: 'session.unsubscribe',
params: {
events: this.events,
},
}
if (this.browsingContexts.length > 0) {
params.params.contexts = this.browsingContexts
}
await this.send(params)
}
/**
* Close ws connection.
* @returns {Promise<unknown>}
*/
close() {
const closeWebSocket = (callback) => {
// don't close if it's already closed
if (this._ws.readyState === 3) {
callback()
} else {
// don't notify on user-initiated shutdown ('disconnect' event)
this._ws.removeAllListeners('close')
this._ws.once('close', () => {
this._ws.removeAllListeners()
callback()
})
this._ws.close()
}
}
return new Promise((fulfill, _) => {
closeWebSocket(fulfill)
})
}
}
/**
* API
* @type {function(*): Promise<Index>}
*/
module.exports = Index

View File

@@ -0,0 +1,141 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
// type: module added to package.json
// import { WebElement } from '../lib/webdriver'
const { WebElement } = require('../lib/webdriver')
const { RemoteReferenceType, ReferenceValue } = require('./protocolValue')
/**
* Represents commands and events related to the Input module (simulated user input).
* Described in https://w3c.github.io/webdriver-bidi/#module-input.
*/
class Input {
constructor(driver) {
this._driver = driver
}
async init() {
if (!(await this._driver.getCapabilities()).get('webSocketUrl')) {
throw Error('WebDriver instance must support BiDi protocol')
}
this.bidi = await this._driver.getBidi()
}
/**
* Performs the specified actions on the given browsing context.
*
* @param {string} browsingContextId - The ID of the browsing context.
* @param {Array} actions - The actions to be performed.
* @returns {Promise} A promise that resolves with the response from the server.
*/
async perform(browsingContextId, actions) {
const _actions = await updateActions(actions)
const command = {
method: 'input.performActions',
params: {
context: browsingContextId,
actions: _actions,
},
}
return await this.bidi.send(command)
}
/**
* Resets the input state in the specified browsing context.
*
* @param {string} browsingContextId - The ID of the browsing context.
* @returns {Promise} A promise that resolves when the release actions are sent.
*/
async release(browsingContextId) {
const command = {
method: 'input.releaseActions',
params: {
context: browsingContextId,
},
}
return await this.bidi.send(command)
}
/**
* Sets the files property of a given input element.
*
* @param {string} browsingContextId - The ID of the browsing context.
* @param {string | ReferenceValue} element - The ID of the element or a ReferenceValue object representing the element.
* @param {string | string[]} files - The file path or an array of file paths to be set.
* @throws {Error} If the element is not a string or a ReferenceValue.
* @returns {Promise<void>} A promise that resolves when the files are set.
*/
async setFiles(browsingContextId, element, files) {
if (typeof element !== 'string' && !(element instanceof ReferenceValue)) {
throw Error(`Pass in a WebElement id as a string or a ReferenceValue. Received: ${element}`)
}
const command = {
method: 'input.setFiles',
params: {
context: browsingContextId,
element:
typeof element === 'string'
? new ReferenceValue(RemoteReferenceType.SHARED_ID, element).asMap()
: element.asMap(),
files: typeof files === 'string' ? [files] : files,
},
}
await this.bidi.send(command)
}
}
async function updateActions(actions) {
const _actions = []
for (const action of actions) {
const sequenceList = action.actions
let updatedSequenceList = []
if (action.type === 'pointer' || action.type === 'wheel') {
for (const sequence of sequenceList) {
if ((sequence.type === 'pointerMove' || sequence.type === 'scroll') && sequence.origin instanceof WebElement) {
const element = sequence.origin
const elementId = await element.getId()
sequence.origin = {
type: 'element',
element: { sharedId: elementId },
}
}
updatedSequenceList.push(sequence)
}
const updatedAction = { ...action, actions: updatedSequenceList }
_actions.push(updatedAction)
} else {
_actions.push(action)
}
}
return _actions
}
async function getInputInstance(driver) {
let instance = new Input(driver)
await instance.init()
return instance
}
module.exports = getInputInstance

View File

@@ -0,0 +1,28 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* Represents the different phases of intercepting network requests and responses.
* @enum {string}
*/
const InterceptPhase = {
BEFORE_REQUEST_SENT: 'beforeRequestSent',
RESPONSE_STARTED: 'responseStarted',
AUTH_REQUIRED: 'authRequired',
}
module.exports = { InterceptPhase }

View File

@@ -0,0 +1,156 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
/**
* Represents a base log entry.
* Desribed in https://w3c.github.io/webdriver-bidi/#types-log-logentry.
*/
class BaseLogEntry {
/**
* Creates a new instance of BaseLogEntry.
* @param {string} level - The log level.
* @param {string} text - The log text.
* @param {number} timeStamp - The log timestamp.
* @param {string} stackTrace - The log stack trace.
*/
constructor(level, text, timeStamp, stackTrace) {
this._level = level
this._text = text
this._timeStamp = timeStamp
this._stackTrace = stackTrace
}
/**
* Gets the log level.
* @returns {string} The log level.
*/
get level() {
return this._level
}
/**
* Gets the log text.
* @returns {string} The log text.
*/
get text() {
return this._text
}
/**
* Gets the log timestamp.
* @returns {number} The log timestamp.
*/
get timeStamp() {
return this._timeStamp
}
/**
* Gets the log stack trace.
* @returns {string} The log stack trace.
*/
get stackTrace() {
return this._stackTrace
}
}
/**
* Represents a generic log entry.
* @class
* @extends BaseLogEntry
*/
class GenericLogEntry extends BaseLogEntry {
/**
* Creates an instance of GenericLogEntry.
* @param {string} level - The log level.
* @param {string} text - The log text.
* @param {Date} timeStamp - The log timestamp.
* @param {string} type - The log type.
* @param {string} stackTrace - The log stack trace.
*/
constructor(level, text, timeStamp, type, stackTrace) {
super(level, text, timeStamp, stackTrace)
this._type = type
}
/**
* Gets the log type.
* @returns {string} The log type.
*/
get type() {
return this._type
}
}
/**
* Represents a log entry for console logs.
* @class
* @extends GenericLogEntry
*/
class ConsoleLogEntry extends GenericLogEntry {
constructor(level, text, timeStamp, type, method, realm, args, stackTrace) {
super(level, text, timeStamp, type, stackTrace)
this._method = method
this._realm = realm
this._args = args
}
/**
* Gets the method associated with the log entry.
* @returns {string} The method associated with the log entry.
*/
get method() {
return this._method
}
/**
* Gets the realm associated with the log entry.
* @returns {string} The realm associated with the log entry.
*/
get realm() {
return this._realm
}
/**
* Gets the arguments associated with the log entry.
* @returns {Array} The arguments associated with the log entry.
*/
get args() {
return this._args
}
}
/**
* Represents a log entry for JavaScript logs.
* @class
* @extends GenericLogEntry
*/
class JavascriptLogEntry extends GenericLogEntry {
constructor(level, text, timeStamp, type, stackTrace) {
super(level, text, timeStamp, type, stackTrace)
}
}
// PUBLIC API
module.exports = {
BaseLogEntry,
GenericLogEntry,
ConsoleLogEntry,
JavascriptLogEntry,
}

View File

@@ -0,0 +1,353 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const { FilterBy } = require('./filterBy')
const { ConsoleLogEntry, JavascriptLogEntry, GenericLogEntry } = require('./logEntries')
const LOG = {
TYPE_CONSOLE: 'console',
TYPE_JS_LOGS: 'javascript',
TYPE_JS_EXCEPTION: 'javascriptException',
TYPE_LOGS: 'logs',
TYPE_CONSOLE_FILTER: 'console_filter',
TYPE_JS_LOGS_FILTER: 'javascript_filter',
TYPE_JS_EXCEPTION_FILTER: 'javascriptException_filter',
TYPE_LOGS_FILTER: 'logs_filter',
}
class LogInspector {
bidi
ws
#callbackId = 0
constructor(driver, browsingContextIds) {
this._driver = driver
this._browsingContextIds = browsingContextIds
this.listener = new Map()
this.listener.set(LOG.TYPE_CONSOLE, new Map())
this.listener.set(LOG.TYPE_JS_LOGS, new Map())
this.listener.set(LOG.TYPE_JS_EXCEPTION, new Map())
this.listener.set(LOG.TYPE_LOGS, new Map())
this.listener.set(LOG.TYPE_CONSOLE_FILTER, new Map())
this.listener.set(LOG.TYPE_JS_LOGS_FILTER, new Map())
this.listener.set(LOG.TYPE_JS_EXCEPTION_FILTER, new Map())
this.listener.set(LOG.TYPE_LOGS_FILTER, new Map())
}
/**
* Subscribe to log event
* @returns {Promise<void>}
*/
async init() {
this.bidi = await this._driver.getBidi()
await this.bidi.subscribe('log.entryAdded', this._browsingContextIds)
}
addCallback(eventType, callback) {
const id = ++this.#callbackId
const eventCallbackMap = this.listener.get(eventType)
eventCallbackMap.set(id, callback)
return id
}
removeCallback(id) {
let hasId = false
for (const [, callbacks] of this.listener) {
if (callbacks.has(id)) {
callbacks.delete(id)
hasId = true
}
}
if (!hasId) {
throw Error(`Callback with id ${id} not found`)
}
}
invokeCallbacks(eventType, data) {
const callbacks = this.listener.get(eventType)
if (callbacks) {
for (const [, callback] of callbacks) {
callback(data)
}
}
}
invokeCallbacksWithFilter(eventType, data, filterLevel) {
const callbacks = this.listener.get(eventType)
if (callbacks) {
for (const [, value] of callbacks) {
const callback = value.callback
const filter = value.filter
if (filterLevel === filter.getLevel()) {
callback(data)
}
}
}
}
/**
* Listen to Console logs
* @param callback
* @param filterBy
* @returns {Promise<number>}
*/
async onConsoleEntry(callback, filterBy = undefined) {
if (filterBy !== undefined && !(filterBy instanceof FilterBy)) {
throw Error(`Pass valid FilterBy object. Received: ${filterBy}`)
}
let id
if (filterBy !== undefined) {
id = this.addCallback(LOG.TYPE_CONSOLE_FILTER, { callback: callback, filter: filterBy })
} else {
id = this.addCallback(LOG.TYPE_CONSOLE, callback)
}
this.ws = await this.bidi.socket
this.ws.on('message', (event) => {
const { params } = JSON.parse(Buffer.from(event.toString()))
if (params?.type === LOG.TYPE_CONSOLE) {
let consoleEntry = new ConsoleLogEntry(
params.level,
params.text,
params.timestamp,
params.type,
params.method,
params.realm,
params.args,
params.stackTrace,
)
if (filterBy !== undefined) {
if (params?.level === filterBy.getLevel()) {
this.invokeCallbacksWithFilter(LOG.TYPE_CONSOLE_FILTER, consoleEntry, filterBy.getLevel())
}
return
}
this.invokeCallbacks(LOG.TYPE_CONSOLE, consoleEntry)
}
})
return id
}
/**
* Listen to JS logs
* @param callback
* @param filterBy
* @returns {Promise<number>}
*/
async onJavascriptLog(callback, filterBy = undefined) {
if (filterBy !== undefined && !(filterBy instanceof FilterBy)) {
throw Error(`Pass valid FilterBy object. Received: ${filterBy}`)
}
let id
if (filterBy !== undefined) {
id = this.addCallback(LOG.TYPE_JS_LOGS_FILTER, { callback: callback, filter: filterBy })
} else {
id = this.addCallback(LOG.TYPE_JS_LOGS, callback)
}
this.ws = await this.bidi.socket
this.ws.on('message', (event) => {
const { params } = JSON.parse(Buffer.from(event.toString()))
if (params?.type === LOG.TYPE_JS_LOGS) {
let jsEntry = new JavascriptLogEntry(
params.level,
params.text,
params.timestamp,
params.type,
params.stackTrace,
)
if (filterBy !== undefined) {
if (params?.level === filterBy.getLevel()) {
this.invokeCallbacksWithFilter(LOG.TYPE_JS_LOGS_FILTER, jsEntry, filterBy.getLevel())
}
return
}
this.invokeCallbacks(LOG.TYPE_JS_LOGS, jsEntry)
}
})
return id
}
/**
* Listen to JS Exceptions
* @param callback
* @returns {Promise<number>}
*/
async onJavascriptException(callback) {
const id = this.addCallback(LOG.TYPE_JS_EXCEPTION, callback)
this.ws = await this.bidi.socket
this.ws.on('message', (event) => {
const { params } = JSON.parse(Buffer.from(event.toString()))
if (params?.type === 'javascript' && params?.level === 'error') {
let jsErrorEntry = new JavascriptLogEntry(
params.level,
params.text,
params.timestamp,
params.type,
params.stackTrace,
)
this.invokeCallbacks(LOG.TYPE_JS_EXCEPTION, jsErrorEntry)
}
})
return id
}
/**
* Listen to any logs
* @param callback
* @param filterBy
* @returns {Promise<number>}
*/
async onLog(callback, filterBy = undefined) {
if (filterBy !== undefined && !(filterBy instanceof FilterBy)) {
throw Error(`Pass valid FilterBy object. Received: ${filterBy}`)
}
let id
if (filterBy !== undefined) {
id = this.addCallback(LOG.TYPE_LOGS_FILTER, { callback: callback, filter: filterBy })
} else {
id = this.addCallback(LOG.TYPE_LOGS, callback)
}
this.ws = await this.bidi.socket
this.ws.on('message', (event) => {
const { params } = JSON.parse(Buffer.from(event.toString()))
if (params?.type === 'javascript') {
let jsEntry = new JavascriptLogEntry(
params.level,
params.text,
params.timestamp,
params.type,
params.stackTrace,
)
if (filterBy !== undefined) {
if (params?.level === filterBy.getLevel()) {
callback(jsEntry)
}
return
}
if (filterBy !== undefined) {
if (params?.level === filterBy.getLevel()) {
{
this.invokeCallbacksWithFilter(LOG.TYPE_LOGS_FILTER, jsEntry, filterBy.getLevel())
}
return
}
}
this.invokeCallbacks(LOG.TYPE_LOGS, jsEntry)
return
}
if (params?.type === 'console') {
let consoleEntry = new ConsoleLogEntry(
params.level,
params.text,
params.timestamp,
params.type,
params.method,
params.realm,
params.args,
params.stackTrace,
)
if (filterBy !== undefined) {
if (params?.level === filterBy.getLevel()) {
this.invokeCallbacksWithFilter(LOG.TYPE_LOGS_FILTER, consoleEntry, filterBy.getLevel())
}
return
}
this.invokeCallbacks(LOG.TYPE_LOGS, consoleEntry)
return
}
if (params !== undefined && !['console', 'javascript'].includes(params?.type)) {
let genericEntry = new GenericLogEntry(
params.level,
params.text,
params.timestamp,
params.type,
params.stackTrace,
)
if (filterBy !== undefined) {
if (params?.level === filterBy.getLevel()) {
{
this.invokeCallbacksWithFilter(LOG.TYPE_LOGS_FILTER, genericEntry, filterBy.getLevel())
}
return
}
}
this.invokeCallbacks(LOG.TYPE_LOGS, genericEntry)
return
}
})
return id
}
/**
* Unsubscribe to log event
* @returns {Promise<void>}
*/
async close() {
await this.bidi.unsubscribe('log.entryAdded', this._browsingContextIds)
}
}
/**
* initiate inspector instance and return
* @param driver
* @param browsingContextIds
* @returns {Promise<LogInspector>}
*/
async function getLogInspectorInstance(driver, browsingContextIds) {
let instance = new LogInspector(driver, browsingContextIds)
await instance.init()
return instance
}
/**
* API
* @type {function(*, *): Promise<LogInspector>}
*/
module.exports = getLogInspectorInstance

View File

@@ -0,0 +1,331 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const { BeforeRequestSent, ResponseStarted, FetchError } = require('./networkTypes')
const { AddInterceptParameters } = require('./addInterceptParameters')
const { ContinueResponseParameters } = require('./continueResponseParameters')
const { ContinueRequestParameters } = require('./continueRequestParameters')
const { ProvideResponseParameters } = require('./provideResponseParameters')
/**
* Represents all commands and events of Network module.
* Described in https://w3c.github.io/webdriver-bidi/#module-network.
*/
class Network {
/**
* Represents a Network object.
* @constructor
* @param {Driver} driver - The driver to fetch the BiDi connection.
* @param {Array} browsingContextIds - An array of browsing context IDs that the network events will be subscribed to.
*/
constructor(driver, browsingContextIds) {
this._driver = driver
this._browsingContextIds = browsingContextIds
}
async init() {
this.bidi = await this._driver.getBidi()
}
/**
* Subscribes to the 'network.beforeRequestSent' event and handles it with the provided callback.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
*/
async beforeRequestSent(callback) {
await this.subscribeAndHandleEvent('network.beforeRequestSent', callback)
}
/**
* Subscribes to the 'network.responseStarted' event and handles it with the provided callback.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
*/
async responseStarted(callback) {
await this.subscribeAndHandleEvent('network.responseStarted', callback)
}
/**
* Subscribes to the 'network.responseCompleted' event and handles it with the provided callback.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
*/
async responseCompleted(callback) {
await this.subscribeAndHandleEvent('network.responseCompleted', callback)
}
/**
* Subscribes to the 'network.authRequired' event and handles it with the provided callback.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
*/
async authRequired(callback) {
await this.subscribeAndHandleEvent('network.authRequired', callback)
}
/**
* Subscribes to the 'network.fetchError' event and handles it with the provided callback.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
*/
async fetchError(callback) {
await this.subscribeAndHandleEvent('network.fetchError', callback)
}
async subscribeAndHandleEvent(eventType, callback) {
if (this._browsingContextIds != null) {
await this.bidi.subscribe(eventType, this._browsingContextIds)
} else {
await this.bidi.subscribe(eventType)
}
await this._on(callback)
}
async _on(callback) {
this.ws = await this.bidi.socket
this.ws.on('message', (event) => {
const { params } = JSON.parse(Buffer.from(event.toString()))
if (params) {
let response = null
if ('initiator' in params) {
response = new BeforeRequestSent(
params.context,
params.navigation,
params.redirectCount,
params.request,
params.timestamp,
params.initiator,
)
} else if ('response' in params) {
response = new ResponseStarted(
params.context,
params.navigation,
params.redirectCount,
params.request,
params.timestamp,
params.response,
)
} else if ('errorText' in params) {
response = new FetchError(
params.context,
params.navigation,
params.redirectCount,
params.request,
params.timestamp,
params.errorText,
)
}
callback(response)
}
})
}
/**
* Adds a network intercept.
*
* @param {AddInterceptParameters} params - The parameters for the network intercept.
* @returns {Promise<string>} - A promise that resolves to the added intercept's id.
* @throws {Error} - If params is not an instance of AddInterceptParameters.
*/
async addIntercept(params) {
if (!(params instanceof AddInterceptParameters)) {
throw new Error(`Params must be an instance of AddInterceptParameters. Received:'${params}'`)
}
const command = {
method: 'network.addIntercept',
params: Object.fromEntries(params.asMap()),
}
let response = await this.bidi.send(command)
return response.result.intercept
}
/**
* Removes an intercept.
*
* @param {string} interceptId - The ID of the intercept to be removed.
* @returns {Promise<void>} - A promise that resolves when the intercept is successfully removed.
*/
async removeIntercept(interceptId) {
const command = {
method: 'network.removeIntercept',
params: { intercept: interceptId },
}
await this.bidi.send(command)
}
/**
* Continues the network request with authentication credentials.
* @param {string} requestId - The ID of the request to continue.
* @param {string} username - The username for authentication.
* @param {string} password - The password for authentication.
* @returns {Promise<void>} - A promise that resolves when the command is sent.
*/
async continueWithAuth(requestId, username, password) {
const command = {
method: 'network.continueWithAuth',
params: {
request: requestId.toString(),
action: 'provideCredentials',
credentials: {
type: 'password',
username: username,
password: password,
},
},
}
await this.bidi.send(command)
}
/**
* Fails a network request.
*
* @param {number} requestId - The ID of the request to fail.
* @returns {Promise<void>} - A promise that resolves when the command is sent.
*/
async failRequest(requestId) {
const command = {
method: 'network.failRequest',
params: {
request: requestId.toString(),
},
}
await this.bidi.send(command)
}
/**
* Continues the network request with authentication but without providing credentials.
* @param {string} requestId - The ID of the request to continue with authentication.
* @returns {Promise<void>} - A promise that resolves when the command is sent.
*/
async continueWithAuthNoCredentials(requestId) {
const command = {
method: 'network.continueWithAuth',
params: {
request: requestId.toString(),
action: 'default',
},
}
await this.bidi.send(command)
}
/**
* Cancels the authentication for a specific request.
*
* @param {string} requestId - The ID of the request to cancel authentication for.
* @returns {Promise<void>} - A promise that resolves when the command is sent.
*/
async cancelAuth(requestId) {
const command = {
method: 'network.continueWithAuth',
params: {
request: requestId.toString(),
action: 'cancel',
},
}
await this.bidi.send(command)
}
/**
* Continues the network request with the provided parameters.
*
* @param {ContinueRequestParameters} params - The parameters for continuing the request.
* @throws {Error} If params is not an instance of ContinueRequestParameters.
* @returns {Promise<void>} A promise that resolves when the command is sent.
*/
async continueRequest(params) {
if (!(params instanceof ContinueRequestParameters)) {
throw new Error(`Params must be an instance of ContinueRequestParameters. Received:'${params}'`)
}
const command = {
method: 'network.continueRequest',
params: Object.fromEntries(params.asMap()),
}
await this.bidi.send(command)
}
/**
* Continues the network response with the given parameters.
*
* @param {ContinueResponseParameters} params - The parameters for continuing the response.
* @throws {Error} If params is not an instance of ContinueResponseParameters.
* @returns {Promise<void>} A promise that resolves when the command is sent.
*/
async continueResponse(params) {
if (!(params instanceof ContinueResponseParameters)) {
throw new Error(`Params must be an instance of ContinueResponseParameters. Received:'${params}'`)
}
const command = {
method: 'network.continueResponse',
params: Object.fromEntries(params.asMap()),
}
await this.bidi.send(command)
}
/**
* Provides a response for the network.
*
* @param {ProvideResponseParameters} params - The parameters for providing the response.
* @throws {Error} If params is not an instance of ProvideResponseParameters.
* @returns {Promise<void>} A promise that resolves when the command is sent.
*/
async provideResponse(params) {
if (!(params instanceof ProvideResponseParameters)) {
throw new Error(`Params must be an instance of ProvideResponseParameters. Received:'${params}'`)
}
const command = {
method: 'network.provideResponse',
params: Object.fromEntries(params.asMap()),
}
await this.bidi.send(command)
}
/**
* Unsubscribes from network events for all browsing contexts.
* @returns {Promise<void>} A promise that resolves when the network connection is closed.
*/
async close() {
await this.bidi.unsubscribe(
'network.beforeRequestSent',
'network.responseStarted',
'network.responseCompleted',
'network.authRequired',
)
}
}
async function getNetworkInstance(driver, browsingContextIds = null) {
let instance = new Network(driver, browsingContextIds)
await instance.init()
return instance
}
module.exports = getNetworkInstance

View File

@@ -0,0 +1,98 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const { BeforeRequestSent, ResponseStarted } = require('./networkTypes')
/**
* @deprecated
* in favor of using the `Network` class from `bidi/network.js`
* Inspector is specific to listening to events.
* Goal is to club commands and events under one class called Network.
*/
class NetworkInspector {
constructor(driver, browsingContextIds) {
this._driver = driver
this._browsingContextIds = browsingContextIds
}
async init() {
this.bidi = await this._driver.getBidi()
}
async beforeRequestSent(callback) {
await this.subscribeAndHandleEvent('network.beforeRequestSent', callback)
}
async responseStarted(callback) {
await this.subscribeAndHandleEvent('network.responseStarted', callback)
}
async responseCompleted(callback) {
await this.subscribeAndHandleEvent('network.responseCompleted', callback)
}
async authRequired(callback) {
await this.subscribeAndHandleEvent('network.authRequired', callback)
}
async subscribeAndHandleEvent(eventType, callback) {
if (this._browsingContextIds != null) {
await this.bidi.subscribe(eventType, this._browsingContextIds)
} else {
await this.bidi.subscribe(eventType)
}
await this._on(callback)
}
async _on(callback) {
this.ws = await this.bidi.socket
this.ws.on('message', (event) => {
const { params } = JSON.parse(Buffer.from(event.toString()))
if (params) {
let response = null
if ('initiator' in params) {
response = new BeforeRequestSent(
params.context,
params.navigation,
params.redirectCount,
params.request,
params.timestamp,
params.initiator,
)
} else if ('response' in params) {
response = new ResponseStarted(
params.context,
params.navigation,
params.redirectCount,
params.request,
params.timestamp,
params.response,
)
}
callback(response)
}
})
}
}
async function getNetworkInspectorInstance(driver, browsingContextIds = null) {
let instance = new NetworkInspector(driver, browsingContextIds)
await instance.init()
return instance
}
module.exports = getNetworkInspectorInstance

View File

@@ -0,0 +1,833 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const { NavigationInfo } = require('./browsingContextTypes')
/**
* Represents the possible values for the SameSite attribute of a cookie.
* @enum {string}
*/
const SameSite = {
STRICT: 'strict',
LAX: 'lax',
NONE: 'none',
findByName(name) {
return (
Object.values(this).find((type) => {
return typeof type === 'string' && name.toLowerCase() === type.toLowerCase()
}) || null
)
},
}
/**
* Represents a BytesValue object.
* Described in https://w3c.github.io/webdriver-bidi/#type-network-BytesValue.
*/
class BytesValue {
static Type = {
STRING: 'string',
BASE64: 'base64',
}
/**
* Creates a new BytesValue instance.
* @param {string} type - The type of the BytesValue.
* @param {string} value - The value of the BytesValue.
*/
constructor(type, value) {
this._type = type
this._value = value
}
/**
* Gets the type of the BytesValue.
* @returns {string} The type of the BytesValue.
*/
get type() {
return this._type
}
/**
* Gets the value of the BytesValue.
* @returns {string} The value of the BytesValue.
*/
get value() {
return this._value
}
/**
* Converts the BytesValue to a map.
* @returns {Map<string, string>} A map representation of the BytesValue.
*/
asMap() {
const map = new Map()
map.set('type', this._type)
map.set('value', this._value)
return map
}
}
/**
* Represents a header with a name and value.
* Described in https://w3c.github.io/webdriver-bidi/#type-network-Header.
*/
class Header {
/**
* Creates a new Header instance.
* @param {string} name - The name of the header.
* @param {BytesValue} value - The value of the header.
* @throws {Error} If the value is not an instance of BytesValue.
*/
constructor(name, value) {
this._name = name
if (!(value instanceof BytesValue)) {
throw new Error(`Value must be an instance of BytesValue. Received: '${value}'`)
}
this._value = value
}
/**
* Gets the name of the header.
* @returns {string} The name of the header.
*/
get name() {
return this._name
}
/**
* Gets the value of the header.
* @returns {BytesValue} The value of the header.
*/
get value() {
return this._value
}
}
/**
* Represents a cookie.
* Described in https://w3c.github.io/webdriver-bidi/#type-network-Cookie.
* @class
*/
class Cookie {
constructor(name, value, domain, path, size, httpOnly, secure, sameSite, expires) {
this._name = name
this._value = value
this._domain = domain
this._path = path
this._expires = expires
this._size = size
this._httpOnly = httpOnly
this._secure = secure
this._sameSite = sameSite
}
/**
* Gets the name of the cookie.
* @returns {string} The name of the cookie.
*/
get name() {
return this._name
}
/**
* Gets the value of the cookie.
* @returns {BytesValue} The value of the cookie.
*/
get value() {
return this._value
}
/**
* Gets the domain of the cookie.
* @returns {string} The domain of the cookie.
*/
get domain() {
return this._domain
}
/**
* Gets the path of the cookie.
* @returns {string} The path of the cookie.
*/
get path() {
return this._path
}
/**
* Gets the expiration date of the cookie.
* @returns {number} The expiration date of the cookie.
*/
get expires() {
return this._expires
}
/**
* Gets the size of the cookie.
* @returns {number} The size of the cookie.
*/
get size() {
return this._size
}
/**
* Checks if the cookie is HTTP-only.
* @returns {boolean} True if the cookie is HTTP-only, false otherwise.
*/
get httpOnly() {
return this._httpOnly
}
/**
* Checks if the cookie is secure.
* @returns {boolean} True if the cookie is secure, false otherwise.
*/
get secure() {
return this._secure
}
/**
* Gets the same-site attribute of the cookie.
* @returns {string} The same-site attribute of the cookie.
*/
get sameSite() {
return this._sameSite
}
}
// No tests written for FetchTimingInfo. Must be updated after browsers implment it and corresponding WPT test are written.
/**
* Represents the time of each part of the request.
* Described in https://w3c.github.io/webdriver-bidi/#type-network-FetchTimingInfo.
* @class
*/
class FetchTimingInfo {
constructor(
originTime,
requestTime,
redirectStart,
redirectEnd,
fetchStart,
dnsStart,
dnsEnd,
connectStart,
connectEnd,
tlsStart,
requestStart,
responseStart,
responseEnd,
) {
this._originTime = originTime
this._requestTime = requestTime
this._redirectStart = redirectStart
this._redirectEnd = redirectEnd
this._fetchStart = fetchStart
this._dnsStart = dnsStart
this._dnsEnd = dnsEnd
this._connectStart = connectStart
this._connectEnd = connectEnd
this._tlsStart = tlsStart
this._requestStart = requestStart
this._responseStart = responseStart
this._responseEnd = responseEnd
}
/**
* Gets the origin time.
*
* @returns {number} The origin time.
*/
get originTime() {
return this._originTime
}
/**
* Get the request time.
*
* @returns {number} The request time.
*/
get requestTime() {
return this._requestTime
}
/**
* Gets the timestamp when the redirect started.
*
* @returns {number} The timestamp when the redirect started.
*/
get redirectStart() {
return this._redirectStart
}
/**
* Gets the timestamp when the redirect ended.
*
* @returns {number} The timestamp when the redirect ended.
*/
get redirectEnd() {
return this._redirectEnd
}
/**
* Gets the timestamp when the fetch started.
*
* @returns {number} The timestamp when the fetch started.
*/
get fetchStart() {
return this._fetchStart
}
/**
* Gets the timestamp when the domain lookup started.
*
* @returns {number} The timestamp when the domain lookup started.
*/
get dnsStart() {
return this._dnsStart
}
/**
* Gets the timestamp when the domain lookup ended.
*
* @returns {number} The timestamp when the domain lookup ended.
*/
get dnsEnd() {
return this._dnsEnd
}
/**
* Gets the timestamp when the connection started.
*
* @returns {number} The timestamp when the connection ended.
*/
get connectStart() {
return this._connectStart
}
/**
* Gets the timestamp when the connection ended.
*
* @returns {number} The timestamp when the connection ended.
*/
get connectEnd() {
return this._connectEnd
}
/**
* Gets the timestamp when the secure connection started.
*
* @returns {number} The timestamp when the secure connection started.
*/
get tlsStart() {
return this._tlsStart
}
/**
* Gets the timestamp when the request started.
*
* @returns {number} The timestamp when the request started.
*/
get requestStart() {
return this._requestStart
}
/**
* Gets the timestamp when the response started.
*
* @returns {number} The timestamp when the response started.
*/
get responseStart() {
return this._responseStart
}
/**
* Gets the timestamp when the response ended.
*
* @returns {number} The timestamp when the response ended.
*/
get responseEnd() {
return this._responseEnd
}
}
/**
* Represents the data of a network request.
* Described in https://w3c.github.io/webdriver-bidi/#type-network-RequestData.
*/
class RequestData {
constructor(request, url, method, headers, cookies, headersSize, bodySize, timings) {
this._request = request
this._url = url
this._method = method
this._headers = []
headers.forEach((header) => {
let name = header.name
let value = 'value' in header ? header.value : null
this._headers.push(new Header(name, new BytesValue(value.type, value.value)))
})
this._cookies = []
cookies.forEach((cookie) => {
let name = cookie.name
let domain = cookie.domain
let path = cookie.path
let size = cookie.size
let httpOnly = cookie.httpOnly
let secure = cookie.secure
let sameSite = cookie.sameSite
let value = 'value' in cookie ? cookie.value : null
let expires = 'expires' in cookie ? cookie.expires : null
this._cookies.push(new Cookie(name, value, domain, path, size, httpOnly, secure, sameSite, expires))
})
this._headersSize = headersSize
this._bodySize = bodySize
this._timings = new FetchTimingInfo(
timings.originTime,
timings.requestTime,
timings.redirectStart,
timings.redirectEnd,
timings.fetchStart,
timings.dnsStart,
timings.dnsEnd,
timings.connectStart,
timings.connectEnd,
timings.tlsStart,
timings.requestStart,
timings.responseStart,
timings.responseEnd,
)
}
/**
* Get the request id.
* @returns {string} The request id.
*/
get request() {
return this._request
}
/**
* Get the URL of the request.
* @returns {string} The URL of the request.
*/
get url() {
return this._url
}
/**
* Get the HTTP method of the request.
* @returns {string} The HTTP method of the request.
*/
get method() {
return this._method
}
/**
* Get the headers of the request.
* @returns {Header[]} An array of header objects.
*/
get headers() {
return this._headers
}
/**
* Get the cookies of the request.
* @returns {Cookie[]} An array of cookie objects.
*/
get cookies() {
return this._cookies
}
/**
* Get the size of the headers in bytes.
* @returns {number} The size of the headers in bytes.
*/
get headersSize() {
return this._headersSize
}
/**
* Get the size of the request body in bytes.
* @returns {number} The size of the request body in bytes.
*/
get bodySize() {
return this._bodySize
}
/**
* Get the timing information of the request.
* @returns {FetchTimingInfo} The timing information of the request.
*/
get timings() {
return this._timings
}
}
/**
* Represents the base parameters for a network request.
* Described in https://w3c.github.io/webdriver-bidi/#type-network-BaseParameters.
*/
class BaseParameters {
constructor(id, navigation, redirectCount, request, timestamp) {
this._id = id
this._navigation =
navigation != null
? new NavigationInfo(navigation.context, navigation.navigation, navigation.timestamp, navigation.url)
: null
this._redirectCount = redirectCount
this._request = new RequestData(
request.request,
request.url,
request.method,
request.headers,
request.cookies,
request.headersSize,
request.bodySize,
request.timings,
)
this._timestamp = timestamp
}
/**
* Gets the browsing context ID of the network request.
* @returns {string|null} The browsing context ID of the network request.
*/
get id() {
return this._id
}
/**
* Gets the navigation information associated with the network request.
* @returns {NavigationInfo|null} The navigation information associated with the network request, or null if not available.
*/
get navigation() {
return this._navigation
}
/**
* Gets the number of redirects that occurred during the network request.
* @returns {number} The number of redirects that occurred during the network request.
*/
get redirectCount() {
return this._redirectCount
}
/**
* Gets the request data for the network request.
* @returns {RequestData} The request data for the network request.
*/
get request() {
return this._request
}
/**
* Gets the timestamp of the network request.
* @returns {number} The timestamp of the network request.
*/
get timestamp() {
return this._timestamp
}
}
/**
* Represents source in the network.
* Described in https://w3c.github.io/webdriver-bidi/#type-network-Initiator.
*/
class Initiator {
/**
* Constructs a new Initiator instance.
* @param {string} type - The type of the initiator.
* @param {number} columnNumber - The column number.
* @param {number} lineNumber - The line number.
* @param {string} stackTrace - The stack trace.
* @param {string} request - The request id.
*/
constructor(type, columnNumber, lineNumber, stackTrace, request) {
this._type = type
this._columnNumber = columnNumber
this._lineNumber = lineNumber
this._stackTrace = stackTrace
this._request = request
}
/**
* Gets the type of the initiator.
* @returns {string} The type of the initiator.
*/
get type() {
return this._type
}
/**
* Gets the column number.
* @returns {number} The column number.
*/
get columnNumber() {
return this._columnNumber
}
/**
* Gets the line number.
* @returns {number} The line number.
*/
get lineNumber() {
return this._lineNumber
}
/**
* Gets the stack trace.
* @returns {string} The stack trace.
*/
get stackTrace() {
return this._stackTrace
}
/**
* Gets the request ID.
* @returns {string} The request ID.
*/
get request() {
return this._request
}
}
/**
* Represents the BeforeRequestSent event parameters.
* @class
* @extends BaseParameters
* Described in https://w3c.github.io/webdriver-bidi/#event-network-beforeSendRequest.
*/
class BeforeRequestSent extends BaseParameters {
constructor(id, navigation, redirectCount, request, timestamp, initiator) {
super(id, navigation, redirectCount, request, timestamp)
this._initiator = new Initiator(
initiator.type,
initiator.columnNumber,
initiator.lineNumber,
initiator.stackTrace,
initiator.request,
)
}
/**
* Get the initiator of the request.
* @returns {Initiator} The initiator object.
*/
get initiator() {
return this._initiator
}
}
/**
* Represents the FetchError event parameters.
* Described https://w3c.github.io/webdriver-bidi/#event-network-fetchError
* @extends BaseParameters
*/
class FetchError extends BaseParameters {
/**
* Creates a new FetchError instance.
* @param {string} id - The ID of the error.
* @param {string} navigation - The navigation information.
* @param {number} redirectCount - The number of redirects.
* @param {RequestData} request - The request object.
* @param {number} timestamp - The timestamp of the error.
* @param {string} errorText - The error text.
*/
constructor(id, navigation, redirectCount, request, timestamp, errorText) {
super(id, navigation, redirectCount, request, timestamp)
this._errorText = errorText
}
/**
* Gets the error text.
* @returns {string} The error text.
*/
get errorText() {
return this._errorText
}
}
/**
* Represents the response data received from a network request.
* Described in https://w3c.github.io/webdriver-bidi/#type-network-ResponseData.
* @class
*/
class ResponseData {
constructor(
url,
protocol,
status,
statusText,
fromCache,
headers,
mimeType,
bytesReceived,
headersSize,
bodySize,
content,
) {
this._url = url
this._protocol = protocol
this._status = status
this._statusText = statusText
this._fromCache = fromCache
this._headers = headers
this._mimeType = mimeType
this._bytesReceived = bytesReceived
this._headersSize = headersSize
this._bodySize = bodySize
this._content = content
}
/**
* Get the URL.
*
* @returns {string} The URL.
*/
get url() {
return this._url
}
/**
* Get the protocol.
*
* @returns {string} The protocol.
*/
get protocol() {
return this._protocol
}
/**
* Get the HTTP status.
*
* @returns {string} The HTTP status.
*/
get status() {
return this._status
}
/**
* Gets the status text.
*
* @returns {string} The status text.
*/
get statusText() {
return this._statusText
}
/**
* Gets the value indicating whether the data is retrieved from cache.
*
* @returns {boolean} The value indicating whether the data is retrieved from cache.
*/
get fromCache() {
return this._fromCache
}
/**
* Get the headers.
*
* @returns {Object} The headers object.
*/
get headers() {
return this._headers
}
/**
* The MIME type of the network resource.
*
* @type {string}
*/
get mimeType() {
return this._mimeType
}
/**
* Gets the number of bytes received.
*
* @returns {number} The number of bytes received.
*/
get bytesReceived() {
return this._bytesReceived
}
/**
* Get the size of the headers.
*
* @returns {number} The size of the headers.
*/
get headerSize() {
return this._headersSize
}
/**
* Get the size of the body.
*
* @returns {number} The size of the body.
*/
get bodySize() {
return this._bodySize
}
/**
* Gets the content.
*
* @returns {any} The content.
*/
get content() {
return this._content
}
}
/**
* Represents the ResponseStarted event parameters.
* Described in https://w3c.github.io/webdriver-bidi/#event-network-responseStarted.
* @class
* @extends BaseParameters
*/
class ResponseStarted extends BaseParameters {
constructor(id, navigation, redirectCount, request, timestamp, response) {
super(id, navigation, redirectCount, request, timestamp)
this._response = new ResponseData(
response.url,
response.protocol,
response.status,
response.statusText,
response.fromCache,
response.headers,
response.mimeType,
response.bytesReceived,
response.headerSize,
response.bodySize,
response.content,
)
}
/**
* Get the response data.
* @returns {ResponseData} The response data.
*/
get response() {
return this._response
}
}
module.exports = { Header, BytesValue, Cookie, SameSite, BeforeRequestSent, ResponseStarted, FetchError }

View File

@@ -0,0 +1,115 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const { BytesValue } = require('./networkTypes')
/**
* Represents a partial cookie used to set cookies.
* Described in https://w3c.github.io/webdriver-bidi/#command-storage-setCookie.
* @class
*/
class PartialCookie {
#map = new Map()
/**
* Represents a partial cookie.
* @class
* @param {string} name - The name of the cookie.
* @param {BytesValue} value - The value of the cookie as an instance of BytesValue.
* @param {string} domain - The domain of the cookie.
*/
constructor(name, value, domain) {
this.#map.set('name', name)
if (!(value instanceof BytesValue)) {
throw new Error(`Value must be an instance of BytesValue. Received:'${value}'`)
}
this.#map.set('value', Object.fromEntries(value.asMap()))
this.#map.set('domain', domain)
}
/**
* Sets the path for the cookie.
*
* @param {string} path - The path for the cookie.
* @returns {PartialCookie} - The updated PartialCookie instance for chaining.
*/
path(path) {
this.#map.set('path', path)
return this
}
/**
* Sets the size of the cookie.
*
* @param {number} size - The size of the cookie.
* @returns {PartialCookie} - The updated PartialCookie instance for chaining.
*/
size(size) {
this.#map.set('size', size)
return this
}
/**
* Sets the `httpOnly` flag for the cookie.
*
* @param {boolean} httpOnly - The value to set for the `httpOnly` flag.
* @returns {PartialCookie} - The updated PartialCookie instance for chaining.
*/
httpOnly(httpOnly) {
this.#map.set('httpOnly', httpOnly)
return this
}
/**
* Sets the secure flag for the cookie.
*
* @param {boolean} secure - Indicates whether the cookie should only be sent over secure connections.
* @returns {PartialCookie} - The updated PartialCookie instance for chaining.
*/
secure(secure) {
this.#map.set('secure', secure)
return this
}
/**
* Sets the SameSite attribute for the cookie.
*
* @param {SameSite} sameSite - The SameSite attribute value for the cookie.
* @returns {PartialCookie} - The updated PartialCookie instance for chaining.
*/
sameSite(sameSite) {
this.#map.set('sameSite', sameSite)
return this
}
/**
* Sets the expiry for the cookie.
*
* @param {number} expiry - The expiry time of the cookie.
* @returns {PartialCookie} - The updated PartialCookie instance for chaining.
*/
expiry(expiry) {
this.#map.set('expiry', expiry)
return this
}
asMap() {
return this.#map
}
}
module.exports = { PartialCookie }

View File

@@ -0,0 +1,102 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* Represents the types of partition descriptors.
* @enum {string}
* Described in https://w3c.github.io/webdriver-bidi/#command-storage-getCookies.
*/
const Type = {
CONTEXT: 'context',
STORAGE_KEY: 'storageKey',
}
/**
* Represents a partition descriptor.
* Described in https://w3c.github.io/webdriver-bidi/#command-storage-getCookies.
*/
class PartitionDescriptor {
/*eslint no-unused-private-class-members: "off"*/
#type
/**
* Constructs a new PartitionDescriptor instance.
* @param {Type} type - The type of the partition.
*/
constructor(type) {
this.#type = type
}
}
/**
* Represents a partition descriptor for a browsing context.
* @extends PartitionDescriptor
*/
class BrowsingContextPartitionDescriptor extends PartitionDescriptor {
#context = null
constructor(context) {
super(Type.CONTEXT)
this.#context = context
}
asMap() {
const map = new Map()
map.set('type', Type.CONTEXT)
map.set('context', this.#context)
return map
}
}
/**
* Represents a partition descriptor for storage key.
* @extends PartitionDescriptor
*/
class StorageKeyPartitionDescriptor extends PartitionDescriptor {
#map = new Map()
constructor() {
super(Type.STORAGE_KEY)
}
/**
* Sets the user context for the partition descriptor.
* @param {any} userContext - The user context to set.
* @returns {PartitionDescriptor} - The updated partition descriptor instance for chaining.
*/
userContext(userContext) {
this.#map.set('userContext', userContext)
return this
}
/**
* Sets the source origin for the partition descriptor.
*
* @param {string} sourceOrigin - The source origin to set.
* @returns {PartitionDescriptor} - The updated PartitionDescriptor instance for chaining.
*/
sourceOrigin(sourceOrigin) {
this.#map.set('sourceOrigin', sourceOrigin)
return this
}
asMap() {
return this.#map
}
}
module.exports = { BrowsingContextPartitionDescriptor, StorageKeyPartitionDescriptor }

View File

@@ -0,0 +1,53 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* Represents a partition key of cookie storage.
* Described in https://w3c.github.io/webdriver-bidi/#type-storage-PartitionKey.
*/
class PartitionKey {
#userContext
#sourceOrigin
/**
* Constructs a new PartitionKey object.
* @param {string} userContext - The user context.
* @param {string} sourceOrigin - The source origin.
*/
constructor(userContext, sourceOrigin) {
this.#userContext = userContext
this.#sourceOrigin = sourceOrigin
}
/**
* Gets the user context.
* @returns {string} The user context.
*/
get userContext() {
return this.#userContext
}
/**
* Gets the source origin.
* @returns {string} The source origin.
*/
get sourceOrigin() {
return this.#sourceOrigin
}
}
module.exports = { PartitionKey }

View File

@@ -0,0 +1,112 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* Represents a primitive type.
* @enum
* Described in https://w3c.github.io/webdriver-bidi/#type-script-PrimitiveProtocolValue.
*/
const PrimitiveType = {
UNDEFINED: 'undefined',
NULL: 'null',
STRING: 'string',
NUMBER: 'number',
SPECIAL_NUMBER: 'number',
BOOLEAN: 'boolean',
BIGINT: 'bigint',
findByName(name) {
return (
Object.values(this).find((type) => {
return typeof type === 'string' && name.toLowerCase() === type.toLowerCase()
}) || null
)
},
}
/**
* Represents a non-primitive type.
* @enum
* Described inhttps://w3c.github.io/webdriver-bidi/#type-script-RemoteValue.
*/
const NonPrimitiveType = {
ARRAY: 'array',
DATE: 'date',
MAP: 'map',
OBJECT: 'object',
REGULAR_EXPRESSION: 'regexp',
SET: 'set',
CHANNEL: 'channel',
findByName(name) {
return (
Object.values(this).find((type) => {
return typeof type === 'string' && name.toLowerCase() === type.toLowerCase()
}) || null
)
},
}
/**
* Represents a remote value type.
* @enum
* Described inhttps://w3c.github.io/webdriver-bidi/#type-script-RemoteValue.
*/
const RemoteType = {
SYMBOL: 'symbol',
FUNCTION: 'function',
WEAK_MAP: 'weakmap',
WEAK_SET: 'weakset',
ITERATOR: 'iterator',
GENERATOR: 'generator',
ERROR: 'error',
PROXY: 'proxy',
PROMISE: 'promise',
TYPED_ARRAY: 'typedarray',
ARRAY_BUFFER: 'arraybuffer',
NODE_LIST: 'nodelist',
HTML_COLLECTION: 'htmlcollection',
NODE: 'node',
WINDOW: 'window',
findByName(name) {
return (
Object.values(this).find((type) => {
return typeof type === 'string' && name.toLowerCase() === type.toLowerCase()
}) || null
)
},
}
/**
* Represents a speacial number type.
* @enum
* Described in https://w3c.github.io/webdriver-bidi/#type-script-PrimitiveProtocolValue.
*/
const SpecialNumberType = {
NAN: 'NaN',
MINUS_ZERO: '-0',
INFINITY: 'Infinity',
MINUS_INFINITY: '-Infinity',
}
module.exports = {
PrimitiveType,
NonPrimitiveType,
RemoteType,
SpecialNumberType,
}

View File

@@ -0,0 +1,366 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const { PrimitiveType, NonPrimitiveType, RemoteType } = require('./protocolType')
const TYPE_CONSTANT = 'type'
const VALUE_CONSTANT = 'value'
/**
* Represents the types of remote reference.
* @enum {string}
*/
const RemoteReferenceType = {
HANDLE: 'handle',
SHARED_ID: 'sharedId',
}
/**
* Represents a local value with a specified type and optional value.
* @class
* Described in https://w3c.github.io/webdriver-bidi/#type-script-LocalValue
*/
class LocalValue {
constructor(type, value = null) {
if (type === PrimitiveType.UNDEFINED || type === PrimitiveType.NULL) {
this.type = type
} else {
this.type = type
this.value = value
}
}
/**
* Creates a new LocalValue object with a string value.
*
* @param {string} value - The string value to be stored in the LocalValue object.
* @returns {LocalValue} - The created LocalValue object.
*/
static createStringValue(value) {
return new LocalValue(PrimitiveType.STRING, value)
}
/**
* Creates a new LocalValue object with a number value.
*
* @param {number} value - The number value.
* @returns {LocalValue} - The created LocalValue object.
*/
static createNumberValue(value) {
return new LocalValue(PrimitiveType.NUMBER, value)
}
/**
* Creates a new LocalValue object with a special number value.
*
* @param {number} value - The value of the special number.
* @returns {LocalValue} - The created LocalValue object.
*/
static createSpecialNumberValue(value) {
return new LocalValue(PrimitiveType.SPECIAL_NUMBER, value)
}
/**
* Creates a new LocalValue object with an undefined value.
* @returns {LocalValue} - The created LocalValue object.
*/
static createUndefinedValue() {
return new LocalValue(PrimitiveType.UNDEFINED)
}
/**
* Creates a new LocalValue object with a null value.
* @returns {LocalValue} - The created LocalValue object.
*/
static createNullValue() {
return new LocalValue(PrimitiveType.NULL)
}
/**
* Creates a new LocalValue object with a boolean value.
*
* @param {boolean} value - The boolean value.
* @returns {LocalValue} - The created LocalValue object.
*/
static createBooleanValue(value) {
return new LocalValue(PrimitiveType.BOOLEAN, value)
}
/**
* Creates a new LocalValue object with a BigInt value.
*
* @param {BigInt} value - The BigInt value.
* @returns {LocalValue} - The created LocalValue object.
*/
static createBigIntValue(value) {
return new LocalValue(PrimitiveType.BIGINT, value)
}
/**
* Creates a new LocalValue object with an array.
*
* @param {Array} value - The array.
* @returns {LocalValue} - The created LocalValue object.
*/
static createArrayValue(value) {
return new LocalValue(NonPrimitiveType.ARRAY, value)
}
/**
* Creates a new LocalValue object with date value.
*
* @param {string} value - The date.
* @returns {LocalValue} - The created LocalValue object.
*/
static createDateValue(value) {
return new LocalValue(NonPrimitiveType.DATE, value)
}
/**
* Creates a new LocalValue object of map value.
* @param {Map} map - The map.
* @returns {LocalValue} - The created LocalValue object.
*/
static createMapValue(map) {
let value = []
Object.entries(map).forEach((entry) => {
value.push(entry)
})
return new LocalValue(NonPrimitiveType.MAP, value)
}
/**
* Creates a new LocalValue object from the passed object.
*
* @param {Object} map - The object.
* @returns {LocalValue} - The created LocalValue object.
*/
static createObjectValue(object) {
let value = []
Object.entries(object).forEach((entry) => {
value.push(entry)
})
return new LocalValue(NonPrimitiveType.OBJECT, value)
}
/**
* Creates a new LocalValue object of regular expression value.
*
* @param {string} value - The value of the regular expression.
* @returns {LocalValue} - The created LocalValue object.
*/
static createRegularExpressionValue(value) {
return new LocalValue(NonPrimitiveType.REGULAR_EXPRESSION, value)
}
/**
* Creates a new LocalValue object with the specified value.
* @param {Set} value - The value to be set.
* @returns {LocalValue} - The created LocalValue object.
*/
static createSetValue(value) {
return new LocalValue(NonPrimitiveType.SET, value)
}
/**
* Creates a new LocalValue object with the given channel value
*
* @param {ChannelValue} value - The channel value.
* @returns {LocalValue} - The created LocalValue object.
*/
static createChannelValue(value) {
return new LocalValue(NonPrimitiveType.CHANNEL, value)
}
toJson() {
let toReturn = {}
toReturn[TYPE_CONSTANT] = this.type
if (!(this.type === PrimitiveType.NULL || this.type === PrimitiveType.UNDEFINED)) {
toReturn[VALUE_CONSTANT] = this.value
}
return toReturn
}
}
/**
* Represents a remote value.
* Described in https://w3c.github.io/webdriver-bidi/#type-script-RemoteValue.
* @class
*/
class RemoteValue {
constructor(remoteValue) {
this.type = null
this.handle = null
this.internalId = null
this.value = null
this.sharedId = null
if ('type' in remoteValue) {
const typeString = remoteValue['type']
if (PrimitiveType.findByName(typeString) != null) {
this.type = PrimitiveType.findByName(typeString)
} else if (NonPrimitiveType.findByName(typeString) != null) {
this.type = NonPrimitiveType.findByName(typeString)
} else {
this.type = RemoteType.findByName(typeString)
}
}
if ('handle' in remoteValue) {
this.handle = remoteValue['handle']
}
if ('internalId' in remoteValue) {
this.internalId = remoteValue['internalId']
}
if ('value' in remoteValue) {
this.value = remoteValue['value']
}
if ('sharedId' in remoteValue) {
this.sharedId = remoteValue['sharedId']
}
if (this.value != null) {
this.value = this.deserializeValue(this.value, this.type)
}
}
deserializeValue(value, type) {
if ([NonPrimitiveType.MAP, NonPrimitiveType.OBJECT].includes(type)) {
return Object.fromEntries(value)
} else if (type === NonPrimitiveType.REGULAR_EXPRESSION) {
return new RegExpValue(value.pattern, value.flags)
}
return value
}
}
/**
* Represents a reference value in the protocol.
* Described in https://w3c.github.io/webdriver-bidi/#type-script-RemoteReference.
*/
class ReferenceValue {
#handle
#sharedId
/**
* Constructs a new ReferenceValue object.
* @param {string} handle - The handle value.
* @param {string} sharedId - The shared ID value.
*/
constructor(handle, sharedId) {
if (handle === RemoteReferenceType.HANDLE) {
this.#handle = sharedId
} else if (handle === RemoteReferenceType.SHARED_ID) {
this.#sharedId = sharedId
} else {
this.#handle = handle
this.#sharedId = sharedId
}
}
asMap() {
const toReturn = {}
if (this.#handle != null) {
toReturn[RemoteReferenceType.HANDLE] = this.#handle
}
if (this.#sharedId != null) {
toReturn[RemoteReferenceType.SHARED_ID] = this.#sharedId
}
return toReturn
}
}
/**
* Represents a regular expression value.
* Described in https://w3c.github.io/webdriver-bidi/#type-script-LocalValue.
*/
class RegExpValue {
/**
* Constructs a new RegExpValue object.
* @param {string} pattern - The pattern of the regular expression.
* @param {string|null} [flags=null] - The flags of the regular expression.
*/
constructor(pattern, flags = null) {
this.pattern = pattern
this.flags = flags
}
}
/**
* Represents serialization options.
* Described in https://w3c.github.io/webdriver-bidi/#type-script-SerializationOptions.
*/
class SerializationOptions {
/**
* Constructs a new instance of SerializationOptions.
* @param {number} [maxDomDepth=0] - The maximum depth to serialize the DOM.
* @param {number|null} [maxObjectDepth=null] - The maximum depth to serialize objects.
* @param {'none'|'open'|'all'} [includeShadowTree='none'] - The inclusion level of the shadow tree.
* @throws {Error} If the `includeShadowTree` value is not one of 'none', 'open', or 'all'.
*/
constructor(maxDomDepth = 0, maxObjectDepth = null, includeShadowTree = 'none') {
this._maxDomDepth = maxDomDepth
this._maxObjectDepth = maxObjectDepth
if (['none', 'open', 'all'].includes(includeShadowTree)) {
throw Error(`Valid types are 'none', 'open', and 'all'. Received: ${includeShadowTree}`)
}
this._includeShadowTree = includeShadowTree
}
}
/**
* Represents a channel value.
* Described in https://w3c.github.io/webdriver-bidi/#type-script-ChannelValue.
* @class
*/
class ChannelValue {
constructor(channel, options = undefined, resultOwnership = undefined) {
this.channel = channel
if (options !== undefined) {
if (options instanceof SerializationOptions) {
this.options = options
} else {
throw Error(`Pass in SerializationOptions object. Received: ${options} `)
}
}
if (resultOwnership != undefined) {
if (['root', 'none'].includes(resultOwnership)) {
this.resultOwnership = resultOwnership
} else {
throw Error(`Valid types are 'root' and 'none. Received: ${resultOwnership}`)
}
}
}
}
module.exports = {
ChannelValue,
LocalValue,
RemoteValue,
ReferenceValue,
RemoteReferenceType,
RegExpValue,
SerializationOptions,
}

View File

@@ -0,0 +1,123 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const { BytesValue, Header } = require('./networkTypes')
/**
* Represents parameters for providingResponse command.
* Described in https://w3c.github.io/webdriver-bidi/#command-network-provideResponse.
* @class
*/
class ProvideResponseParameters {
#map = new Map()
constructor(request) {
this.#map.set('request', request)
}
/**
* Sets the body value for the response parameters.
*
* @param {BytesValue} value - The value to set as the body. Must be an instance of BytesValue.
* @returns {ProvideResponseParameters} - Returns the ProvideResponseParameters object for chaining.
* @throws {Error} - Throws an error if the value is not an instance of BytesValue.
*/
body(value) {
if (!(value instanceof BytesValue)) {
throw new Error(`Value must be an instance of BytesValue. Received: ${typeof value} with value: ${value}`)
}
this.#map.set('body', Object.fromEntries(value.asMap()))
return this
}
/**
* Sets the cookie headers for the response.
*
* @param {Header[]} cookieHeaders - An array of cookie headers.
* @returns {ProvideResponseParameters} - Returns the ProvideResponseParameters object for chaining.
* @throws {Error} - Throws an error if a cookie header is not an instance of Header.
*/
cookies(cookieHeaders) {
const cookies = []
cookieHeaders.forEach((header) => {
if (!(header instanceof Header)) {
throw new Error(`CookieHeader must be an instance of Header. Received:'${header}'`)
}
cookies.push(Object.fromEntries(header.asMap()))
})
this.#map.set('cookies', cookies)
return this
}
/**
* Sets the headers for the response.
*
* @param {Header[]} headers - The headers to be set.
* @returns {ProvideResponseParameters} - Returns the ProvideResponseParameters object for chaining.
* @throws {Error} - If the provided header is not an instance of Header.
*/
headers(headers) {
const headerList = []
headers.forEach((header) => {
if (!(header instanceof Header)) {
throw new Error(`Header must be an instance of Header. Received:'${header}'`)
}
headerList.push(Object.fromEntries(header.asMap()))
})
this.#map.set('headers', headerList)
return this
}
/**
* Sets the reason phrase for the response.
*
* @param {string} reasonPhrase - The reason phrase to set.
* @returns {ProvideResponseParameters} - Returns the ProvideResponseParameters object for chaining.
* @throws {Error} - If the reason phrase is not a string.
*/
reasonPhrase(reasonPhrase) {
if (typeof reasonPhrase !== 'string') {
throw new Error(`Reason phrase must be a string. Received: '${reasonPhrase})'`)
}
this.#map.set('reasonPhrase', reasonPhrase)
return this
}
/**
* Sets the status code for the response.
*
* @param {number} statusCode - The status code to set.
* @returns {ProvideResponseParameters} - Returns the ProvideResponseParameters object for chaining.
* @throws {Error} - If the status code is not an integer.
*/
statusCode(statusCode) {
if (!Number.isInteger(statusCode)) {
throw new Error(`Status must be an integer. Received:'${statusCode}'`)
}
this.#map.set('statusCode', statusCode)
return this
}
asMap() {
return this.#map
}
}
module.exports = { ProvideResponseParameters }

View File

@@ -0,0 +1,119 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* Represents the types of realms.
* Described in https://w3c.github.io/webdriver-bidi/#type-script-RealmType.
* @enum
*/
const RealmType = {
AUDIO_WORKLET: 'audio-worklet',
DEDICATED_WORKER: 'dedicated-worker',
PAINT_WORKLET: 'paint-worklet',
SERVICE_WORKED: 'service-worker',
SHARED_WORKED: 'shared-worker',
WINDOW: 'window',
WORKER: 'worker',
WORKLET: 'worklet',
findByName(name) {
return (
Object.values(this).find((type) => {
return typeof type === 'string' && name.toLowerCase() === type.toLowerCase()
}) || null
)
},
}
/**
* Represents information about a realm.
* Described in https://w3c.github.io/webdriver-bidi/#type-script-RealmInfo.
*/
class RealmInfo {
/**
* Constructs a new RealmInfo object.
* @param {string} realmId - The ID of the realm.
* @param {string} origin - The origin of the realm.
* @param {string} realmType - The type of the realm.
*/
constructor(realmId, origin, realmType) {
this.realmId = realmId
this.origin = origin
this.realmType = realmType
}
static fromJson(input) {
let realmId = null
let origin = null
let realmType = null
let browsingContext = null
let sandbox = null
if ('type' in input) {
let typeString = input['type']
realmType = RealmType.findByName(typeString)
}
if ('realm' in input) {
realmId = input['realm']
}
if ('origin' in input) {
origin = input['origin']
}
if ('context' in input) {
browsingContext = input['context']
}
if ('sandbox' in input) {
sandbox = input['sandbox']
}
if (realmType === RealmType.WINDOW) {
return new WindowRealmInfo(realmId, origin, realmType, browsingContext, sandbox)
}
return new RealmInfo(realmId, origin, realmType)
}
}
/**
* Represents information about a window realm.
* @extends RealmInfo
*/
class WindowRealmInfo extends RealmInfo {
/**
* Constructs a new instance of the WindowRealmInfo class.
* @param {string} realmId - The ID of the realm.
* @param {string} origin - The origin of the realm.
* @param {string} realmType - The type of the realm.
* @param {string} browsingContext - The browsing context of the realm.
* @param {string|null} sandbox - The sandbox of the realm (optional).
*/
constructor(realmId, origin, realmType, browsingContext, sandbox = null) {
super(realmId, origin, realmType)
this.browsingContext = browsingContext
this.sandbox = sandbox
}
}
module.exports = {
RealmInfo,
RealmType,
WindowRealmInfo,
}

View File

@@ -0,0 +1,27 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* Enum representing the ownership types.
* @enum {string}
*/
const ResultOwnership = {
ROOT: 'root',
NONE: 'none',
}
module.exports = { ResultOwnership }

View File

@@ -0,0 +1,497 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const {
EvaluateResultType,
EvaluateResultSuccess,
EvaluateResultException,
ExceptionDetails,
} = require('./evaluateResult')
const { Message } = require('./scriptTypes')
const { RealmInfo, RealmType, WindowRealmInfo } = require('./realmInfo')
const { RemoteValue } = require('./protocolValue')
const { Source } = require('./scriptTypes')
const { WebDriverError } = require('../lib/error')
/**
* Represents class to run events and commands of Script module.
* Described in https://w3c.github.io/webdriver-bidi/#module-script.
* @class
*/
class ScriptManager {
constructor(driver) {
this._driver = driver
}
async init(browsingContextIds) {
if (!(await this._driver.getCapabilities()).get('webSocketUrl')) {
throw Error('WebDriver instance must support BiDi protocol')
}
this.bidi = await this._driver.getBidi()
this._browsingContextIds = browsingContextIds
}
/**
* Disowns the handles in the specified realm.
*
* @param {string} realmId - The ID of the realm.
* @param {string[]} handles - The handles to disown to allow garbage collection.
* @returns {Promise<void>} - A promise that resolves when the command is sent.
*/
async disownRealmScript(realmId, handles) {
const params = {
method: 'script.disown',
params: {
handles: handles,
target: {
realm: realmId,
},
},
}
await this.bidi.send(params)
}
/**
* Disowns the handles in the specified browsing context.
* @param {string} browsingContextId - The ID of the browsing context.
* @param {string[]} handles - The handles to disown to allow garbage collection.
* @param {String|null} [sandbox=null] - The sandbox name.
* @returns {Promise<void>} - A promise that resolves when the command is sent.
*/
async disownBrowsingContextScript(browsingContextId, handles, sandbox = null) {
const params = {
method: 'script.disown',
params: {
handles: handles,
target: {
context: browsingContextId,
},
},
}
if (sandbox != null) {
params.params.target['sandbox'] = sandbox
}
await this.bidi.send(params)
}
/**
* Calls a function in the specified realm.
*
* @param {string} realmId - The ID of the realm.
* @param {string} functionDeclaration - The function to call.
* @param {boolean} awaitPromise - Whether to await the promise returned by the function.
* @param {LocalValue[]} [argumentValueList|null] - The list of argument values to pass to the function.
* @param {Object} [thisParameter|null] - The value of 'this' parameter for the function.
* @param {ResultOwnership} [resultOwnership|null] - The ownership of the result.
* @returns {Promise<EvaluateResultSuccess|EvaluateResultException>} - A promise that resolves to the evaluation result or exception.
*/
async callFunctionInRealm(
realmId,
functionDeclaration,
awaitPromise,
argumentValueList = null,
thisParameter = null,
resultOwnership = null,
) {
const params = this.getCallFunctionParams(
'realm',
realmId,
null,
functionDeclaration,
awaitPromise,
argumentValueList,
thisParameter,
resultOwnership,
)
const command = {
method: 'script.callFunction',
params,
}
let response = await this.bidi.send(command)
return this.createEvaluateResult(response)
}
/**
* Calls a function in the specified browsing context.
*
* @param {string} realmId - The ID of the browsing context.
* @param {string} functionDeclaration - The function to call.
* @param {boolean} awaitPromise - Whether to await the promise returned by the function.
* @param {LocalValue[]} [argumentValueList|null] - The list of argument values to pass to the function.
* @param {Object} [thisParameter|null] - The value of 'this' parameter for the function.
* @param {ResultOwnership} [resultOwnership|null] - The ownership of the result.
* @returns {Promise<EvaluateResultSuccess|EvaluateResultException>} - A promise that resolves to the evaluation result or exception.
*/
async callFunctionInBrowsingContext(
browsingContextId,
functionDeclaration,
awaitPromise,
argumentValueList = null,
thisParameter = null,
resultOwnership = null,
sandbox = null,
) {
const params = this.getCallFunctionParams(
'contextTarget',
browsingContextId,
sandbox,
functionDeclaration,
awaitPromise,
argumentValueList,
thisParameter,
resultOwnership,
)
const command = {
method: 'script.callFunction',
params,
}
const response = await this.bidi.send(command)
return this.createEvaluateResult(response)
}
/**
* Evaluates a function in the specified realm.
*
* @param {string} realmId - The ID of the realm.
* @param {string} expression - The expression to function to evaluate.
* @param {boolean} awaitPromise - Whether to await the promise.
* @param {ResultOwnership|null} resultOwnership - The ownership of the result.
* @returns {Promise<EvaluateResultSuccess|EvaluateResultException>} - A promise that resolves to the evaluation result or exception.
*/
async evaluateFunctionInRealm(realmId, expression, awaitPromise, resultOwnership = null) {
const params = this.getEvaluateParams('realm', realmId, null, expression, awaitPromise, resultOwnership)
const command = {
method: 'script.evaluate',
params,
}
let response = await this.bidi.send(command)
return this.createEvaluateResult(response)
}
/**
* Evaluates a function in the browsing context.
*
* @param {string} realmId - The ID of the browsing context.
* @param {string} expression - The expression to function to evaluate.
* @param {boolean} awaitPromise - Whether to await the promise.
* @param {ResultOwnership|null} resultOwnership - The ownership of the result.
* @returns {Promise<EvaluateResultSuccess|EvaluateResultException>} - A promise that resolves to the evaluation result or exception.
*/
async evaluateFunctionInBrowsingContext(
browsingContextId,
expression,
awaitPromise,
resultOwnership = null,
sandbox = null,
) {
const params = this.getEvaluateParams(
'contextTarget',
browsingContextId,
sandbox,
expression,
awaitPromise,
resultOwnership,
)
const command = {
method: 'script.evaluate',
params,
}
let response = await this.bidi.send(command)
return this.createEvaluateResult(response)
}
/**
* Adds a preload script.
*
* @param {string} functionDeclaration - The declaration of the function to be added as a preload script.
* @param {LocalValue[]} [argumentValueList=[]] - The list of argument values to be passed to the preload script function.
* @param {string} [sandbox|null] - The sandbox object to be used for the preload script.
* @returns {Promise<number>} - A promise that resolves to the added preload script ID.
*/
async addPreloadScript(functionDeclaration, argumentValueList = [], sandbox = null) {
const params = {
functionDeclaration: functionDeclaration,
arguments: argumentValueList,
}
if (sandbox !== null) {
params.sandbox = sandbox
}
if (Array.isArray(this._browsingContextIds) && this._browsingContextIds.length > 0) {
params.contexts = this._browsingContextIds
}
if (typeof this._browsingContextIds === 'string') {
params.contexts = new Array(this._browsingContextIds)
}
const command = {
method: 'script.addPreloadScript',
params,
}
let response = await this.bidi.send(command)
return response.result.script
}
/**
* Removes a preload script.
*
* @param {string} script - The ID for the script to be removed.
* @returns {Promise<any>} - A promise that resolves with the result of the removal.
* @throws {WebDriverError} - If an error occurs during the removal process.
*/
async removePreloadScript(script) {
const params = { script: script }
const command = {
method: 'script.removePreloadScript',
params,
}
let response = await this.bidi.send(command)
if ('error' in response) {
throw new WebDriverError(response.error)
}
return response.result
}
getCallFunctionParams(
targetType,
id,
sandbox,
functionDeclaration,
awaitPromise,
argumentValueList = null,
thisParameter = null,
resultOwnership = null,
) {
const params = {
functionDeclaration: functionDeclaration,
awaitPromise: awaitPromise,
}
if (targetType === 'contextTarget') {
if (sandbox != null) {
params['target'] = { context: id, sandbox: sandbox }
} else {
params['target'] = { context: id }
}
} else {
params['target'] = { realm: id }
}
if (argumentValueList != null) {
let argumentParams = []
argumentValueList.forEach((argumentValue) => {
argumentParams.push(argumentValue.asMap())
})
params['arguments'] = argumentParams
}
if (thisParameter != null) {
params['this'] = thisParameter
}
if (resultOwnership != null) {
params['resultOwnership'] = resultOwnership
}
return params
}
getEvaluateParams(targetType, id, sandbox, expression, awaitPromise, resultOwnership = null) {
const params = {
expression: expression,
awaitPromise: awaitPromise,
}
if (targetType === 'contextTarget') {
if (sandbox != null) {
params['target'] = { context: id, sandbox: sandbox }
} else {
params['target'] = { context: id }
}
} else {
params['target'] = { realm: id }
}
if (resultOwnership != null) {
params['resultOwnership'] = resultOwnership
}
return params
}
createEvaluateResult(response) {
const type = response.result.type
const realmId = response.result.realm
let evaluateResult
if (type === EvaluateResultType.SUCCESS) {
const result = response.result.result
evaluateResult = new EvaluateResultSuccess(realmId, new RemoteValue(result))
} else {
const exceptionDetails = response.result.exceptionDetails
evaluateResult = new EvaluateResultException(realmId, new ExceptionDetails(exceptionDetails))
}
return evaluateResult
}
realmInfoMapper(realms) {
const realmsList = []
realms.forEach((realm) => {
realmsList.push(RealmInfo.fromJson(realm))
})
return realmsList
}
/**
* Retrieves all realms.
* @returns {Promise<RealmInfo[]>} - A promise that resolves to an array of RealmInfo objects.
*/
async getAllRealms() {
const command = {
method: 'script.getRealms',
params: {},
}
let response = await this.bidi.send(command)
return this.realmInfoMapper(response.result.realms)
}
/**
* Retrieves the realms by type.
*
* @param {Type} type - The type of realms to retrieve.
* @returns {Promise<RealmInfo[]>} - A promise that resolves to an array of RealmInfo objects.
*/
async getRealmsByType(type) {
const command = {
method: 'script.getRealms',
params: { type: type },
}
let response = await this.bidi.send(command)
return this.realmInfoMapper(response.result.realms)
}
/**
* Retrieves the realms in the specified browsing context.
*
* @param {string} browsingContext - The browsing context ID.
* @returns {Promise<RealmInfo[]>} - A promise that resolves to an array of RealmInfo objects.
*/
async getRealmsInBrowsingContext(browsingContext) {
const command = {
method: 'script.getRealms',
params: { context: browsingContext },
}
let response = await this.bidi.send(command)
return this.realmInfoMapper(response.result.realms)
}
/**
* Retrieves the realms in a browsing context based on the specified type.
*
* @param {string} browsingContext - The browsing context ID.
* @param {string} type - The type of realms to retrieve.
* @returns {Promise<RealmInfo[]>} - A promise that resolves to an array of RealmInfo objects.
*/
async getRealmsInBrowsingContextByType(browsingContext, type) {
const command = {
method: 'script.getRealms',
params: { context: browsingContext, type: type },
}
let response = await this.bidi.send(command)
return this.realmInfoMapper(response.result.realms)
}
/**
* Subscribes to the 'script.message' event and handles the callback function when a message is received.
*
* @param {Function} callback - The callback function to be executed when a message is received.
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
*/
async onMessage(callback) {
await this.subscribeAndHandleEvent('script.message', callback)
}
/**
* Subscribes to the 'script.realmCreated' event and handles it with the provided callback.
*
* @param {Function} callback - The callback function to handle the 'script.realmCreated' event.
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
*/
async onRealmCreated(callback) {
await this.subscribeAndHandleEvent('script.realmCreated', callback)
}
/**
* Subscribes to the 'script.realmDestroyed' event and handles it with the provided callback function.
*
* @param {Function} callback - The callback function to be executed when the 'script.realmDestroyed' event occurs.
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
*/
async onRealmDestroyed(callback) {
await this.subscribeAndHandleEvent('script.realmDestroyed', callback)
}
async subscribeAndHandleEvent(eventType, callback) {
if (this.browsingContextIds != null) {
await this.bidi.subscribe(eventType, this.browsingContextIds)
} else {
await this.bidi.subscribe(eventType)
}
await this._on(callback)
}
async _on(callback) {
this.ws = await this.bidi.socket
this.ws.on('message', (event) => {
const { params } = JSON.parse(Buffer.from(event.toString()))
if (params) {
let response = null
if ('channel' in params) {
response = new Message(params.channel, new RemoteValue(params.data), new Source(params.source))
} else if ('realm' in params) {
if (params.type === RealmType.WINDOW) {
response = new WindowRealmInfo(params.realm, params.origin, params.type, params.context, params.sandbox)
} else if (params.realm !== null && params.type !== null) {
response = new RealmInfo(params.realm, params.origin, params.type)
} else if (params.realm !== null) {
response = params.realm
}
}
callback(response)
}
})
}
}
async function getScriptManagerInstance(browsingContextId, driver) {
let instance = new ScriptManager(driver)
await instance.init(browsingContextId)
return instance
}
module.exports = getScriptManagerInstance

View File

@@ -0,0 +1,94 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* Represents a message received through a channel.
* Described in https://w3c.github.io/webdriver-bidi/#event-script-message.
* @class
*/
class Message {
/**
* Creates a new Message instance.
* @param {string} channel - The channel through which the message is received.
* @param {RemoteValue} data - The data contained in the message.
* @param {Source} source - The source of the message.
*/
constructor(channel, data, source) {
this._channel = channel
this._data = data
this._source = source
}
/**
* Gets the channel through which the message is received.
* @returns {string} The channel.
*/
get channel() {
return this._channel
}
/**
* Gets the data contained in the message.
* @returns {RemoteValue} The data.
*/
get data() {
return this._data
}
/**
* Gets the source of the message.
* @returns {Source} The source.
*/
get source() {
return this._source
}
}
/**
* Represents a source object.
* Described in https://w3c.github.io/webdriver-bidi/#type-script-Source.
* @class
*/
class Source {
constructor(source) {
this._browsingContextId = null
this._realmId = source.realm
// Browsing context is returned as an optional parameter
if ('context' in source) {
this._browsingContextId = source.context
}
}
/**
* Get the browsing context ID.
* @returns {string|null} The browsing context ID.
*/
get browsingContextId() {
return this._browsingContextId
}
/**
* Get the realm ID.
* @returns {string} The realm ID.
*/
get realmId() {
return this._realmId
}
}
module.exports = { Message, Source }

View File

@@ -0,0 +1,201 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const { CookieFilter } = require('./cookieFilter')
const { BrowsingContextPartitionDescriptor, StorageKeyPartitionDescriptor } = require('./partitionDescriptor')
const { PartitionKey } = require('./partitionKey')
const { PartialCookie } = require('./partialCookie')
const { Cookie, BytesValue } = require('./networkTypes')
/**
* Represents commands of Storage module.
* Described in https://w3c.github.io/webdriver-bidi/#module-storage.
* @class
*/
class Storage {
constructor(driver) {
this._driver = driver
}
async init() {
if (!(await this._driver.getCapabilities()).get('webSocketUrl')) {
throw Error('WebDriver instance must support BiDi protocol')
}
this.bidi = await this._driver.getBidi()
}
/**
* Retrieves cookies based on the provided filter and partition.
*
* @param {CookieFilter} [filter] - The filter to apply to the cookies.
* @param {(BrowsingContextPartitionDescriptor|StorageKeyPartitionDescriptor)} [partition] - The partition to retrieve cookies from.
* @returns {Promise<{ cookies: Cookie[], partitionKey?: PartitionKey }>} - A promise that resolves to an object containing the retrieved cookies and an optional partition key.
* @throws {Error} If the filter parameter is provided but is not an instance of CookieFilter.
* @throws {Error} If the partition parameter is provided but is not an instance of BrowsingContextPartitionDescriptor or StorageKeyPartitionDescriptor.
*/
async getCookies(filter = undefined, partition = undefined) {
if (filter !== undefined && !(filter instanceof CookieFilter)) {
throw new Error(`Params must be an instance of CookieFilter. Received:'${filter}'`)
}
if (
partition !== undefined &&
!(partition instanceof (BrowsingContextPartitionDescriptor || StorageKeyPartitionDescriptor))
) {
throw new Error(`Params must be an instance of PartitionDescriptor. Received:'${partition}'`)
}
const command = {
method: 'storage.getCookies',
params: {
filter: filter ? Object.fromEntries(filter.asMap()) : undefined,
partition: partition ? Object.fromEntries(partition.asMap()) : undefined,
},
}
let response = await this.bidi.send(command)
let cookies = []
response.result.cookies.forEach((cookie) => {
cookies.push(
new Cookie(
cookie.name,
new BytesValue(cookie.value.type, cookie.value.value),
cookie.domain,
cookie.path,
cookie.size,
cookie.httpOnly,
cookie.secure,
cookie.sameSite,
cookie.expiry,
),
)
})
if (Object.prototype.hasOwnProperty.call(response.result, 'partitionKey')) {
if (
Object.prototype.hasOwnProperty.call(response.result.partitionKey, 'userContext') &&
Object.prototype.hasOwnProperty.call(response.result.partitionKey, 'sourceOrigin')
) {
let partitionKey = new PartitionKey(
response.result.partitionKey.userContext,
response.result.partitionKey.sourceOrigin,
)
return { cookies, partitionKey }
}
return { cookies }
}
}
/**
* Sets a cookie using the provided cookie object and partition.
*
* @param {PartialCookie} cookie - The cookie object to set.
* @param {(BrowsingContextPartitionDescriptor|StorageKeyPartitionDescriptor)} [partition] - The partition to use for the cookie.
* @returns {PartitionKey} The partition key of the set cookie.
* @throws {Error} If the cookie parameter is not an instance of PartialCookie or if the partition parameter is not an instance of PartitionDescriptor.
*/
async setCookie(cookie, partition = undefined) {
if (!(cookie instanceof PartialCookie)) {
throw new Error(`Params must be an instance of PartialCookie. Received:'${cookie}'`)
}
if (
partition !== undefined &&
!(partition instanceof (BrowsingContextPartitionDescriptor || StorageKeyPartitionDescriptor))
) {
throw new Error(`Params must be an instance of PartitionDescriptor. Received:'${partition}'`)
}
const command = {
method: 'storage.setCookie',
params: {
cookie: cookie ? Object.fromEntries(cookie.asMap()) : undefined,
partition: partition ? Object.fromEntries(partition.asMap()) : undefined,
},
}
let response = await this.bidi.send(command)
if (Object.prototype.hasOwnProperty.call(response.result, 'partitionKey')) {
if (
Object.prototype.hasOwnProperty.call(response.result.partitionKey, 'userContext') &&
Object.prototype.hasOwnProperty.call(response.result.partitionKey, 'sourceOrigin')
) {
let partitionKey = new PartitionKey(
response.result.partitionKey.userContext,
response.result.partitionKey.sourceOrigin,
)
return partitionKey
}
}
}
/**
* Deletes cookies based on the provided filter and partition.
*
* @param {CookieFilter} [cookieFilter] - The filter to apply to the cookies. Must be an instance of CookieFilter.
* @param {(BrowsingContextPartitionDescriptor|StorageKeyPartitionDescriptor)} [partition] - The partition to delete cookies from. Must be an instance of either BrowsingContextPartitionDescriptor or StorageKeyPartitionDescriptor.
* @returns {PartitionKey} - The partition key of the deleted cookies, if available.
* @throws {Error} - If the provided parameters are not of the correct type.
*/
async deleteCookies(cookieFilter = undefined, partition = undefined) {
if (cookieFilter !== undefined && !(cookieFilter instanceof CookieFilter)) {
throw new Error(`Params must be an instance of CookieFilter. Received:'${cookieFilter}'`)
}
if (
partition !== undefined &&
!(partition instanceof (BrowsingContextPartitionDescriptor || StorageKeyPartitionDescriptor))
) {
throw new Error(`Params must be an instance of PartitionDescriptor. Received:'${partition}'`)
}
const command = {
method: 'storage.deleteCookies',
params: {
filter: cookieFilter ? Object.fromEntries(cookieFilter.asMap()) : undefined,
partition: partition ? Object.fromEntries(partition.asMap()) : undefined,
},
}
let response = await this.bidi.send(command)
if (Object.prototype.hasOwnProperty.call(response.result, 'partitionKey')) {
if (
Object.prototype.hasOwnProperty.call(response.result.partitionKey, 'userContext') &&
Object.prototype.hasOwnProperty.call(response.result.partitionKey, 'sourceOrigin')
) {
let partitionKey = new PartitionKey(
response.result.partitionKey.userContext,
response.result.partitionKey.sourceOrigin,
)
return partitionKey
}
}
}
}
async function getStorageInstance(driver) {
let instance = new Storage(driver)
await instance.init()
return instance
}
module.exports = getStorageInstance

View File

@@ -0,0 +1,91 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* Represents a URL pattern to intercept.
* Described in network.UrlPatternPattern https://w3c.github.io/webdriver-bidi/#type-network-UrlPattern
*/
class UrlPattern {
#map = new Map()
/**
* Sets the protocol for the URL pattern.
*
* @param {string} protocol - The protocol to set.
* @returns {UrlPattern} - Returns the updated instance of the URL pattern for chaining.
*/
protocol(protocol) {
this.#map.set('protocol', protocol)
return this
}
/**
* Sets the hostname for the URL pattern.
*
* @param {string} hostname - The hostname to set.
* @returns {UrlPattern} - Returns the updated instance of the URL pattern for chaining.
*/
hostname(hostname) {
this.#map.set('hostname', hostname)
return this
}
/**
* Sets the port for the URL pattern.
*
* @param {number} port - The port number to set.
* @returns {UrlPattern} - Returns the updated instance of the URL pattern for chaining.
* @throws {Error} - Throws an error if the port is not a number.
*/
port(port) {
if (typeof port === 'number') {
this.#map.set('port', port.toString())
} else {
throw new Error(`Port must be a number. Received:'${port}'`)
}
return this
}
/**
* Sets the pathname for the URL pattern.
*
* @param {string} pathname - The pathname to set.
* @returns {UrlPattern} - Returns the updated instance of the URL pattern for chaining.
*/
pathname(pathname) {
this.#map.set('pathname', pathname)
return this
}
/**
* Sets the search parameter in the URL pattern.
*
* @param {string} search - The search parameter to be set.
* @returns {UrlPattern} - Returns the updated instance of the URL pattern for chaining.
*/
search(search) {
this.#map.set('search', search)
return this
}
asMap() {
this.#map.set('type', 'pattern')
return this.#map
}
}
module.exports = { UrlPattern }

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,239 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Defines a {@linkplain Driver WebDriver} client for the Chrome
* web browser. Before using this module, you must download the latest
* [ChromeDriver release] and ensure it can be found on your system [PATH].
*
* There are three primary classes exported by this module:
*
* 1. {@linkplain ServiceBuilder}: configures the
* {@link selenium-webdriver/remote.DriverService remote.DriverService}
* that manages the [ChromeDriver] child process.
*
* 2. {@linkplain Options}: defines configuration options for each new Chrome
* session, such as which {@linkplain Options#setProxy proxy} to use,
* what {@linkplain Options#addExtensions extensions} to install, or
* what {@linkplain Options#addArguments command-line switches} to use when
* starting the browser.
*
* 3. {@linkplain Driver}: the WebDriver client; each new instance will control
* a unique browser session with a clean user profile (unless otherwise
* configured through the {@link Options} class).
*
* let chrome = require('selenium-webdriver/chrome');
* let {Builder} = require('selenium-webdriver');
*
* let driver = new Builder()
* .forBrowser('chrome')
* .setChromeOptions(new chrome.Options())
* .build();
*
* __Customizing the ChromeDriver Server__ <a id="custom-server"></a>
*
* By default, every Chrome session will use a single driver service, which is
* started the first time a {@link Driver} instance is created and terminated
* when this process exits. The default service will inherit its environment
* from the current process and direct all output to /dev/null. You may obtain
* a handle to this default service using
* {@link #getDefaultService getDefaultService()} and change its configuration
* with {@link #setDefaultService setDefaultService()}.
*
* You may also create a {@link Driver} with its own driver service. This is
* useful if you need to capture the server's log output for a specific session:
*
* let chrome = require('selenium-webdriver/chrome');
*
* let service = new chrome.ServiceBuilder()
* .loggingTo('/my/log/file.txt')
* .enableVerboseLogging()
* .build();
*
* let options = new chrome.Options();
* // configure browser options ...
*
* let driver = chrome.Driver.createSession(options, service);
*
* Users should only instantiate the {@link Driver} class directly when they
* need a custom driver service configuration (as shown above). For normal
* operation, users should start Chrome using the
* {@link selenium-webdriver.Builder}.
*
* __Working with Android__ <a id="android"></a>
*
* The [ChromeDriver][android] supports running tests on the Chrome browser as
* well as [WebView apps][webview] starting in Android 4.4 (KitKat). In order to
* work with Android, you must first start the adb
*
* adb start-server
*
* By default, adb will start on port 5037. You may change this port, but this
* will require configuring a [custom server](#custom-server) that will connect
* to adb on the {@linkplain ServiceBuilder#setAdbPort correct port}:
*
* let service = new chrome.ServiceBuilder()
* .setAdbPort(1234)
* build();
* // etc.
*
* The ChromeDriver may be configured to launch Chrome on Android using
* {@link Options#androidChrome()}:
*
* let driver = new Builder()
* .forBrowser('chrome')
* .setChromeOptions(new chrome.Options().androidChrome())
* .build();
*
* Alternatively, you can configure the ChromeDriver to launch an app with a
* Chrome-WebView by setting the {@linkplain Options#androidActivity
* androidActivity} option:
*
* let driver = new Builder()
* .forBrowser('chrome')
* .setChromeOptions(new chrome.Options()
* .androidPackage('com.example')
* .androidActivity('com.example.Activity'))
* .build();
*
* [Refer to the ChromeDriver site] for more information on using the
* [ChromeDriver with Android][android].
*
* [ChromeDriver]: https://chromedriver.chromium.org/
* [ChromeDriver release]: http://chromedriver.storage.googleapis.com/index.html
* [PATH]: http://en.wikipedia.org/wiki/PATH_%28variable%29
* [android]: https://chromedriver.chromium.org/getting-started/getting-started---android
* [webview]: https://developer.chrome.com/multidevice/webview/overview
*
* @module selenium-webdriver/chrome
*/
'use strict'
const { Browser } = require('./lib/capabilities')
const chromium = require('./chromium')
const CHROME_CAPABILITY_KEY = 'goog:chromeOptions'
/** @type {remote.DriverService} */
/**
* Creates {@link selenium-webdriver/remote.DriverService} instances that manage
* a [ChromeDriver](https://chromedriver.chromium.org/)
* server in a child process.
*/
class ServiceBuilder extends chromium.ServiceBuilder {
/**
* @param {string=} opt_exe Path to the server executable to use. If omitted,
* the builder will attempt to locate the chromedriver on the current
* PATH. If the chromedriver is not available in path, selenium-manager will
* download the chromedriver
* @throws {Error} If provided executable does not exist, or the chromedriver
* cannot be found on the PATH.
*/
constructor(opt_exe) {
super(opt_exe)
}
}
/**
* Class for managing ChromeDriver specific options.
*/
class Options extends chromium.Options {
/**
* Sets the path to the Chrome binary to use. On Mac OS X, this path should
* reference the actual Chrome executable, not just the application binary
* (e.g. "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome").
*
* The binary path be absolute or relative to the chromedriver server
* executable, but it must exist on the machine that will launch Chrome.
*
* @param {string} path The path to the Chrome binary to use.
* @return {!Options} A self reference.
*/
setChromeBinaryPath(path) {
return this.setBinaryPath(path)
}
/**
* Configures the ChromeDriver to launch Chrome on Android via adb. This
* function is shorthand for
* {@link #androidPackage options.androidPackage('com.android.chrome')}.
* @return {!Options} A self reference.
*/
androidChrome() {
return this.androidPackage('com.android.chrome')
}
/**
* Sets the path to Chrome's log file. This path should exist on the machine
* that will launch Chrome.
* @param {string} path Path to the log file to use.
* @return {!Options} A self reference.
*/
setChromeLogFile(path) {
return this.setBrowserLogFile(path)
}
/**
* Sets the directory to store Chrome minidumps in. This option is only
* supported when ChromeDriver is running on Linux.
* @param {string} path The directory path.
* @return {!Options} A self reference.
*/
setChromeMinidumpPath(path) {
return this.setBrowserMinidumpPath(path)
}
}
/**
* Creates a new WebDriver client for Chrome.
*/
class Driver extends chromium.Driver {
/**
* Creates a new session with the ChromeDriver.
*
* @param {(Capabilities|Options)=} opt_config The configuration options.
* @param {(remote.DriverService|http.Executor)=} opt_serviceExecutor Either
* a DriverService to use for the remote end, or a preconfigured executor
* for an externally managed endpoint. If neither is provided, the
* {@linkplain ##getDefaultService default service} will be used by
* default.
* @return {!Driver} A new driver instance.
*/
static createSession(opt_config, opt_serviceExecutor) {
let caps = opt_config || new Options()
return /** @type {!Driver} */ (super.createSession(caps, opt_serviceExecutor, 'goog', CHROME_CAPABILITY_KEY))
}
/**
* returns new instance chrome driver service
* @returns {remote.DriverService}
*/
static getDefaultService() {
return new ServiceBuilder().build()
}
}
Options.prototype.CAPABILITY_KEY = CHROME_CAPABILITY_KEY
Options.prototype.BROWSER_NAME_VALUE = Browser.CHROME
// PUBLIC API
module.exports = {
Driver,
Options,
ServiceBuilder,
}

View File

@@ -0,0 +1,855 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Defines an abstract {@linkplain Driver WebDriver} client for
* Chromium-based web browsers. These classes should not be instantiated
* directly.
*
* There are three primary classes exported by this module:
*
* 1. {@linkplain ServiceBuilder}: configures the
* {@link selenium-webdriver/remote.DriverService remote.DriverService}
* that manages a WebDriver server child process.
*
* 2. {@linkplain Options}: defines configuration options for each new Chromium
* session, such as which {@linkplain Options#setProxy proxy} to use,
* what {@linkplain Options#addExtensions extensions} to install, or
* what {@linkplain Options#addArguments command-line switches} to use when
* starting the browser.
*
* 3. {@linkplain Driver}: the WebDriver client; each new instance will control
* a unique browser session with a clean user profile (unless otherwise
* configured through the {@link Options} class).
*
* let chrome = require('selenium-webdriver/chrome');
* let {Builder} = require('selenium-webdriver');
*
* let driver = new Builder()
* .forBrowser('chrome')
* .setChromeOptions(new chrome.Options())
* .build();
*
* __Customizing the Chromium WebDriver Server__ <a id="custom-server"></a>
*
* Subclasses of {@link Driver} are expected to provide a static
* getDefaultService method. By default, this method will be called every time
* a {@link Driver} instance is created to obtain the default driver service
* for that specific browser (e.g. Chrome or Chromium Edge). Subclasses are
* responsible for managing the lifetime of the default service.
*
* You may also create a {@link Driver} with its own driver service. This is
* useful if you need to capture the server's log output for a specific session:
*
* let chrome = require('selenium-webdriver/chrome');
*
* let service = new chrome.ServiceBuilder()
* .loggingTo('/my/log/file.txt')
* .enableVerboseLogging()
* .build();
*
* let options = new chrome.Options();
* // configure browser options ...
*
* let driver = chrome.Driver.createSession(options, service);
*
* @module selenium-webdriver/chromium
*/
'use strict'
const http = require('./http')
const io = require('./io')
const { Capabilities, Capability } = require('./lib/capabilities')
const command = require('./lib/command')
const error = require('./lib/error')
const Symbols = require('./lib/symbols')
const webdriver = require('./lib/webdriver')
const remote = require('./remote')
const { getBinaryPaths } = require('./common/driverFinder')
/**
* Custom command names supported by Chromium WebDriver.
* @enum {string}
*/
const Command = {
LAUNCH_APP: 'launchApp',
GET_NETWORK_CONDITIONS: 'getNetworkConditions',
SET_NETWORK_CONDITIONS: 'setNetworkConditions',
DELETE_NETWORK_CONDITIONS: 'deleteNetworkConditions',
SEND_DEVTOOLS_COMMAND: 'sendDevToolsCommand',
SEND_AND_GET_DEVTOOLS_COMMAND: 'sendAndGetDevToolsCommand',
SET_PERMISSION: 'setPermission',
GET_CAST_SINKS: 'getCastSinks',
SET_CAST_SINK_TO_USE: 'setCastSinkToUse',
START_CAST_DESKTOP_MIRRORING: 'startDesktopMirroring',
START_CAST_TAB_MIRRORING: 'setCastTabMirroring',
GET_CAST_ISSUE_MESSAGE: 'getCastIssueMessage',
STOP_CASTING: 'stopCasting',
}
/**
* Creates a command executor with support for Chromium's custom commands.
* @param {!Promise<string>} url The server's URL.
* @param vendorPrefix
* @return {!command.Executor} The new command executor.
*/
function createExecutor(url, vendorPrefix) {
const agent = new http.Agent({ keepAlive: true })
const client = url.then((url) => new http.HttpClient(url, agent))
const executor = new http.Executor(client)
configureExecutor(executor, vendorPrefix)
return executor
}
/**
* Configures the given executor with Chromium-specific commands.
* @param {!http.Executor} executor the executor to configure.
*/
function configureExecutor(executor, vendorPrefix) {
executor.defineCommand(Command.LAUNCH_APP, 'POST', '/session/:sessionId/chromium/launch_app')
executor.defineCommand(Command.GET_NETWORK_CONDITIONS, 'GET', '/session/:sessionId/chromium/network_conditions')
executor.defineCommand(Command.SET_NETWORK_CONDITIONS, 'POST', '/session/:sessionId/chromium/network_conditions')
executor.defineCommand(Command.DELETE_NETWORK_CONDITIONS, 'DELETE', '/session/:sessionId/chromium/network_conditions')
executor.defineCommand(Command.SEND_DEVTOOLS_COMMAND, 'POST', '/session/:sessionId/chromium/send_command')
executor.defineCommand(
Command.SEND_AND_GET_DEVTOOLS_COMMAND,
'POST',
'/session/:sessionId/chromium/send_command_and_get_result',
)
executor.defineCommand(Command.SET_PERMISSION, 'POST', '/session/:sessionId/permissions')
executor.defineCommand(Command.GET_CAST_SINKS, 'GET', `/session/:sessionId/${vendorPrefix}/cast/get_sinks`)
executor.defineCommand(
Command.SET_CAST_SINK_TO_USE,
'POST',
`/session/:sessionId/${vendorPrefix}/cast/set_sink_to_use`,
)
executor.defineCommand(
Command.START_CAST_DESKTOP_MIRRORING,
'POST',
`/session/:sessionId/${vendorPrefix}/cast/start_desktop_mirroring`,
)
executor.defineCommand(
Command.START_CAST_TAB_MIRRORING,
'POST',
`/session/:sessionId/${vendorPrefix}/cast/start_tab_mirroring`,
)
executor.defineCommand(
Command.GET_CAST_ISSUE_MESSAGE,
'GET',
`/session/:sessionId/${vendorPrefix}/cast/get_issue_message`,
)
executor.defineCommand(Command.STOP_CASTING, 'POST', `/session/:sessionId/${vendorPrefix}/cast/stop_casting`)
}
/**
* Creates {@link selenium-webdriver/remote.DriverService} instances that manage
* a WebDriver server in a child process.
*/
class ServiceBuilder extends remote.DriverService.Builder {
/**
* @param {string=} exe Path to the server executable to use. Subclasses
* should ensure a valid path to the appropriate exe is provided.
*/
constructor(exe) {
super(exe)
this.setLoopback(true) // Required
}
/**
* Sets which port adb is listening to. _The driver will connect to adb
* if an {@linkplain Options#androidPackage Android session} is requested, but
* adb **must** be started beforehand._
*
* @param {number} port Which port adb is running on.
* @return {!ServiceBuilder} A self reference.
*/
setAdbPort(port) {
return this.addArguments('--adb-port=' + port)
}
/**
* Sets the path of the log file the driver should log to. If a log file is
* not specified, the driver will log to stderr.
* @param {string} path Path of the log file to use.
* @return {!ServiceBuilder} A self reference.
*/
loggingTo(path) {
return this.addArguments('--log-path=' + path)
}
/**
* Enables Chrome logging.
* @returns {!ServiceBuilder} A self reference.
*/
enableChromeLogging() {
return this.addArguments('--enable-chrome-logs')
}
/**
* Enables verbose logging.
* @return {!ServiceBuilder} A self reference.
*/
enableVerboseLogging() {
return this.addArguments('--verbose')
}
/**
* Sets the number of threads the driver should use to manage HTTP requests.
* By default, the driver will use 4 threads.
* @param {number} n The number of threads to use.
* @return {!ServiceBuilder} A self reference.
*/
setNumHttpThreads(n) {
return this.addArguments('--http-threads=' + n)
}
/**
* @override
*/
setPath(path) {
super.setPath(path)
return this.addArguments('--url-base=' + path)
}
}
/**
* Class for managing WebDriver options specific to a Chromium-based browser.
*/
class Options extends Capabilities {
/**
* @param {(Capabilities|Map<string, ?>|Object)=} other Another set of
* capabilities to initialize this instance from.
*/
constructor(other = undefined) {
super(other)
/** @private {!Object} */
this.options_ = this.get(this.CAPABILITY_KEY) || {}
this.setBrowserName(this.BROWSER_NAME_VALUE)
this.set(this.CAPABILITY_KEY, this.options_)
}
/**
* Add additional command line arguments to use when launching the browser.
* Each argument may be specified with or without the "--" prefix
* (e.g. "--foo" and "foo"). Arguments with an associated value should be
* delimited by an "=": "foo=bar".
*
* @param {...(string|!Array<string>)} args The arguments to add.
* @return {!Options} A self reference.
*/
addArguments(...args) {
let newArgs = (this.options_.args || []).concat(...args)
if (newArgs.length) {
this.options_.args = newArgs
}
return this
}
/**
* Sets the address of a Chromium remote debugging server to connect to.
* Address should be of the form "{hostname|IP address}:port"
* (e.g. "localhost:9222").
*
* @param {string} address The address to connect to.
* @return {!Options} A self reference.
*/
debuggerAddress(address) {
this.options_.debuggerAddress = address
return this
}
/**
* Sets the initial window size.
*
* @param {{width: number, height: number}} size The desired window size.
* @return {!Options} A self reference.
* @throws {TypeError} if width or height is unspecified, not a number, or
* less than or equal to 0.
*/
windowSize({ width, height }) {
function checkArg(arg) {
if (typeof arg !== 'number' || arg <= 0) {
throw TypeError('Arguments must be {width, height} with numbers > 0')
}
}
checkArg(width)
checkArg(height)
return this.addArguments(`window-size=${width},${height}`)
}
/**
* List of Chrome command line switches to exclude that ChromeDriver by default
* passes when starting Chrome. Do not prefix switches with "--".
*
* @param {...(string|!Array<string>)} args The switches to exclude.
* @return {!Options} A self reference.
*/
excludeSwitches(...args) {
let switches = (this.options_.excludeSwitches || []).concat(...args)
if (switches.length) {
this.options_.excludeSwitches = switches
}
return this
}
/**
* Add additional extensions to install when launching the browser. Each extension
* should be specified as the path to the packed CRX file, or a Buffer for an
* extension.
* @param {...(string|!Buffer|!Array<(string|!Buffer)>)} args The
* extensions to add.
* @return {!Options} A self reference.
*/
addExtensions(...args) {
let extensions = this.options_.extensions || new Extensions()
extensions.add(...args)
if (extensions.length) {
this.options_.extensions = extensions
}
return this
}
/**
* Sets the path to the browser binary to use. On Mac OS X, this path should
* reference the actual Chromium executable, not just the application binary
* (e.g. "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome").
*
* The binary path can be absolute or relative to the WebDriver server
* executable, but it must exist on the machine that will launch the browser.
*
* @param {string} path The path to the browser binary to use.
* @return {!Options} A self reference.
*/
setBinaryPath(path) {
this.options_.binary = path
return this
}
/**
* Sets whether to leave the started browser process running if the controlling
* driver service is killed before {@link webdriver.WebDriver#quit()} is
* called.
* @param {boolean} detach Whether to leave the browser running if the
* driver service is killed before the session.
* @return {!Options} A self reference.
*/
detachDriver(detach) {
this.options_.detach = detach
return this
}
/**
* Sets the user preferences for Chrome's user profile. See the "Preferences"
* file in Chrome's user data directory for examples.
* @param {!Object} prefs Dictionary of user preferences to use.
* @return {!Options} A self reference.
*/
setUserPreferences(prefs) {
this.options_.prefs = prefs
return this
}
/**
* Sets the performance logging preferences. Options include:
*
* - `enableNetwork`: Whether or not to collect events from Network domain.
* - `enablePage`: Whether or not to collect events from Page domain.
* - `enableTimeline`: Whether or not to collect events from Timeline domain.
* Note: when tracing is enabled, Timeline domain is implicitly disabled,
* unless `enableTimeline` is explicitly set to true.
* - `traceCategories`: A comma-separated string of Chromium tracing
* categories for which trace events should be collected. An unspecified
* or empty string disables tracing.
* - `bufferUsageReportingInterval`: The requested number of milliseconds
* between DevTools trace buffer usage events. For example, if 1000, then
* once per second, DevTools will report how full the trace buffer is. If
* a report indicates the buffer usage is 100%, a warning will be issued.
*
* @param {{enableNetwork: boolean,
* enablePage: boolean,
* enableTimeline: boolean,
* traceCategories: string,
* bufferUsageReportingInterval: number}} prefs The performance
* logging preferences.
* @return {!Options} A self reference.
*/
setPerfLoggingPrefs(prefs) {
this.options_.perfLoggingPrefs = prefs
return this
}
/**
* Sets preferences for the "Local State" file in Chrome's user data
* directory.
* @param {!Object} state Dictionary of local state preferences.
* @return {!Options} A self reference.
*/
setLocalState(state) {
this.options_.localState = state
return this
}
/**
* Sets the name of the activity hosting a Chrome-based Android WebView. This
* option must be set to connect to an [Android WebView](
* https://chromedriver.chromium.org/getting-started/getting-started---android)
*
* @param {string} name The activity name.
* @return {!Options} A self reference.
*/
androidActivity(name) {
this.options_.androidActivity = name
return this
}
/**
* Sets the device serial number to connect to via ADB. If not specified, the
* WebDriver server will select an unused device at random. An error will be
* returned if all devices already have active sessions.
*
* @param {string} serial The device serial number to connect to.
* @return {!Options} A self reference.
*/
androidDeviceSerial(serial) {
this.options_.androidDeviceSerial = serial
return this
}
/**
* Sets the package name of the Chrome or WebView app.
*
* @param {?string} pkg The package to connect to, or `null` to disable Android
* and switch back to using desktop browser.
* @return {!Options} A self reference.
*/
androidPackage(pkg) {
this.options_.androidPackage = pkg
return this
}
/**
* Sets the process name of the Activity hosting the WebView (as given by
* `ps`). If not specified, the process name is assumed to be the same as
* {@link #androidPackage}.
*
* @param {string} processName The main activity name.
* @return {!Options} A self reference.
*/
androidProcess(processName) {
this.options_.androidProcess = processName
return this
}
/**
* Sets whether to connect to an already-running instead of the specified
* {@linkplain #androidProcess app} instead of launching the app with a clean
* data directory.
*
* @param {boolean} useRunning Whether to connect to a running instance.
* @return {!Options} A self reference.
*/
androidUseRunningApp(useRunning) {
this.options_.androidUseRunningApp = useRunning
return this
}
/**
* Sets the path to the browser's log file. This path should exist on the machine
* that will launch the browser.
* @param {string} path Path to the log file to use.
* @return {!Options} A self reference.
*/
setBrowserLogFile(path) {
this.options_.logPath = path
return this
}
/**
* Sets the directory to store browser minidumps in. This option is only
* supported when the driver is running on Linux.
* @param {string} path The directory path.
* @return {!Options} A self reference.
*/
setBrowserMinidumpPath(path) {
this.options_.minidumpPath = path
return this
}
/**
* Configures the browser to emulate a mobile device. For more information, refer
* to the ChromeDriver project page on [mobile emulation][em]. Configuration
* options include:
*
* - `deviceName`: The name of a pre-configured [emulated device][devem]
* - `width`: screen width, in pixels
* - `height`: screen height, in pixels
* - `pixelRatio`: screen pixel ratio
*
* __Example 1: Using a Pre-configured Device__
*
* let options = new chrome.Options().setMobileEmulation(
* {deviceName: 'Google Nexus 5'});
*
* let driver = chrome.Driver.createSession(options);
*
* __Example 2: Using Custom Screen Configuration__
*
* let options = new chrome.Options().setMobileEmulation({deviceMetrics: {
* width: 360,
* height: 640,
* pixelRatio: 3.0
* }});
*
* let driver = chrome.Driver.createSession(options);
*
*
* [em]: https://chromedriver.chromium.org/mobile-emulation
* [devem]: https://developer.chrome.com/devtools/docs/device-mode
*
* @param {?({deviceName: string}|
* {width: number, height: number, pixelRatio: number})} config The
* mobile emulation configuration, or `null` to disable emulation.
* @return {!Options} A self reference.
*/
setMobileEmulation(config) {
this.options_.mobileEmulation = config
return this
}
/**
* Sets a list of the window types that will appear when getting window
* handles. For access to <webview> elements, include "webview" in the list.
* @param {...(string|!Array<string>)} args The window types that will appear
* when getting window handles.
* @return {!Options} A self reference.
*/
windowTypes(...args) {
let windowTypes = (this.options_.windowTypes || []).concat(...args)
if (windowTypes.length) {
this.options_.windowTypes = windowTypes
}
return this
}
/**
* Enable bidi connection
* @returns {!Capabilities}
*/
enableBidi() {
return this.set('webSocketUrl', true)
}
}
/**
* A list of extensions to install when launching the browser.
*/
class Extensions {
constructor() {
this.extensions = []
}
/**
* @return {number} The length of the extensions list.
*/
get length() {
return this.extensions.length
}
/**
* Add additional extensions to install when launching the browser. Each
* extension should be specified as the path to the packed CRX file, or a
* Buffer for an extension.
*
* @param {...(string|!Buffer|!Array<(string|!Buffer)>)} args The
* extensions to add.
*/
add(...args) {
this.extensions = this.extensions.concat(...args)
}
/**
* @return {!Object} A serialized representation of this Extensions object.
*/
[Symbols.serialize]() {
return this.extensions.map(function (extension) {
if (Buffer.isBuffer(extension)) {
return extension.toString('base64')
}
return io.read(/** @type {string} */ (extension)).then((buffer) => buffer.toString('base64'))
})
}
}
/**
* Creates a new WebDriver client for Chromium-based browsers.
*/
class Driver extends webdriver.WebDriver {
/**
* Creates a new session with the WebDriver server.
*
* @param {(Capabilities|Options)=} caps The configuration options.
* @param {(remote.DriverService|http.Executor)=} opt_serviceExecutor Either
* a DriverService to use for the remote end, or a preconfigured executor
* for an externally managed endpoint. If neither is provided, the
* {@linkplain ##getDefaultService default service} will be used by
* default.
* @param vendorPrefix Either 'goog' or 'ms'
* @param vendorCapabilityKey Either 'goog:chromeOptions' or 'ms:edgeOptions'
* @return {!Driver} A new driver instance.
*/
static createSession(caps, opt_serviceExecutor, vendorPrefix = '', vendorCapabilityKey = '') {
let executor
let onQuit
if (opt_serviceExecutor instanceof http.Executor) {
executor = opt_serviceExecutor
configureExecutor(executor, vendorPrefix)
} else {
let service = opt_serviceExecutor || this.getDefaultService()
if (!service.getExecutable()) {
const { driverPath, browserPath } = getBinaryPaths(caps)
service.setExecutable(driverPath)
if (browserPath) {
const vendorOptions = caps.get(vendorCapabilityKey)
if (vendorOptions) {
vendorOptions['binary'] = browserPath
caps.set(vendorCapabilityKey, vendorOptions)
} else {
caps.set(vendorCapabilityKey, { binary: browserPath })
}
caps.delete(Capability.BROWSER_VERSION)
}
}
onQuit = () => service.kill()
executor = createExecutor(service.start(), vendorPrefix)
}
// W3C spec requires noProxy value to be an array of strings, but Chromium
// expects a single host as a string.
let proxy = caps.get(Capability.PROXY)
if (proxy && Array.isArray(proxy.noProxy)) {
proxy.noProxy = proxy.noProxy[0]
if (!proxy.noProxy) {
proxy.noProxy = undefined
}
}
return /** @type {!Driver} */ (super.createSession(executor, caps, onQuit))
}
/**
* This function is a no-op as file detectors are not supported by this
* implementation.
* @override
*/
setFileDetector() {}
/**
* Schedules a command to launch Chrome App with given ID.
* @param {string} id ID of the App to launch.
* @return {!Promise<void>} A promise that will be resolved
* when app is launched.
*/
launchApp(id) {
return this.execute(new command.Command(Command.LAUNCH_APP).setParameter('id', id))
}
/**
* Schedules a command to get Chromium network emulation settings.
* @return {!Promise} A promise that will be resolved when network
* emulation settings are retrieved.
*/
getNetworkConditions() {
return this.execute(new command.Command(Command.GET_NETWORK_CONDITIONS))
}
/**
* Schedules a command to delete Chromium network emulation settings.
* @return {!Promise} A promise that will be resolved when network
* emulation settings have been deleted.
*/
deleteNetworkConditions() {
return this.execute(new command.Command(Command.DELETE_NETWORK_CONDITIONS))
}
/**
* Schedules a command to set Chromium network emulation settings.
*
* __Sample Usage:__
*
* driver.setNetworkConditions({
* offline: false,
* latency: 5, // Additional latency (ms).
* download_throughput: 500 * 1024, // Maximal aggregated download throughput.
* upload_throughput: 500 * 1024 // Maximal aggregated upload throughput.
* });
*
* @param {Object} spec Defines the network conditions to set
* @return {!Promise<void>} A promise that will be resolved when network
* emulation settings are set.
*/
setNetworkConditions(spec) {
if (!spec || typeof spec !== 'object') {
throw TypeError('setNetworkConditions called with non-network-conditions parameter')
}
return this.execute(new command.Command(Command.SET_NETWORK_CONDITIONS).setParameter('network_conditions', spec))
}
/**
* Sends an arbitrary devtools command to the browser.
*
* @param {string} cmd The name of the command to send.
* @param {Object=} params The command parameters.
* @return {!Promise<void>} A promise that will be resolved when the command
* has finished.
* @see <https://chromedevtools.github.io/devtools-protocol/>
*/
sendDevToolsCommand(cmd, params = {}) {
return this.execute(
new command.Command(Command.SEND_DEVTOOLS_COMMAND).setParameter('cmd', cmd).setParameter('params', params),
)
}
/**
* Sends an arbitrary devtools command to the browser and get the result.
*
* @param {string} cmd The name of the command to send.
* @param {Object=} params The command parameters.
* @return {!Promise<string>} A promise that will be resolved when the command
* has finished.
* @see <https://chromedevtools.github.io/devtools-protocol/>
*/
sendAndGetDevToolsCommand(cmd, params = {}) {
return this.execute(
new command.Command(Command.SEND_AND_GET_DEVTOOLS_COMMAND)
.setParameter('cmd', cmd)
.setParameter('params', params),
)
}
/**
* Set a permission state to the given value.
*
* @param {string} name A name of the permission to update.
* @param {("granted"|"denied"|"prompt")} state State to set permission to.
* @returns {!Promise<Object>} A promise that will be resolved when the
* command has finished.
* @see <https://w3c.github.io/permissions/#permission-registry> for valid
* names
*/
setPermission(name, state) {
return this.execute(
new command.Command(Command.SET_PERMISSION).setParameter('descriptor', { name }).setParameter('state', state),
)
}
/**
* Sends a DevTools command to change the browser's download directory.
*
* @param {string} path The desired download directory.
* @return {!Promise<void>} A promise that will be resolved when the command
* has finished.
* @see #sendDevToolsCommand
*/
async setDownloadPath(path) {
if (!path || typeof path !== 'string') {
throw new error.InvalidArgumentError('invalid download path')
}
const stat = await io.stat(path)
if (!stat.isDirectory()) {
throw new error.InvalidArgumentError('not a directory: ' + path)
}
return this.sendDevToolsCommand('Page.setDownloadBehavior', {
behavior: 'allow',
downloadPath: path,
})
}
/**
* Returns the list of cast sinks (Cast devices) available to the Chrome media router.
*
* @return {!promise.Thenable<void>} A promise that will be resolved with an array of Strings
* containing the friendly device names of available cast sink targets.
*/
getCastSinks() {
return this.execute(new command.Command(Command.GET_CAST_SINKS))
}
/**
* Selects a cast sink (Cast device) as the recipient of media router intents (connect or play).
*
* @param {String} deviceName name of the target device.
* @return {!promise.Thenable<void>} A promise that will be resolved
* when the target device has been selected to respond further webdriver commands.
*/
setCastSinkToUse(deviceName) {
return this.execute(new command.Command(Command.SET_CAST_SINK_TO_USE).setParameter('sinkName', deviceName))
}
/**
* Initiates desktop mirroring for the current browser tab on the specified device.
*
* @param {String} deviceName name of the target device.
* @return {!promise.Thenable<void>} A promise that will be resolved
* when the mirror command has been issued to the device.
*/
startDesktopMirroring(deviceName) {
return this.execute(new command.Command(Command.START_CAST_DESKTOP_MIRRORING).setParameter('sinkName', deviceName))
}
/**
* Initiates tab mirroring for the current browser tab on the specified device.
*
* @param {String} deviceName name of the target device.
* @return {!promise.Thenable<void>} A promise that will be resolved
* when the mirror command has been issued to the device.
*/
startCastTabMirroring(deviceName) {
return this.execute(new command.Command(Command.START_CAST_TAB_MIRRORING).setParameter('sinkName', deviceName))
}
/**
* Returns an error message when there is any issue in a Cast session.
* @return {!promise.Thenable<void>} A promise that will be resolved
* when the mirror command has been issued to the device.
*/
getCastIssueMessage() {
return this.execute(new command.Command(Command.GET_CAST_ISSUE_MESSAGE))
}
/**
* Stops casting from media router to the specified device, if connected.
*
* @param {String} deviceName name of the target device.
* @return {!promise.Thenable<void>} A promise that will be resolved
* when the stop command has been issued to the device.
*/
stopCasting(deviceName) {
return this.execute(new command.Command(Command.STOP_CASTING).setParameter('sinkName', deviceName))
}
}
// PUBLIC API
module.exports = {
Driver,
Options,
ServiceBuilder,
}

View File

@@ -0,0 +1,76 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* This implementation is still in beta, and may change.
*
* Utility to find if a given file is present and executable.
*/
const path = require('node:path')
const { binaryPaths } = require('./seleniumManager')
/**
* Determines the path of the correct Selenium Manager binary
* @param {Capabilities} capabilities browser options to fetch the driver
* @returns {{browserPath: string, driverPath: string}} path of the driver
* and browser location
*/
function getBinaryPaths(capabilities) {
try {
const args = getArgs(capabilities)
return binaryPaths(args)
} catch (e) {
throw Error(
`Unable to obtain browser driver.
For more information on how to install drivers see
https://www.selenium.dev/documentation/webdriver/troubleshooting/errors/driver_location/. ${e}`,
)
}
}
function getArgs(options) {
let args = ['--browser', options.getBrowserName(), '--language-binding', 'javascript', '--output', 'json']
if (options.getBrowserVersion() && options.getBrowserVersion() !== '') {
args.push('--browser-version', options.getBrowserVersion())
}
const vendorOptions =
options.get('goog:chromeOptions') || options.get('ms:edgeOptions') || options.get('moz:firefoxOptions')
if (vendorOptions && vendorOptions.binary && vendorOptions.binary !== '') {
args.push('--browser-path', path.resolve(vendorOptions.binary))
}
const proxyOptions = options.getProxy()
// Check if proxyOptions exists and has properties
if (proxyOptions && Object.keys(proxyOptions).length > 0) {
const httpProxy = proxyOptions['httpProxy']
const sslProxy = proxyOptions['sslProxy']
if (httpProxy !== undefined) {
args.push('--proxy', httpProxy)
} else if (sslProxy !== undefined) {
args.push('--proxy', sslProxy)
}
}
return args
}
// PUBLIC API
module.exports = { getBinaryPaths }

View File

@@ -0,0 +1,115 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* This implementation is still in beta, and may change.
*
* Wrapper for getting information from the Selenium Manager binaries
*/
const { platform } = require('node:process')
const path = require('node:path')
const fs = require('node:fs')
const spawnSync = require('node:child_process').spawnSync
const logging = require('../lib/logging')
const log_ = logging.getLogger(logging.Type.DRIVER)
let debugMessagePrinted = false
/**
* Determines the path of the correct Selenium Manager binary
* @returns {string}
*/
function getBinary() {
const directory = {
darwin: 'macos',
win32: 'windows',
cygwin: 'windows',
linux: 'linux',
}[platform]
const file = directory === 'windows' ? 'selenium-manager.exe' : 'selenium-manager'
let seleniumManagerBasePath = path.join(__dirname, '..', '/bin')
const filePath = process.env.SE_MANAGER_PATH || path.join(seleniumManagerBasePath, directory, file)
if (!fs.existsSync(filePath)) {
throw new Error(`Unable to obtain Selenium Manager at ${filePath}`)
}
if (!debugMessagePrinted) {
log_.debug(`Selenium Manager binary found at ${filePath}`)
debugMessagePrinted = true // Set the flag to true after printing the debug message
}
return filePath
}
/**
* Determines the path of the correct driver
* @param {string[]} args arguments to invoke Selenium Manager
* @returns {{browserPath: string, driverPath: string}} path of the driver and
* browser location
*/
function binaryPaths(args) {
const smBinary = getBinary()
const spawnResult = spawnSync(smBinary, args)
let output
if (spawnResult.status) {
let errorMessage
if (spawnResult.stderr.toString()) {
errorMessage = spawnResult.stderr.toString()
}
if (spawnResult.stdout.toString()) {
try {
output = JSON.parse(spawnResult.stdout.toString())
logOutput(output)
errorMessage = output.result.message
} catch (e) {
errorMessage = e.toString()
}
}
throw new Error(`Error executing command for ${smBinary} with ${args}: ${errorMessage}`)
}
try {
output = JSON.parse(spawnResult.stdout.toString())
} catch (e) {
throw new Error(`Error executing command for ${smBinary} with ${args}: ${e.toString()}`)
}
logOutput(output)
return {
driverPath: output.result.driver_path,
browserPath: output.result.browser_path,
}
}
function logOutput(output) {
for (const key in output.logs) {
if (output.logs[key].level === 'WARN') {
log_.warning(`${output.logs[key].message}`)
}
if (['DEBUG', 'INFO'].includes(output.logs[key].level)) {
log_.debug(`${output.logs[key].message}`)
}
}
}
// PUBLIC API
module.exports = { binaryPaths }

View File

@@ -0,0 +1,80 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const logging = require('../lib/logging')
const RESPONSE_TIMEOUT = 1000 * 30
class CDPConnection {
constructor(wsConnection) {
this._wsConnection = wsConnection
this.cmd_id = 0
this.targetID = null
this.sessionId = null
}
execute(method, params, callback) {
let message = {
method,
id: this.cmd_id++,
}
if (this.sessionId) {
message['sessionId'] = this.sessionId
}
const mergedMessage = Object.assign({ params: params }, message)
this._wsConnection.send(JSON.stringify(mergedMessage), callback)
}
async send(method, params) {
let cdp_id = this.cmd_id++
let message = {
method,
id: cdp_id,
}
if (this.sessionId) {
message['sessionId'] = this.sessionId
}
const mergedMessage = Object.assign({ params: params }, message)
this._wsConnection.send(JSON.stringify(mergedMessage))
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error(`Request with id ${cdp_id} timed out`))
handler.off('message', listener)
}, RESPONSE_TIMEOUT)
const listener = (data) => {
try {
const payload = JSON.parse(data.toString())
if (payload.id === cdp_id) {
clearTimeout(timeoutId)
handler.off('message', listener)
resolve(payload)
}
} catch (err) {
logging.getLogger(logging.Type.BROWSER).severe(`Failed parse message: ${err.message}`)
}
}
const handler = this._wsConnection.on('message', listener)
})
}
}
exports.CdpConnection = CDPConnection

View File

@@ -0,0 +1,89 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
class HttpResponse {
/**
* Creates a HTTP Response that will be used to
* mock out network interceptions.
* @param {*} urlToIntercept
*/
constructor(urlToIntercept = '') {
this.returnBody = ''
this.returnHeaders = []
this.returnMethod = 'GET'
this.returnStatus = 200
this.urlToIntercept = urlToIntercept
}
/**
* Add headers that will be returned when we intercept
* a HTTP Request
* @param {*} header
* @param {*} value
*/
addHeaders(header, value) {
this.returnHeaders.push({ name: header, value: value })
}
get headers() {
return this.returnHeaders
}
/**
* Set the STATUS value of the returned HTTP Request
* @param {*} value
*/
set status(value) {
// Add in check that his should be a number
this.returnStatus = value
}
get status() {
return this.returnStatus
}
/**
* Sets the value of the body of the HTTP Request that
* will be returned.
* @param {*} value
*/
set body(value) {
this.returnBody = value
}
get body() {
let buff = Buffer.from(this.returnBody, 'utf-8')
return buff.toString('base64')
}
/**
* Sets the method of the HTTP Request
* @param {*} value the method of the request.
*/
set method(value) {
this.returnMethod = value
}
/**
* Returns the Method to be used in the intercept
*/
get method() {
return this.returnMethod
}
}
exports.HttpResponse = HttpResponse

View File

@@ -0,0 +1,180 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Defines a {@linkplain Driver WebDriver} client for
* Microsoft's Edge web browser. Edge (Chromium) is supported and support
* for Edge Legacy (EdgeHTML) as part of https://github.com/SeleniumHQ/selenium/issues/9166.
* Before using this module, you must download and install the correct
* [WebDriver](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/) server.
*
* Ensure that the msedgedriver (Chromium)
* is on your [PATH](http://en.wikipedia.org/wiki/PATH_%28variable%29).
*
* You may use {@link Options} to specify whether Edge Chromium options should be used:
* const edge = require('selenium-webdriver/edge');
* const options = new edge.Options();
* There are three primary classes exported by this module:
*
* 1. {@linkplain ServiceBuilder}: configures the
* {@link ./remote.DriverService remote.DriverService}
* that manages the [WebDriver] child process.
*
* 2. {@linkplain Options}: defines configuration options for each new
* WebDriver session, such as which
* {@linkplain Options#setProxy proxy} to use when starting the browser.
*
* 3. {@linkplain Driver}: the WebDriver client; each new instance will control
* a unique browser session.
*
* __Customizing the WebDriver Server__ <a id="custom-server"></a>
*
* By default, every MicrosoftEdge session will use a single driver service,
* which is started the first time a {@link Driver} instance is created and
* terminated when this process exits. The default service will inherit its
* environment from the current process.
* You may obtain a handle to this default service using
* {@link #getDefaultService getDefaultService()} and change its configuration
* with {@link #setDefaultService setDefaultService()}.
*
* You may also create a {@link Driver} with its own driver service. This is
* useful if you need to capture the server's log output for a specific session:
*
* const edge = require('selenium-webdriver/edge');
*
* const service = new edge.ServiceBuilder()
* .setPort(55555)
* .build();
*
* let options = new edge.Options();
* // configure browser options ...
*
* let driver = edge.Driver.createSession(options, service);
*
* Users should only instantiate the {@link Driver} class directly when they
* need a custom driver service configuration (as shown above). For normal
* operation, users should start msedgedriver using the
* {@link ./builder.Builder selenium-webdriver.Builder}.
*
* [WebDriver (Chromium)]: https://docs.microsoft.com/en-us/microsoft-edge/webdriver-chromium
*
* @module selenium-webdriver/edge
*/
'use strict'
const { Browser } = require('./lib/capabilities')
const chromium = require('./chromium')
const EDGE_CAPABILITY_KEY = 'ms:edgeOptions'
/** @type {remote.DriverService} */
/**
* Creates {@link selenium-webdriver/remote.DriverService} instances that manage
* a [MSEdgeDriver](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/)
* server in a child process.
*/
class ServiceBuilder extends chromium.ServiceBuilder {
/**
* @param {string=} opt_exe Path to the server executable to use. If omitted,
* the builder will attempt to locate the msedgedriver on the current
* PATH.
* @throws {Error} If provided executable does not exist, or the msedgedriver
* cannot be found on the PATH.
*/
constructor(opt_exe) {
super(opt_exe)
this.setLoopback(true)
}
}
/**
* Class for managing edge chromium specific options.
*/
class Options extends chromium.Options {
/**
* Sets the path to the edge binary to use
*
* The binary path be absolute or relative to the msedgedriver server
* executable, but it must exist on the machine that will launch edge chromium.
*
* @param {string} path The path to the msedgedriver binary to use.
* @return {!Options} A self reference.
*/
setEdgeChromiumBinaryPath(path) {
return this.setBinaryPath(path)
}
/**
* Changes the browser name to 'webview2' to enable
* <a href="https://learn.microsoft.com/en-us/microsoft-edge/webview2/how-to/webdriver">
* test automation of WebView2 apps with Microsoft Edge WebDriver
* </a>
*
* @param {boolean} enable flag to enable or disable the 'webview2' usage
*/
useWebView(enable) {
const browserName = enable ? 'webview2' : Browser.EDGE
return this.setBrowserName(browserName)
}
}
/**
* Creates a new WebDriver client for Microsoft's Edge.
*/
class Driver extends chromium.Driver {
/**
* Creates a new browser session for Microsoft's Edge browser.
*
* @param {(Capabilities|Options)=} opt_config The configuration options.
* @param {remote.DriverService=} opt_serviceExecutor The service to use; will create
* a new Legacy or Chromium service based on {@linkplain Options} by default.
* @return {!Driver} A new driver instance.
*/
static createSession(opt_config, opt_serviceExecutor) {
let caps = opt_config || new Options()
return /** @type {!Driver} */ (super.createSession(caps, opt_serviceExecutor, 'ms', EDGE_CAPABILITY_KEY))
}
/**
* returns new instance of edge driver service
* @returns {remote.DriverService}
*/
static getDefaultService() {
return new ServiceBuilder().build()
}
/**
* This function is a no-op as file detectors are not supported by this
* implementation.
* @override
*/
setFileDetector() {}
}
Options.prototype.BROWSER_NAME_VALUE = Browser.EDGE
Options.prototype.CAPABILITY_KEY = EDGE_CAPABILITY_KEY
// PUBLIC API
module.exports = {
Driver,
Options,
ServiceBuilder,
}

View File

@@ -0,0 +1,107 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const globals = require('globals')
const noOnlyTests = require('eslint-plugin-no-only-tests')
const js = require('@eslint/js')
const eslintPluginPrettierRecommended = require('eslint-plugin-prettier/recommended')
const mochaPlugin = require('eslint-plugin-mocha')
const nodePlugin = require('eslint-plugin-n')
module.exports = [
js.configs.recommended,
eslintPluginPrettierRecommended,
mochaPlugin.configs.flat.recommended,
nodePlugin.configs['flat/recommended-script'],
{
languageOptions: {
globals: {
mocha: true,
es6: true,
...globals.node,
},
parserOptions: {
ecmaVersion: 2022,
},
},
files: ['**/*.js', 'lib/http.js'],
ignores: ['node_modules/*', 'generator/*', 'devtools/generator/'],
plugins: {
'no-only-tests': noOnlyTests,
},
rules: {
'no-const-assign': 'error',
'no-this-before-super': 'error',
'no-undef': 'error',
'no-unreachable': 'error',
'no-unused-vars': [
'error',
{
varsIgnorePattern: '^_',
args: 'all',
argsIgnorePattern: '^_',
},
],
'constructor-super': 'error',
'valid-typeof': 'error',
'no-only-tests/no-only-tests': 'error',
'n/no-deprecated-api': ['error'],
'n/no-missing-import': ['error'],
'n/no-missing-require': ['error'],
'n/no-mixed-requires': ['error'],
'n/no-new-require': ['error'],
'n/no-unpublished-import': ['error'],
'n/no-unpublished-require': [
'error',
{
allowModules: [
'globals',
'@eslint/js',
'eslint-plugin-mocha',
'eslint-plugin-prettier',
'eslint-plugin-n',
'eslint-plugin-no-only-tests',
],
tryExtensions: ['.js'],
},
],
'n/prefer-node-protocol': ['error'],
'mocha/no-skipped-tests': ['off'],
'mocha/no-mocha-arrows': ['off'],
'mocha/no-setup-in-describe': ['off'],
'mocha/no-top-level-hooks': ['off'],
'mocha/no-sibling-hooks': ['off'],
'mocha/no-exports': ['off'],
'mocha/no-empty-description': ['off'],
'mocha/max-top-level-suites': ['off'],
'mocha/consistent-spacing-between-blocks': ['off'],
'mocha/no-nested-tests': ['off'],
'mocha/no-pending-tests': ['off'],
'mocha/no-identical-title': ['off'],
'prettier/prettier': [
'error',
{
endOfLine: 'lf',
printWidth: 120,
semi: false,
singleQuote: true,
trailingComma: 'all',
},
],
},
},
]

View File

@@ -0,0 +1,42 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview A basic example of working with Chrome on Android. Before
* running this example, you must start adb and connect a device (or start an
* AVD).
*/
'use strict'
const { Builder, By, Key, until } = require('..')
const { Options } = require('../chrome')
;(async function () {
let driver
try {
driver = await new Builder().forBrowser('chrome').setChromeOptions(new Options().androidChrome()).build()
await driver.get('http://www.google.com/ncr')
await driver.findElement(By.name('q')).sendKeys('webdriver', Key.RETURN)
await driver.wait(until.titleIs('webdriver - Google Search'), 1000)
} finally {
;(await driver) && driver.quit()
}
})().then(
(_) => console.log('SUCCESS'),
(err) => console.error('ERROR: ' + err),
)

View File

@@ -0,0 +1,44 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview This is an example of emulating a mobile device using the
* ChromeDriver.
*/
'use strict'
const { Builder, By, Key, until } = require('..')
const { Options } = require('../chrome')
;(async function () {
let driver
try {
driver = await new Builder()
.forBrowser('chrome')
.setChromeOptions(new Options().setMobileEmulation({ deviceName: 'Nexus 5X' }))
.build()
await driver.get('http://www.google.com/ncr')
await driver.findElement(By.name('q')).sendKeys('webdriver', Key.RETURN)
await driver.wait(until.titleIs('webdriver - Google Search'), 1000)
} finally {
;(await driver) && driver.quit()
}
})().then(
(_) => console.log('SUCCESS'),
(err) => console.error('ERROR: ' + err),
)

View File

@@ -0,0 +1,48 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview An example WebDriver script.
*
* Before running this script, ensure that Mozilla's geckodriver is present on
* your system PATH: <https://github.com/mozilla/geckodriver/releases>
*
* Usage:
* // Default behavior
* node selenium-webdriver/example/google_search.js
*
* // Target Chrome locally; the chromedriver must be on your PATH
* SELENIUM_BROWSER=chrome node selenium-webdriver/example/google_search.js
*
* // Use a local copy of the standalone Selenium server
* SELENIUM_SERVER_JAR=/path/to/selenium-server-standalone.jar \
* node selenium-webdriver/example/google_search.js
*
* // Target a remote Selenium server
* SELENIUM_REMOTE_URL=http://www.example.com:4444/wd/hub \
* node selenium-webdriver/example/google_search.js
*/
const { Builder, By, Key, until } = require('..')
const driver = new Builder().forBrowser('firefox').build()
driver
.get('http://www.google.com/ncr')
.then((_) => driver.findElement(By.name('q')).sendKeys('webdriver', Key.RETURN))
.then((_) => driver.wait(until.titleIs('webdriver - Google Search'), 1000))
.then((_) => driver.quit())

View File

@@ -0,0 +1,70 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview An example test that may be run using Mocha.
*
* This example uses the `selenium-webdriver/testing.suite` function, which will
* automatically run tests against every available WebDriver browser on the
* current system. Alternatively, you may use the `SELENIUM_BROWSER`
* environment variable to narrow the scope at runtime.
*
* Usage:
*
* # Automatically determine which browsers to run against.
* mocha -t 10000 selenium-webdriver/example/google_search_test.js
*
* # Configure tests to only run against Google Chrome.
* SELENIUM_BROWSER=chrome \
* mocha -t 10000 selenium-webdriver/example/google_search_test.js
*/
const { Browser, By, Key, until } = require('..')
const { ignore, suite } = require('../testing')
suite(function (env) {
describe('Google Search', function () {
let driver
before(async function () {
// env.builder() returns a Builder instance preconfigured for the
// envrionment's target browser (you may still define browser specific
// options if necessary (i.e. firefox.Options or chrome.Options)).
driver = await env.builder().build()
})
it('demo', async function () {
await driver.get('https://www.google.com/ncr')
await driver.findElement(By.name('q')).sendKeys('webdriver', Key.RETURN)
await driver.wait(until.titleIs('webdriver - Google Search'), 1000)
})
// The ignore function returns wrappers around describe & it that will
// suppress tests if the provided predicate returns true. You may provide
// any synchronous predicate. The env.browsers(...) function generates a
// predicate that will suppress tests if the env targets one of the
// specified browsers.
//
// This example is always configured to skip Chrome.
ignore(env.browsers(Browser.CHROME)).it('demo 2', async function () {
await driver.get('https://www.google.com/ncr')
await driver.wait(until.urlIs('https://www.google.com/'), 1500)
})
after(() => driver && driver.quit())
})
})

View File

@@ -0,0 +1,57 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview An example of running Chrome or Firefox in headless mode.
*
* To run with Chrome, ensure you have Chrome 59+ installed and that
* chromedriver 2.30+ is present on your system PATH:
* <https://chromedriver.chromium.org/downloads>
*
* SELENIUM_BROWSER=chrome node selenium-webdriver/example/headless.js
*
* To run with Firefox, ensure you have Firefox 57+ installed and that
* geckodriver 0.19.0+ is present on your system PATH:
* <https://github.com/mozilla/geckodriver/releases>
*
* SELENIUM_BROWSER=firefox node selenium-webdriver/example/headless.js
*/
const chrome = require('../chrome')
const firefox = require('../firefox')
const { Builder, By, Key, until } = require('..')
const width = 640
const height = 480
let driver = new Builder()
.forBrowser('chrome')
.setChromeOptions(new chrome.Options().addArguments('-headless').windowSize({ width, height }))
.setFirefoxOptions(new firefox.Options().addArguments('-headless').windowSize({ width, height }))
.build()
driver
.get('http://www.google.com/ncr')
.then((_) => driver.findElement(By.name('q')).sendKeys('webdriver', Key.RETURN))
.then((_) => driver.wait(until.titleIs('webdriver - Google Search'), 1000))
.then(
(_) => driver.quit(),
(e) =>
driver.quit().then(() => {
throw e
}),
)

View File

@@ -0,0 +1,58 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Demonstrates how to use WebDriver's logging sysem.
*/
'use strict'
const chrome = require('../chrome')
const firefox = require('../firefox')
const edge = require('../edge')
const { Builder, By, Key, logging, until } = require('..')
logging.installConsoleHandler()
logging.getLogger(`${logging.Type.DRIVER}.http`).setLevel(logging.Level.ALL)
;(async function () {
let driver
try {
driver = await new Builder()
// Default to using Firefox. This can be overridden at runtime by
// setting the SELENIUM_BROWSER environment variable:
//
// SELENIUM_BROWSER=chrome node selenium-webdriver/example/logging.js
.forBrowser('firefox')
// Configure the service for each browser to enable verbose logging and
// to inherit the stdio settings from the current process. The builder
// will only start the service if needed for the selected browser.
.setChromeService(new chrome.ServiceBuilder().enableVerboseLogging().setStdio('inherit'))
.setEdgeService(
process.platform === 'win32' ? new edge.ServiceBuilder().enableVerboseLogging().setStdio('inherit') : null,
)
.setFirefoxService(new firefox.ServiceBuilder().enableVerboseLogging().setStdio('inherit'))
.build()
await driver.get('http://www.google.com/ncr')
await driver.findElement(By.name('q')).sendKeys('webdriver', Key.RETURN)
await driver.wait(until.titleIs('webdriver - Google Search'), 1000)
} finally {
if (driver) {
await driver.quit()
}
}
})()

View File

@@ -0,0 +1,776 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Defines the {@linkplain Driver WebDriver} client for Firefox.
* Before using this module, you must download the latest
* [geckodriver release] and ensure it can be found on your system [PATH].
*
* Each FirefoxDriver instance will be created with an anonymous profile,
* ensuring browser historys do not share session data (cookies, history, cache,
* offline storage, etc.)
*
* __Customizing the Firefox Profile__
*
* The profile used for each WebDriver session may be configured using the
* {@linkplain Options} class. For example, you may install an extension, like
* Firebug:
*
* const {Builder} = require('selenium-webdriver');
* const firefox = require('selenium-webdriver/firefox');
*
* let options = new firefox.Options()
* .addExtensions('/path/to/firebug.xpi')
* .setPreference('extensions.firebug.showChromeErrors', true);
*
* let driver = new Builder()
* .forBrowser('firefox')
* .setFirefoxOptions(options)
* .build();
*
* The {@linkplain Options} class may also be used to configure WebDriver based
* on a pre-existing browser profile:
*
* let profile = '/usr/local/home/bob/.mozilla/firefox/3fgog75h.testing';
* let options = new firefox.Options().setProfile(profile);
*
* The FirefoxDriver will _never_ modify a pre-existing profile; instead it will
* create a copy for it to modify. By extension, there are certain browser
* preferences that are required for WebDriver to function properly and they
* will always be overwritten.
*
* __Using a Custom Firefox Binary__
*
* On Windows and MacOS, the FirefoxDriver will search for Firefox in its
* default installation location:
*
* - Windows: C:\Program Files and C:\Program Files (x86).
* - MacOS: /Applications/Firefox.app
*
* For Linux, Firefox will always be located on the PATH: `$(where firefox)`.
*
* You can provide a custom location for Firefox by setting the binary in the
* {@link Options}:setBinary method.
*
* const {Builder} = require('selenium-webdriver');
* const firefox = require('selenium-webdriver/firefox');
*
* let options = new firefox.Options()
* .setBinary('/my/firefox/install/dir/firefox');
* let driver = new Builder()
* .forBrowser('firefox')
* .setFirefoxOptions(options)
* .build();
*
* __Remote Testing__
*
* You may customize the Firefox binary and profile when running against a
* remote Selenium server. Your custom profile will be packaged as a zip and
* transferred to the remote host for use. The profile will be transferred
* _once for each new session_. The performance impact should be minimal if
* you've only configured a few extra browser preferences. If you have a large
* profile with several extensions, you should consider installing it on the
* remote host and defining its path via the {@link Options} class. Custom
* binaries are never copied to remote machines and must be referenced by
* installation path.
*
* const {Builder} = require('selenium-webdriver');
* const firefox = require('selenium-webdriver/firefox');
*
* let options = new firefox.Options()
* .setProfile('/profile/path/on/remote/host')
* .setBinary('/install/dir/on/remote/host/firefox');
*
* let driver = new Builder()
* .forBrowser('firefox')
* .usingServer('http://127.0.0.1:4444/wd/hub')
* .setFirefoxOptions(options)
* .build();
*
* [geckodriver release]: https://github.com/mozilla/geckodriver/releases/
* [PATH]: http://en.wikipedia.org/wiki/PATH_%28variable%29
*
* @module selenium-webdriver/firefox
*/
'use strict'
const fs = require('node:fs')
const path = require('node:path')
const Symbols = require('./lib/symbols')
const command = require('./lib/command')
const http = require('./http')
const io = require('./io')
const remote = require('./remote')
const webdriver = require('./lib/webdriver')
const zip = require('./io/zip')
const { Browser, Capabilities, Capability } = require('./lib/capabilities')
const { Zip } = require('./io/zip')
const { getBinaryPaths } = require('./common/driverFinder')
const FIREFOX_CAPABILITY_KEY = 'moz:firefoxOptions'
/**
* Thrown when there an add-on is malformed.
* @final
*/
class AddonFormatError extends Error {
/** @param {string} msg The error message. */
constructor(msg) {
super(msg)
/** @override */
this.name = this.constructor.name
}
}
/**
* Installs an extension to the given directory.
* @param {string} extension Path to the xpi extension file to install.
* @param {string} dir Path to the directory to install the extension in.
* @return {!Promise<string>} A promise for the add-on ID once
* installed.
*/
async function installExtension(extension, dir) {
const ext = extension.slice(-4)
if (ext !== '.xpi' && ext !== '.zip') {
throw Error('File name does not end in ".zip" or ".xpi": ' + ext)
}
let archive = await zip.load(extension)
if (!archive.has('manifest.json')) {
throw new AddonFormatError(`Couldn't find manifest.json in ${extension}`)
}
let buf = await archive.getFile('manifest.json')
let parsedJSON = JSON.parse(buf.toString('utf8'))
let { browser_specific_settings } =
/** @type {{browser_specific_settings:{gecko:{id:string}}}} */
parsedJSON
if (browser_specific_settings && browser_specific_settings.gecko) {
/* browser_specific_settings is an alternative to applications
* It is meant to facilitate cross-browser plugins since Firefox48
* see https://bugzilla.mozilla.org/show_bug.cgi?id=1262005
*/
parsedJSON.applications = browser_specific_settings
}
let { applications } =
/** @type {{applications:{gecko:{id:string}}}} */
parsedJSON
if (!(applications && applications.gecko && applications.gecko.id)) {
throw new AddonFormatError(`Could not find add-on ID for ${extension}`)
}
await io.copy(extension, `${path.join(dir, applications.gecko.id)}.xpi`)
return applications.gecko.id
}
class Profile {
constructor() {
/** @private {?string} */
this.template_ = null
/** @private {!Array<string>} */
this.extensions_ = []
}
addExtensions(/** !Array<string> */ paths) {
this.extensions_ = this.extensions_.concat(...paths)
}
/**
* @return {(!Promise<string>|undefined)} a promise for a base64 encoded
* profile, or undefined if there's no data to include.
*/
[Symbols.serialize]() {
if (this.template_ || this.extensions_.length) {
return buildProfile(this.template_, this.extensions_)
}
return undefined
}
}
/**
* @param {?string} template path to an existing profile to use as a template.
* @param {!Array<string>} extensions paths to extensions to install in the new
* profile.
* @return {!Promise<string>} a promise for the base64 encoded profile.
*/
async function buildProfile(template, extensions) {
let dir = template
if (extensions.length) {
dir = await io.tmpDir()
if (template) {
await io.copyDir(/** @type {string} */ (template), dir, /(parent\.lock|lock|\.parentlock)/)
}
const extensionsDir = path.join(dir, 'extensions')
await io.mkdir(extensionsDir)
for (let i = 0; i < extensions.length; i++) {
await installExtension(extensions[i], extensionsDir)
}
}
let zip = new Zip()
return zip
.addDir(dir)
.then(() => zip.toBuffer())
.then((buf) => buf.toString('base64'))
}
/**
* Configuration options for the FirefoxDriver.
*/
class Options extends Capabilities {
/**
* @param {(Capabilities|Map<string, ?>|Object)=} other Another set of
* capabilities to initialize this instance from.
*/
constructor(other) {
super(other)
this.setBrowserName(Browser.FIREFOX)
// Firefox 129 onwards the CDP protocol will not be enabled by default. Setting this preference will enable it.
// https://fxdx.dev/deprecating-cdp-support-in-firefox-embracing-the-future-with-webdriver-bidi/.
this.setPreference('remote.active-protocols', 3)
}
/**
* @return {!Object}
* @private
*/
firefoxOptions_() {
let options = this.get(FIREFOX_CAPABILITY_KEY)
if (!options) {
options = {}
this.set(FIREFOX_CAPABILITY_KEY, options)
}
return options
}
/**
* @return {!Profile}
* @private
*/
profile_() {
let options = this.firefoxOptions_()
if (!options.profile) {
options.profile = new Profile()
}
return options.profile
}
/**
* Specify additional command line arguments that should be used when starting
* the Firefox browser.
*
* @param {...(string|!Array<string>)} args The arguments to include.
* @return {!Options} A self reference.
*/
addArguments(...args) {
if (args.length) {
let options = this.firefoxOptions_()
options.args = options.args ? options.args.concat(...args) : args
}
return this
}
/**
* Sets the initial window size
*
* @param {{width: number, height: number}} size The desired window size.
* @return {!Options} A self reference.
* @throws {TypeError} if width or height is unspecified, not a number, or
* less than or equal to 0.
*/
windowSize({ width, height }) {
function checkArg(arg) {
if (typeof arg !== 'number' || arg <= 0) {
throw TypeError('Arguments must be {width, height} with numbers > 0')
}
}
checkArg(width)
checkArg(height)
return this.addArguments(`--width=${width}`, `--height=${height}`)
}
/**
* Add extensions that should be installed when starting Firefox.
*
* @param {...string} paths The paths to the extension XPI files to install.
* @return {!Options} A self reference.
*/
addExtensions(...paths) {
this.profile_().addExtensions(paths)
return this
}
/**
* @param {string} key the preference key.
* @param {(string|number|boolean)} value the preference value.
* @return {!Options} A self reference.
* @throws {TypeError} if either the key or value has an invalid type.
*/
setPreference(key, value) {
if (typeof key !== 'string') {
throw TypeError(`key must be a string, but got ${typeof key}`)
}
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
throw TypeError(`value must be a string, number, or boolean, but got ${typeof value}`)
}
let options = this.firefoxOptions_()
options.prefs = options.prefs || {}
options.prefs[key] = value
return this
}
/**
* Sets the path to an existing profile to use as a template for new browser
* sessions. This profile will be copied for each new session - changes will
* not be applied to the profile itself.
*
* @param {string} profile The profile to use.
* @return {!Options} A self reference.
* @throws {TypeError} if profile is not a string.
*/
setProfile(profile) {
if (typeof profile !== 'string') {
throw TypeError(`profile must be a string, but got ${typeof profile}`)
}
this.profile_().template_ = profile
return this
}
/**
* Sets the binary to use. The binary may be specified as the path to a
* Firefox executable.
*
* @param {(string)} binary The binary to use.
* @return {!Options} A self reference.
* @throws {TypeError} If `binary` is an invalid type.
*/
setBinary(binary) {
if (binary instanceof Channel || typeof binary === 'string') {
this.firefoxOptions_().binary = binary
return this
}
throw TypeError('binary must be a string path ')
}
/**
* Enables Mobile start up features
*
* @param {string} androidPackage The package to use
* @return {!Options} A self reference
*/
enableMobile(androidPackage = 'org.mozilla.firefox', androidActivity = null, deviceSerial = null) {
this.firefoxOptions_().androidPackage = androidPackage
if (androidActivity) {
this.firefoxOptions_().androidActivity = androidActivity
}
if (deviceSerial) {
this.firefoxOptions_().deviceSerial = deviceSerial
}
return this
}
/**
* Enables moz:debuggerAddress for firefox cdp
*/
enableDebugger() {
return this.set('moz:debuggerAddress', true)
}
/**
* Enable bidi connection
* @returns {!Capabilities}
*/
enableBidi() {
return this.set('webSocketUrl', true)
}
}
/**
* Enum of available command contexts.
*
* Command contexts are specific to Marionette, and may be used with the
* {@link #context=} method. Contexts allow you to direct all subsequent
* commands to either "content" (default) or "chrome". The latter gives
* you elevated security permissions.
*
* @enum {string}
*/
const Context = {
CONTENT: 'content',
CHROME: 'chrome',
}
/**
* @param {string} file Path to the file to find, relative to the program files
* root.
* @return {!Promise<?string>} A promise for the located executable.
* The promise will resolve to {@code null} if Firefox was not found.
*/
function findInProgramFiles(file) {
let files = [
process.env['PROGRAMFILES'] || 'C:\\Program Files',
process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)',
].map((prefix) => path.join(prefix, file))
return io.exists(files[0]).then(function (exists) {
return exists
? files[0]
: io.exists(files[1]).then(function (exists) {
return exists ? files[1] : null
})
})
}
/** @enum {string} */
const ExtensionCommand = {
GET_CONTEXT: 'getContext',
SET_CONTEXT: 'setContext',
INSTALL_ADDON: 'install addon',
UNINSTALL_ADDON: 'uninstall addon',
FULL_PAGE_SCREENSHOT: 'fullPage screenshot',
}
/**
* Creates a command executor with support for Marionette's custom commands.
* @param {!Promise<string>} serverUrl The server's URL.
* @return {!command.Executor} The new command executor.
*/
function createExecutor(serverUrl) {
let client = serverUrl.then((url) => new http.HttpClient(url))
let executor = new http.Executor(client)
configureExecutor(executor)
return executor
}
/**
* Configures the given executor with Firefox-specific commands.
* @param {!http.Executor} executor the executor to configure.
*/
function configureExecutor(executor) {
executor.defineCommand(ExtensionCommand.GET_CONTEXT, 'GET', '/session/:sessionId/moz/context')
executor.defineCommand(ExtensionCommand.SET_CONTEXT, 'POST', '/session/:sessionId/moz/context')
executor.defineCommand(ExtensionCommand.INSTALL_ADDON, 'POST', '/session/:sessionId/moz/addon/install')
executor.defineCommand(ExtensionCommand.UNINSTALL_ADDON, 'POST', '/session/:sessionId/moz/addon/uninstall')
executor.defineCommand(ExtensionCommand.FULL_PAGE_SCREENSHOT, 'GET', '/session/:sessionId/moz/screenshot/full')
}
/**
* Creates {@link selenium-webdriver/remote.DriverService} instances that manage
* a [geckodriver](https://github.com/mozilla/geckodriver) server in a child
* process.
*/
class ServiceBuilder extends remote.DriverService.Builder {
/**
* @param {string=} opt_exe Path to the server executable to use. If omitted,
* the builder will attempt to locate the geckodriver on the system PATH.
*/
constructor(opt_exe) {
super(opt_exe)
this.setLoopback(true) // Required.
}
/**
* Enables verbose logging.
*
* @param {boolean=} opt_trace Whether to enable trace-level logging. By
* default, only debug logging is enabled.
* @return {!ServiceBuilder} A self reference.
*/
enableVerboseLogging(opt_trace) {
return this.addArguments(opt_trace ? '-vv' : '-v')
}
}
/**
* A WebDriver client for Firefox.
*/
class Driver extends webdriver.WebDriver {
/**
* Creates a new Firefox session.
*
* @param {(Options|Capabilities|Object)=} opt_config The
* configuration options for this driver, specified as either an
* {@link Options} or {@link Capabilities}, or as a raw hash object.
* @param {(http.Executor|remote.DriverService)=} opt_executor Either a
* pre-configured command executor to use for communicating with an
* externally managed remote end (which is assumed to already be running),
* or the `DriverService` to use to start the geckodriver in a child
* process.
*
* If an executor is provided, care should e taken not to use reuse it with
* other clients as its internal command mappings will be updated to support
* Firefox-specific commands.
*
* _This parameter may only be used with Mozilla's GeckoDriver._
*
* @throws {Error} If a custom command executor is provided and the driver is
* configured to use the legacy FirefoxDriver from the Selenium project.
* @return {!Driver} A new driver instance.
*/
static createSession(opt_config, opt_executor) {
let caps = opt_config instanceof Capabilities ? opt_config : new Options(opt_config)
let firefoxBrowserPath = null
let executor
let onQuit
if (opt_executor instanceof http.Executor) {
executor = opt_executor
configureExecutor(executor)
} else if (opt_executor instanceof remote.DriverService) {
if (!opt_executor.getExecutable()) {
const { driverPath, browserPath } = getBinaryPaths(caps)
opt_executor.setExecutable(driverPath)
firefoxBrowserPath = browserPath
}
executor = createExecutor(opt_executor.start())
onQuit = () => opt_executor.kill()
} else {
let service = new ServiceBuilder().build()
if (!service.getExecutable()) {
const { driverPath, browserPath } = getBinaryPaths(caps)
service.setExecutable(driverPath)
firefoxBrowserPath = browserPath
}
executor = createExecutor(service.start())
onQuit = () => service.kill()
}
if (firefoxBrowserPath) {
const vendorOptions = caps.get(FIREFOX_CAPABILITY_KEY)
if (vendorOptions) {
vendorOptions['binary'] = firefoxBrowserPath
caps.set(FIREFOX_CAPABILITY_KEY, vendorOptions)
} else {
caps.set(FIREFOX_CAPABILITY_KEY, { binary: firefoxBrowserPath })
}
caps.delete(Capability.BROWSER_VERSION)
}
return /** @type {!Driver} */ (super.createSession(executor, caps, onQuit))
}
/**
* This function is a no-op as file detectors are not supported by this
* implementation.
* @override
*/
setFileDetector() {}
/**
* Get the context that is currently in effect.
*
* @return {!Promise<Context>} Current context.
*/
getContext() {
return this.execute(new command.Command(ExtensionCommand.GET_CONTEXT))
}
/**
* Changes target context for commands between chrome- and content.
*
* Changing the current context has a stateful impact on all subsequent
* commands. The {@link Context.CONTENT} context has normal web
* platform document permissions, as if you would evaluate arbitrary
* JavaScript. The {@link Context.CHROME} context gets elevated
* permissions that lets you manipulate the browser chrome itself,
* with full access to the XUL toolkit.
*
* Use your powers wisely.
*
* @param {!Promise<void>} ctx The context to switch to.
*/
setContext(ctx) {
return this.execute(new command.Command(ExtensionCommand.SET_CONTEXT).setParameter('context', ctx))
}
/**
* Installs a new addon with the current session. This function will return an
* ID that may later be used to {@linkplain #uninstallAddon uninstall} the
* addon.
*
*
* @param {string} path Path on the local filesystem to the web extension to
* install.
* @param {boolean} temporary Flag indicating whether the extension should be
* installed temporarily - gets removed on restart
* @return {!Promise<string>} A promise that will resolve to an ID for the
* newly installed addon.
* @see #uninstallAddon
*/
async installAddon(path, temporary = false) {
let stats = fs.statSync(path)
let buf
if (stats.isDirectory()) {
let zip = new Zip()
await zip.addDir(path)
buf = await zip.toBuffer('DEFLATE')
} else {
buf = await io.read(path)
}
return this.execute(
new command.Command(ExtensionCommand.INSTALL_ADDON)
.setParameter('addon', buf.toString('base64'))
.setParameter('temporary', temporary),
)
}
/**
* Uninstalls an addon from the current browser session's profile.
*
* @param {(string|!Promise<string>)} id ID of the addon to uninstall.
* @return {!Promise} A promise that will resolve when the operation has
* completed.
* @see #installAddon
*/
async uninstallAddon(id) {
id = await Promise.resolve(id)
return this.execute(new command.Command(ExtensionCommand.UNINSTALL_ADDON).setParameter('id', id))
}
/**
* Take full page screenshot of the visible region
*
* @return {!Promise<string>} A promise that will be
* resolved to the screenshot as a base-64 encoded PNG.
*/
takeFullPageScreenshot() {
return this.execute(new command.Command(ExtensionCommand.FULL_PAGE_SCREENSHOT))
}
}
/**
* Provides methods for locating the executable for a Firefox release channel
* on Windows and MacOS. For other systems (i.e. Linux), Firefox will always
* be located on the system PATH.
* @deprecated Instead of using this class, you should configure the
* {@link Options} with the appropriate binary location or let Selenium
* Manager handle it for you.
* @final
*/
class Channel {
/**
* @param {string} darwin The path to check when running on MacOS.
* @param {string} win32 The path to check when running on Windows.
*/
constructor(darwin, win32) {
/** @private @const */ this.darwin_ = darwin
/** @private @const */ this.win32_ = win32
/** @private {Promise<string>} */
this.found_ = null
}
/**
* Attempts to locate the Firefox executable for this release channel. This
* will first check the default installation location for the channel before
* checking the user's PATH. The returned promise will be rejected if Firefox
* can not be found.
*
* @return {!Promise<string>} A promise for the location of the located
* Firefox executable.
*/
locate() {
if (this.found_) {
return this.found_
}
let found
switch (process.platform) {
case 'darwin':
found = io.exists(this.darwin_).then((exists) => (exists ? this.darwin_ : io.findInPath('firefox')))
break
case 'win32':
found = findInProgramFiles(this.win32_).then((found) => found || io.findInPath('firefox.exe'))
break
default:
found = Promise.resolve(io.findInPath('firefox'))
break
}
this.found_ = found.then((found) => {
if (found) {
// TODO: verify version info.
return found
}
throw Error('Could not locate Firefox on the current system')
})
return this.found_
}
/** @return {!Promise<string>} */
[Symbols.serialize]() {
return this.locate()
}
}
/**
* Firefox's developer channel.
* @const
* @see <https://www.mozilla.org/en-US/firefox/channel/desktop/#developer>
*/
Channel.DEV = new Channel(
'/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox',
'Firefox Developer Edition\\firefox.exe',
)
/**
* Firefox's beta channel. Note this is provided mainly for convenience as
* the beta channel has the same installation location as the main release
* channel.
* @const
* @see <https://www.mozilla.org/en-US/firefox/channel/desktop/#beta>
*/
Channel.BETA = new Channel('/Applications/Firefox.app/Contents/MacOS/firefox', 'Mozilla Firefox\\firefox.exe')
/**
* Firefox's release channel.
* @const
* @see <https://www.mozilla.org/en-US/firefox/desktop/>
*/
Channel.RELEASE = new Channel('/Applications/Firefox.app/Contents/MacOS/firefox', 'Mozilla Firefox\\firefox.exe')
/**
* Firefox's nightly release channel.
* @const
* @see <https://www.mozilla.org/en-US/firefox/channel/desktop/#nightly>
*/
Channel.NIGHTLY = new Channel('/Applications/Firefox Nightly.app/Contents/MacOS/firefox', 'Nightly\\firefox.exe')
// PUBLIC API
module.exports = {
Channel,
Context,
Driver,
Options,
ServiceBuilder,
}

View File

@@ -0,0 +1,344 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Defines an {@linkplain cmd.Executor command executor} that
* communicates with a remote end using HTTP + JSON.
*/
'use strict'
const http = require('node:http')
const https = require('node:https')
const url = require('node:url')
const httpLib = require('../lib/http')
/**
* @typedef {{protocol: (?string|undefined),
* auth: (?string|undefined),
* hostname: (?string|undefined),
* host: (?string|undefined),
* port: (?string|undefined),
* path: (?string|undefined),
* pathname: (?string|undefined)}}
*/
let RequestOptions // eslint-disable-line
/**
* @param {string} aUrl The request URL to parse.
* @return {RequestOptions} The request options.
* @throws {Error} if the URL does not include a hostname.
*/
function getRequestOptions(aUrl) {
// eslint-disable-next-line n/no-deprecated-api
let options = url.parse(aUrl)
if (!options.hostname) {
throw new Error('Invalid URL: ' + aUrl)
}
// Delete the search and has portions as they are not used.
options.search = null
options.hash = null
options.path = options.pathname
options.hostname = options.hostname === 'localhost' ? '127.0.0.1' : options.hostname // To support Node 17 and above. Refer https://github.com/nodejs/node/issues/40702 for details.
return options
}
/** @const {string} */
const USER_AGENT = (function () {
const version = require('../package.json').version
const platform = { darwin: 'mac', win32: 'windows' }[process.platform] || 'linux'
return `selenium/${version} (js ${platform})`
})()
/**
* A basic HTTP client used to send messages to a remote end.
*
* @implements {httpLib.Client}
*/
class HttpClient {
/**
* @param {string} serverUrl URL for the WebDriver server to send commands to.
* @param {http.Agent=} opt_agent The agent to use for each request.
* Defaults to `http.globalAgent`.
* @param {?string=} opt_proxy The proxy to use for the connection to the
* server. Default is to use no proxy.
* @param {?Object.<string,Object>} client_options
*/
constructor(serverUrl, opt_agent, opt_proxy, client_options = {}) {
/** @private {http.Agent} */
this.agent_ = opt_agent || null
/**
* Base options for each request.
* @private {RequestOptions}
*/
this.options_ = getRequestOptions(serverUrl)
/**
* client options, header overrides
*/
this.client_options = client_options
/**
* sets keep-alive for the agent
* see https://stackoverflow.com/a/58332910
*/
this.keepAlive = this.client_options['keep-alive']
/** @private {?RequestOptions} */
this.proxyOptions_ = opt_proxy ? getRequestOptions(opt_proxy) : null
}
get keepAlive() {
return this.agent_.keepAlive
}
set keepAlive(value) {
if (value === 'true' || value === true) {
this.agent_.keepAlive = true
}
}
/** @override */
send(httpRequest) {
let data
let headers = {}
if (httpRequest.headers) {
httpRequest.headers.forEach(function (value, name) {
headers[name] = value
})
}
headers['User-Agent'] = this.client_options['user-agent'] || USER_AGENT
headers['Content-Length'] = 0
if (httpRequest.method == 'POST' || httpRequest.method == 'PUT') {
data = JSON.stringify(httpRequest.data)
headers['Content-Length'] = Buffer.byteLength(data, 'utf8')
headers['Content-Type'] = 'application/json;charset=UTF-8'
}
let path = this.options_.path
if (path.endsWith('/') && httpRequest.path.startsWith('/')) {
path += httpRequest.path.substring(1)
} else {
path += httpRequest.path
}
// eslint-disable-next-line n/no-deprecated-api
let parsedPath = url.parse(path)
let options = {
agent: this.agent_ || null,
method: httpRequest.method,
auth: this.options_.auth,
hostname: this.options_.hostname,
port: this.options_.port,
protocol: this.options_.protocol,
path: parsedPath.path,
pathname: parsedPath.pathname,
search: parsedPath.search,
hash: parsedPath.hash,
headers,
}
return new Promise((fulfill, reject) => {
sendRequest(options, fulfill, reject, data, this.proxyOptions_)
})
}
}
/**
* Sends a single HTTP request.
* @param {!Object} options The request options.
* @param {function(!httpLib.Response)} onOk The function to call if the
* request succeeds.
* @param {function(!Error)} onError The function to call if the request fails.
* @param {?string=} opt_data The data to send with the request.
* @param {?RequestOptions=} opt_proxy The proxy server to use for the request.
* @param {number=} opt_retries The current number of retries.
*/
function sendRequest(options, onOk, onError, opt_data, opt_proxy, opt_retries) {
var hostname = options.hostname
var port = options.port
if (opt_proxy) {
let proxy = /** @type {RequestOptions} */ (opt_proxy)
// RFC 2616, section 5.1.2:
// The absoluteURI form is REQUIRED when the request is being made to a
// proxy.
let absoluteUri = url.format(options)
// RFC 2616, section 14.23:
// An HTTP/1.1 proxy MUST ensure that any request message it forwards does
// contain an appropriate Host header field that identifies the service
// being requested by the proxy.
let targetHost = options.hostname
if (options.port) {
targetHost += ':' + options.port
}
// Update the request options with our proxy info.
options.headers['Host'] = targetHost
options.path = absoluteUri
options.host = proxy.host
options.hostname = proxy.hostname
options.port = proxy.port
// Update the protocol to avoid EPROTO errors when the webdriver proxy
// uses a different protocol from the remote selenium server.
options.protocol = opt_proxy.protocol
if (proxy.auth) {
options.headers['Proxy-Authorization'] = 'Basic ' + Buffer.from(proxy.auth).toString('base64')
}
}
let requestFn = options.protocol === 'https:' ? https.request : http.request
var request = requestFn(options, function onResponse(response) {
if (response.statusCode == 302 || response.statusCode == 303) {
let location
try {
// eslint-disable-next-line n/no-deprecated-api
location = url.parse(response.headers['location'])
} catch (ex) {
onError(
Error(
'Failed to parse "Location" header for server redirect: ' +
ex.message +
'\nResponse was: \n' +
new httpLib.Response(response.statusCode, response.headers, ''),
),
)
return
}
if (!location.hostname) {
location.hostname = hostname
location.port = port
location.auth = options.auth
}
request.destroy()
sendRequest(
{
method: 'GET',
protocol: location.protocol || options.protocol,
hostname: location.hostname,
port: location.port,
path: location.path,
auth: location.auth,
pathname: location.pathname,
search: location.search,
hash: location.hash,
headers: {
Accept: 'application/json; charset=utf-8',
'User-Agent': options.headers['User-Agent'] || USER_AGENT,
},
},
onOk,
onError,
undefined,
opt_proxy,
)
return
}
const body = []
response.on('data', body.push.bind(body))
response.on('end', function () {
const resp = new httpLib.Response(
/** @type {number} */ (response.statusCode),
/** @type {!Object<string>} */ (response.headers),
Buffer.concat(body).toString('utf8').replace(/\0/g, ''),
)
onOk(resp)
})
})
request.on('error', function (e) {
if (typeof opt_retries === 'undefined') {
opt_retries = 0
}
if (shouldRetryRequest(opt_retries, e)) {
opt_retries += 1
setTimeout(function () {
sendRequest(options, onOk, onError, opt_data, opt_proxy, opt_retries)
}, 15)
} else {
let message = e.message
if (e.code) {
message = e.code + ' ' + message
}
onError(new Error(message))
}
})
if (opt_data) {
request.write(opt_data)
}
request.end()
}
const MAX_RETRIES = 3
/**
* A retry is sometimes needed on Windows where we may quickly run out of
* ephemeral ports. A more robust solution is bumping the MaxUserPort setting
* as described here: http://msdn.microsoft.com/en-us/library/aa560610%28v=bts.20%29.aspx
*
* @param {!number} retries
* @param {!Error} err
* @return {boolean}
*/
function shouldRetryRequest(retries, err) {
return retries < MAX_RETRIES && isRetryableNetworkError(err)
}
/**
* @param {!Error} err
* @return {boolean}
*/
function isRetryableNetworkError(err) {
if (err && err.code) {
return (
err.code === 'ECONNABORTED' ||
err.code === 'ECONNRESET' ||
err.code === 'ECONNREFUSED' ||
err.code === 'EADDRINUSE' ||
err.code === 'EPIPE' ||
err.code === 'ETIMEDOUT'
)
}
return false
}
// PUBLIC API
module.exports.Agent = http.Agent
module.exports.Executor = httpLib.Executor
module.exports.HttpClient = HttpClient
module.exports.Request = httpLib.Request
module.exports.Response = httpLib.Response

View File

@@ -0,0 +1,167 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Various HTTP utilities.
*/
'use strict'
const Executor = require('./index').Executor
const HttpClient = require('./index').HttpClient
const HttpRequest = require('./index').Request
const Command = require('../lib/command').Command
const CommandName = require('../lib/command').Name
const error = require('../lib/error')
/**
* Queries a WebDriver server for its current status.
* @param {string} url Base URL of the server to query.
* @return {!Promise<!Object>} A promise that resolves with
* a hash of the server status.
*/
function getStatus(url) {
const client = new HttpClient(url)
const executor = new Executor(client)
const command = new Command(CommandName.GET_SERVER_STATUS)
return executor.execute(command)
}
class CancellationError {}
/**
* Waits for a WebDriver server to be healthy and accepting requests.
* @param {string} url Base URL of the server to query.
* @param {number} timeout How long to wait for the server.
* @param {Promise=} opt_cancelToken A promise used as a cancellation signal:
* if resolved before the server is ready, the wait will be terminated
* early with a {@link CancellationError}.
* @return {!Promise} A promise that will resolve when the server is ready, or
* if the wait is cancelled.
*/
function waitForServer(url, timeout, opt_cancelToken) {
return new Promise((onResolve, onReject) => {
let start = Date.now()
let done = false
let resolve = (status) => {
done = true
onResolve(status)
}
let reject = (err) => {
done = true
onReject(err)
}
if (opt_cancelToken) {
opt_cancelToken.then((_) => reject(new CancellationError()))
}
checkServerStatus()
function checkServerStatus() {
return getStatus(url).then((status) => resolve(status), onError)
}
function onError(e) {
// Some servers don't support the status command. If they are able to
// response with an error, then can consider the server ready.
if (e instanceof error.UnsupportedOperationError) {
resolve({})
return
}
if (Date.now() - start > timeout) {
reject(Error('Timed out waiting for the WebDriver server at ' + url))
} else {
setTimeout(function () {
if (!done) {
checkServerStatus()
}
}, 50)
}
}
})
}
/**
* Polls a URL with GET requests until it returns a 2xx response or the
* timeout expires.
* @param {string} url The URL to poll.
* @param {number} timeout How long to wait, in milliseconds.
* @param {Promise=} opt_cancelToken A promise used as a cancellation signal:
* if resolved before the a 2xx response is received, the wait will be
* terminated early with a {@link CancellationError}.
* @return {!Promise} A promise that will resolve when a 2xx is received from
* the given URL, or if the wait is cancelled.
*/
function waitForUrl(url, timeout, opt_cancelToken) {
return new Promise((onResolve, onReject) => {
let client = new HttpClient(url)
let request = new HttpRequest('GET', '')
let start = Date.now()
let done = false
let resolve = () => {
done = true
onResolve()
}
let reject = (err) => {
done = true
onReject(err)
}
if (opt_cancelToken) {
opt_cancelToken.then((_) => reject(new CancellationError()))
}
testUrl()
function testUrl() {
client.send(request).then(onResponse, onError)
}
function onError() {
if (Date.now() - start > timeout) {
reject(Error('Timed out waiting for the URL to return 2xx: ' + url))
} else {
setTimeout(function () {
if (!done) {
testUrl()
}
}, 50)
}
}
function onResponse(response) {
if (done) {
return
}
if (response.status > 199 && response.status < 300) {
resolve()
return
}
onError()
}
})
}
// PUBLIC API
module.exports.getStatus = getStatus
module.exports.CancellationError = CancellationError
module.exports.waitForServer = waitForServer
module.exports.waitForUrl = waitForUrl

View File

@@ -0,0 +1,481 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Defines a {@linkplain Driver WebDriver} client for Microsoft's
* Internet Explorer. Before using the IEDriver, you must download the latest
* [IEDriverServer](https://www.selenium.dev/downloads/)
* and place it on your
* [PATH](http://en.wikipedia.org/wiki/PATH_%28variable%29). You must also apply
* the system configuration outlined on the Selenium project
* [wiki](https://github.com/SeleniumHQ/selenium/wiki/InternetExplorerDriver)
*
* @module selenium-webdriver/ie
*/
'use strict'
const http = require('./http')
const portprober = require('./net/portprober')
const remote = require('./remote')
const webdriver = require('./lib/webdriver')
const { Browser, Capabilities } = require('./lib/capabilities')
const error = require('./lib/error')
const { getBinaryPaths } = require('./common/driverFinder')
const OPTIONS_CAPABILITY_KEY = 'se:ieOptions'
const SCROLL_BEHAVIOUR = {
BOTTOM: 1,
TOP: 0,
}
/**
* IEDriverServer logging levels.
* @enum {string}
*/
const Level = {
FATAL: 'FATAL',
ERROR: 'ERROR',
WARN: 'WARN',
INFO: 'INFO',
DEBUG: 'DEBUG',
TRACE: 'TRACE',
}
/**
* Option keys:
* https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities#ie-specific
* @enum {string}
*/
const Key = {
IGNORE_PROTECTED_MODE_SETTINGS: 'ignoreProtectedModeSettings',
IGNORE_ZOOM_SETTING: 'ignoreZoomSetting',
INITIAL_BROWSER_URL: 'initialBrowserUrl',
ENABLE_PERSISTENT_HOVER: 'enablePersistentHover',
ENABLE_ELEMENT_CACHE_CLEANUP: 'enableElementCacheCleanup',
ELEMENT_SCROLL_BEHAVIOR: 'elementScrollBehavior',
REQUIRE_WINDOW_FOCUS: 'requireWindowFocus',
BROWSER_ATTACH_TIMEOUT: 'browserAttachTimeout',
FORCE_CREATE_PROCESS: 'ie.forceCreateProcessApi',
BROWSER_COMMAND_LINE_SWITCHES: 'ie.browserCommandLineSwitches',
USE_PER_PROCESS_PROXY: 'ie.usePerProcessProxy',
ENSURE_CLEAN_SESSION: 'ie.ensureCleanSession',
LOG_FILE: 'logFile',
LOG_LEVEL: 'logLevel',
HOST: 'host',
EXTRACT_PATH: 'extractPath',
SILENT: 'silent',
FILE_UPLOAD_DIALOG_TIMEOUT: 'ie.fileUploadDialogTimeout',
ATTACH_TO_EDGE_CHROMIUM: 'ie.edgechromium',
EDGE_EXECUTABLE_PATH: 'ie.edgepath',
IGNORE_PROCESS_MATCH: 'ie.ignoreprocessmatch',
}
/**
* Class for managing IEDriver specific options.
*/
class Options extends Capabilities {
/**
* @param {(Capabilities|Map<string, ?>|Object)=} other Another set of
* capabilities to initialize this instance from.
*/
constructor(other = undefined) {
super(other)
/** @private {!Object} */
this.options_ = this.get(OPTIONS_CAPABILITY_KEY) || {}
this.set(OPTIONS_CAPABILITY_KEY, this.options_)
this.setBrowserName(Browser.INTERNET_EXPLORER)
}
/**
* Whether to disable the protected mode settings check when the session is
* created. Disabling this setting may lead to significant instability as the
* browser may become unresponsive/hang. Only "best effort" support is provided
* when using this capability.
*
* For more information, refer to the IEDriver's
* [required system configuration](http://goo.gl/eH0Yi3).
*
* @param {boolean} ignoreSettings Whether to ignore protected mode settings.
* @return {!Options} A self reference.
*/
introduceFlakinessByIgnoringProtectedModeSettings(ignoreSettings) {
this.options_[Key.IGNORE_PROTECTED_MODE_SETTINGS] = !!ignoreSettings
return this
}
/**
* Indicates whether to skip the check that the browser's zoom level is set to
* 100%.
*
* @param {boolean} ignore Whether to ignore the browser's zoom level settings.
* @return {!Options} A self reference.
*/
ignoreZoomSetting(ignore) {
this.options_[Key.IGNORE_ZOOM_SETTING] = !!ignore
return this
}
/**
* Sets the initial URL loaded when IE starts. This is intended to be used with
* {@link #introduceFlakinessByIgnoringProtectedModeSettings} to allow the user to initialize IE in
* the proper Protected Mode zone. Setting this option may cause browser
* instability or flaky and unresponsive code. Only "best effort" support is
* provided when using this option.
*
* @param {string} url The initial browser URL.
* @return {!Options} A self reference.
*/
initialBrowserUrl(url) {
this.options_[Key.INITIAL_BROWSER_URL] = url
return this
}
/**
* Configures whether to enable persistent mouse hovering (true by default).
* Persistent hovering is achieved by continuously firing mouse over events at
* the last location the mouse cursor has been moved to.
*
* @param {boolean} enable Whether to enable persistent hovering.
* @return {!Options} A self reference.
*/
enablePersistentHover(enable) {
this.options_[Key.ENABLE_PERSISTENT_HOVER] = !!enable
return this
}
/**
* Configures whether the driver should attempt to remove obsolete
* {@linkplain webdriver.WebElement WebElements} from its internal cache on
* page navigation (true by default). Disabling this option will cause the
* driver to run with a larger memory footprint.
*
* @param {boolean} enable Whether to enable element reference cleanup.
* @return {!Options} A self reference.
*/
enableElementCacheCleanup(enable) {
this.options_[Key.ENABLE_ELEMENT_CACHE_CLEANUP] = !!enable
return this
}
/**
* Configures whether to require the IE window to have input focus before
* performing any user interactions (i.e. mouse or keyboard events). This
* option is disabled by default, but delivers much more accurate interaction
* events when enabled.
*
* @param {boolean} require Whether to require window focus.
* @return {!Options} A self reference.
*/
requireWindowFocus(require) {
this.options_[Key.REQUIRE_WINDOW_FOCUS] = !!require
return this
}
/**
* Configures the timeout, in milliseconds, that the driver will attempt to
* located and attach to a newly opened instance of Internet Explorer. The
* default is zero, which indicates waiting indefinitely.
*
* @param {number} timeout How long to wait for IE.
* @return {!Options} A self reference.
*/
browserAttachTimeout(timeout) {
this.options_[Key.BROWSER_ATTACH_TIMEOUT] = Math.max(timeout, 0)
return this
}
/**
* Configures whether to launch Internet Explorer using the CreateProcess API.
* If this option is not specified, IE is launched using IELaunchURL, if
* available. For IE 8 and above, this option requires the TabProcGrowth
* registry value to be set to 0.
*
* @param {boolean} force Whether to use the CreateProcess API.
* @return {!Options} A self reference.
*/
forceCreateProcessApi(force) {
this.options_[Key.FORCE_CREATE_PROCESS] = !!force
return this
}
/**
* Specifies command-line switches to use when launching Internet Explorer.
* This is only valid when used with {@link #forceCreateProcessApi}.
*
* @param {...(string|!Array.<string>)} args The arguments to add.
* @return {!Options} A self reference.
*/
addBrowserCommandSwitches(...args) {
let current = this.options_[Key.BROWSER_COMMAND_LINE_SWITCHES] || []
if (typeof current == 'string') {
current = current.split(' ')
}
this.options_[Key.BROWSER_COMMAND_LINE_SWITCHES] = current.concat(args).join(' ')
return this
}
/**
* Specifies command-line switches to use when launching Internet Explorer.
* This is only valid when used with {@link #forceCreateProcessApi}.
*
* @param {...(string|!Array.<string>)} args The arguments to add.
* @deprecated Use {@link #addBrowserCommandSwitches} instead.
* @return {!Options} A self reference.
*/
addArguments(...args) {
let current = this.options_[Key.BROWSER_COMMAND_LINE_SWITCHES] || []
if (typeof current == 'string') {
current = current.split(' ')
}
this.options_[Key.BROWSER_COMMAND_LINE_SWITCHES] = current.concat(args).join(' ')
return this
}
/**
* Configures whether proxies should be configured on a per-process basis. If
* not set, setting a {@linkplain #setProxy proxy} will configure the system
* proxy. The default behavior is to use the system proxy.
*
* @param {boolean} enable Whether to enable per-process proxy settings.
* @return {!Options} A self reference.
*/
usePerProcessProxy(enable) {
this.options_[Key.USE_PER_PROCESS_PROXY] = !!enable
return this
}
/**
* Configures whether to clear the cache, cookies, history, and saved form data
* before starting the browser. _Using this capability will clear session data
* for all running instances of Internet Explorer, including those started
* manually._
*
* @param {boolean} cleanSession Whether to clear all session data on startup.
* @return {!Options} A self reference.
*/
ensureCleanSession(cleanSession) {
this.options_[Key.ENSURE_CLEAN_SESSION] = !!cleanSession
return this
}
/**
* Sets the path to the log file the driver should log to.
* @param {string} file The log file path.
* @return {!Options} A self reference.
*/
setLogFile(file) {
this.options_[Key.LOG_FILE] = file
return this
}
/**
* Sets the IEDriverServer's logging {@linkplain Level level}.
* @param {Level} level The logging level.
* @return {!Options} A self reference.
*/
setLogLevel(level) {
this.options_[Key.LOG_LEVEL] = level
return this
}
/**
* Sets the IP address of the driver's host adapter.
* @param {string} host The IP address to use.
* @return {!Options} A self reference.
*/
setHost(host) {
this.options_[Key.HOST] = host
return this
}
/**
* Sets the path of the temporary data directory to use.
* @param {string} path The log file path.
* @return {!Options} A self reference.
*/
setExtractPath(path) {
this.options_[Key.EXTRACT_PATH] = path
return this
}
/**
* Sets whether the driver should start in silent mode.
* @param {boolean} silent Whether to run in silent mode.
* @return {!Options} A self reference.
*/
silent(silent) {
this.options_[Key.SILENT] = silent
return this
}
/**
* The options File Upload Dialog Timeout in milliseconds
*
* @param {number} timeout How long to wait for IE.
* @return {!Options} A self reference.
*/
fileUploadDialogTimeout(timeout) {
this.options_[Key.FILE_UPLOAD_DIALOG_TIMEOUT] = Math.max(timeout, 0)
return this
}
/**
* Sets the path of the EdgeChromium driver.
* @param {string} path The EdgeChromium driver path.
* @return {!Options} A self reference.
*/
setEdgePath(path) {
this.options_[Key.EDGE_EXECUTABLE_PATH] = path
return this
}
/**
* Sets the IEDriver to drive Chromium-based Edge in Internet Explorer mode.
*
* @param {boolean} attachEdgeChromium Whether to run in Chromium-based-Edge in IE mode
* @return {!Options} A self reference.
*/
setEdgeChromium(attachEdgeChromium) {
this.options_[Key.ATTACH_TO_EDGE_CHROMIUM] = !!attachEdgeChromium
return this
}
/**
* Sets how elements should be scrolled into view for interaction.
* @param {number} behavior The desired scroll behavior: either 0 to align with
* the top of the viewport or 1 to align with the bottom.
* @return {!Options} A self reference.
*/
setScrollBehavior(behavior) {
if (behavior && behavior !== SCROLL_BEHAVIOUR.TOP && behavior !== SCROLL_BEHAVIOUR.BOTTOM) {
throw new error.InvalidArgumentError(`Element Scroll Behavior out of range.
It should be either ${SCROLL_BEHAVIOUR.TOP} or ${SCROLL_BEHAVIOUR.BOTTOM}`)
}
this.options_[Key.ELEMENT_SCROLL_BEHAVIOR] = behavior
return this
}
}
function createServiceFromCapabilities(capabilities) {
if (process.platform !== 'win32') {
throw Error(
'The IEDriver may only be used on Windows, but you appear to be on ' +
process.platform +
'. Did you mean to run against a remote ' +
'WebDriver server?',
)
}
let exe = null // Let Selenium Manager find it
var args = []
if (capabilities.has(Key.HOST)) {
args.push('--host=' + capabilities.get(Key.HOST))
}
if (capabilities.has(Key.LOG_FILE)) {
args.push('--log-file=' + capabilities.get(Key.LOG_FILE))
}
if (capabilities.has(Key.LOG_LEVEL)) {
args.push('--log-level=' + capabilities.get(Key.LOG_LEVEL))
}
if (capabilities.has(Key.EXTRACT_PATH)) {
args.push('--extract-path=' + capabilities.get(Key.EXTRACT_PATH))
}
if (capabilities.get(Key.SILENT)) {
args.push('--silent')
}
var port = portprober.findFreePort()
return new remote.DriverService(exe, {
loopback: true,
port: port,
args: port.then(function (port) {
return args.concat('--port=' + port)
}),
stdio: 'ignore',
})
}
/**
* Creates {@link selenium-webdriver/remote.DriverService} instances that manage
* an [IEDriverServer](https://github.com/SeleniumHQ/selenium/wiki/InternetExplorerDriver)
* server in a child process.
*/
class ServiceBuilder extends remote.DriverService.Builder {
/**
* @param {string=} opt_exe Path to the server executable to use. If omitted,
* the builder will attempt to locate the IEDriverServer on the system PATH.
*/
constructor(opt_exe) {
super(opt_exe)
this.setLoopback(true) // Required.
}
}
/**
* A WebDriver client for Microsoft's Internet Explorer.
*/
class Driver extends webdriver.WebDriver {
/**
* Creates a new session for Microsoft's Internet Explorer.
*
* @param {(Capabilities|Options)=} options The configuration options.
* @param {(remote.DriverService)=} opt_service The `DriverService` to use
* to start the IEDriverServer in a child process, optionally.
* @return {!Driver} A new driver instance.
*/
static createSession(options, opt_service) {
options = options || new Options()
let service
if (opt_service instanceof remote.DriverService) {
service = opt_service
} else {
service = createServiceFromCapabilities(options)
}
if (!service.getExecutable()) {
service.setExecutable(getBinaryPaths(options).driverPath)
}
let client = service.start().then((url) => new http.HttpClient(url))
let executor = new http.Executor(client)
return /** @type {!Driver} */ (super.createSession(executor, options, () => service.kill()))
}
/**
* This function is a no-op as file detectors are not supported by this
* implementation.
* @override
*/
setFileDetector() {}
}
// PUBLIC API
exports.Driver = Driver
exports.Options = Options
exports.Level = Level
exports.ServiceBuilder = ServiceBuilder
exports.Key = Key
exports.VENDOR_COMMAND_PREFIX = OPTIONS_CAPABILITY_KEY
exports.Behavior = SCROLL_BEHAVIOUR

View File

@@ -0,0 +1,796 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview The main user facing module. Exports WebDriver's primary
* public API and provides convenience assessors to certain sub-modules.
*/
'use strict'
const _http = require('./http')
const by = require('./lib/by')
const capabilities = require('./lib/capabilities')
const chrome = require('./chrome')
const edge = require('./edge')
const error = require('./lib/error')
const firefox = require('./firefox')
const ie = require('./ie')
const input = require('./lib/input')
const logging = require('./lib/logging')
const promise = require('./lib/promise')
const remote = require('./remote')
const safari = require('./safari')
const session = require('./lib/session')
const until = require('./lib/until')
const webdriver = require('./lib/webdriver')
const select = require('./lib/select')
const LogInspector = require('./bidi/logInspector')
const BrowsingContext = require('./bidi/browsingContext')
const BrowsingContextInspector = require('./bidi/browsingContextInspector')
const ScriptManager = require('./bidi/scriptManager')
const NetworkInspector = require('./bidi/networkInspector')
const Browser = capabilities.Browser
const Capabilities = capabilities.Capabilities
const Capability = capabilities.Capability
const WebDriver = webdriver.WebDriver
let seleniumServer
/**
* Starts an instance of the Selenium server if not yet running.
* @param {string} jar Path to the server jar to use.
* @return {!Promise<string>} A promise for the server's
* address once started.
*/
function startSeleniumServer(jar) {
if (!seleniumServer) {
seleniumServer = new remote.SeleniumServer(jar)
}
return seleniumServer.start()
}
/**
* {@linkplain webdriver.WebDriver#setFileDetector WebDriver's setFileDetector}
* method uses a non-standard command to transfer files from the local client
* to the remote end hosting the browser. Many of the WebDriver sub-types, like
* the {@link chrome.Driver} and {@link firefox.Driver}, do not support this
* command. Thus, these classes override the `setFileDetector` to no-op.
*
* This function uses a mixin to re-enable `setFileDetector` by calling the
* original method on the WebDriver prototype directly. This is used only when
* the builder creates a Chrome or Firefox instance that communicates with a
* remote end (and thus, support for remote file detectors is unknown).
*
* @param {function(new: webdriver.WebDriver, ...?)} ctor
* @return {function(new: webdriver.WebDriver, ...?)}
*/
function ensureFileDetectorsAreEnabled(ctor) {
return class extends ctor {
/** @param {input.FileDetector} detector */
setFileDetector(detector) {
webdriver.WebDriver.prototype.setFileDetector.call(this, detector)
}
}
}
/**
* A thenable wrapper around a {@linkplain webdriver.IWebDriver IWebDriver}
* instance that allows commands to be issued directly instead of having to
* repeatedly call `then`:
*
* let driver = new Builder().build();
* driver.then(d => d.get(url)); // You can do this...
* driver.get(url); // ...or this
*
* If the driver instance fails to resolve (e.g. the session cannot be created),
* every issued command will fail.
*
* @extends {webdriver.IWebDriver}
* @extends {IThenable<!webdriver.IWebDriver>}
* @interface
*/
class ThenableWebDriver {
/** @param {...?} args */
static createSession(...args) {} // eslint-disable-line
}
/**
* @const {!Map<function(new: WebDriver, !IThenable<!Session>, ...?),
* function(new: ThenableWebDriver, !IThenable<!Session>, ...?)>}
*/
const THENABLE_DRIVERS = new Map()
/**
* @param {function(new: WebDriver, !IThenable<!Session>, ...?)} ctor
* @param {...?} args
* @return {!ThenableWebDriver}
*/
function createDriver(ctor, ...args) {
let thenableWebDriverProxy = THENABLE_DRIVERS.get(ctor)
if (!thenableWebDriverProxy) {
/**
* @extends {WebDriver} // Needed since `ctor` is dynamically typed.
* @implements {ThenableWebDriver}
*/
thenableWebDriverProxy = class extends ctor {
/**
* @param {!IThenable<!Session>} session
* @param {...?} rest
*/
constructor(session, ...rest) {
super(session, ...rest)
const pd = this.getSession().then((session) => {
return new ctor(session, ...rest)
})
/** @override */
this.then = pd.then.bind(pd)
/** @override */
this.catch = pd.catch.bind(pd)
}
}
THENABLE_DRIVERS.set(ctor, thenableWebDriverProxy)
}
return thenableWebDriverProxy.createSession(...args)
}
/**
* Creates new {@link webdriver.WebDriver WebDriver} instances. The environment
* variables listed below may be used to override a builder's configuration,
* allowing quick runtime changes.
*
* - {@code SELENIUM_BROWSER}: defines the target browser in the form
* {@code browser[:version][:platform]}.
*
* - {@code SELENIUM_REMOTE_URL}: defines the remote URL for all builder
* instances. This environment variable should be set to a fully qualified
* URL for a WebDriver server (e.g. http://localhost:4444/wd/hub). This
* option always takes precedence over {@code SELENIUM_SERVER_JAR}.
*
* - {@code SELENIUM_SERVER_JAR}: defines the path to the
* <a href="https://www.selenium.dev/downloads/">
* standalone Selenium server</a> jar to use. The server will be started the
* first time a WebDriver instance and be killed when the process exits.
*
* Suppose you had mytest.js that created WebDriver with
*
* var driver = new webdriver.Builder()
* .forBrowser('chrome')
* .build();
*
* This test could be made to use Firefox on the local machine by running with
* `SELENIUM_BROWSER=firefox node mytest.js`. Rather than change the code to
* target Google Chrome on a remote machine, you can simply set the
* `SELENIUM_BROWSER` and `SELENIUM_REMOTE_URL` environment variables:
*
* SELENIUM_BROWSER=chrome:36:LINUX \
* SELENIUM_REMOTE_URL=http://www.example.com:4444/wd/hub \
* node mytest.js
*
* You could also use a local copy of the standalone Selenium server:
*
* SELENIUM_BROWSER=chrome:36:LINUX \
* SELENIUM_SERVER_JAR=/path/to/selenium-server-standalone.jar \
* node mytest.js
*/
class Builder {
constructor() {
/** @private @const */
this.log_ = logging.getLogger(`${logging.Type.DRIVER}.Builder`)
/** @private {string} */
this.url_ = ''
/** @private {?string} */
this.proxy_ = null
/** @private {!Capabilities} */
this.capabilities_ = new Capabilities()
/** @private {chrome.Options} */
this.chromeOptions_ = null
/** @private {chrome.ServiceBuilder} */
this.chromeService_ = null
/** @private {firefox.Options} */
this.firefoxOptions_ = null
/** @private {firefox.ServiceBuilder} */
this.firefoxService_ = null
/** @private {ie.Options} */
this.ieOptions_ = null
/** @private {ie.ServiceBuilder} */
this.ieService_ = null
/** @private {safari.Options} */
this.safariOptions_ = null
/** @private {edge.Options} */
this.edgeOptions_ = null
/** @private {remote.DriverService.Builder} */
this.edgeService_ = null
/** @private {boolean} */
this.ignoreEnv_ = false
/** @private {http.Agent} */
this.agent_ = null
}
/**
* Configures this builder to ignore any environment variable overrides and to
* only use the configuration specified through this instance's API.
*
* @return {!Builder} A self reference.
*/
disableEnvironmentOverrides() {
this.ignoreEnv_ = true
return this
}
/**
* Sets the URL of a remote WebDriver server to use. Once a remote URL has
* been specified, the builder direct all new clients to that server. If this
* method is never called, the Builder will attempt to create all clients
* locally.
*
* As an alternative to this method, you may also set the
* `SELENIUM_REMOTE_URL` environment variable.
*
* @param {string} url The URL of a remote server to use.
* @return {!Builder} A self reference.
*/
usingServer(url) {
this.url_ = url
return this
}
/**
* @return {string} The URL of the WebDriver server this instance is
* configured to use.
*/
getServerUrl() {
return this.url_
}
/**
* Sets the URL of the proxy to use for the WebDriver's HTTP connections.
* If this method is never called, the Builder will create a connection
* without a proxy.
*
* @param {string} proxy The URL of a proxy to use.
* @return {!Builder} A self reference.
*/
usingWebDriverProxy(proxy) {
this.proxy_ = proxy
return this
}
/**
* @return {?string} The URL of the proxy server to use for the WebDriver's
* HTTP connections, or `null` if not set.
*/
getWebDriverProxy() {
return this.proxy_
}
/**
* Sets the http agent to use for each request.
* If this method is not called, the Builder will use http.globalAgent by default.
*
* @param {http.Agent} agent The agent to use for each request.
* @return {!Builder} A self reference.
*/
usingHttpAgent(agent) {
this.agent_ = agent
return this
}
/**
* @return {http.Agent} The http agent used for each request
*/
getHttpAgent() {
return this.agent_
}
/**
* Recommended way is to use set*Options where * is the browser(eg setChromeOptions)
*
* Sets the desired capabilities when requesting a new session. This will
* overwrite any previously set capabilities.
* @param {!(Object|Capabilities)} capabilities The desired capabilities for
* a new session.
* @return {!Builder} A self reference.
*/
withCapabilities(capabilities) {
this.capabilities_ = new Capabilities(capabilities)
return this
}
/**
* Returns the base set of capabilities this instance is currently configured
* to use.
* @return {!Capabilities} The current capabilities for this builder.
*/
getCapabilities() {
return this.capabilities_
}
/**
* Sets the desired capability when requesting a new session.
* If there is already a capability named key, its value will be overwritten with value.
* This is a convenience wrapper around builder.getCapabilities().set(key, value) to support Builder method chaining.
* @param {string} key The capability key.
* @param {*} value The capability value.
* @return {!Builder} A self reference.
*/
setCapability(key, value) {
this.capabilities_.set(key, value)
return this
}
/**
* Configures the target browser for clients created by this instance.
* Any calls to {@link #withCapabilities} after this function will
* overwrite these settings.
*
* You may also define the target browser using the {@code SELENIUM_BROWSER}
* environment variable. If set, this environment variable should be of the
* form `browser[:[version][:platform]]`.
*
* @param {(string|!Browser)} name The name of the target browser;
* common defaults are available on the {@link webdriver.Browser} enum.
* @param {string=} opt_version A desired version; may be omitted if any
* version should be used.
* @param {(string|!capabilities.Platform)=} opt_platform
* The desired platform; may be omitted if any platform may be used.
* @return {!Builder} A self reference.
*/
forBrowser(name, opt_version, opt_platform) {
this.capabilities_.setBrowserName(name)
if (opt_version) {
this.capabilities_.setBrowserVersion(opt_version)
}
if (opt_platform) {
this.capabilities_.setPlatform(opt_platform)
}
return this
}
/**
* Sets the proxy configuration for the target browser.
* Any calls to {@link #withCapabilities} after this function will
* overwrite these settings.
*
* @param {!./lib/proxy.Config} config The configuration to use.
* @return {!Builder} A self reference.
*/
setProxy(config) {
this.capabilities_.setProxy(config)
return this
}
/**
* Sets the logging preferences for the created session. Preferences may be
* changed by repeated calls, or by calling {@link #withCapabilities}.
* @param {!(./lib/logging.Preferences|Object<string, string>)} prefs The
* desired logging preferences.
* @return {!Builder} A self reference.
*/
setLoggingPrefs(prefs) {
this.capabilities_.setLoggingPrefs(prefs)
return this
}
/**
* Sets the default action to take with an unexpected alert before returning
* an error.
*
* @param {?capabilities.UserPromptHandler} behavior The desired behavior.
* @return {!Builder} A self reference.
* @see capabilities.Capabilities#setAlertBehavior
*/
setAlertBehavior(behavior) {
this.capabilities_.setAlertBehavior(behavior)
return this
}
/**
* Sets Chrome specific {@linkplain chrome.Options options} for drivers
* created by this builder. Any logging or proxy settings defined on the given
* options will take precedence over those set through
* {@link #setLoggingPrefs} and {@link #setProxy}, respectively.
*
* @param {!chrome.Options} options The ChromeDriver options to use.
* @return {!Builder} A self reference.
*/
setChromeOptions(options) {
this.chromeOptions_ = options
return this
}
/**
* @return {chrome.Options} the Chrome specific options currently configured
* for this builder.
*/
getChromeOptions() {
return this.chromeOptions_
}
/**
* Sets the service builder to use for managing the chromedriver child process
* when creating new Chrome sessions.
*
* @param {chrome.ServiceBuilder} service the service to use.
* @return {!Builder} A self reference.
*/
setChromeService(service) {
if (service && !(service instanceof chrome.ServiceBuilder)) {
throw TypeError('not a chrome.ServiceBuilder object')
}
this.chromeService_ = service
return this
}
/**
* Sets Firefox specific {@linkplain firefox.Options options} for drivers
* created by this builder. Any logging or proxy settings defined on the given
* options will take precedence over those set through
* {@link #setLoggingPrefs} and {@link #setProxy}, respectively.
*
* @param {!firefox.Options} options The FirefoxDriver options to use.
* @return {!Builder} A self reference.
*/
setFirefoxOptions(options) {
this.firefoxOptions_ = options
return this
}
/**
* @return {firefox.Options} the Firefox specific options currently configured
* for this instance.
*/
getFirefoxOptions() {
return this.firefoxOptions_
}
/**
* Sets the {@link firefox.ServiceBuilder} to use to manage the geckodriver
* child process when creating Firefox sessions locally.
*
* @param {firefox.ServiceBuilder} service the service to use.
* @return {!Builder} a self reference.
*/
setFirefoxService(service) {
if (service && !(service instanceof firefox.ServiceBuilder)) {
throw TypeError('not a firefox.ServiceBuilder object')
}
this.firefoxService_ = service
return this
}
/**
* Set Internet Explorer specific {@linkplain ie.Options options} for drivers
* created by this builder. Any proxy settings defined on the given options
* will take precedence over those set through {@link #setProxy}.
*
* @param {!ie.Options} options The IEDriver options to use.
* @return {!Builder} A self reference.
*/
setIeOptions(options) {
this.ieOptions_ = options
return this
}
/**
* Sets the {@link ie.ServiceBuilder} to use to manage the geckodriver
* child process when creating IE sessions locally.
*
* @param {ie.ServiceBuilder} service the service to use.
* @return {!Builder} a self reference.
*/
setIeService(service) {
this.ieService_ = service
return this
}
/**
* Set {@linkplain edge.Options options} specific to Microsoft's Edge browser
* for drivers created by this builder. Any proxy settings defined on the
* given options will take precedence over those set through
* {@link #setProxy}.
*
* @param {!edge.Options} options The MicrosoftEdgeDriver options to use.
* @return {!Builder} A self reference.
*/
setEdgeOptions(options) {
this.edgeOptions_ = options
return this
}
/**
* Sets the {@link edge.ServiceBuilder} to use to manage the
* MicrosoftEdgeDriver child process when creating sessions locally.
*
* @param {edge.ServiceBuilder} service the service to use.
* @return {!Builder} a self reference.
*/
setEdgeService(service) {
if (service && !(service instanceof edge.ServiceBuilder)) {
throw TypeError('not a edge.ServiceBuilder object')
}
this.edgeService_ = service
return this
}
/**
* Sets Safari specific {@linkplain safari.Options options} for drivers
* created by this builder. Any logging settings defined on the given options
* will take precedence over those set through {@link #setLoggingPrefs}.
*
* @param {!safari.Options} options The Safari options to use.
* @return {!Builder} A self reference.
*/
setSafariOptions(options) {
this.safariOptions_ = options
return this
}
/**
* @return {safari.Options} the Safari specific options currently configured
* for this instance.
*/
getSafariOptions() {
return this.safariOptions_
}
/**
* Creates a new WebDriver client based on this builder's current
* configuration.
*
* This method will return a {@linkplain ThenableWebDriver} instance, allowing
* users to issue commands directly without calling `then()`. The returned
* thenable wraps a promise that will resolve to a concrete
* {@linkplain webdriver.WebDriver WebDriver} instance. The promise will be
* rejected if the remote end fails to create a new session.
*
* @return {!ThenableWebDriver} A new WebDriver instance.
* @throws {Error} If the current configuration is invalid.
*/
build() {
// Create a copy for any changes we may need to make based on the current
// environment.
const capabilities = new Capabilities(this.capabilities_)
let browser
if (!this.ignoreEnv_ && process.env.SELENIUM_BROWSER) {
this.log_.fine(`SELENIUM_BROWSER=${process.env.SELENIUM_BROWSER}`)
browser = process.env.SELENIUM_BROWSER.split(/:/, 3)
capabilities.setBrowserName(browser[0])
browser[1] && capabilities.setBrowserVersion(browser[1])
browser[2] && capabilities.setPlatform(browser[2])
}
browser = capabilities.get(Capability.BROWSER_NAME)
/**
* If browser is not defined in forBrowser, check if browserOptions are defined to pick the browserName
*/
if (!browser) {
const options =
this.chromeOptions_ || this.firefoxOptions_ || this.ieOptions_ || this.safariOptions_ || this.edgeOptions_
if (options) {
browser = options['map_'].get(Capability.BROWSER_NAME)
}
}
if (typeof browser !== 'string') {
throw TypeError(
`Target browser must be a string, but is <${typeof browser}>;` + ' did you forget to call forBrowser()?',
)
}
if (browser === 'ie') {
browser = Browser.INTERNET_EXPLORER
}
// Apply browser specific overrides.
if (browser === Browser.CHROME && this.chromeOptions_) {
capabilities.merge(this.chromeOptions_)
} else if (browser === Browser.FIREFOX && this.firefoxOptions_) {
capabilities.merge(this.firefoxOptions_)
} else if (browser === Browser.INTERNET_EXPLORER && this.ieOptions_) {
capabilities.merge(this.ieOptions_)
} else if (browser === Browser.SAFARI && this.safariOptions_) {
capabilities.merge(this.safariOptions_)
} else if (browser === Browser.EDGE && this.edgeOptions_) {
capabilities.merge(this.edgeOptions_)
}
checkOptions(capabilities, 'chromeOptions', chrome.Options, 'setChromeOptions')
checkOptions(capabilities, 'moz:firefoxOptions', firefox.Options, 'setFirefoxOptions')
checkOptions(capabilities, 'safari.options', safari.Options, 'setSafariOptions')
// Check for a remote browser.
let url = this.url_
if (!this.ignoreEnv_) {
if (process.env.SELENIUM_REMOTE_URL) {
this.log_.fine(`SELENIUM_REMOTE_URL=${process.env.SELENIUM_REMOTE_URL}`)
url = process.env.SELENIUM_REMOTE_URL
} else if (process.env.SELENIUM_SERVER_JAR) {
this.log_.fine(`SELENIUM_SERVER_JAR=${process.env.SELENIUM_SERVER_JAR}`)
url = startSeleniumServer(process.env.SELENIUM_SERVER_JAR)
}
}
if (url) {
this.log_.fine('Creating session on remote server')
let client = Promise.resolve(url).then((url) => new _http.HttpClient(url, this.agent_, this.proxy_))
let executor = new _http.Executor(client)
if (browser === Browser.CHROME) {
const driver = ensureFileDetectorsAreEnabled(chrome.Driver)
return createDriver(driver, capabilities, executor)
}
if (browser === Browser.FIREFOX) {
const driver = ensureFileDetectorsAreEnabled(firefox.Driver)
return createDriver(driver, capabilities, executor)
}
return createDriver(WebDriver, executor, capabilities)
}
// Check for a native browser.
switch (browser) {
case Browser.CHROME: {
let service = null
if (this.chromeService_) {
service = this.chromeService_.build()
}
return createDriver(chrome.Driver, capabilities, service)
}
case Browser.FIREFOX: {
let service = null
if (this.firefoxService_) {
service = this.firefoxService_.build()
}
return createDriver(firefox.Driver, capabilities, service)
}
case Browser.INTERNET_EXPLORER: {
let service = null
if (this.ieService_) {
service = this.ieService_.build()
}
return createDriver(ie.Driver, capabilities, service)
}
case Browser.EDGE: {
let service = null
if (this.edgeService_) {
service = this.edgeService_.build()
}
return createDriver(edge.Driver, capabilities, service)
}
case Browser.SAFARI:
return createDriver(safari.Driver, capabilities)
default:
throw new Error('Do not know how to build driver: ' + browser + '; did you forget to call usingServer(url)?')
}
}
}
/**
* In the 3.x releases, the various browser option classes
* (e.g. firefox.Options) had to be manually set as an option using the
* Capabilities class:
*
* let ffo = new firefox.Options();
* // Configure firefox options...
*
* let caps = new Capabilities();
* caps.set('moz:firefoxOptions', ffo);
*
* let driver = new Builder()
* .withCapabilities(caps)
* .build();
*
* The options are now subclasses of Capabilities and can be used directly. A
* direct translation of the above is:
*
* let ffo = new firefox.Options();
* // Configure firefox options...
*
* let driver = new Builder()
* .withCapabilities(ffo)
* .build();
*
* You can also set the options for various browsers at once and let the builder
* choose the correct set at runtime (see Builder docs above):
*
* let ffo = new firefox.Options();
* // Configure ...
*
* let co = new chrome.Options();
* // Configure ...
*
* let driver = new Builder()
* .setChromeOptions(co)
* .setFirefoxOptions(ffo)
* .build();
*
* @param {!Capabilities} caps
* @param {string} key
* @param {function(new: Capabilities)} optionType
* @param {string} setMethod
* @throws {error.InvalidArgumentError}
*/
function checkOptions(caps, key, optionType, setMethod) {
let val = caps.get(key)
if (val instanceof optionType) {
throw new error.InvalidArgumentError(
'Options class extends Capabilities and should not be set as key ' +
`"${key}"; set browser-specific options with ` +
`Builder.${setMethod}(). For more information, see the ` +
'documentation attached to the function that threw this error',
)
}
}
// PUBLIC API
exports.Browser = capabilities.Browser
exports.Builder = Builder
exports.Button = input.Button
exports.By = by.By
exports.RelativeBy = by.RelativeBy
exports.withTagName = by.withTagName
exports.locateWith = by.locateWith
exports.Capabilities = capabilities.Capabilities
exports.Capability = capabilities.Capability
exports.Condition = webdriver.Condition
exports.FileDetector = input.FileDetector
exports.Key = input.Key
exports.Origin = input.Origin
exports.Session = session.Session
exports.ThenableWebDriver = ThenableWebDriver
exports.WebDriver = webdriver.WebDriver
exports.WebElement = webdriver.WebElement
exports.WebElementCondition = webdriver.WebElementCondition
exports.WebElementPromise = webdriver.WebElementPromise
exports.error = error
exports.logging = logging
exports.promise = promise
exports.until = until
exports.Select = select.Select
exports.LogInspector = LogInspector
exports.BrowsingContext = BrowsingContext
exports.BrowsingContextInspector = BrowsingContextInspector
exports.ScriptManager = ScriptManager
exports.NetworkInspector = NetworkInspector

View File

@@ -0,0 +1,169 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
const childProcess = require('node:child_process')
/**
* Options for configuring an executed command.
*
* @record
*/
class Options {
constructor() {
/**
* Command line arguments for the child process, if any.
* @type (!Array<string>|undefined)
*/
this.args
/**
* Environment variables for the spawned process. If unspecified, the
* child will inherit this process' environment.
*
* @type {(!Object<string, string>|undefined)}
*/
this.env
/**
* IO conifguration for the spawned server child process. If unspecified,
* the child process' IO output will be ignored.
*
* @type {(string|!Array<string|number|!stream.Stream|null|undefined>|
* undefined)}
* @see <https://nodejs.org/dist/latest-v8.x/docs/api/child_process.html#child_process_options_stdio>
*/
this.stdio
}
}
/**
* Describes a command's termination conditions.
*/
class Result {
/**
* @param {?number} code The exit code, or {@code null} if the command did not
* exit normally.
* @param {?string} signal The signal used to kill the command, or
* {@code null}.
*/
constructor(code, signal) {
/** @type {?number} */
this.code = code
/** @type {?string} */
this.signal = signal
}
/** @override */
toString() {
return `Result(code=${this.code}, signal=${this.signal})`
}
}
const COMMAND_RESULT = /** !WeakMap<!Command, !Promise<!Result>> */ new WeakMap()
const KILL_HOOK = /** !WeakMap<!Command, function(string)> */ new WeakMap()
/**
* Represents a command running in a sub-process.
*/
class Command {
/**
* @param {!Promise<!Result>} result The command result.
* @param {function(string)} onKill The function to call when {@link #kill()}
* is called.
*/
constructor(result, onKill) {
COMMAND_RESULT.set(this, result)
KILL_HOOK.set(this, onKill)
}
/**
* @return {!Promise<!Result>} A promise for the result of this
* command.
*/
result() {
return /** @type {!Promise<!Result>} */ (COMMAND_RESULT.get(this))
}
/**
* Sends a signal to the underlying process.
* @param {string=} opt_signal The signal to send; defaults to `SIGTERM`.
*/
kill(opt_signal) {
KILL_HOOK.get(this)(opt_signal || 'SIGTERM')
}
}
// PUBLIC API
/**
* Spawns a child process. The returned {@link Command} may be used to wait
* for the process result or to send signals to the process.
*
* @param {string} command The executable to spawn.
* @param {Options=} opt_options The command options.
* @return {!Command} The launched command.
*/
function exec(command, opt_options) {
const options = opt_options || {}
let proc = childProcess.spawn(command, options.args || [], {
env: options.env || process.env,
stdio: options.stdio || 'ignore',
})
// This process should not wait on the spawned child, however, we do
// want to ensure the child is killed when this process exits.
proc.unref()
process.once('exit', onProcessExit)
const result = new Promise((resolve, reject) => {
proc.once('exit', (code, signal) => {
proc = null
process.removeListener('exit', onProcessExit)
resolve(new Result(code, signal))
})
proc.once('error', (err) => {
reject(err)
})
})
return new Command(result, killCommand)
function onProcessExit() {
killCommand('SIGTERM')
}
function killCommand(signal) {
process.removeListener('exit', onProcessExit)
if (proc) {
proc.kill(signal)
proc = null
}
}
}
// Exported to improve generated API documentation.
module.exports = {
Command,
Options,
Result,
exec,
}

View File

@@ -0,0 +1,352 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
const fs = require('node:fs')
const path = require('node:path')
const tmp = require('tmp')
/**
* @param {!Function} fn .
* @return {!Promise<T>} .
* @template T
*/
function checkedCall(fn) {
return new Promise((resolve, reject) => {
try {
fn((err, value) => {
if (err) {
reject(err)
} else {
resolve(value)
}
})
} catch (e) {
reject(e)
}
})
}
/**
* Recursively removes a directory and all of its contents. This is equivalent
* to {@code rm -rf} on a POSIX system.
* @param {string} dirPath Path to the directory to remove.
* @return {!Promise} A promise to be resolved when the operation has
* completed.
*/
function rmDir(dirPath) {
return new Promise(function (fulfill, reject) {
fs.rm(dirPath, { recursive: true, maxRetries: 2 }, function (err) {
if (err && err.code === 'ENOENT') {
fulfill()
} else if (err) {
reject(err)
}
fulfill()
})
})
}
/**
* Copies one file to another.
* @param {string} src The source file.
* @param {string} dst The destination file.
* @return {!Promise<string>} A promise for the copied file's path.
*/
function copy(src, dst) {
return new Promise(function (fulfill, reject) {
const rs = fs.createReadStream(src)
rs.on('error', reject)
const ws = fs.createWriteStream(dst)
ws.on('error', reject)
ws.on('close', () => fulfill(dst))
rs.pipe(ws)
})
}
/**
* Recursively copies the contents of one directory to another.
* @param {string} src The source directory to copy.
* @param {string} dst The directory to copy into.
* @param {(RegExp|function(string): boolean)=} opt_exclude An exclusion filter
* as either a regex or predicate function. All files matching this filter
* will not be copied.
* @return {!Promise<string>} A promise for the destination
* directory's path once all files have been copied.
*/
function copyDir(src, dst, opt_exclude) {
let predicate = opt_exclude
if (opt_exclude && typeof opt_exclude !== 'function') {
predicate = function (p) {
return !opt_exclude.test(p)
}
}
if (!fs.existsSync(dst)) {
fs.mkdirSync(dst)
}
let files = fs.readdirSync(src)
files = files.map(function (file) {
return path.join(src, file)
})
if (predicate) {
files = files.filter(/** @type {function(string): boolean} */ (predicate))
}
const results = []
files.forEach(function (file) {
const stats = fs.statSync(file)
const target = path.join(dst, path.basename(file))
if (stats.isDirectory()) {
if (!fs.existsSync(target)) {
fs.mkdirSync(target, stats.mode)
}
results.push(copyDir(file, target, predicate))
} else {
results.push(copy(file, target))
}
})
return Promise.all(results).then(() => dst)
}
/**
* Tests if a file path exists.
* @param {string} aPath The path to test.
* @return {!Promise<boolean>} A promise for whether the file exists.
*/
function exists(aPath) {
return new Promise(function (fulfill, reject) {
let type = typeof aPath
if (type !== 'string') {
reject(TypeError(`expected string path, but got ${type}`))
} else {
fulfill(fs.existsSync(aPath))
}
})
}
/**
* Calls `stat(2)`.
* @param {string} aPath The path to stat.
* @return {!Promise<!fs.Stats>} A promise for the file stats.
*/
function stat(aPath) {
return checkedCall((callback) => fs.stat(aPath, callback))
}
/**
* Deletes a name from the filesystem and possibly the file it refers to. Has
* no effect if the file does not exist.
* @param {string} aPath The path to remove.
* @return {!Promise} A promise for when the file has been removed.
*/
function unlink(aPath) {
return new Promise(function (fulfill, reject) {
const exists = fs.existsSync(aPath)
if (exists) {
fs.unlink(aPath, function (err) {
;(err && reject(err)) || fulfill()
})
} else {
fulfill()
}
})
}
/**
* @return {!Promise<string>} A promise for the path to a temporary directory.
* @see https://www.npmjs.org/package/tmp
*/
function tmpDir() {
return checkedCall((callback) => tmp.dir({ unsafeCleanup: true }, callback))
}
/**
* @param {{postfix: string}=} opt_options Temporary file options.
* @return {!Promise<string>} A promise for the path to a temporary file.
* @see https://www.npmjs.org/package/tmp
*/
function tmpFile(opt_options) {
return checkedCall((callback) => {
/** check fixed in v > 0.2.1 if
* (typeof options === 'function') {
* return [{}, options];
* }
*/
tmp.file(opt_options, callback)
})
}
/**
* Searches the {@code PATH} environment variable for the given file.
* @param {string} file The file to locate on the PATH.
* @param {boolean=} opt_checkCwd Whether to always start with the search with
* the current working directory, regardless of whether it is explicitly
* listed on the PATH.
* @return {?string} Path to the located file, or {@code null} if it could
* not be found.
*/
function findInPath(file, opt_checkCwd) {
const dirs = []
if (opt_checkCwd) {
dirs.push(process.cwd())
}
dirs.push.apply(dirs, process.env['PATH'].split(path.delimiter))
let foundInDir = dirs.find((dir) => {
let tmp = path.join(dir, file)
try {
let stats = fs.statSync(tmp)
return stats.isFile() && !stats.isDirectory()
/*eslint no-unused-vars: "off"*/
} catch (ex) {
return false
}
})
return foundInDir ? path.join(foundInDir, file) : null
}
/**
* Reads the contents of the given file.
*
* @param {string} aPath Path to the file to read.
* @return {!Promise<!Buffer>} A promise that will resolve with a buffer of the
* file contents.
*/
function read(aPath) {
return checkedCall((callback) => fs.readFile(aPath, callback))
}
/**
* Writes to a file.
*
* @param {string} aPath Path to the file to write to.
* @param {(string|!Buffer)} data The data to write.
* @return {!Promise} A promise that will resolve when the operation has
* completed.
*/
function write(aPath, data) {
return checkedCall((callback) => fs.writeFile(aPath, data, callback))
}
/**
* Creates a directory.
*
* @param {string} aPath The directory path.
* @return {!Promise<string>} A promise that will resolve with the path of the
* created directory.
*/
function mkdir(aPath) {
return checkedCall((callback) => {
fs.mkdir(aPath, undefined, (err) => {
if (err && err.code !== 'EEXIST') {
callback(err)
} else {
callback(null, aPath)
}
})
})
}
/**
* Recursively creates a directory and any ancestors that do not yet exist.
*
* @param {string} dir The directory path to create.
* @return {!Promise<string>} A promise that will resolve with the path of the
* created directory.
*/
function mkdirp(dir) {
return checkedCall((callback) => {
fs.mkdir(dir, undefined, (err) => {
if (!err) {
callback(null, dir)
return
}
switch (err.code) {
case 'EEXIST':
callback(null, dir)
return
case 'ENOENT':
return mkdirp(path.dirname(dir))
.then(() => mkdirp(dir))
.then(
() => callback(null, dir),
(err) => callback(err),
)
default:
callback(err)
return
}
})
})
}
/**
* Recursively walks a directory, returning a promise that will resolve with
* a list of all files/directories seen.
*
* @param {string} rootPath the directory to walk.
* @return {!Promise<!Array<{path: string, dir: boolean}>>} a promise that will
* resolve with a list of entries seen. For each entry, the recorded path
* will be relative to `rootPath`.
*/
function walkDir(rootPath) {
const seen = []
return (function walk(dir) {
return checkedCall((callback) => fs.readdir(dir, callback)).then((files) =>
Promise.all(
files.map((file) => {
file = path.join(dir, file)
return checkedCall((cb) => fs.stat(file, cb)).then((stats) => {
seen.push({
path: path.relative(rootPath, file),
dir: stats.isDirectory(),
})
return stats.isDirectory() && walk(file)
})
}),
),
)
})(rootPath).then(() => seen)
}
// PUBLIC API
module.exports = {
walkDir,
rmDir,
mkdirp,
mkdir,
write,
read,
findInPath,
tmpFile,
tmpDir,
unlink,
copy,
copyDir,
exists,
stat,
}

View File

@@ -0,0 +1,199 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
const jszip = require('jszip')
const path = require('node:path')
const io = require('./index')
const { InvalidArgumentError } = require('../lib/error')
/**
* Manages a zip archive.
*/
class Zip {
constructor() {
/** @private @const */
this.z_ = new jszip()
/** @private @const {!Set<!Promise<?>>} */
this.pendingAdds_ = new Set()
}
/**
* Adds a file to this zip.
*
* @param {string} filePath path to the file to add.
* @param {string=} zipPath path to the file in the zip archive, defaults
* to the basename of `filePath`.
* @return {!Promise<?>} a promise that will resolve when added.
*/
addFile(filePath, zipPath = path.basename(filePath)) {
let add = io
.read(filePath)
.then((buffer) => this.z_.file(/** @type {string} */ (zipPath.replace(/\\/g, '/')), buffer))
this.pendingAdds_.add(add)
return add.then(
() => this.pendingAdds_.delete(add),
(e) => {
this.pendingAdds_.delete(add)
throw e
},
)
}
/**
* Recursively adds a directory and all of its contents to this archive.
*
* @param {string} dirPath path to the directory to add.
* @param {string=} zipPath path to the folder in the archive to add the
* directory contents to. Defaults to the root folder.
* @return {!Promise<?>} returns a promise that will resolve when
* the operation is complete.
*/
addDir(dirPath, zipPath = '') {
return io.walkDir(dirPath).then((entries) => {
let archive = this.z_
if (zipPath) {
archive = archive.folder(zipPath)
}
let files = []
entries.forEach((spec) => {
if (spec.dir) {
archive.folder(spec.path)
} else {
files.push(this.addFile(path.join(dirPath, spec.path), path.join(zipPath, spec.path)))
}
})
return Promise.all(files)
})
}
/**
* @param {string} path File path to test for within the archive.
* @return {boolean} Whether this zip archive contains an entry with the given
* path.
*/
has(path) {
return this.z_.file(path) !== null
}
/**
* Returns the contents of the file in this zip archive with the given `path`.
* The returned promise will be rejected with an {@link InvalidArgumentError}
* if either `path` does not exist within the archive, or if `path` refers
* to a directory.
*
* @param {string} path the path to the file whose contents to return.
* @return {!Promise<!Buffer>} a promise that will be resolved with the file's
* contents as a buffer.
*/
getFile(path) {
let file = this.z_.file(path)
if (!file) {
return Promise.reject(new InvalidArgumentError(`No such file in zip archive: ${path}`))
}
if (file.dir) {
return Promise.reject(new InvalidArgumentError(`The requested file is a directory: ${path}`))
}
return Promise.resolve(file.async('nodebuffer'))
}
/**
* Returns the compressed data for this archive in a buffer. _This method will
* not wait for any outstanding {@link #addFile add}
* {@link #addDir operations} before encoding the archive._
*
* @param {string} compression The desired compression.
* Must be `STORE` (the default) or `DEFLATE`.
* @return {!Promise<!Buffer>} a promise that will resolve with this archive
* as a buffer.
*/
toBuffer(compression = 'STORE') {
if (compression !== 'STORE' && compression !== 'DEFLATE') {
return Promise.reject(new InvalidArgumentError(`compression must be one of {STORE, DEFLATE}, got ${compression}`))
}
return Promise.resolve(this.z_.generateAsync({ compression, type: 'nodebuffer' }))
}
}
/**
* Asynchronously opens a zip archive.
*
* @param {string} path to the zip archive to load.
* @return {!Promise<!Zip>} a promise that will resolve with the opened
* archive.
*/
function load(path) {
return io.read(path).then((data) => {
let zip = new Zip()
return zip.z_.loadAsync(data).then(() => zip)
})
}
/**
* Asynchronously unzips an archive file.
*
* @param {string} src path to the source file to unzip.
* @param {string} dst path to the destination directory.
* @return {!Promise<string>} a promise that will resolve with `dst` once the
* archive has been unzipped.
*/
function unzip(src, dst) {
return load(src).then((zip) => {
const promisedDirs = new Map()
const promises = []
zip.z_.forEach((relPath, file) => {
let p
if (file.dir) {
p = createDir(relPath)
} else {
let dirname = path.dirname(relPath)
if (dirname === '.') {
p = writeFile(relPath, file)
} else {
p = createDir(dirname).then(() => writeFile(relPath, file))
}
}
promises.push(p)
})
return Promise.all(promises).then(() => dst)
function createDir(dir) {
let p = promisedDirs.get(dir)
if (!p) {
p = io.mkdirp(path.join(dst, dir))
promisedDirs.set(dir, p)
}
return p
}
function writeFile(relPath, file) {
return file.async('nodebuffer').then((buffer) => io.write(path.join(dst, relPath), buffer))
}
})
}
// PUBLIC API
module.exports = { Zip, load, unzip }

View File

@@ -0,0 +1,123 @@
// GENERATED CODE - DO NOT EDIT
module.exports = function(){return (function(){var aa=this||self;function ba(a){return"string"==typeof a}function ca(a,b){a=a.split(".");var c=aa;a[0]in c||"undefined"==typeof c.execScript||c.execScript("var "+a[0]);for(var d;a.length&&(d=a.shift());)a.length||void 0===b?c[d]&&c[d]!==Object.prototype[d]?c=c[d]:c=c[d]={}:c[d]=b}
function da(a){var b=typeof a;if("object"==b)if(a){if(a instanceof Array)return"array";if(a instanceof Object)return b;var c=Object.prototype.toString.call(a);if("[object Window]"==c)return"object";if("[object Array]"==c||"number"==typeof a.length&&"undefined"!=typeof a.splice&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("splice"))return"array";if("[object Function]"==c||"undefined"!=typeof a.call&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("call"))return"function"}else return"null";
else if("function"==b&&"undefined"==typeof a.call)return"object";return b}function ea(a){return"function"==da(a)}function ha(a){var b=typeof a;return"object"==b&&null!=a||"function"==b}function ia(a,b,c){return a.call.apply(a.bind,arguments)}
function ja(a,b,c){if(!a)throw Error();if(2<arguments.length){var d=Array.prototype.slice.call(arguments,2);return function(){var e=Array.prototype.slice.call(arguments);Array.prototype.unshift.apply(e,d);return a.apply(b,e)}}return function(){return a.apply(b,arguments)}}function ka(a,b,c){Function.prototype.bind&&-1!=Function.prototype.bind.toString().indexOf("native code")?ka=ia:ka=ja;return ka.apply(null,arguments)}
function la(a,b){var c=Array.prototype.slice.call(arguments,1);return function(){var d=c.slice();d.push.apply(d,arguments);return a.apply(this,d)}}function k(a,b){function c(){}c.prototype=b.prototype;a.prototype=new c;a.prototype.constructor=a};/*
The MIT License
Copyright (c) 2007 Cybozu Labs, Inc.
Copyright (c) 2012 Google Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
*/
function ma(a,b,c){this.a=a;this.b=b||1;this.f=c||1};var na;var oa=Array.prototype.indexOf?function(a,b){return Array.prototype.indexOf.call(a,b,void 0)}:function(a,b){if("string"===typeof a)return"string"!==typeof b||1!=b.length?-1:a.indexOf(b,0);for(var c=0;c<a.length;c++)if(c in a&&a[c]===b)return c;return-1},l=Array.prototype.forEach?function(a,b,c){Array.prototype.forEach.call(a,b,c)}:function(a,b,c){for(var d=a.length,e="string"===typeof a?a.split(""):a,f=0;f<d;f++)f in e&&b.call(c,e[f],f,a)},pa=Array.prototype.filter?function(a,b){return Array.prototype.filter.call(a,
b,void 0)}:function(a,b){for(var c=a.length,d=[],e=0,f="string"===typeof a?a.split(""):a,g=0;g<c;g++)if(g in f){var h=f[g];b.call(void 0,h,g,a)&&(d[e++]=h)}return d},qa=Array.prototype.map?function(a,b){return Array.prototype.map.call(a,b,void 0)}:function(a,b){for(var c=a.length,d=Array(c),e="string"===typeof a?a.split(""):a,f=0;f<c;f++)f in e&&(d[f]=b.call(void 0,e[f],f,a));return d},ra=Array.prototype.reduce?function(a,b,c){return Array.prototype.reduce.call(a,b,c)}:function(a,b,c){var d=c;l(a,
function(e,f){d=b.call(void 0,d,e,f,a)});return d},sa=Array.prototype.some?function(a,b){return Array.prototype.some.call(a,b,void 0)}:function(a,b){for(var c=a.length,d="string"===typeof a?a.split(""):a,e=0;e<c;e++)if(e in d&&b.call(void 0,d[e],e,a))return!0;return!1},ta=Array.prototype.every?function(a,b,c){return Array.prototype.every.call(a,b,c)}:function(a,b,c){for(var d=a.length,e="string"===typeof a?a.split(""):a,f=0;f<d;f++)if(f in e&&!b.call(c,e[f],f,a))return!1;return!0};
function ua(a,b){a:{for(var c=a.length,d="string"===typeof a?a.split(""):a,e=0;e<c;e++)if(e in d&&b.call(void 0,d[e],e,a)){b=e;break a}b=-1}return 0>b?null:"string"===typeof a?a.charAt(b):a[b]}function va(a){return Array.prototype.concat.apply([],arguments)}function wa(a,b,c){return 2>=arguments.length?Array.prototype.slice.call(a,b):Array.prototype.slice.call(a,b,c)}function xa(a,b){a.sort(b||ya)}function ya(a,b){return a>b?1:a<b?-1:0};function za(a){var b=a.length-1;return 0<=b&&a.indexOf(" ",b)==b}var Aa=String.prototype.trim?function(a){return a.trim()}:function(a){return/^[\s\xa0]*([\s\S]*?)[\s\xa0]*$/.exec(a)[1]};
function Ba(a,b){var c=0;a=Aa(String(a)).split(".");b=Aa(String(b)).split(".");for(var d=Math.max(a.length,b.length),e=0;0==c&&e<d;e++){var f=a[e]||"",g=b[e]||"";do{f=/(\d*)(\D*)(.*)/.exec(f)||["","","",""];g=/(\d*)(\D*)(.*)/.exec(g)||["","","",""];if(0==f[0].length&&0==g[0].length)break;c=Ca(0==f[1].length?0:parseInt(f[1],10),0==g[1].length?0:parseInt(g[1],10))||Ca(0==f[2].length,0==g[2].length)||Ca(f[2],g[2]);f=f[3];g=g[3]}while(0==c)}return c}function Ca(a,b){return a<b?-1:a>b?1:0};var q;a:{var Da=aa.navigator;if(Da){var Ea=Da.userAgent;if(Ea){q=Ea;break a}}q=""}function r(a){return-1!=q.indexOf(a)};function Fa(){return r("Firefox")||r("FxiOS")}function Ga(){return(r("Chrome")||r("CriOS"))&&!r("Edge")};function Ha(a){return String(a).replace(/\-([a-z])/g,function(b,c){return c.toUpperCase()})};function Ia(){return r("iPhone")&&!r("iPod")&&!r("iPad")};function Ja(a,b){var c=Ka;return Object.prototype.hasOwnProperty.call(c,a)?c[a]:c[a]=b(a)};var La=r("Opera"),t=r("Trident")||r("MSIE"),Ma=r("Edge"),Na=r("Gecko")&&!(-1!=q.toLowerCase().indexOf("webkit")&&!r("Edge"))&&!(r("Trident")||r("MSIE"))&&!r("Edge"),Oa=-1!=q.toLowerCase().indexOf("webkit")&&!r("Edge");function Pa(){var a=aa.document;return a?a.documentMode:void 0}var Qa;
a:{var Ra="",Sa=function(){var a=q;if(Na)return/rv:([^\);]+)(\)|;)/.exec(a);if(Ma)return/Edge\/([\d\.]+)/.exec(a);if(t)return/\b(?:MSIE|rv)[: ]([^\);]+)(\)|;)/.exec(a);if(Oa)return/WebKit\/(\S+)/.exec(a);if(La)return/(?:Version)[ \/]?(\S+)/.exec(a)}();Sa&&(Ra=Sa?Sa[1]:"");if(t){var Ta=Pa();if(null!=Ta&&Ta>parseFloat(Ra)){Qa=String(Ta);break a}}Qa=Ra}var Ka={};function Ua(a){return Ja(a,function(){return 0<=Ba(Qa,a)})}var w;w=aa.document&&t?Pa():void 0;var x=t&&!(9<=Number(w)),Va=t&&!(8<=Number(w));function Wa(a,b,c,d){this.a=a;this.nodeName=c;this.nodeValue=d;this.nodeType=2;this.parentNode=this.ownerElement=b}function Xa(a,b){var c=Va&&"href"==b.nodeName?a.getAttribute(b.nodeName,2):b.nodeValue;return new Wa(b,a,b.nodeName,c)};function Ya(a){this.b=a;this.a=0}function Za(a){a=a.match($a);for(var b=0;b<a.length;b++)ab.test(a[b])&&a.splice(b,1);return new Ya(a)}var $a=/\$?(?:(?![0-9-\.])(?:\*|[\w-\.]+):)?(?![0-9-\.])(?:\*|[\w-\.]+)|\/\/|\.\.|::|\d+(?:\.\d*)?|\.\d+|"[^"]*"|'[^']*'|[!<>]=|\s+|./g,ab=/^\s/;function y(a,b){return a.b[a.a+(b||0)]}function z(a){return a.b[a.a++]}function bb(a){return a.b.length<=a.a};function cb(a,b){this.x=void 0!==a?a:0;this.y=void 0!==b?b:0}cb.prototype.ceil=function(){this.x=Math.ceil(this.x);this.y=Math.ceil(this.y);return this};cb.prototype.floor=function(){this.x=Math.floor(this.x);this.y=Math.floor(this.y);return this};cb.prototype.round=function(){this.x=Math.round(this.x);this.y=Math.round(this.y);return this};function db(a,b){this.width=a;this.height=b}db.prototype.aspectRatio=function(){return this.width/this.height};db.prototype.ceil=function(){this.width=Math.ceil(this.width);this.height=Math.ceil(this.height);return this};db.prototype.floor=function(){this.width=Math.floor(this.width);this.height=Math.floor(this.height);return this};db.prototype.round=function(){this.width=Math.round(this.width);this.height=Math.round(this.height);return this};function eb(a){return a?new fb(A(a)):na||(na=new fb)}function gb(a){for(;a&&1!=a.nodeType;)a=a.previousSibling;return a}function hb(a,b){if(!a||!b)return!1;if(a.contains&&1==b.nodeType)return a==b||a.contains(b);if("undefined"!=typeof a.compareDocumentPosition)return a==b||!!(a.compareDocumentPosition(b)&16);for(;b&&a!=b;)b=b.parentNode;return b==a}
function ib(a,b){if(a==b)return 0;if(a.compareDocumentPosition)return a.compareDocumentPosition(b)&2?1:-1;if(t&&!(9<=Number(w))){if(9==a.nodeType)return-1;if(9==b.nodeType)return 1}if("sourceIndex"in a||a.parentNode&&"sourceIndex"in a.parentNode){var c=1==a.nodeType,d=1==b.nodeType;if(c&&d)return a.sourceIndex-b.sourceIndex;var e=a.parentNode,f=b.parentNode;return e==f?jb(a,b):!c&&hb(e,b)?-1*kb(a,b):!d&&hb(f,a)?kb(b,a):(c?a.sourceIndex:e.sourceIndex)-(d?b.sourceIndex:f.sourceIndex)}d=A(a);c=d.createRange();
c.selectNode(a);c.collapse(!0);a=d.createRange();a.selectNode(b);a.collapse(!0);return c.compareBoundaryPoints(aa.Range.START_TO_END,a)}function kb(a,b){var c=a.parentNode;if(c==b)return-1;for(;b.parentNode!=c;)b=b.parentNode;return jb(b,a)}function jb(a,b){for(;b=b.previousSibling;)if(b==a)return-1;return 1}function A(a){return 9==a.nodeType?a:a.ownerDocument||a.document}function lb(a,b){a&&(a=a.parentNode);for(var c=0;a;){if(b(a))return a;a=a.parentNode;c++}return null}
function fb(a){this.a=a||aa.document||document}fb.prototype.getElementsByTagName=function(a,b){return(b||this.a).getElementsByTagName(String(a))};
function mb(a,b,c,d){a=d||a.a;var e=b&&"*"!=b?String(b).toUpperCase():"";if(a.querySelectorAll&&a.querySelector&&(e||c))c=a.querySelectorAll(e+(c?"."+c:""));else if(c&&a.getElementsByClassName)if(b=a.getElementsByClassName(c),e){a={};for(var f=d=0,g;g=b[f];f++)e==g.nodeName&&(a[d++]=g);a.length=d;c=a}else c=b;else if(b=a.getElementsByTagName(e||"*"),c){a={};for(f=d=0;g=b[f];f++){e=g.className;var h;if(h="function"==typeof e.split)h=0<=oa(e.split(/\s+/),c);h&&(a[d++]=g)}a.length=d;c=a}else c=b;return c}
;function B(a){var b=null,c=a.nodeType;1==c&&(b=a.textContent,b=void 0==b||null==b?a.innerText:b,b=void 0==b||null==b?"":b);if("string"!=typeof b)if(x&&"title"==a.nodeName.toLowerCase()&&1==c)b=a.text;else if(9==c||1==c){a=9==c?a.documentElement:a.firstChild;c=0;var d=[];for(b="";a;){do 1!=a.nodeType&&(b+=a.nodeValue),x&&"title"==a.nodeName.toLowerCase()&&(b+=a.text),d[c++]=a;while(a=a.firstChild);for(;c&&!(a=d[--c].nextSibling););}}else b=a.nodeValue;return b}
function nb(a,b,c){if(null===b)return!0;try{if(!a.getAttribute)return!1}catch(d){return!1}Va&&"class"==b&&(b="className");return null==c?!!a.getAttribute(b):a.getAttribute(b,2)==c}function ob(a,b,c,d,e){return(x?pb:qb).call(null,a,b,ba(c)?c:null,ba(d)?d:null,e||new C)}
function pb(a,b,c,d,e){if(a instanceof rb||8==a.b||c&&null===a.b){var f=b.all;if(!f)return e;a=sb(a);if("*"!=a&&(f=b.getElementsByTagName(a),!f))return e;if(c){for(var g=[],h=0;b=f[h++];)nb(b,c,d)&&g.push(b);f=g}for(h=0;b=f[h++];)"*"==a&&"!"==b.tagName||e.add(b);return e}tb(a,b,c,d,e);return e}
function qb(a,b,c,d,e){b.getElementsByName&&d&&"name"==c&&!t?(b=b.getElementsByName(d),l(b,function(f){a.a(f)&&e.add(f)})):b.getElementsByClassName&&d&&"class"==c?(b=b.getElementsByClassName(d),l(b,function(f){f.className==d&&a.a(f)&&e.add(f)})):a instanceof D?tb(a,b,c,d,e):b.getElementsByTagName&&(b=b.getElementsByTagName(a.f()),l(b,function(f){nb(f,c,d)&&e.add(f)}));return e}
function ub(a,b,c,d,e){var f;if((a instanceof rb||8==a.b||c&&null===a.b)&&(f=b.childNodes)){var g=sb(a);if("*"!=g&&(f=pa(f,function(h){return h.tagName&&h.tagName.toLowerCase()==g}),!f))return e;c&&(f=pa(f,function(h){return nb(h,c,d)}));l(f,function(h){"*"==g&&("!"==h.tagName||"*"==g&&1!=h.nodeType)||e.add(h)});return e}return vb(a,b,c,d,e)}function vb(a,b,c,d,e){for(b=b.firstChild;b;b=b.nextSibling)nb(b,c,d)&&a.a(b)&&e.add(b);return e}
function tb(a,b,c,d,e){for(b=b.firstChild;b;b=b.nextSibling)nb(b,c,d)&&a.a(b)&&e.add(b),tb(a,b,c,d,e)}function sb(a){if(a instanceof D){if(8==a.b)return"!";if(null===a.b)return"*"}return a.f()};function C(){this.b=this.a=null;this.m=0}function wb(a){this.f=a;this.a=this.b=null}function xb(a,b){if(!a.a)return b;if(!b.a)return a;var c=a.a;b=b.a;for(var d=null,e,f=0;c&&b;){e=c.f;var g=b.f;e==g||e instanceof Wa&&g instanceof Wa&&e.a==g.a?(e=c,c=c.a,b=b.a):0<ib(c.f,b.f)?(e=b,b=b.a):(e=c,c=c.a);(e.b=d)?d.a=e:a.a=e;d=e;f++}for(e=c||b;e;)e.b=d,d=d.a=e,f++,e=e.a;a.b=d;a.m=f;return a}function yb(a,b){b=new wb(b);b.a=a.a;a.b?a.a.b=b:a.a=a.b=b;a.a=b;a.m++}
C.prototype.add=function(a){a=new wb(a);a.b=this.b;this.a?this.b.a=a:this.a=this.b=a;this.b=a;this.m++};function zb(a){return(a=a.a)?a.f:null}function Bb(a){return(a=zb(a))?B(a):""}function Cb(a,b){return new Db(a,!!b)}function Db(a,b){this.f=a;this.b=(this.A=b)?a.b:a.a;this.a=null}function E(a){var b=a.b;if(null==b)return null;var c=a.a=b;a.b=a.A?b.b:b.a;return c.f};function F(a){this.i=a;this.b=this.g=!1;this.f=null}function H(a){return"\n "+a.toString().split("\n").join("\n ")}function Eb(a,b){a.g=b}function Fb(a,b){a.b=b}function I(a,b){a=a.a(b);return a instanceof C?+Bb(a):+a}function J(a,b){a=a.a(b);return a instanceof C?Bb(a):""+a}function Gb(a,b){a=a.a(b);return a instanceof C?!!a.m:!!a};function Hb(a,b,c){F.call(this,a.i);this.c=a;this.h=b;this.v=c;this.g=b.g||c.g;this.b=b.b||c.b;this.c==Ib&&(c.b||c.g||4==c.i||0==c.i||!b.f?b.b||b.g||4==b.i||0==b.i||!c.f||(this.f={name:c.f.name,B:b}):this.f={name:b.f.name,B:c})}k(Hb,F);
function Jb(a,b,c,d,e){b=b.a(d);c=c.a(d);var f;if(b instanceof C&&c instanceof C){b=Cb(b);for(d=E(b);d;d=E(b))for(e=Cb(c),f=E(e);f;f=E(e))if(a(B(d),B(f)))return!0;return!1}if(b instanceof C||c instanceof C){b instanceof C?(e=b,d=c):(e=c,d=b);f=Cb(e);for(var g=typeof d,h=E(f);h;h=E(f)){switch(g){case "number":h=+B(h);break;case "boolean":h=!!B(h);break;case "string":h=B(h);break;default:throw Error("Illegal primitive type for comparison.");}if(e==b&&a(h,d)||e==c&&a(d,h))return!0}return!1}return e?
"boolean"==typeof b||"boolean"==typeof c?a(!!b,!!c):"number"==typeof b||"number"==typeof c?a(+b,+c):a(b,c):a(+b,+c)}Hb.prototype.a=function(a){return this.c.s(this.h,this.v,a)};Hb.prototype.toString=function(){var a="Binary Expression: "+this.c;a+=H(this.h);return a+=H(this.v)};function Kb(a,b,c,d){this.$=a;this.M=b;this.i=c;this.s=d}Kb.prototype.toString=function(){return this.$};var Lb={};
function K(a,b,c,d){if(Lb.hasOwnProperty(a))throw Error("Binary operator already created: "+a);a=new Kb(a,b,c,d);return Lb[a.toString()]=a}K("div",6,1,function(a,b,c){return I(a,c)/I(b,c)});K("mod",6,1,function(a,b,c){return I(a,c)%I(b,c)});K("*",6,1,function(a,b,c){return I(a,c)*I(b,c)});K("+",5,1,function(a,b,c){return I(a,c)+I(b,c)});K("-",5,1,function(a,b,c){return I(a,c)-I(b,c)});K("<",4,2,function(a,b,c){return Jb(function(d,e){return d<e},a,b,c)});
K(">",4,2,function(a,b,c){return Jb(function(d,e){return d>e},a,b,c)});K("<=",4,2,function(a,b,c){return Jb(function(d,e){return d<=e},a,b,c)});K(">=",4,2,function(a,b,c){return Jb(function(d,e){return d>=e},a,b,c)});var Ib=K("=",3,2,function(a,b,c){return Jb(function(d,e){return d==e},a,b,c,!0)});K("!=",3,2,function(a,b,c){return Jb(function(d,e){return d!=e},a,b,c,!0)});K("and",2,2,function(a,b,c){return Gb(a,c)&&Gb(b,c)});K("or",1,2,function(a,b,c){return Gb(a,c)||Gb(b,c)});function Mb(a,b){if(b.a.length&&4!=a.i)throw Error("Primary expression must evaluate to nodeset if filter has predicate(s).");F.call(this,a.i);this.c=a;this.h=b;this.g=a.g;this.b=a.b}k(Mb,F);Mb.prototype.a=function(a){a=this.c.a(a);return Nb(this.h,a)};Mb.prototype.toString=function(){var a="Filter:"+H(this.c);return a+=H(this.h)};function Ob(a,b){if(b.length<a.L)throw Error("Function "+a.l+" expects at least"+a.L+" arguments, "+b.length+" given");if(null!==a.H&&b.length>a.H)throw Error("Function "+a.l+" expects at most "+a.H+" arguments, "+b.length+" given");a.Z&&l(b,function(c,d){if(4!=c.i)throw Error("Argument "+d+" to function "+a.l+" is not of type Nodeset: "+c);});F.call(this,a.i);this.D=a;this.c=b;Eb(this,a.g||sa(b,function(c){return c.g}));Fb(this,a.Y&&!b.length||a.X&&!!b.length||sa(b,function(c){return c.b}))}
k(Ob,F);Ob.prototype.a=function(a){return this.D.s.apply(null,va(a,this.c))};Ob.prototype.toString=function(){var a="Function: "+this.D;if(this.c.length){var b=ra(this.c,function(c,d){return c+H(d)},"Arguments:");a+=H(b)}return a};function Pb(a,b,c,d,e,f,g,h){this.l=a;this.i=b;this.g=c;this.Y=d;this.X=!1;this.s=e;this.L=f;this.H=void 0!==g?g:f;this.Z=!!h}Pb.prototype.toString=function(){return this.l};var Qb={};
function L(a,b,c,d,e,f,g,h){if(Qb.hasOwnProperty(a))throw Error("Function already created: "+a+".");Qb[a]=new Pb(a,b,c,d,e,f,g,h)}L("boolean",2,!1,!1,function(a,b){return Gb(b,a)},1);L("ceiling",1,!1,!1,function(a,b){return Math.ceil(I(b,a))},1);L("concat",3,!1,!1,function(a,b){return ra(wa(arguments,1),function(c,d){return c+J(d,a)},"")},2,null);L("contains",2,!1,!1,function(a,b,c){b=J(b,a);a=J(c,a);return a=-1!=b.indexOf(a)},2);L("count",1,!1,!1,function(a,b){return b.a(a).m},1,1,!0);
L("false",2,!1,!1,function(){return!1},0);L("floor",1,!1,!1,function(a,b){return Math.floor(I(b,a))},1);L("id",4,!1,!1,function(a,b){function c(h){if(x){var n=e.all[h];if(n){if(n.nodeType&&h==n.id)return n;if(n.length)return ua(n,function(u){return h==u.id})}return null}return e.getElementById(h)}var d=a.a,e=9==d.nodeType?d:d.ownerDocument;a=J(b,a).split(/\s+/);var f=[];l(a,function(h){h=c(h);!h||0<=oa(f,h)||f.push(h)});f.sort(ib);var g=new C;l(f,function(h){g.add(h)});return g},1);
L("lang",2,!1,!1,function(){return!1},1);L("last",1,!0,!1,function(a){if(1!=arguments.length)throw Error("Function last expects ()");return a.f},0);L("local-name",3,!1,!0,function(a,b){return(a=b?zb(b.a(a)):a.a)?a.localName||a.nodeName.toLowerCase():""},0,1,!0);L("name",3,!1,!0,function(a,b){return(a=b?zb(b.a(a)):a.a)?a.nodeName.toLowerCase():""},0,1,!0);L("namespace-uri",3,!0,!1,function(){return""},0,1,!0);
L("normalize-space",3,!1,!0,function(a,b){return(b?J(b,a):B(a.a)).replace(/[\s\xa0]+/g," ").replace(/^\s+|\s+$/g,"")},0,1);L("not",2,!1,!1,function(a,b){return!Gb(b,a)},1);L("number",1,!1,!0,function(a,b){return b?I(b,a):+B(a.a)},0,1);L("position",1,!0,!1,function(a){return a.b},0);L("round",1,!1,!1,function(a,b){return Math.round(I(b,a))},1);L("starts-with",2,!1,!1,function(a,b,c){b=J(b,a);a=J(c,a);return 0==b.lastIndexOf(a,0)},2);L("string",3,!1,!0,function(a,b){return b?J(b,a):B(a.a)},0,1);
L("string-length",1,!1,!0,function(a,b){return(b?J(b,a):B(a.a)).length},0,1);L("substring",3,!1,!1,function(a,b,c,d){c=I(c,a);if(isNaN(c)||Infinity==c||-Infinity==c)return"";d=d?I(d,a):Infinity;if(isNaN(d)||-Infinity===d)return"";c=Math.round(c)-1;var e=Math.max(c,0);a=J(b,a);return Infinity==d?a.substring(e):a.substring(e,c+Math.round(d))},2,3);L("substring-after",3,!1,!1,function(a,b,c){b=J(b,a);a=J(c,a);c=b.indexOf(a);return-1==c?"":b.substring(c+a.length)},2);
L("substring-before",3,!1,!1,function(a,b,c){b=J(b,a);a=J(c,a);a=b.indexOf(a);return-1==a?"":b.substring(0,a)},2);L("sum",1,!1,!1,function(a,b){a=Cb(b.a(a));b=0;for(var c=E(a);c;c=E(a))b+=+B(c);return b},1,1,!0);L("translate",3,!1,!1,function(a,b,c,d){b=J(b,a);c=J(c,a);var e=J(d,a);a={};for(d=0;d<c.length;d++){var f=c.charAt(d);f in a||(a[f]=e.charAt(d))}c="";for(d=0;d<b.length;d++)f=b.charAt(d),c+=f in a?a[f]:f;return c},3);L("true",2,!1,!1,function(){return!0},0);function D(a,b){this.h=a;this.c=void 0!==b?b:null;this.b=null;switch(a){case "comment":this.b=8;break;case "text":this.b=3;break;case "processing-instruction":this.b=7;break;case "node":break;default:throw Error("Unexpected argument");}}function Rb(a){return"comment"==a||"text"==a||"processing-instruction"==a||"node"==a}D.prototype.a=function(a){return null===this.b||this.b==a.nodeType};D.prototype.f=function(){return this.h};
D.prototype.toString=function(){var a="Kind Test: "+this.h;null===this.c||(a+=H(this.c));return a};function Sb(a){F.call(this,3);this.c=a.substring(1,a.length-1)}k(Sb,F);Sb.prototype.a=function(){return this.c};Sb.prototype.toString=function(){return"Literal: "+this.c};function rb(a,b){this.l=a.toLowerCase();a="*"==this.l?"*":"http://www.w3.org/1999/xhtml";this.c=b?b.toLowerCase():a}rb.prototype.a=function(a){var b=a.nodeType;if(1!=b&&2!=b)return!1;b=void 0!==a.localName?a.localName:a.nodeName;return"*"!=this.l&&this.l!=b.toLowerCase()?!1:"*"==this.c?!0:this.c==(a.namespaceURI?a.namespaceURI.toLowerCase():"http://www.w3.org/1999/xhtml")};rb.prototype.f=function(){return this.l};
rb.prototype.toString=function(){return"Name Test: "+("http://www.w3.org/1999/xhtml"==this.c?"":this.c+":")+this.l};function Tb(a){F.call(this,1);this.c=a}k(Tb,F);Tb.prototype.a=function(){return this.c};Tb.prototype.toString=function(){return"Number: "+this.c};function Ub(a,b){F.call(this,a.i);this.h=a;this.c=b;this.g=a.g;this.b=a.b;1==this.c.length&&(a=this.c[0],a.G||a.c!=Vb||(a=a.v,"*"!=a.f()&&(this.f={name:a.f(),B:null})))}k(Ub,F);function Wb(){F.call(this,4)}k(Wb,F);Wb.prototype.a=function(a){var b=new C;a=a.a;9==a.nodeType?b.add(a):b.add(a.ownerDocument);return b};Wb.prototype.toString=function(){return"Root Helper Expression"};function Xb(){F.call(this,4)}k(Xb,F);Xb.prototype.a=function(a){var b=new C;b.add(a.a);return b};Xb.prototype.toString=function(){return"Context Helper Expression"};
function Yb(a){return"/"==a||"//"==a}Ub.prototype.a=function(a){var b=this.h.a(a);if(!(b instanceof C))throw Error("Filter expression must evaluate to nodeset.");a=this.c;for(var c=0,d=a.length;c<d&&b.m;c++){var e=a[c],f=Cb(b,e.c.A);if(e.g||e.c!=Zb)if(e.g||e.c!=$b){var g=E(f);for(b=e.a(new ma(g));null!=(g=E(f));)g=e.a(new ma(g)),b=xb(b,g)}else g=E(f),b=e.a(new ma(g));else{for(g=E(f);(b=E(f))&&(!g.contains||g.contains(b))&&b.compareDocumentPosition(g)&8;g=b);b=e.a(new ma(g))}}return b};
Ub.prototype.toString=function(){var a="Path Expression:"+H(this.h);if(this.c.length){var b=ra(this.c,function(c,d){return c+H(d)},"Steps:");a+=H(b)}return a};function ac(a,b){this.a=a;this.A=!!b}
function Nb(a,b,c){for(c=c||0;c<a.a.length;c++)for(var d=a.a[c],e=Cb(b),f=b.m,g,h=0;g=E(e);h++){var n=a.A?f-h:h+1;g=d.a(new ma(g,n,f));if("number"==typeof g)n=n==g;else if("string"==typeof g||"boolean"==typeof g)n=!!g;else if(g instanceof C)n=0<g.m;else throw Error("Predicate.evaluate returned an unexpected type.");if(!n){n=e;g=n.f;var u=n.a;if(!u)throw Error("Next must be called at least once before remove.");var p=u.b;u=u.a;p?p.a=u:g.a=u;u?u.b=p:g.b=p;g.m--;n.a=null}}return b}
ac.prototype.toString=function(){return ra(this.a,function(a,b){return a+H(b)},"Predicates:")};function bc(a,b,c,d){F.call(this,4);this.c=a;this.v=b;this.h=c||new ac([]);this.G=!!d;b=this.h;b=0<b.a.length?b.a[0].f:null;a.ca&&b&&(a=b.name,a=x?a.toLowerCase():a,this.f={name:a,B:b.B});a:{a=this.h;for(b=0;b<a.a.length;b++)if(c=a.a[b],c.g||1==c.i||0==c.i){a=!0;break a}a=!1}this.g=a}k(bc,F);
bc.prototype.a=function(a){var b=a.a,c=this.f,d=null,e=null,f=0;c&&(d=c.name,e=c.B?J(c.B,a):null,f=1);if(this.G)if(this.g||this.c!=cc)if(b=Cb((new bc(dc,new D("node"))).a(a)),c=E(b))for(a=this.s(c,d,e,f);null!=(c=E(b));)a=xb(a,this.s(c,d,e,f));else a=new C;else a=ob(this.v,b,d,e),a=Nb(this.h,a,f);else a=this.s(a.a,d,e,f);return a};bc.prototype.s=function(a,b,c,d){a=this.c.D(this.v,a,b,c);return a=Nb(this.h,a,d)};
bc.prototype.toString=function(){var a="Step:"+H("Operator: "+(this.G?"//":"/"));this.c.l&&(a+=H("Axis: "+this.c));a+=H(this.v);if(this.h.a.length){var b=ra(this.h.a,function(c,d){return c+H(d)},"Predicates:");a+=H(b)}return a};function ec(a,b,c,d){this.l=a;this.D=b;this.A=c;this.ca=d}ec.prototype.toString=function(){return this.l};var fc={};function M(a,b,c,d){if(fc.hasOwnProperty(a))throw Error("Axis already created: "+a);b=new ec(a,b,c,!!d);return fc[a]=b}
M("ancestor",function(a,b){for(var c=new C;b=b.parentNode;)a.a(b)&&yb(c,b);return c},!0);M("ancestor-or-self",function(a,b){var c=new C;do a.a(b)&&yb(c,b);while(b=b.parentNode);return c},!0);
var Vb=M("attribute",function(a,b){var c=new C,d=a.f();if("style"==d&&x&&b.style)return c.add(new Wa(b.style,b,"style",b.style.cssText)),c;var e=b.attributes;if(e)if(a instanceof D&&null===a.b||"*"==d)for(a=0;d=e[a];a++)x?d.nodeValue&&c.add(Xa(b,d)):c.add(d);else(d=e.getNamedItem(d))&&(x?d.nodeValue&&c.add(Xa(b,d)):c.add(d));return c},!1),cc=M("child",function(a,b,c,d,e){return(x?ub:vb).call(null,a,b,ba(c)?c:null,ba(d)?d:null,e||new C)},!1,!0);M("descendant",ob,!1,!0);
var dc=M("descendant-or-self",function(a,b,c,d){var e=new C;nb(b,c,d)&&a.a(b)&&e.add(b);return ob(a,b,c,d,e)},!1,!0),Zb=M("following",function(a,b,c,d){var e=new C;do for(var f=b;f=f.nextSibling;)nb(f,c,d)&&a.a(f)&&e.add(f),e=ob(a,f,c,d,e);while(b=b.parentNode);return e},!1,!0);M("following-sibling",function(a,b){for(var c=new C;b=b.nextSibling;)a.a(b)&&c.add(b);return c},!1);M("namespace",function(){return new C},!1);
var gc=M("parent",function(a,b){var c=new C;if(9==b.nodeType)return c;if(2==b.nodeType)return c.add(b.ownerElement),c;b=b.parentNode;a.a(b)&&c.add(b);return c},!1),$b=M("preceding",function(a,b,c,d){var e=new C,f=[];do f.unshift(b);while(b=b.parentNode);for(var g=1,h=f.length;g<h;g++){var n=[];for(b=f[g];b=b.previousSibling;)n.unshift(b);for(var u=0,p=n.length;u<p;u++)b=n[u],nb(b,c,d)&&a.a(b)&&e.add(b),e=ob(a,b,c,d,e)}return e},!0,!0);
M("preceding-sibling",function(a,b){for(var c=new C;b=b.previousSibling;)a.a(b)&&yb(c,b);return c},!0);var hc=M("self",function(a,b){var c=new C;a.a(b)&&c.add(b);return c},!1);function ic(a){F.call(this,1);this.c=a;this.g=a.g;this.b=a.b}k(ic,F);ic.prototype.a=function(a){return-I(this.c,a)};ic.prototype.toString=function(){return"Unary Expression: -"+H(this.c)};function jc(a){F.call(this,4);this.c=a;Eb(this,sa(this.c,function(b){return b.g}));Fb(this,sa(this.c,function(b){return b.b}))}k(jc,F);jc.prototype.a=function(a){var b=new C;l(this.c,function(c){c=c.a(a);if(!(c instanceof C))throw Error("Path expression must evaluate to NodeSet.");b=xb(b,c)});return b};jc.prototype.toString=function(){return ra(this.c,function(a,b){return a+H(b)},"Union Expression:")};function kc(a,b){this.a=a;this.b=b}function lc(a){for(var b,c=[];;){N(a,"Missing right hand side of binary expression.");b=mc(a);var d=z(a.a);if(!d)break;var e=(d=Lb[d]||null)&&d.M;if(!e){a.a.a--;break}for(;c.length&&e<=c[c.length-1].M;)b=new Hb(c.pop(),c.pop(),b);c.push(b,d)}for(;c.length;)b=new Hb(c.pop(),c.pop(),b);return b}function N(a,b){if(bb(a.a))throw Error(b);}function nc(a,b){a=z(a.a);if(a!=b)throw Error("Bad token, expected: "+b+" got: "+a);}
function oc(a){a=z(a.a);if(")"!=a)throw Error("Bad token: "+a);}function rc(a){a=z(a.a);if(2>a.length)throw Error("Unclosed literal string");return new Sb(a)}
function sc(a){var b=[];if(Yb(y(a.a))){var c=z(a.a);var d=y(a.a);if("/"==c&&(bb(a.a)||"."!=d&&".."!=d&&"@"!=d&&"*"!=d&&!/(?![0-9])[\w]/.test(d)))return new Wb;d=new Wb;N(a,"Missing next location step.");c=tc(a,c);b.push(c)}else{a:{c=y(a.a);d=c.charAt(0);switch(d){case "$":throw Error("Variable reference not allowed in HTML XPath");case "(":z(a.a);c=lc(a);N(a,'unclosed "("');nc(a,")");break;case '"':case "'":c=rc(a);break;default:if(isNaN(+c))if(!Rb(c)&&/(?![0-9])[\w]/.test(d)&&"("==y(a.a,1)){c=z(a.a);
c=Qb[c]||null;z(a.a);for(d=[];")"!=y(a.a);){N(a,"Missing function argument list.");d.push(lc(a));if(","!=y(a.a))break;z(a.a)}N(a,"Unclosed function argument list.");oc(a);c=new Ob(c,d)}else{c=null;break a}else c=new Tb(+z(a.a))}"["==y(a.a)&&(d=new ac(uc(a)),c=new Mb(c,d))}if(c)if(Yb(y(a.a)))d=c;else return c;else c=tc(a,"/"),d=new Xb,b.push(c)}for(;Yb(y(a.a));)c=z(a.a),N(a,"Missing next location step."),c=tc(a,c),b.push(c);return new Ub(d,b)}
function tc(a,b){if("/"!=b&&"//"!=b)throw Error('Step op should be "/" or "//"');if("."==y(a.a)){var c=new bc(hc,new D("node"));z(a.a);return c}if(".."==y(a.a))return c=new bc(gc,new D("node")),z(a.a),c;if("@"==y(a.a)){var d=Vb;z(a.a);N(a,"Missing attribute name")}else if("::"==y(a.a,1)){if(!/(?![0-9])[\w]/.test(y(a.a).charAt(0)))throw Error("Bad token: "+z(a.a));var e=z(a.a);d=fc[e]||null;if(!d)throw Error("No axis with name: "+e);z(a.a);N(a,"Missing node name")}else d=cc;e=y(a.a);if(/(?![0-9])[\w\*]/.test(e.charAt(0)))if("("==
y(a.a,1)){if(!Rb(e))throw Error("Invalid node type: "+e);e=z(a.a);if(!Rb(e))throw Error("Invalid type name: "+e);nc(a,"(");N(a,"Bad nodetype");var f=y(a.a).charAt(0),g=null;if('"'==f||"'"==f)g=rc(a);N(a,"Bad nodetype");oc(a);e=new D(e,g)}else if(e=z(a.a),f=e.indexOf(":"),-1==f)e=new rb(e);else{g=e.substring(0,f);if("*"==g)var h="*";else if(h=a.b(g),!h)throw Error("Namespace prefix not declared: "+g);e=e.substr(f+1);e=new rb(e,h)}else throw Error("Bad token: "+z(a.a));a=new ac(uc(a),d.A);return c||
new bc(d,e,a,"//"==b)}function uc(a){for(var b=[];"["==y(a.a);){z(a.a);N(a,"Missing predicate expression.");var c=lc(a);b.push(c);N(a,"Unclosed predicate expression.");nc(a,"]")}return b}function mc(a){if("-"==y(a.a))return z(a.a),new ic(mc(a));var b=sc(a);if("|"!=y(a.a))a=b;else{for(b=[b];"|"==z(a.a);)N(a,"Missing next union location path."),b.push(sc(a));a.a.a--;a=new jc(b)}return a};function vc(a){switch(a.nodeType){case 1:return la(wc,a);case 9:return vc(a.documentElement);case 11:case 10:case 6:case 12:return xc;default:return a.parentNode?vc(a.parentNode):xc}}function xc(){return null}function wc(a,b){if(a.prefix==b)return a.namespaceURI||"http://www.w3.org/1999/xhtml";var c=a.getAttributeNode("xmlns:"+b);return c&&c.specified?c.value||null:a.parentNode&&9!=a.parentNode.nodeType?wc(a.parentNode,b):null};function yc(a,b){if(!a.length)throw Error("Empty XPath expression.");a=Za(a);if(bb(a))throw Error("Invalid XPath expression.");b?ea(b)||(b=ka(b.lookupNamespaceURI,b)):b=function(){return null};var c=lc(new kc(a,b));if(!bb(a))throw Error("Bad token: "+z(a));this.evaluate=function(d,e){d=c.a(new ma(d));return new O(d,e)}}
function O(a,b){if(0==b)if(a instanceof C)b=4;else if("string"==typeof a)b=2;else if("number"==typeof a)b=1;else if("boolean"==typeof a)b=3;else throw Error("Unexpected evaluation result.");if(2!=b&&1!=b&&3!=b&&!(a instanceof C))throw Error("value could not be converted to the specified type");this.resultType=b;switch(b){case 2:this.stringValue=a instanceof C?Bb(a):""+a;break;case 1:this.numberValue=a instanceof C?+Bb(a):+a;break;case 3:this.booleanValue=a instanceof C?0<a.m:!!a;break;case 4:case 5:case 6:case 7:var c=
Cb(a);var d=[];for(var e=E(c);e;e=E(c))d.push(e instanceof Wa?e.a:e);this.snapshotLength=a.m;this.invalidIteratorState=!1;break;case 8:case 9:a=zb(a);this.singleNodeValue=a instanceof Wa?a.a:a;break;default:throw Error("Unknown XPathResult type.");}var f=0;this.iterateNext=function(){if(4!=b&&5!=b)throw Error("iterateNext called with wrong result type");return f>=d.length?null:d[f++]};this.snapshotItem=function(g){if(6!=b&&7!=b)throw Error("snapshotItem called with wrong result type");return g>=d.length||
0>g?null:d[g]}}O.ANY_TYPE=0;O.NUMBER_TYPE=1;O.STRING_TYPE=2;O.BOOLEAN_TYPE=3;O.UNORDERED_NODE_ITERATOR_TYPE=4;O.ORDERED_NODE_ITERATOR_TYPE=5;O.UNORDERED_NODE_SNAPSHOT_TYPE=6;O.ORDERED_NODE_SNAPSHOT_TYPE=7;O.ANY_UNORDERED_NODE_TYPE=8;O.FIRST_ORDERED_NODE_TYPE=9;function zc(a){this.lookupNamespaceURI=vc(a)}
function Ac(a,b){a=a||aa;var c=a.Document&&a.Document.prototype||a.document;if(!c.evaluate||b)a.XPathResult=O,c.evaluate=function(d,e,f,g){return(new yc(d,f)).evaluate(e,g)},c.createExpression=function(d,e){return new yc(d,e)},c.createNSResolver=function(d){return new zc(d)}}ca("wgxpath.install",Ac);ca("wgxpath.install",Ac);var Bc=window;function P(a,b){this.code=a;this.a=Q[a]||Cc;this.message=b||"";a=this.a.replace(/((?:^|\s+)[a-z])/g,function(c){return c.toUpperCase().replace(/^[\s\xa0]+/g,"")});b=a.length-5;if(0>b||a.indexOf("Error",b)!=b)a+="Error";this.name=a;a=Error(this.message);a.name=this.name;this.stack=a.stack||""}k(P,Error);var Cc="unknown error",Q={15:"element not selectable",11:"element not visible"};Q[31]=Cc;Q[30]=Cc;Q[24]="invalid cookie domain";Q[29]="invalid element coordinates";Q[12]="invalid element state";
Q[32]="invalid selector";Q[51]="invalid selector";Q[52]="invalid selector";Q[17]="javascript error";Q[405]="unsupported operation";Q[34]="move target out of bounds";Q[27]="no such alert";Q[7]="no such element";Q[8]="no such frame";Q[23]="no such window";Q[28]="script timeout";Q[33]="session not created";Q[10]="stale element reference";Q[21]="timeout";Q[25]="unable to set cookie";Q[26]="unexpected alert open";Q[13]=Cc;Q[9]="unknown command";var Dc={C:function(a){return!(!a.querySelectorAll||!a.querySelector)},o:function(a,b){if(!a)throw new P(32,"No class name specified");a=Aa(a);if(-1!==a.indexOf(" "))throw new P(32,"Compound class names not permitted");if(Dc.C(b))try{return b.querySelector("."+a.replace(/\./g,"\\."))||null}catch(c){throw new P(32,"An invalid or illegal class name was specified");}a=mb(eb(b),"*",a,b);return a.length?a[0]:null},j:function(a,b){if(!a)throw new P(32,"No class name specified");a=Aa(a);if(-1!==a.indexOf(" "))throw new P(32,
"Compound class names not permitted");if(Dc.C(b))try{return b.querySelectorAll("."+a.replace(/\./g,"\\."))}catch(c){throw new P(32,"An invalid or illegal class name was specified");}return mb(eb(b),"*",a,b)}};var Ec=Fa(),Fc=Ia()||r("iPod"),Gc=r("iPad"),Hc=r("Android")&&!(Ga()||Fa()||r("Opera")||r("Silk")),Ic=Ga(),Jc=r("Safari")&&!(Ga()||r("Coast")||r("Opera")||r("Edge")||r("Edg/")||r("OPR")||Fa()||r("Silk")||r("Android"))&&!(Ia()||r("iPad")||r("iPod"));function Kc(a){return(a=a.exec(q))?a[1]:""}(function(){if(Ec)return Kc(/Firefox\/([0-9.]+)/);if(t||Ma||La)return Qa;if(Ic)return Ia()||r("iPad")||r("iPod")?Kc(/CriOS\/([0-9.]+)/):Kc(/Chrome\/([0-9.]+)/);if(Jc&&!(Ia()||r("iPad")||r("iPod")))return Kc(/Version\/([0-9.]+)/);if(Fc||Gc){var a=/Version\/(\S+).*Mobile\/(\S+)/.exec(q);if(a)return a[1]+"."+a[2]}else if(Hc)return(a=Kc(/Android\s+([0-9.]+)/))?a:Kc(/Version\/([0-9.]+)/);return""})();var Lc=t&&!(8<=Number(w)),Mc=t&&!(9<=Number(w));var Nc={o:function(a,b){if(!ea(b.querySelector)&&t&&(t?0<=Ba(w,8):Ua(8))&&!ha(b.querySelector))throw Error("CSS selection is not supported");if(!a)throw new P(32,"No selector specified");a=Aa(a);try{var c=b.querySelector(a)}catch(d){throw new P(32,"An invalid or illegal selector was specified");}return c&&1==c.nodeType?c:null},j:function(a,b){if(!ea(b.querySelectorAll)&&t&&(t?0<=Ba(w,8):Ua(8))&&!ha(b.querySelector))throw Error("CSS selection is not supported");if(!a)throw new P(32,"No selector specified");
a=Aa(a);try{return b.querySelectorAll(a)}catch(c){throw new P(32,"An invalid or illegal selector was specified");}}};var Oc={aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",
darkgrey:"#a9a9a9",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",
ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",green:"#008000",greenyellow:"#adff2f",grey:"#808080",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgray:"#d3d3d3",lightgreen:"#90ee90",lightgrey:"#d3d3d3",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",
lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370db",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",
moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#db7093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",
seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",slategrey:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32"};var Pc="backgroundColor borderTopColor borderRightColor borderBottomColor borderLeftColor color outlineColor".split(" "),Qc=/#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])/,Rc=/^#(?:[0-9a-f]{3}){1,2}$/i,Sc=/^(?:rgba)?\((\d{1,3}),\s?(\d{1,3}),\s?(\d{1,3}),\s?(0|1|0\.\d*)\)$/i,Tc=/^(?:rgb)?\((0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2})\)$/i;function Uc(a,b){b=b.toLowerCase();return"style"==b?Vc(a.style.cssText):Lc&&"value"==b&&S(a,"INPUT")?a.value:Mc&&!0===a[b]?String(a.getAttribute(b)):(a=a.getAttributeNode(b))&&a.specified?a.value:null}var Wc=/[;]+(?=(?:(?:[^"]*"){2})*[^"]*$)(?=(?:(?:[^']*'){2})*[^']*$)(?=(?:[^()]*\([^()]*\))*[^()]*$)/;
function Vc(a){var b=[];l(a.split(Wc),function(c){var d=c.indexOf(":");0<d&&(c=[c.slice(0,d),c.slice(d+1)],2==c.length&&b.push(c[0].toLowerCase(),":",c[1],";"))});b=b.join("");return b=";"==b.charAt(b.length-1)?b:b+";"}function S(a,b){b&&"string"!==typeof b&&(b=b.toString());return!!a&&1==a.nodeType&&(!b||a.tagName.toUpperCase()==b)};function Xc(a,b,c,d){this.c=a;this.a=b;this.b=c;this.f=d}Xc.prototype.ceil=function(){this.c=Math.ceil(this.c);this.a=Math.ceil(this.a);this.b=Math.ceil(this.b);this.f=Math.ceil(this.f);return this};Xc.prototype.floor=function(){this.c=Math.floor(this.c);this.a=Math.floor(this.a);this.b=Math.floor(this.b);this.f=Math.floor(this.f);return this};Xc.prototype.round=function(){this.c=Math.round(this.c);this.a=Math.round(this.a);this.b=Math.round(this.b);this.f=Math.round(this.f);return this};function T(a,b,c,d){this.a=a;this.b=b;this.width=c;this.height=d}T.prototype.ceil=function(){this.a=Math.ceil(this.a);this.b=Math.ceil(this.b);this.width=Math.ceil(this.width);this.height=Math.ceil(this.height);return this};T.prototype.floor=function(){this.a=Math.floor(this.a);this.b=Math.floor(this.b);this.width=Math.floor(this.width);this.height=Math.floor(this.height);return this};
T.prototype.round=function(){this.a=Math.round(this.a);this.b=Math.round(this.b);this.width=Math.round(this.width);this.height=Math.round(this.height);return this};var Yc="function"===typeof ShadowRoot;function Zc(a){for(a=a.parentNode;a&&1!=a.nodeType&&9!=a.nodeType&&11!=a.nodeType;)a=a.parentNode;return S(a)?a:null}
function U(a,b){b=Ha(b);if("float"==b||"cssFloat"==b||"styleFloat"==b)b=Mc?"styleFloat":"cssFloat";a:{var c=b;var d=A(a);if(d.defaultView&&d.defaultView.getComputedStyle&&(d=d.defaultView.getComputedStyle(a,null))){c=d[c]||d.getPropertyValue(c)||"";break a}c=""}a=c||$c(a,b);if(null===a)a=null;else if(0<=oa(Pc,b)){b:{var e=a.match(Sc);if(e&&(b=Number(e[1]),c=Number(e[2]),d=Number(e[3]),e=Number(e[4]),0<=b&&255>=b&&0<=c&&255>=c&&0<=d&&255>=d&&0<=e&&1>=e)){b=[b,c,d,e];break b}b=null}if(!b)b:{if(d=a.match(Tc))if(b=
Number(d[1]),c=Number(d[2]),d=Number(d[3]),0<=b&&255>=b&&0<=c&&255>=c&&0<=d&&255>=d){b=[b,c,d,1];break b}b=null}if(!b)b:{b=a.toLowerCase();c=Oc[b.toLowerCase()];if(!c&&(c="#"==b.charAt(0)?b:"#"+b,4==c.length&&(c=c.replace(Qc,"#$1$1$2$2$3$3")),!Rc.test(c))){b=null;break b}b=[parseInt(c.substr(1,2),16),parseInt(c.substr(3,2),16),parseInt(c.substr(5,2),16),1]}a=b?"rgba("+b.join(", ")+")":a}return a}
function $c(a,b){var c=a.currentStyle||a.style,d=c[b];void 0===d&&ea(c.getPropertyValue)&&(d=c.getPropertyValue(b));return"inherit"!=d?void 0!==d?d:null:(a=Zc(a))?$c(a,b):null}
function ad(a,b,c){function d(g){var h=V(g);return 0<h.height&&0<h.width?!0:S(g,"PATH")&&(0<h.height||0<h.width)?(g=U(g,"stroke-width"),!!g&&0<parseInt(g,10)):"hidden"!=U(g,"overflow")&&sa(g.childNodes,function(n){return 3==n.nodeType||S(n)&&d(n)})}function e(g){return bd(g)==W&&ta(g.childNodes,function(h){return!S(h)||e(h)||!d(h)})}if(!S(a))throw Error("Argument to isShown must be of type Element");if(S(a,"BODY"))return!0;if(S(a,"OPTION")||S(a,"OPTGROUP"))return a=lb(a,function(g){return S(g,"SELECT")}),
!!a&&ad(a,!0,c);var f=cd(a);if(f)return!!f.image&&0<f.rect.width&&0<f.rect.height&&ad(f.image,b,c);if(S(a,"INPUT")&&"hidden"==a.type.toLowerCase()||S(a,"NOSCRIPT"))return!1;f=U(a,"visibility");return"collapse"!=f&&"hidden"!=f&&c(a)&&(b||0!=dd(a))&&d(a)?!e(a):!1}
function ed(a){function b(c){if(S(c)&&"none"==U(c,"display"))return!1;var d;if((d=c.parentNode)&&d.shadowRoot&&void 0!==c.assignedSlot)d=c.assignedSlot?c.assignedSlot.parentNode:null;else if(c.getDestinationInsertionPoints){var e=c.getDestinationInsertionPoints();0<e.length&&(d=e[e.length-1])}if(Yc&&d instanceof ShadowRoot){if(d.host.shadowRoot&&d.host.shadowRoot!==d)return!1;d=d.host}return!d||9!=d.nodeType&&11!=d.nodeType?d&&S(d,"DETAILS")&&!d.open&&!S(c,"SUMMARY")?!1:!!d&&b(d):!0}return ad(a,!1,
b)}var W="hidden";
function bd(a){function b(m){function v(Ab){if(Ab==g)return!0;var pc=U(Ab,"display");return 0==pc.lastIndexOf("inline",0)||"contents"==pc||"absolute"==qc&&"static"==U(Ab,"position")?!1:!0}var qc=U(m,"position");if("fixed"==qc)return u=!0,m==g?null:g;for(m=Zc(m);m&&!v(m);)m=Zc(m);return m}function c(m){var v=m;if("visible"==n)if(m==g&&h)v=h;else if(m==h)return{x:"visible",y:"visible"};v={x:U(v,"overflow-x"),y:U(v,"overflow-y")};m==g&&(v.x="visible"==v.x?"auto":v.x,v.y="visible"==v.y?"auto":v.y);return v}
function d(m){if(m==g){var v=(new fb(f)).a;m=v.scrollingElement?v.scrollingElement:Oa||"CSS1Compat"!=v.compatMode?v.body||v.documentElement:v.documentElement;v=v.parentWindow||v.defaultView;m=t&&Ua("10")&&v.pageYOffset!=m.scrollTop?new cb(m.scrollLeft,m.scrollTop):new cb(v.pageXOffset||m.scrollLeft,v.pageYOffset||m.scrollTop)}else m=new cb(m.scrollLeft,m.scrollTop);return m}var e=fd(a),f=A(a),g=f.documentElement,h=f.body,n=U(g,"overflow"),u;for(a=b(a);a;a=b(a)){var p=c(a);if("visible"!=p.x||"visible"!=
p.y){var G=V(a);if(0==G.width||0==G.height)return W;var R=e.a<G.a,fa=e.b<G.b;if(R&&"hidden"==p.x||fa&&"hidden"==p.y)return W;if(R&&"visible"!=p.x||fa&&"visible"!=p.y){R=d(a);fa=e.b<G.b-R.y;if(e.a<G.a-R.x&&"visible"!=p.x||fa&&"visible"!=p.x)return W;e=bd(a);return e==W?W:"scroll"}R=e.f>=G.a+G.width;G=e.c>=G.b+G.height;if(R&&"hidden"==p.x||G&&"hidden"==p.y)return W;if(R&&"visible"!=p.x||G&&"visible"!=p.y){if(u&&(p=d(a),e.f>=g.scrollWidth-p.x||e.a>=g.scrollHeight-p.y))return W;e=bd(a);return e==W?W:
"scroll"}}}return"none"}
function V(a){var b=cd(a);if(b)return b.rect;if(S(a,"HTML"))return a=A(a),a=((a?a.parentWindow||a.defaultView:window)||window).document,a="CSS1Compat"==a.compatMode?a.documentElement:a.body,a=new db(a.clientWidth,a.clientHeight),new T(0,0,a.width,a.height);try{var c=a.getBoundingClientRect()}catch(d){return new T(0,0,0,0)}b=new T(c.left,c.top,c.right-c.left,c.bottom-c.top);t&&a.ownerDocument.body&&(a=A(a),b.a-=a.documentElement.clientLeft+a.body.clientLeft,b.b-=a.documentElement.clientTop+a.body.clientTop);
return b}function cd(a){var b=S(a,"MAP");if(!b&&!S(a,"AREA"))return null;var c=b?a:S(a.parentNode,"MAP")?a.parentNode:null,d=null,e=null;c&&c.name&&(d=Nc.o('*[usemap="#'+c.name+'"]',A(c)))&&(e=V(d),b||"default"==a.shape.toLowerCase()||(a=gd(a),b=Math.min(Math.max(a.a,0),e.width),c=Math.min(Math.max(a.b,0),e.height),e=new T(b+e.a,c+e.b,Math.min(a.width,e.width-b),Math.min(a.height,e.height-c))));return{image:d,rect:e||new T(0,0,0,0)}}
function gd(a){var b=a.shape.toLowerCase();a=a.coords.split(",");if("rect"==b&&4==a.length){b=a[0];var c=a[1];return new T(b,c,a[2]-b,a[3]-c)}if("circle"==b&&3==a.length)return b=a[2],new T(a[0]-b,a[1]-b,2*b,2*b);if("poly"==b&&2<a.length){b=a[0];c=a[1];for(var d=b,e=c,f=2;f+1<a.length;f+=2)b=Math.min(b,a[f]),d=Math.max(d,a[f]),c=Math.min(c,a[f+1]),e=Math.max(e,a[f+1]);return new T(b,c,d-b,e-c)}return new T(0,0,0,0)}function fd(a){a=V(a);return new Xc(a.b,a.a+a.width,a.b+a.height,a.a)}
function hd(a){return a.replace(/^[^\S\xa0]+|[^\S\xa0]+$/g,"")}function id(a){var b=[];Yc?jd(a,b):kd(a,b);a=qa(b,hd);return hd(a.join("\n")).replace(/\xa0/g," ")}
function ld(a,b,c){if(S(a,"BR"))b.push("");else{var d=S(a,"TD"),e=U(a,"display"),f=!d&&!(0<=oa(md,e)),g=void 0!==a.previousElementSibling?a.previousElementSibling:gb(a.previousSibling);g=g?U(g,"display"):"";var h=U(a,"float")||U(a,"cssFloat")||U(a,"styleFloat");!f||"run-in"==g&&"none"==h||/^[\s\xa0]*$/.test(b[b.length-1]||"")||b.push("");var n=ed(a),u=null,p=null;n&&(u=U(a,"white-space"),p=U(a,"text-transform"));l(a.childNodes,function(G){c(G,b,n,u,p)});a=b[b.length-1]||"";!d&&"table-cell"!=e||!a||
za(a)||(b[b.length-1]+=" ");f&&"run-in"!=e&&!/^[\s\xa0]*$/.test(a)&&b.push("")}}function kd(a,b){ld(a,b,function(c,d,e,f,g){3==c.nodeType&&e?nd(c,d,f,g):S(c)&&kd(c,d)})}var md="inline inline-block inline-table none table-cell table-column table-column-group".split(" ");
function nd(a,b,c,d){a=a.nodeValue.replace(/[\u200b\u200e\u200f]/g,"");a=a.replace(/(\r\n|\r|\n)/g,"\n");if("normal"==c||"nowrap"==c)a=a.replace(/\n/g," ");a="pre"==c||"pre-wrap"==c?a.replace(/[ \f\t\v\u2028\u2029]/g,"\u00a0"):a.replace(/[ \f\t\v\u2028\u2029]+/g," ");"capitalize"==d?a=a.replace(t?/(^|\s|\b)(\S)/g:/(^|\s|\b)(\S)/gu,function(e,f,g){return f+g.toUpperCase()}):"uppercase"==d?a=a.toUpperCase():"lowercase"==d&&(a=a.toLowerCase());c=b.pop()||"";za(c)&&0==a.lastIndexOf(" ",0)&&(a=a.substr(1));
b.push(c+a)}function dd(a){if(Mc){if("relative"==U(a,"position"))return 1;a=U(a,"filter");return(a=a.match(/^alpha\(opacity=(\d*)\)/)||a.match(/^progid:DXImageTransform.Microsoft.Alpha\(Opacity=(\d*)\)/))?Number(a[1])/100:1}return od(a)}function od(a){var b=1,c=U(a,"opacity");c&&(b=Number(c));(a=Zc(a))&&(b*=od(a));return b}
function pd(a,b,c,d,e){if(3==a.nodeType&&c)nd(a,b,d,e);else if(S(a))if(S(a,"CONTENT")||S(a,"SLOT")){for(var f=a;f.parentNode;)f=f.parentNode;f instanceof ShadowRoot?(f=S(a,"CONTENT")?a.getDistributedNodes():a.assignedNodes(),l(0<f.length?f:a.childNodes,function(g){pd(g,b,c,d,e)})):jd(a,b)}else if(S(a,"SHADOW")){for(f=a;f.parentNode;)f=f.parentNode;if(f instanceof ShadowRoot&&(a=f))for(a=a.olderShadowRoot;a;)l(a.childNodes,function(g){pd(g,b,c,d,e)}),a=a.olderShadowRoot}else jd(a,b)}
function jd(a,b){a.shadowRoot&&l(a.shadowRoot.childNodes,function(c){pd(c,b,!0,null,null)});ld(a,b,function(c,d,e,f,g){var h=null;1==c.nodeType?h=c:3==c.nodeType&&(h=c);null!=h&&(null!=h.assignedSlot||h.getDestinationInsertionPoints&&0<h.getDestinationInsertionPoints().length)||pd(c,d,e,f,g)})};var qd={C:function(a,b){return!(!a.querySelectorAll||!a.querySelector)&&!/^\d.*/.test(b)},o:function(a,b){var c=eb(b),d="string"===typeof a?c.a.getElementById(a):a;return d?Uc(d,"id")==a&&b!=d&&hb(b,d)?d:ua(mb(c,"*"),function(e){return Uc(e,"id")==a&&b!=e&&hb(b,e)}):null},j:function(a,b){if(!a)return[];if(qd.C(b,a))try{return b.querySelectorAll("#"+qd.T(a))}catch(c){return[]}b=mb(eb(b),"*",null,b);return pa(b,function(c){return Uc(c,"id")==a})},T:function(a){return a.replace(/([\s'"\\#.:;,!?+<>=~*^$|%&@`{}\-\/\[\]\(\)])/g,
"\\$1")}};var X={},rd={};X.N=function(a,b,c){try{var d=Nc.j("a",b)}catch(e){d=mb(eb(b),"A",null,b)}return ua(d,function(e){e=id(e);e=e.replace(/^[\s]+|[\s]+$/g,"");return c&&-1!=e.indexOf(a)||e==a})};X.K=function(a,b,c){try{var d=Nc.j("a",b)}catch(e){d=mb(eb(b),"A",null,b)}return pa(d,function(e){e=id(e);e=e.replace(/^[\s]+|[\s]+$/g,"");return c&&-1!=e.indexOf(a)||e==a})};X.o=function(a,b){return X.N(a,b,!1)};X.j=function(a,b){return X.K(a,b,!1)};rd.o=function(a,b){return X.N(a,b,!0)};
rd.j=function(a,b){return X.K(a,b,!0)};var Y={F:function(a,b){return function(c){var d=Y.u(a);d=V(d);c=V(c);return b.call(null,d,c)}},R:function(a){return Y.F(a,function(b,c){return c.b+c.height<b.b})},S:function(a){return Y.F(a,function(b,c){return b.b+b.height<c.b})},V:function(a){return Y.F(a,function(b,c){return c.a+c.width<b.a})},aa:function(a){return Y.F(a,function(b,c){return b.a+b.width<c.a})},W:function(a,b){var c;b?c=b:"number"==typeof a.distance&&(c=a.distance);c||(c=50);return function(d){var e=Y.u(a);if(e===d)return!1;e=V(e);
d=V(d);e=new T(e.a-c,e.b-c,e.width+2*c,e.height+2*c);return e.a<=d.a+d.width&&d.a<=e.a+e.width&&e.b<=d.b+d.height&&d.b<=e.b+e.height}},u:function(a){if(ha(a)&&1==a.nodeType)return a;if(ea(a))return Y.u(a.call(null));if(ha(a)){var b;a:{if(b=sd(a)){var c=td[b];if(c&&ea(c.o)){b=c.o(a[b],Bc.document);break a}}throw new P(61,"Unsupported locator strategy: "+b);}if(!b)throw new P(7,"No element has been found by "+JSON.stringify(a));return b}throw new P(61,"Selector is of wrong type: "+JSON.stringify(a));
}};Y.P={left:Y.V,right:Y.aa,above:Y.R,below:Y.S,near:Y.W};Y.O={left:Y.u,right:Y.u,above:Y.u,below:Y.u,near:Y.u};Y.U=function(a,b){var c=[];l(a,function(e){e&&ta(b,function(f){var g=f.kind,h=Y.P[g];if(!h)throw new P(61,"Cannot find filter suitable for "+g);return h.apply(null,f.args)(e)},null)&&c.push(e)},null);a=b[b.length-1];var d=Y.O[a?a.kind:"unknown"];return d?(a=d.apply(null,a.args))?Y.ba(a,c):c:c};
Y.ba=function(a,b){function c(f){f=V(f);return Math.sqrt(Math.pow(d-(f.a+Math.max(1,f.width)/2),2)+Math.pow(e-(f.b+Math.max(1,f.height)/2),2))}a=V(a);var d=a.a+Math.max(1,a.width)/2,e=a.b+Math.max(1,a.height)/2;xa(b,function(f,g){return c(f)-c(g)});return b};Y.o=function(a,b){a=Y.j(a,b);return 0==a.length?null:a[0]};
Y.j=function(a,b){if(!a.hasOwnProperty("root")||!a.hasOwnProperty("filters"))throw new P(61,"Locator not suitable for relative locators: "+JSON.stringify(a));var c=a.filters,d=da(c);if("array"!=d&&("object"!=d||"number"!=typeof c.length))throw new P(61,"Targets should be an array: "+JSON.stringify(a));var e;S(a.root)?e=[a.root]:e=ud(a.root,b);return 0==e.length?[]:Y.U(e,a.filters)};var vd={o:function(a,b){if(""===a)throw new P(32,'Unable to locate an element with the tagName ""');return b.getElementsByTagName(a)[0]||null},j:function(a,b){if(""===a)throw new P(32,'Unable to locate an element with the tagName ""');return b.getElementsByTagName(a)}};var Z={};Z.I=function(){var a={da:"http://www.w3.org/2000/svg"};return function(b){return a[b]||null}}();
Z.s=function(a,b,c){var d=A(a);if(!d.documentElement)return null;(t||Hc)&&Ac(d?d.parentWindow||d.defaultView:window);try{var e=d.createNSResolver?d.createNSResolver(d.documentElement):Z.I;if(t&&!Ua(7))return d.evaluate.call(d,b,a,e,c,null);if(!t||9<=Number(w)){for(var f={},g=d.getElementsByTagName("*"),h=0;h<g.length;++h){var n=g[h],u=n.namespaceURI;if(u&&!f[u]){var p=n.lookupPrefix(u);if(!p){var G=u.match(".*/(\\w+)/?$");p=G?G[1]:"xhtml"}f[u]=p}}var R={},fa;for(fa in f)R[f[fa]]=fa;e=function(m){return R[m]||
null}}try{return d.evaluate(b,a,e,c,null)}catch(m){if("TypeError"===m.name)return e=d.createNSResolver?d.createNSResolver(d.documentElement):Z.I,d.evaluate(b,a,e,c,null);throw m;}}catch(m){if(!Na||"NS_ERROR_ILLEGAL_VALUE"!=m.name)throw new P(32,"Unable to locate an element with the xpath expression "+b+" because of the following error:\n"+m);}};Z.J=function(a,b){if(!a||1!=a.nodeType)throw new P(32,'The result of the xpath expression "'+b+'" is: '+a+". It should be an element.");};
Z.o=function(a,b){var c=function(){var d=Z.s(b,a,9);return d?d.singleNodeValue||null:b.selectSingleNode?(d=A(b),d.setProperty&&d.setProperty("SelectionLanguage","XPath"),b.selectSingleNode(a)):null}();null===c||Z.J(c,a);return c};
Z.j=function(a,b){var c=function(){var d=Z.s(b,a,7);if(d){for(var e=d.snapshotLength,f=[],g=0;g<e;++g)f.push(d.snapshotItem(g));return f}return b.selectNodes?(d=A(b),d.setProperty&&d.setProperty("SelectionLanguage","XPath"),b.selectNodes(a)):[]}();l(c,function(d){Z.J(d,a)});return c};var td={className:Dc,"class name":Dc,css:Nc,"css selector":Nc,relative:Y,id:qd,linkText:X,"link text":X,name:{o:function(a,b){b=mb(eb(b),"*",null,b);return ua(b,function(c){return Uc(c,"name")==a})},j:function(a,b){b=mb(eb(b),"*",null,b);return pa(b,function(c){return Uc(c,"name")==a})}},partialLinkText:rd,"partial link text":rd,tagName:vd,"tag name":vd,xpath:Z};function sd(a){for(var b in a)if(a.hasOwnProperty(b))return b;return null}
function ud(a,b){var c=sd(a);if(c){var d=td[c];if(d&&ea(d.j))return d.j(a[c],b||Bc.document)}throw new P(61,"Unsupported locator strategy: "+c);};ca("_",ud);; return this._.apply(null,arguments);}).apply({navigator:typeof window!='undefined'?window.navigator:null,document:typeof window!='undefined'?window.document:null}, arguments);};

View File

@@ -0,0 +1,7 @@
// GENERATED CODE - DO NOT EDIT
module.exports = function(){return (function(){var d=this||self;function f(a,b){function c(){}c.prototype=b.prototype;a.prototype=new c;a.prototype.constructor=a};var h=Array.prototype.indexOf?function(a,b){return Array.prototype.indexOf.call(a,b,void 0)}:function(a,b){if("string"===typeof a)return"string"!==typeof b||1!=b.length?-1:a.indexOf(b,0);for(var c=0;c<a.length;c++)if(c in a&&a[c]===b)return c;return-1},k=Array.prototype.forEach?function(a,b){Array.prototype.forEach.call(a,b,void 0)}:function(a,b){for(var c=a.length,e="string"===typeof a?a.split(""):a,g=0;g<c;g++)g in e&&b.call(void 0,e[g],g,a)};function l(a,b){this.code=a;this.a=m[a]||n;this.message=b||"";a=this.a.replace(/((?:^|\s+)[a-z])/g,function(c){return c.toUpperCase().replace(/^[\s\xa0]+/g,"")});b=a.length-5;if(0>b||a.indexOf("Error",b)!=b)a+="Error";this.name=a;a=Error(this.message);a.name=this.name;this.stack=a.stack||""}f(l,Error);var n="unknown error",m={15:"element not selectable",11:"element not visible"};m[31]=n;m[30]=n;m[24]="invalid cookie domain";m[29]="invalid element coordinates";m[12]="invalid element state";m[32]="invalid selector";
m[51]="invalid selector";m[52]="invalid selector";m[17]="javascript error";m[405]="unsupported operation";m[34]="move target out of bounds";m[27]="no such alert";m[7]="no such element";m[8]="no such frame";m[23]="no such window";m[28]="script timeout";m[33]="session not created";m[10]="stale element reference";m[21]="timeout";m[25]="unable to set cookie";m[26]="unexpected alert open";m[13]=n;m[9]="unknown command";var p;a:{var q=d.navigator;if(q){var r=q.userAgent;if(r){p=r;break a}}p=""}function t(a){return-1!=p.indexOf(a)};function u(){return t("Firefox")||t("FxiOS")}function v(){return(t("Chrome")||t("CriOS"))&&!t("Edge")};function w(){return t("iPhone")&&!t("iPod")&&!t("iPad")};var y=t("Opera"),z=t("Trident")||t("MSIE"),A=t("Edge"),B=t("Gecko")&&!(-1!=p.toLowerCase().indexOf("webkit")&&!t("Edge"))&&!(t("Trident")||t("MSIE"))&&!t("Edge"),C=-1!=p.toLowerCase().indexOf("webkit")&&!t("Edge");function D(){var a=d.document;return a?a.documentMode:void 0}var E;
a:{var F="",G=function(){var a=p;if(B)return/rv:([^\);]+)(\)|;)/.exec(a);if(A)return/Edge\/([\d\.]+)/.exec(a);if(z)return/\b(?:MSIE|rv)[: ]([^\);]+)(\)|;)/.exec(a);if(C)return/WebKit\/(\S+)/.exec(a);if(y)return/(?:Version)[ \/]?(\S+)/.exec(a)}();G&&(F=G?G[1]:"");if(z){var H=D();if(null!=H&&H>parseFloat(F)){E=String(H);break a}}E=F}var I;I=d.document&&z?D():void 0;var J=u(),K=w()||t("iPod"),L=t("iPad"),M=t("Android")&&!(v()||u()||t("Opera")||t("Silk")),N=v(),aa=t("Safari")&&!(v()||t("Coast")||t("Opera")||t("Edge")||t("Edg/")||t("OPR")||u()||t("Silk")||t("Android"))&&!(w()||t("iPad")||t("iPod"));function O(a){return(a=a.exec(p))?a[1]:""}(function(){if(J)return O(/Firefox\/([0-9.]+)/);if(z||A||y)return E;if(N)return w()||t("iPad")||t("iPod")?O(/CriOS\/([0-9.]+)/):O(/Chrome\/([0-9.]+)/);if(aa&&!(w()||t("iPad")||t("iPod")))return O(/Version\/([0-9.]+)/);if(K||L){var a=/Version\/(\S+).*Mobile\/(\S+)/.exec(p);if(a)return a[1]+"."+a[2]}else if(M)return(a=O(/Android\s+([0-9.]+)/))?a:O(/Version\/([0-9.]+)/);return""})();var P=z&&!(8<=Number(I)),ba=z&&!(9<=Number(I));var ca={SCRIPT:1,STYLE:1,HEAD:1,IFRAME:1,OBJECT:1},Q={IMG:" ",BR:"\n"};function R(a,b,c){if(!(a.nodeName in ca))if(3==a.nodeType)c?b.push(String(a.nodeValue).replace(/(\r\n|\r|\n)/g,"")):b.push(a.nodeValue);else if(a.nodeName in Q)b.push(Q[a.nodeName]);else for(a=a.firstChild;a;)R(a,b,c),a=a.nextSibling};function S(a,b){b=b.toLowerCase();return"style"==b?da(a.style.cssText):P&&"value"==b&&T(a,"INPUT")?a.value:ba&&!0===a[b]?String(a.getAttribute(b)):(a=a.getAttributeNode(b))&&a.specified?a.value:null}var ea=/[;]+(?=(?:(?:[^"]*"){2})*[^"]*$)(?=(?:(?:[^']*'){2})*[^']*$)(?=(?:[^()]*\([^()]*\))*[^()]*$)/;
function da(a){var b=[];k(a.split(ea),function(c){var e=c.indexOf(":");0<e&&(c=[c.slice(0,e),c.slice(e+1)],2==c.length&&b.push(c[0].toLowerCase(),":",c[1],";"))});b=b.join("");return b=";"==b.charAt(b.length-1)?b:b+";"}function U(a,b){P&&"value"==b&&T(a,"OPTION")&&null===S(a,"value")?(b=[],R(a,b,!1),a=b.join("")):a=a[b];return a}function T(a,b){b&&"string"!==typeof b&&(b=b.toString());return!!a&&1==a.nodeType&&(!b||a.tagName.toUpperCase()==b)}
function V(a){return T(a,"OPTION")?!0:T(a,"INPUT")?(a=a.type.toLowerCase(),"checkbox"==a||"radio"==a):!1};var fa={"class":"className",readonly:"readOnly"},ha="allowfullscreen allowpaymentrequest allowusermedia async autofocus autoplay checked compact complete controls declare default defaultchecked defaultselected defer disabled ended formnovalidate hidden indeterminate iscontenteditable ismap itemscope loop multiple muted nohref nomodule noresize noshade novalidate nowrap open paused playsinline pubdate readonly required reversed scoped seamless seeking selected truespeed typemustmatch willvalidate".split(" ");function W(a,b){var c=null,e=b.toLowerCase();if("style"==e)return(c=a.style)&&"string"!=typeof c&&(c=c.cssText),c;if(("selected"==e||"checked"==e)&&V(a)){if(!V(a))throw new l(15,"Element is not selectable");b="selected";c=a.type&&a.type.toLowerCase();if("checkbox"==c||"radio"==c)b="checked";return U(a,b)?"true":null}var g=T(a,"A");if(T(a,"IMG")&&"src"==e||g&&"href"==e)return(c=S(a,e))&&(c=U(a,e)),c;if("spellcheck"==e){c=S(a,e);if(null!==c){if("false"==c.toLowerCase())return"false";if("true"==c.toLowerCase())return"true"}return U(a,
e)+""}g=fa[b]||b;if(0<=h(ha,e))return(c=null!==S(a,b)||U(a,g))?"true":null;try{var x=U(a,g)}catch(ia){}(e=null==x)||(e=typeof x,e="object"==e&&null!=x||"function"==e);e?c=S(a,b):c=x;return null!=c?c.toString():null}var X=["_"],Y=d;X[0]in Y||"undefined"==typeof Y.execScript||Y.execScript("var "+X[0]);for(var Z;X.length&&(Z=X.shift());)X.length||void 0===W?Y[Z]&&Y[Z]!==Object.prototype[Z]?Y=Y[Z]:Y=Y[Z]={}:Y[Z]=W;; return this._.apply(null,arguments);}).apply({navigator:typeof window!='undefined'?window.navigator:null,document:typeof window!='undefined'?window.document:null}, arguments);};

View File

@@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT EDIT
module.exports = function(){return (function(){var g=this||self;
function aa(a){var b=typeof a;if("object"==b)if(a){if(a instanceof Array)return"array";if(a instanceof Object)return b;var c=Object.prototype.toString.call(a);if("[object Window]"==c)return"object";if("[object Array]"==c||"number"==typeof a.length&&"undefined"!=typeof a.splice&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("splice"))return"array";if("[object Function]"==c||"undefined"!=typeof a.call&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("call"))return"function"}else return"null";else if("function"==
b&&"undefined"==typeof a.call)return"object";return b}function ca(a,b){function c(){}c.prototype=b.prototype;a.prototype=new c;a.prototype.constructor=a};var da=Array.prototype.indexOf?function(a,b){return Array.prototype.indexOf.call(a,b,void 0)}:function(a,b){if("string"===typeof a)return"string"!==typeof b||1!=b.length?-1:a.indexOf(b,0);for(var c=0;c<a.length;c++)if(c in a&&a[c]===b)return c;return-1},ea=Array.prototype.some?function(a,b){return Array.prototype.some.call(a,b,void 0)}:function(a,b){for(var c=a.length,e="string"===typeof a?a.split(""):a,d=0;d<c;d++)if(d in e&&b.call(void 0,e[d],d,a))return!0;return!1},fa=Array.prototype.every?function(a,
b){return Array.prototype.every.call(a,b,void 0)}:function(a,b){for(var c=a.length,e="string"===typeof a?a.split(""):a,d=0;d<c;d++)if(d in e&&!b.call(void 0,e[d],d,a))return!1;return!0};var ha={aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",
darkgrey:"#a9a9a9",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",
ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",green:"#008000",greenyellow:"#adff2f",grey:"#808080",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgray:"#d3d3d3",lightgreen:"#90ee90",lightgrey:"#d3d3d3",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",
lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370db",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",
moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#db7093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",
seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",slategrey:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32"};var ia="backgroundColor borderTopColor borderRightColor borderBottomColor borderLeftColor color outlineColor".split(" "),ja=/#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])/,ka=/^#(?:[0-9a-f]{3}){1,2}$/i,la=/^(?:rgba)?\((\d{1,3}),\s?(\d{1,3}),\s?(\d{1,3}),\s?(0|1|0\.\d*)\)$/i,ma=/^(?:rgb)?\((0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2})\)$/i;function m(a,b){this.code=a;this.a=p[a]||r;this.message=b||"";a=this.a.replace(/((?:^|\s+)[a-z])/g,function(c){return c.toUpperCase().replace(/^[\s\xa0]+/g,"")});b=a.length-5;if(0>b||a.indexOf("Error",b)!=b)a+="Error";this.name=a;a=Error(this.message);a.name=this.name;this.stack=a.stack||""}ca(m,Error);var r="unknown error",p={15:"element not selectable",11:"element not visible"};p[31]=r;p[30]=r;p[24]="invalid cookie domain";p[29]="invalid element coordinates";p[12]="invalid element state";
p[32]="invalid selector";p[51]="invalid selector";p[52]="invalid selector";p[17]="javascript error";p[405]="unsupported operation";p[34]="move target out of bounds";p[27]="no such alert";p[7]="no such element";p[8]="no such frame";p[23]="no such window";p[28]="script timeout";p[33]="session not created";p[10]="stale element reference";p[21]="timeout";p[25]="unable to set cookie";p[26]="unexpected alert open";p[13]=r;p[9]="unknown command";var u=String.prototype.trim?function(a){return a.trim()}:function(a){return/^[\s\xa0]*([\s\S]*?)[\s\xa0]*$/.exec(a)[1]};
function na(a,b){var c=0;a=u(String(a)).split(".");b=u(String(b)).split(".");for(var e=Math.max(a.length,b.length),d=0;0==c&&d<e;d++){var f=a[d]||"",h=b[d]||"";do{f=/(\d*)(\D*)(.*)/.exec(f)||["","","",""];h=/(\d*)(\D*)(.*)/.exec(h)||["","","",""];if(0==f[0].length&&0==h[0].length)break;c=v(0==f[1].length?0:parseInt(f[1],10),0==h[1].length?0:parseInt(h[1],10))||v(0==f[2].length,0==h[2].length)||v(f[2],h[2]);f=f[3];h=h[3]}while(0==c)}return c}function v(a,b){return a<b?-1:a>b?1:0};var w;a:{var oa=g.navigator;if(oa){var sa=oa.userAgent;if(sa){w=sa;break a}}w=""}function x(a){return-1!=w.indexOf(a)};function y(){return x("Firefox")||x("FxiOS")}function A(){return(x("Chrome")||x("CriOS"))&&!x("Edge")};function ta(a){return String(a).replace(/\-([a-z])/g,function(b,c){return c.toUpperCase()})};function B(){return x("iPhone")&&!x("iPod")&&!x("iPad")};function ua(a,b){var c=va;return Object.prototype.hasOwnProperty.call(c,a)?c[a]:c[a]=b(a)};var wa=x("Opera"),C=x("Trident")||x("MSIE"),xa=x("Edge"),ya=x("Gecko")&&!(-1!=w.toLowerCase().indexOf("webkit")&&!x("Edge"))&&!(x("Trident")||x("MSIE"))&&!x("Edge"),za=-1!=w.toLowerCase().indexOf("webkit")&&!x("Edge");function Aa(){var a=g.document;return a?a.documentMode:void 0}var E;
a:{var F="",G=function(){var a=w;if(ya)return/rv:([^\);]+)(\)|;)/.exec(a);if(xa)return/Edge\/([\d\.]+)/.exec(a);if(C)return/\b(?:MSIE|rv)[: ]([^\);]+)(\)|;)/.exec(a);if(za)return/WebKit\/(\S+)/.exec(a);if(wa)return/(?:Version)[ \/]?(\S+)/.exec(a)}();G&&(F=G?G[1]:"");if(C){var H=Aa();if(null!=H&&H>parseFloat(F)){E=String(H);break a}}E=F}var va={};function Ba(a){return ua(a,function(){return 0<=na(E,a)})}var I;I=g.document&&C?Aa():void 0;var Ca=y(),Da=B()||x("iPod"),Ea=x("iPad"),Fa=x("Android")&&!(A()||y()||x("Opera")||x("Silk")),Ga=A(),Ha=x("Safari")&&!(A()||x("Coast")||x("Opera")||x("Edge")||x("Edg/")||x("OPR")||y()||x("Silk")||x("Android"))&&!(B()||x("iPad")||x("iPod"));function J(a){return(a=a.exec(w))?a[1]:""}(function(){if(Ca)return J(/Firefox\/([0-9.]+)/);if(C||xa||wa)return E;if(Ga)return B()||x("iPad")||x("iPod")?J(/CriOS\/([0-9.]+)/):J(/Chrome\/([0-9.]+)/);if(Ha&&!(B()||x("iPad")||x("iPod")))return J(/Version\/([0-9.]+)/);if(Da||Ea){var a=/Version\/(\S+).*Mobile\/(\S+)/.exec(w);if(a)return a[1]+"."+a[2]}else if(Fa)return(a=J(/Android\s+([0-9.]+)/))?a:J(/Version\/([0-9.]+)/);return""})();var K;if(K=C)K=!(9<=Number(I));var Ia=K;function L(a,b){this.x=void 0!==a?a:0;this.y=void 0!==b?b:0}L.prototype.ceil=function(){this.x=Math.ceil(this.x);this.y=Math.ceil(this.y);return this};L.prototype.floor=function(){this.x=Math.floor(this.x);this.y=Math.floor(this.y);return this};L.prototype.round=function(){this.x=Math.round(this.x);this.y=Math.round(this.y);return this};function M(a,b){this.width=a;this.height=b}M.prototype.aspectRatio=function(){return this.width/this.height};M.prototype.ceil=function(){this.width=Math.ceil(this.width);this.height=Math.ceil(this.height);return this};M.prototype.floor=function(){this.width=Math.floor(this.width);this.height=Math.floor(this.height);return this};M.prototype.round=function(){this.width=Math.round(this.width);this.height=Math.round(this.height);return this};function N(a){return 9==a.nodeType?a:a.ownerDocument||a.document}function Ja(a,b){a&&(a=a.parentNode);for(var c=0;a;){if(b(a))return a;a=a.parentNode;c++}return null}function Ka(a){this.a=a||g.document||document};function P(a,b){b&&"string"!==typeof b&&(b=b.toString());return!!a&&1==a.nodeType&&(!b||a.tagName.toUpperCase()==b)};function Q(a,b,c,e){this.f=a;this.a=b;this.b=c;this.c=e}Q.prototype.ceil=function(){this.f=Math.ceil(this.f);this.a=Math.ceil(this.a);this.b=Math.ceil(this.b);this.c=Math.ceil(this.c);return this};Q.prototype.floor=function(){this.f=Math.floor(this.f);this.a=Math.floor(this.a);this.b=Math.floor(this.b);this.c=Math.floor(this.c);return this};Q.prototype.round=function(){this.f=Math.round(this.f);this.a=Math.round(this.a);this.b=Math.round(this.b);this.c=Math.round(this.c);return this};function R(a,b,c,e){this.a=a;this.b=b;this.width=c;this.height=e}R.prototype.ceil=function(){this.a=Math.ceil(this.a);this.b=Math.ceil(this.b);this.width=Math.ceil(this.width);this.height=Math.ceil(this.height);return this};R.prototype.floor=function(){this.a=Math.floor(this.a);this.b=Math.floor(this.b);this.width=Math.floor(this.width);this.height=Math.floor(this.height);return this};
R.prototype.round=function(){this.a=Math.round(this.a);this.b=Math.round(this.b);this.width=Math.round(this.width);this.height=Math.round(this.height);return this};var La="function"===typeof ShadowRoot;function S(a){for(a=a.parentNode;a&&1!=a.nodeType&&9!=a.nodeType&&11!=a.nodeType;)a=a.parentNode;return P(a)?a:null}
function T(a,b){b=ta(b);if("float"==b||"cssFloat"==b||"styleFloat"==b)b=Ia?"styleFloat":"cssFloat";a:{var c=b;var e=N(a);if(e.defaultView&&e.defaultView.getComputedStyle&&(e=e.defaultView.getComputedStyle(a,null))){c=e[c]||e.getPropertyValue(c)||"";break a}c=""}a=c||Ma(a,b);if(null===a)a=null;else if(0<=da(ia,b)){b:{var d=a.match(la);if(d&&(b=Number(d[1]),c=Number(d[2]),e=Number(d[3]),d=Number(d[4]),0<=b&&255>=b&&0<=c&&255>=c&&0<=e&&255>=e&&0<=d&&1>=d)){b=[b,c,e,d];break b}b=null}if(!b)b:{if(e=a.match(ma))if(b=
Number(e[1]),c=Number(e[2]),e=Number(e[3]),0<=b&&255>=b&&0<=c&&255>=c&&0<=e&&255>=e){b=[b,c,e,1];break b}b=null}if(!b)b:{b=a.toLowerCase();c=ha[b.toLowerCase()];if(!c&&(c="#"==b.charAt(0)?b:"#"+b,4==c.length&&(c=c.replace(ja,"#$1$1$2$2$3$3")),!ka.test(c))){b=null;break b}b=[parseInt(c.substr(1,2),16),parseInt(c.substr(3,2),16),parseInt(c.substr(5,2),16),1]}a=b?"rgba("+b.join(", ")+")":a}return a}
function Ma(a,b){var c=a.currentStyle||a.style,e=c[b];void 0===e&&"function"==aa(c.getPropertyValue)&&(e=c.getPropertyValue(b));return"inherit"!=e?void 0!==e?e:null:(a=S(a))?Ma(a,b):null}
function U(a,b,c){function e(h){var n=V(h);return 0<n.height&&0<n.width?!0:P(h,"PATH")&&(0<n.height||0<n.width)?(h=T(h,"stroke-width"),!!h&&0<parseInt(h,10)):"hidden"!=T(h,"overflow")&&ea(h.childNodes,function(D){return 3==D.nodeType||P(D)&&e(D)})}function d(h){return Na(h)==W&&fa(h.childNodes,function(n){return!P(n)||d(n)||!e(n)})}if(!P(a))throw Error("Argument to isShown must be of type Element");if(P(a,"BODY"))return!0;if(P(a,"OPTION")||P(a,"OPTGROUP"))return a=Ja(a,function(h){return P(h,"SELECT")}),
!!a&&U(a,!0,c);var f=Oa(a);if(f)return!!f.image&&0<f.rect.width&&0<f.rect.height&&U(f.image,b,c);if(P(a,"INPUT")&&"hidden"==a.type.toLowerCase()||P(a,"NOSCRIPT"))return!1;f=T(a,"visibility");return"collapse"!=f&&"hidden"!=f&&c(a)&&(b||0!=Pa(a))&&e(a)?!d(a):!1}var W="hidden";
function Na(a){function b(k){function l(ba){if(ba==h)return!0;var pa=T(ba,"display");return 0==pa.lastIndexOf("inline",0)||"contents"==pa||"absolute"==qa&&"static"==T(ba,"position")?!1:!0}var qa=T(k,"position");if("fixed"==qa)return ra=!0,k==h?null:h;for(k=S(k);k&&!l(k);)k=S(k);return k}function c(k){var l=k;if("visible"==D)if(k==h&&n)l=n;else if(k==n)return{x:"visible",y:"visible"};l={x:T(l,"overflow-x"),y:T(l,"overflow-y")};k==h&&(l.x="visible"==l.x?"auto":l.x,l.y="visible"==l.y?"auto":l.y);return l}
function e(k){if(k==h){var l=(new Ka(f)).a;k=l.scrollingElement?l.scrollingElement:za||"CSS1Compat"!=l.compatMode?l.body||l.documentElement:l.documentElement;l=l.parentWindow||l.defaultView;k=C&&Ba("10")&&l.pageYOffset!=k.scrollTop?new L(k.scrollLeft,k.scrollTop):new L(l.pageXOffset||k.scrollLeft,l.pageYOffset||k.scrollTop)}else k=new L(k.scrollLeft,k.scrollTop);return k}var d=Qa(a),f=N(a),h=f.documentElement,n=f.body,D=T(h,"overflow"),ra;for(a=b(a);a;a=b(a)){var q=c(a);if("visible"!=q.x||"visible"!=
q.y){var t=V(a);if(0==t.width||0==t.height)return W;var z=d.a<t.a,O=d.b<t.b;if(z&&"hidden"==q.x||O&&"hidden"==q.y)return W;if(z&&"visible"!=q.x||O&&"visible"!=q.y){z=e(a);O=d.b<t.b-z.y;if(d.a<t.a-z.x&&"visible"!=q.x||O&&"visible"!=q.x)return W;d=Na(a);return d==W?W:"scroll"}z=d.c>=t.a+t.width;t=d.f>=t.b+t.height;if(z&&"hidden"==q.x||t&&"hidden"==q.y)return W;if(z&&"visible"!=q.x||t&&"visible"!=q.y){if(ra&&(q=e(a),d.c>=h.scrollWidth-q.x||d.a>=h.scrollHeight-q.y))return W;d=Na(a);return d==W?W:"scroll"}}}return"none"}
function V(a){var b=Oa(a);if(b)return b.rect;if(P(a,"HTML"))return a=N(a),a=((a?a.parentWindow||a.defaultView:window)||window).document,a="CSS1Compat"==a.compatMode?a.documentElement:a.body,a=new M(a.clientWidth,a.clientHeight),new R(0,0,a.width,a.height);try{var c=a.getBoundingClientRect()}catch(e){return new R(0,0,0,0)}b=new R(c.left,c.top,c.right-c.left,c.bottom-c.top);C&&a.ownerDocument.body&&(a=N(a),b.a-=a.documentElement.clientLeft+a.body.clientLeft,b.b-=a.documentElement.clientTop+a.body.clientTop);
return b}
function Oa(a){var b=P(a,"MAP");if(!b&&!P(a,"AREA"))return null;var c=b?a:P(a.parentNode,"MAP")?a.parentNode:null,e=null,d=null;if(c&&c.name){e='*[usemap="#'+c.name+'"]';c=N(c);var f;if(f="function"!=aa(c.querySelector)&&C&&(C?0<=na(I,8):Ba(8))){f=c.querySelector;var h=typeof f;f=!("object"==h&&null!=f||"function"==h)}if(f)throw Error("CSS selection is not supported");if(!e)throw new m(32,"No selector specified");e=u(e);try{var n=c.querySelector(e)}catch(D){throw new m(32,"An invalid or illegal selector was specified");}if(e=
n&&1==n.nodeType?n:null)d=V(e),b||"default"==a.shape.toLowerCase()||(a=Ra(a),b=Math.min(Math.max(a.a,0),d.width),n=Math.min(Math.max(a.b,0),d.height),d=new R(b+d.a,n+d.b,Math.min(a.width,d.width-b),Math.min(a.height,d.height-n)))}return{image:e,rect:d||new R(0,0,0,0)}}
function Ra(a){var b=a.shape.toLowerCase();a=a.coords.split(",");if("rect"==b&&4==a.length){b=a[0];var c=a[1];return new R(b,c,a[2]-b,a[3]-c)}if("circle"==b&&3==a.length)return b=a[2],new R(a[0]-b,a[1]-b,2*b,2*b);if("poly"==b&&2<a.length){b=a[0];c=a[1];for(var e=b,d=c,f=2;f+1<a.length;f+=2)b=Math.min(b,a[f]),e=Math.max(e,a[f]),c=Math.min(c,a[f+1]),d=Math.max(d,a[f+1]);return new R(b,c,e-b,d-c)}return new R(0,0,0,0)}function Qa(a){a=V(a);return new Q(a.b,a.a+a.width,a.b+a.height,a.a)}
function Pa(a){if(Ia){if("relative"==T(a,"position"))return 1;a=T(a,"filter");return(a=a.match(/^alpha\(opacity=(\d*)\)/)||a.match(/^progid:DXImageTransform.Microsoft.Alpha\(Opacity=(\d*)\)/))?Number(a[1])/100:1}return Sa(a)}function Sa(a){var b=1,c=T(a,"opacity");c&&(b=Number(c));(a=S(a))&&(b*=Sa(a));return b};function Ta(a,b){function c(e){if(P(e)&&"none"==T(e,"display"))return!1;var d;if((d=e.parentNode)&&d.shadowRoot&&void 0!==e.assignedSlot)d=e.assignedSlot?e.assignedSlot.parentNode:null;else if(e.getDestinationInsertionPoints){var f=e.getDestinationInsertionPoints();0<f.length&&(d=f[f.length-1])}if(La&&d instanceof ShadowRoot){if(d.host.shadowRoot&&d.host.shadowRoot!==d)return!1;d=d.host}return!d||9!=d.nodeType&&11!=d.nodeType?d&&P(d,"DETAILS")&&!d.open&&!P(e,"SUMMARY")?!1:!!d&&c(d):!0}return U(a,
!!b,c)}var X=["_"],Y=g;X[0]in Y||"undefined"==typeof Y.execScript||Y.execScript("var "+X[0]);for(var Z;X.length&&(Z=X.shift());)X.length||void 0===Ta?Y[Z]&&Y[Z]!==Object.prototype[Z]?Y=Y[Z]:Y=Y[Z]={}:Y[Z]=Ta;; return this._.apply(null,arguments);}).apply({navigator:typeof window!='undefined'?window.navigator:null,document:typeof window!='undefined'?window.document:null}, arguments);};

View File

@@ -0,0 +1,55 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
(function () {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
switch (mutation.type) {
case 'attributes':
// Don't report our own attribute has changed.
if (mutation.attributeName === "data-__webdriver_id") {
break;
}
const curr = mutation.target.getAttribute(mutation.attributeName);
var id = mutation.target.dataset.__webdriver_id
if (!id) {
id = Math.random().toString(36).substring(2) + Date.now().toString(36);
mutation.target.dataset.__webdriver_id = id;
}
const json = JSON.stringify({
'target': id,
'name': mutation.attributeName,
'value': curr,
'oldValue': mutation.oldValue
});
__webdriver_attribute(json);
break;
default:
break;
}
}
});
observer.observe(document, {
'attributes': true,
'attributeOldValue': true,
'characterData': true,
'characterDataOldValue': true,
'childList': true,
'subtree': true
});
})();

View File

@@ -0,0 +1,429 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
/**
* @fileoverview Factory methods for the supported locator strategies.
*/
/**
* Short-hand expressions for the primary element locator strategies.
* For example the following two statements are equivalent:
*
* var e1 = driver.findElement(By.id('foo'));
* var e2 = driver.findElement({id: 'foo'});
*
* Care should be taken when using JavaScript minifiers (such as the
* Closure compiler), as locator hashes will always be parsed using
* the un-obfuscated properties listed.
*
* @typedef {(
* {className: string}|
* {css: string}|
* {id: string}|
* {js: string}|
* {linkText: string}|
* {name: string}|
* {partialLinkText: string}|
* {tagName: string}|
* {xpath: string})} ByHash
*/
/**
* Error thrown if an invalid character is encountered while escaping a CSS
* identifier.
* @see https://drafts.csswg.org/cssom/#serialize-an-identifier
*/
class InvalidCharacterError extends Error {
constructor() {
super()
this.name = this.constructor.name
}
}
/**
* Escapes a CSS string.
* @param {string} css the string to escape.
* @return {string} the escaped string.
* @throws {TypeError} if the input value is not a string.
* @throws {InvalidCharacterError} if the string contains an invalid character.
* @see https://drafts.csswg.org/cssom/#serialize-an-identifier
*/
function escapeCss(css) {
if (typeof css !== 'string') {
throw new TypeError('input must be a string')
}
let ret = ''
const n = css.length
for (let i = 0; i < n; i++) {
const c = css.charCodeAt(i)
if (c == 0x0) {
throw new InvalidCharacterError()
}
if (
(c >= 0x0001 && c <= 0x001f) ||
c == 0x007f ||
(i == 0 && c >= 0x0030 && c <= 0x0039) ||
(i == 1 && c >= 0x0030 && c <= 0x0039 && css.charCodeAt(0) == 0x002d)
) {
ret += '\\' + c.toString(16) + ' '
continue
}
if (i == 0 && c == 0x002d && n == 1) {
ret += '\\' + css.charAt(i)
continue
}
if (
c >= 0x0080 ||
c == 0x002d || // -
c == 0x005f || // _
(c >= 0x0030 && c <= 0x0039) || // [0-9]
(c >= 0x0041 && c <= 0x005a) || // [A-Z]
(c >= 0x0061 && c <= 0x007a)
) {
// [a-z]
ret += css.charAt(i)
continue
}
ret += '\\' + css.charAt(i)
}
return ret
}
/**
* Describes a mechanism for locating an element on the page.
* @final
*/
class By {
/**
* @param {string} using the name of the location strategy to use.
* @param {string} value the value to search for.
*/
constructor(using, value) {
/** @type {string} */
this.using = using
/** @type {string} */
this.value = value
}
/**
* Locates elements that have a specific class name.
*
* @param {string} name The class name to search for.
* @return {!By} The new locator.
* @see http://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
* @see http://www.w3.org/TR/CSS2/selector.html#class-html
*/
static className(name) {
let names = name
.split(/\s+/g)
.filter((s) => s.length > 0)
.map((s) => escapeCss(s))
return By.css('.' + names.join('.'))
}
/**
* Locates elements using a CSS selector.
*
* @param {string} selector The CSS selector to use.
* @return {!By} The new locator.
* @see http://www.w3.org/TR/CSS2/selector.html
*/
static css(selector) {
return new By('css selector', selector)
}
/**
* Locates elements by the ID attribute. This locator uses the CSS selector
* `*[id="$ID"]`, _not_ `document.getElementById`.
*
* @param {string} id The ID to search for.
* @return {!By} The new locator.
*/
static id(id) {
return By.css('*[id="' + escapeCss(id) + '"]')
}
/**
* Locates link elements whose
* {@linkplain webdriver.WebElement#getText visible text} matches the given
* string.
*
* @param {string} text The link text to search for.
* @return {!By} The new locator.
*/
static linkText(text) {
return new By('link text', text)
}
/**
* Locates elements by evaluating a `script` that defines the body of
* a {@linkplain webdriver.WebDriver#executeScript JavaScript function}.
* The return value of this function must be an element or an array-like
* list of elements. When this locator returns a list of elements, but only
* one is expected, the first element in this list will be used as the
* single element value.
*
* @param {!(string|Function)} script The script to execute.
* @param {...*} var_args The arguments to pass to the script.
* @return {function(!./webdriver.WebDriver): !Promise}
* A new JavaScript-based locator function.
*/
static js(script, ...var_args) {
return function (driver) {
return driver.executeScript.call(driver, script, ...var_args)
}
}
/**
* Locates elements whose `name` attribute has the given value.
*
* @param {string} name The name attribute to search for.
* @return {!By} The new locator.
*/
static name(name) {
return By.css('*[name="' + escapeCss(name) + '"]')
}
/**
* Locates link elements whose
* {@linkplain webdriver.WebElement#getText visible text} contains the given
* substring.
*
* @param {string} text The substring to check for in a link's visible text.
* @return {!By} The new locator.
*/
static partialLinkText(text) {
return new By('partial link text', text)
}
/**
* Locates elements with a given tag name.
*
* @param {string} name The tag name to search for.
* @return {!By} The new locator.
*/
static tagName(name) {
return new By('tag name', name)
}
/**
* Locates elements matching a XPath selector. Care should be taken when
* using an XPath selector with a {@link webdriver.WebElement} as WebDriver
* will respect the context in the specified in the selector. For example,
* given the selector `//div`, WebDriver will search from the document root
* regardless of whether the locator was used with a WebElement.
*
* @param {string} xpath The XPath selector to use.
* @return {!By} The new locator.
* @see http://www.w3.org/TR/xpath/
*/
static xpath(xpath) {
return new By('xpath', xpath)
}
/** @override */
toString() {
// The static By.name() overrides this.constructor.name. Shame...
return `By(${this.using}, ${this.value})`
}
toObject() {
const tmp = {}
tmp[this.using] = this.value
return tmp
}
}
/**
* Start Searching for relative objects using the value returned from
* `By.tagName()`.
*
* Note: this method will likely be removed in the future please use
* `locateWith`.
* @param {By} The value returned from calling By.tagName()
* @returns
*/
function withTagName(tagName) {
return new RelativeBy({ 'css selector': tagName })
}
/**
* Start searching for relative objects using search criteria with By.
* @param {string} A By map that shows how to find the initial element
* @returns {RelativeBy}
*/
function locateWith(by) {
return new RelativeBy(getLocator(by))
}
function getLocator(locatorOrElement) {
let toFind
if (locatorOrElement instanceof By) {
toFind = locatorOrElement.toObject()
} else {
toFind = locatorOrElement
}
return toFind
}
/**
* Describes a mechanism for locating an element relative to others
* on the page.
* @final
*/
class RelativeBy {
/**
* @param {By} findDetails
* @param {Array<Object>} filters
*/
constructor(findDetails, filters = null) {
this.root = findDetails
this.filters = filters || []
}
/**
* Look for elements above the root element passed in
* @param {string|WebElement} locatorOrElement
* @return {!RelativeBy} Return this object
*/
above(locatorOrElement) {
this.filters.push({
kind: 'above',
args: [getLocator(locatorOrElement)],
})
return this
}
/**
* Look for elements below the root element passed in
* @param {string|WebElement} locatorOrElement
* @return {!RelativeBy} Return this object
*/
below(locatorOrElement) {
this.filters.push({
kind: 'below',
args: [getLocator(locatorOrElement)],
})
return this
}
/**
* Look for elements left the root element passed in
* @param {string|WebElement} locatorOrElement
* @return {!RelativeBy} Return this object
*/
toLeftOf(locatorOrElement) {
this.filters.push({
kind: 'left',
args: [getLocator(locatorOrElement)],
})
return this
}
/**
* Look for elements right the root element passed in
* @param {string|WebElement} locatorOrElement
* @return {!RelativeBy} Return this object
*/
toRightOf(locatorOrElement) {
this.filters.push({
kind: 'right',
args: [getLocator(locatorOrElement)],
})
return this
}
/**
* Look for elements near the root element passed in
* @param {string|WebElement} locatorOrElement
* @return {!RelativeBy} Return this object
*/
near(locatorOrElement) {
this.filters.push({
kind: 'near',
args: [getLocator(locatorOrElement)],
})
return this
}
/**
* Returns a marshalled version of the {@link RelativeBy}
* @return {!Object} Object representation of a {@link WebElement}
* that will be used in {@link #findElements}.
*/
marshall() {
return {
relative: {
root: this.root,
filters: this.filters,
},
}
}
/** @override */
toString() {
// The static By.name() overrides this.constructor.name. Shame...
return `RelativeBy(${JSON.stringify(this.marshall())})`
}
}
/**
* Checks if a value is a valid locator.
* @param {!(By|Function|ByHash)} locator The value to check.
* @return {!(By|Function)} The valid locator.
* @throws {TypeError} If the given value does not define a valid locator
* strategy.
*/
function check(locator) {
if (locator instanceof By || locator instanceof RelativeBy || typeof locator === 'function') {
return locator
}
if (
locator &&
typeof locator === 'object' &&
typeof locator.using === 'string' &&
typeof locator.value === 'string'
) {
return new By(locator.using, locator.value)
}
for (let key in locator) {
if (Object.prototype.hasOwnProperty.call(locator, key) && Object.prototype.hasOwnProperty.call(By, key)) {
return By[key](locator[key])
}
}
throw new TypeError('Invalid locator')
}
// PUBLIC API
module.exports = {
By,
RelativeBy,
withTagName,
locateWith,
escapeCss,
checkedLocator: check,
}

View File

@@ -0,0 +1,555 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
/**
* @fileoverview Defines types related to describing the capabilities of a
* WebDriver session.
*/
const Symbols = require('./symbols')
/**
* Recognized browser names.
* @enum {string}
*/
const Browser = {
CHROME: 'chrome',
EDGE: 'MicrosoftEdge',
FIREFOX: 'firefox',
INTERNET_EXPLORER: 'internet explorer',
SAFARI: 'safari',
}
/**
* Strategies for waiting for [document readiness] after a navigation
* event.
*
* [document readiness]: https://html.spec.whatwg.org/#current-document-readiness
*
* @enum {string}
*/
const PageLoadStrategy = {
/**
* Indicates WebDriver should not wait on the document readiness state after a
* navigation event.
*/
NONE: 'none',
/**
* Indicates WebDriver should wait for the document readiness state to
* become "interactive" after navigation.
*/
EAGER: 'eager',
/**
* Indicates WebDriver should wait for the document readiness state to
* be "complete" after navigation. This is the default page loading strategy.
*/
NORMAL: 'normal',
}
/**
* Common platform names. These platforms are not explicitly defined by the
* WebDriver spec, however, their use is encouraged for interoperability.
*
* @enum {string}
* @see <https://w3c.github.io/webdriver/webdriver-spec.html>
*/
const Platform = {
LINUX: 'linux',
MAC: 'mac',
WINDOWS: 'windows',
}
/**
* Record object defining the timeouts that apply to certain WebDriver actions.
*
* @record
*/
function Timeouts() {}
/**
* Defines when, in milliseconds, to interrupt a script that is being
* {@linkplain ./webdriver.IWebDriver#executeScript evaluated}.
* @type {number}
*/
Timeouts.prototype.script
/**
* The timeout, in milliseconds, to apply to navigation events along with the
* {@link PageLoadStrategy}.
* @type {number}
*/
Timeouts.prototype.pageLoad
/**
* The maximum amount of time, in milliseconds, to spend attempting to
* {@linkplain ./webdriver.IWebDriver#findElement locate} an element on the
* current page.
* @type {number}
*/
Timeouts.prototype.implicit
/**
* The possible default actions a WebDriver session can take to respond to
* unhandled user prompts (`window.alert()`, `window.confirm()`, and
* `window.prompt()`).
*
* @enum {string}
*/
const UserPromptHandler = {
/** All prompts should be silently accepted. */
ACCEPT: 'accept',
/** All prompts should be silently dismissed. */
DISMISS: 'dismiss',
/**
* All prompts should be automatically accepted, but an error should be
* returned to the next (or currently executing) WebDriver command.
*/
ACCEPT_AND_NOTIFY: 'accept and notify',
/**
* All prompts should be automatically dismissed, but an error should be
* returned to the next (or currently executing) WebDriver command.
*/
DISMISS_AND_NOTIFY: 'dismiss and notify',
/** All prompts should be left unhandled. */
IGNORE: 'ignore',
}
/**
* The standard WebDriver capability keys.
*
* @enum {string}
* @see <https://w3c.github.io/webdriver/webdriver-spec.html#capabilities>
*/
const Capability = {
/**
* Indicates whether a WebDriver session implicitly trusts otherwise untrusted
* and self-signed TLS certificates during navigation.
*/
ACCEPT_INSECURE_TLS_CERTS: 'acceptInsecureCerts',
/**
* The browser name. Common browser names are defined in the
* {@link ./capabilities.Browser Browser} enum.
*/
BROWSER_NAME: 'browserName',
/** Identifies the browser version. */
BROWSER_VERSION: 'browserVersion',
/**
* Key for the logging driver logging preferences.
* The browser name. Common browser names are defined in the
* {@link ./capabilities.Browser Browser} enum.
*/
LOGGING_PREFS: 'goog:loggingPrefs',
/**
* Defines the session's
* {@linkplain ./capabilities.PageLoadStrategy page loading strategy}.
*/
PAGE_LOAD_STRATEGY: 'pageLoadStrategy',
/**
* Identifies the operating system of the endpoint node. Common values
* recognized by the most WebDriver server implementations are predefined in
* the {@link ./capabilities.Platform Platform} enum.
*/
PLATFORM_NAME: 'platformName',
/**
* Describes the proxy configuration to use for a new WebDriver session.
*/
PROXY: 'proxy',
/**
* Indicates whether the remote end supports all of the window resizing and
* positioning commands:
*
* - {@linkplain ./webdriver.Window#getRect Window.getRect()}
* - {@linkplain ./webdriver.Window#setRect Window.setRect()}
* - {@linkplain ./webdriver.Window#maximize Window.maximize()}
* - {@linkplain ./webdriver.Window#minimize Window.minimize()}
* - {@linkplain ./webdriver.Window#fullscreen Window.fullscreen()}
*/
SET_WINDOW_RECT: 'setWindowRect',
/**
* Describes the {@linkplain ./capabilities.Timeouts timeouts} imposed on
* certain session operations.
*/
TIMEOUTS: 'timeouts',
/**
* Defines how a WebDriver session should
* {@linkplain ./capabilities.UserPromptHandler respond} to unhandled user
* prompts.
*/
UNHANDLED_PROMPT_BEHAVIOR: 'unhandledPromptBehavior',
/**
* Defines the current session’s strict file interactability.
* Used to upload a file when strict file interactability is on
*/
STRICT_FILE_INTERACTABILITY: 'strictFileInteractability',
ENABLE_DOWNLOADS: 'se:downloadsEnabled',
}
/**
* Converts a generic hash object to a map.
* @param {!Object<string, ?>} hash The hash object.
* @return {!Map<string, ?>} The converted map.
*/
function toMap(hash) {
let m = new Map()
for (let key in hash) {
if (Object.prototype.hasOwnProperty.call(hash, key)) {
m.set(key, hash[key])
}
}
return m
}
/**
* Describes a set of capabilities for a WebDriver session.
*/
class Capabilities {
/**
* @param {(Capabilities|Map<string, ?>|Object)=} other Another set of
* capabilities to initialize this instance from.
*/
constructor(other = undefined) {
if (other instanceof Capabilities) {
other = other.map_
} else if (other && !(other instanceof Map)) {
other = toMap(other)
}
/** @private @const {!Map<string, ?>} */
this.map_ = new Map(other)
}
/** @return {number} The number of capabilities set. */
get size() {
return this.map_.size
}
/**
* @return {!Capabilities} A basic set of capabilities for Chrome.
*/
static chrome() {
return new Capabilities().setBrowserName(Browser.CHROME)
}
/**
* @return {!Capabilities} A basic set of capabilities for Microsoft Edge.
*/
static edge() {
return new Capabilities().setBrowserName(Browser.EDGE)
}
/**
* @return {!Capabilities} A basic set of capabilities for Firefox.
*/
static firefox() {
return new Capabilities().setBrowserName(Browser.FIREFOX).set('moz:debuggerAddress', true)
}
/**
* @return {!Capabilities} A basic set of capabilities for Internet Explorer.
*/
static ie() {
return new Capabilities().setBrowserName(Browser.INTERNET_EXPLORER)
}
/**
* @return {!Capabilities} A basic set of capabilities for Safari.
*/
static safari() {
return new Capabilities().setBrowserName(Browser.SAFARI)
}
/**
* @return {!Object<string, ?>} The JSON representation of this instance.
* Note, the returned object may contain nested promised values.
* @suppress {checkTypes} Suppress [] access on a struct (state inherited from
* Map).
*/
[Symbols.serialize]() {
return serialize(this)
}
/**
* @param {string} key the parameter key to get.
* @return {T} the stored parameter value.
* @template T
*/
get(key) {
return this.map_.get(key)
}
/**
* @param {string} key the key to test.
* @return {boolean} whether this capability set has the specified key.
*/
has(key) {
return this.map_.has(key)
}
/**
* @return {!Iterator<string>} an iterator of the keys set.
*/
keys() {
return this.map_.keys()
}
/**
* Merges another set of capabilities into this instance.
* @param {!(Capabilities|Map<String, ?>|Object<string, ?>)} other The other
* set of capabilities to merge.
* @return {!Capabilities} A self reference.
*/
merge(other) {
if (other) {
let otherMap
if (other instanceof Capabilities) {
otherMap = other.map_
} else if (other instanceof Map) {
otherMap = other
} else {
otherMap = toMap(other)
}
otherMap.forEach((value, key) => {
this.set(key, value)
})
return this
} else {
throw new TypeError('no capabilities provided for merge')
}
}
/**
* Deletes an entry from this set of capabilities.
*
* @param {string} key the capability key to delete.
*/
delete(key) {
this.map_.delete(key)
}
/**
* @param {string} key The capability key.
* @param {*} value The capability value.
* @return {!Capabilities} A self reference.
* @throws {TypeError} If the `key` is not a string.
*/
set(key, value) {
if (typeof key !== 'string') {
throw new TypeError('Capability keys must be strings: ' + typeof key)
}
this.map_.set(key, value)
return this
}
/**
* Sets whether a WebDriver session should implicitly accept self-signed, or
* other untrusted TLS certificates on navigation.
*
* @param {boolean} accept whether to accept insecure certs.
* @return {!Capabilities} a self reference.
*/
setAcceptInsecureCerts(accept) {
return this.set(Capability.ACCEPT_INSECURE_TLS_CERTS, accept)
}
/**
* @return {boolean} whether the session is configured to accept insecure
* TLS certificates.
*/
getAcceptInsecureCerts() {
return this.get(Capability.ACCEPT_INSECURE_TLS_CERTS)
}
/**
* Sets the name of the target browser.
*
* @param {(Browser|string)} name the browser name.
* @return {!Capabilities} a self reference.
*/
setBrowserName(name) {
return this.set(Capability.BROWSER_NAME, name)
}
/**
* @return {(string|undefined)} the configured browser name, or undefined if
* not set.
*/
getBrowserName() {
return this.get(Capability.BROWSER_NAME)
}
/**
* Sets the desired version of the target browser.
*
* @param {string} version the desired version.
* @return {!Capabilities} a self reference.
*/
setBrowserVersion(version) {
return this.set(Capability.BROWSER_VERSION, version)
}
/**
* @return {(string|undefined)} the configured browser version, or undefined
* if not set.
*/
getBrowserVersion() {
return this.get(Capability.BROWSER_VERSION)
}
/**
* Sets the desired page loading strategy for a new WebDriver session.
*
* @param {PageLoadStrategy} strategy the desired strategy.
* @return {!Capabilities} a self reference.
*/
setPageLoadStrategy(strategy) {
return this.set(Capability.PAGE_LOAD_STRATEGY, strategy)
}
/**
* Returns the configured page load strategy.
*
* @return {(string|undefined)} the page load strategy.
*/
getPageLoadStrategy() {
return this.get(Capability.PAGE_LOAD_STRATEGY)
}
/**
* Sets the target platform.
*
* @param {(Platform|string)} platform the target platform.
* @return {!Capabilities} a self reference.
*/
setPlatform(platform) {
return this.set(Capability.PLATFORM_NAME, platform)
}
/**
* @return {(string|undefined)} the configured platform or undefined if not
* set.
*/
getPlatform() {
return this.get(Capability.PLATFORM_NAME)
}
/**
* Sets the logging preferences. Preferences may be specified as a
* {@link ./logging.Preferences} instance, or as a map of log-type to
* log-level.
* @param {!(./logging.Preferences|Object<string>)} prefs The logging
* preferences.
* @return {!Capabilities} A self reference.
*/
setLoggingPrefs(prefs) {
return this.set(Capability.LOGGING_PREFS, prefs)
}
/**
* Sets the proxy configuration for this instance.
* @param {proxy.Config} proxy The desired proxy configuration.
* @return {!Capabilities} A self reference.
*/
setProxy(proxy) {
return this.set(Capability.PROXY, proxy)
}
/**
* @return {(proxy.Config|undefined)} the configured proxy settings, or
* undefined if not set.
*/
getProxy() {
return this.get(Capability.PROXY)
}
/**
* Sets the default action to take with an unexpected alert before returning
* an error. If unspecified, WebDriver will default to
* {@link UserPromptHandler.DISMISS_AND_NOTIFY}.
*
* @param {?UserPromptHandler} behavior The way WebDriver should respond to
* unhandled user prompts.
* @return {!Capabilities} A self reference.
*/
setAlertBehavior(behavior) {
return this.set(Capability.UNHANDLED_PROMPT_BEHAVIOR, behavior)
}
/**
* @return {(UserPromptHandler|undefined)} the behavior pattern for responding
* to unhandled user prompts, or undefined if not set.
*/
getAlertBehavior() {
return this.get(Capability.UNHANDLED_PROMPT_BEHAVIOR)
}
/**
* Sets the boolean flag configuration for this instance.
*/
setStrictFileInteractability(strictFileInteractability) {
return this.set(Capability.STRICT_FILE_INTERACTABILITY, strictFileInteractability)
}
enableDownloads() {
return this.set(Capability.ENABLE_DOWNLOADS, true)
}
}
/**
* Serializes a capabilities object. This is defined as a standalone function
* so it may be type checked (where Capabilities[Symbols.serialize] has type
* checking disabled since it is defined with [] access on a struct).
*
* @param {!Capabilities} caps The capabilities to serialize.
* @return {!Object<string, ?>} The JSON representation of this instance.
* Note, the returned object may contain nested promised values.
*/
function serialize(caps) {
let ret = {}
for (let key of caps.keys()) {
let cap = caps.get(key)
if (cap !== undefined && cap !== null) {
ret[key] = cap
}
}
return ret
}
// PUBLIC API
module.exports = {
Browser,
Capabilities,
Capability,
PageLoadStrategy,
Platform,
Timeouts,
UserPromptHandler,
}

View File

@@ -0,0 +1,214 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Contains several classes for handling commands.
*/
'use strict'
/**
* Describes a command to execute.
* @final
*/
class Command {
/** @param {string} name The name of this command. */
constructor(name) {
/** @private {string} */
this.name_ = name
/** @private {!Object<*>} */
this.parameters_ = {}
}
/** @return {string} This command's name. */
getName() {
return this.name_
}
/**
* Sets a parameter to send with this command.
* @param {string} name The parameter name.
* @param {*} value The parameter value.
* @return {!Command} A self reference.
*/
setParameter(name, value) {
this.parameters_[name] = value
return this
}
/**
* Sets the parameters for this command.
* @param {!Object<*>} parameters The command parameters.
* @return {!Command} A self reference.
*/
setParameters(parameters) {
this.parameters_ = parameters
return this
}
/**
* Returns a named command parameter.
* @param {string} key The parameter key to look up.
* @return {*} The parameter value, or undefined if it has not been set.
*/
getParameter(key) {
return this.parameters_[key]
}
/**
* @return {!Object<*>} The parameters to send with this command.
*/
getParameters() {
return this.parameters_
}
}
/**
* Enumeration of predefined names command names that all command processors
* will support.
* @enum {string}
*/
const Name = {
GET_SERVER_STATUS: 'getStatus',
NEW_SESSION: 'newSession',
GET_SESSIONS: 'getSessions',
CLOSE: 'close',
QUIT: 'quit',
GET_CURRENT_URL: 'getCurrentUrl',
GET: 'get',
GO_BACK: 'goBack',
GO_FORWARD: 'goForward',
REFRESH: 'refresh',
ADD_COOKIE: 'addCookie',
GET_COOKIE: 'getCookie',
GET_ALL_COOKIES: 'getCookies',
DELETE_COOKIE: 'deleteCookie',
DELETE_ALL_COOKIES: 'deleteAllCookies',
GET_ACTIVE_ELEMENT: 'getActiveElement',
FIND_ELEMENT: 'findElement',
FIND_ELEMENTS: 'findElements',
FIND_ELEMENTS_RELATIVE: 'findElementsRelative',
FIND_CHILD_ELEMENT: 'findChildElement',
FIND_CHILD_ELEMENTS: 'findChildElements',
CLEAR_ELEMENT: 'clearElement',
CLICK_ELEMENT: 'clickElement',
SEND_KEYS_TO_ELEMENT: 'sendKeysToElement',
GET_CURRENT_WINDOW_HANDLE: 'getCurrentWindowHandle',
GET_WINDOW_HANDLES: 'getWindowHandles',
GET_WINDOW_RECT: 'getWindowRect',
SET_WINDOW_RECT: 'setWindowRect',
MAXIMIZE_WINDOW: 'maximizeWindow',
MINIMIZE_WINDOW: 'minimizeWindow',
FULLSCREEN_WINDOW: 'fullscreenWindow',
SWITCH_TO_WINDOW: 'switchToWindow',
SWITCH_TO_NEW_WINDOW: 'newWindow',
SWITCH_TO_FRAME: 'switchToFrame',
SWITCH_TO_FRAME_PARENT: 'switchToFrameParent',
GET_PAGE_SOURCE: 'getPageSource',
GET_TITLE: 'getTitle',
EXECUTE_SCRIPT: 'executeScript',
EXECUTE_ASYNC_SCRIPT: 'executeAsyncScript',
GET_ELEMENT_TEXT: 'getElementText',
GET_COMPUTED_ROLE: 'getAriaRole',
GET_COMPUTED_LABEL: 'getAccessibleName',
GET_ELEMENT_TAG_NAME: 'getElementTagName',
IS_ELEMENT_SELECTED: 'isElementSelected',
IS_ELEMENT_ENABLED: 'isElementEnabled',
IS_ELEMENT_DISPLAYED: 'isElementDisplayed',
GET_ELEMENT_RECT: 'getElementRect',
GET_ELEMENT_ATTRIBUTE: 'getElementAttribute',
GET_DOM_ATTRIBUTE: 'getDomAttribute',
GET_ELEMENT_VALUE_OF_CSS_PROPERTY: 'getElementValueOfCssProperty',
GET_ELEMENT_PROPERTY: 'getElementProperty',
SCREENSHOT: 'screenshot',
TAKE_ELEMENT_SCREENSHOT: 'takeElementScreenshot',
PRINT_PAGE: 'printPage',
GET_TIMEOUT: 'getTimeout',
SET_TIMEOUT: 'setTimeout',
ACCEPT_ALERT: 'acceptAlert',
DISMISS_ALERT: 'dismissAlert',
GET_ALERT_TEXT: 'getAlertText',
SET_ALERT_TEXT: 'setAlertValue',
// Shadow DOM Commands
GET_SHADOW_ROOT: 'getShadowRoot',
FIND_ELEMENT_FROM_SHADOWROOT: 'findElementFromShadowRoot',
FIND_ELEMENTS_FROM_SHADOWROOT: 'findElementsFromShadowRoot',
// Virtual Authenticator Commands
ADD_VIRTUAL_AUTHENTICATOR: 'addVirtualAuthenticator',
REMOVE_VIRTUAL_AUTHENTICATOR: 'removeVirtualAuthenticator',
ADD_CREDENTIAL: 'addCredential',
GET_CREDENTIALS: 'getCredentials',
REMOVE_CREDENTIAL: 'removeCredential',
REMOVE_ALL_CREDENTIALS: 'removeAllCredentials',
SET_USER_VERIFIED: 'setUserVerified',
GET_AVAILABLE_LOG_TYPES: 'getAvailableLogTypes',
GET_LOG: 'getLog',
// Non-standard commands used by the standalone Selenium server.
UPLOAD_FILE: 'uploadFile',
ACTIONS: 'actions',
CLEAR_ACTIONS: 'clearActions',
GET_DOWNLOADABLE_FILES: 'getDownloadableFiles',
DOWNLOAD_FILE: 'downloadFile',
DELETE_DOWNLOADABLE_FILES: 'deleteDownloadableFiles',
}
/**
* Handles the execution of WebDriver {@link Command commands}.
* @record
*/
class Executor {
/**
* Executes the given {@code command}. If there is an error executing the
* command, the provided callback will be invoked with the offending error.
* Otherwise, the callback will be invoked with a null Error and non-null
* response object.
*
* @param {!Command} command The command to execute.
* @return {!Promise<?>} A promise that will be fulfilled with the command
* result.
*/
execute(command) {} // eslint-disable-line
}
// PUBLIC API
module.exports = {
Command,
Name,
Executor,
}

View File

@@ -0,0 +1,603 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
const { isObject } = require('./util')
/**
* The base WebDriver error type. This error type is only used directly when a
* more appropriate category is not defined for the offending error.
*/
class WebDriverError extends Error {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
/** @override */
this.name = this.constructor.name
/**
* A stacktrace reported by the remote webdriver endpoint that initially
* reported this error. This property will be an empty string if the remote
* end did not provide a stacktrace.
* @type {string}
*/
this.remoteStacktrace = ''
}
}
/**
* Indicates the shadow root is no longer attached to the DOM
*/
class DetachedShadowRootError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* Indicates a {@linkplain ./webdriver.WebElement#click click command} could not
* completed because the click target is obscured by other elements on the
* page.
*/
class ElementClickInterceptedError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* An attempt was made to select an element that cannot be selected.
*/
class ElementNotSelectableError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* Indicates a command could not be completed because the target element is
* not pointer or keyboard interactable. This will often occur if an element
* is present in the DOM, but not rendered (i.e. its CSS style has
* "display: none").
*/
class ElementNotInteractableError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* Indicates a navigation event caused the browser to generate a certificate
* warning. This is usually caused by an expired or invalid TLS certificate.
*/
class InsecureCertificateError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* The arguments passed to a command are either invalid or malformed.
*/
class InvalidArgumentError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* An illegal attempt was made to set a cookie under a different domain than
* the current page.
*/
class InvalidCookieDomainError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* The coordinates provided to an interactions operation are invalid.
*/
class InvalidCoordinatesError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* An element command could not be completed because the element is in an
* invalid state, e.g. attempting to click an element that is no longer attached
* to the document.
*/
class InvalidElementStateError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* Argument was an invalid selector.
*/
class InvalidSelectorError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* Occurs when a command is directed to a session that does not exist.
*/
class NoSuchSessionError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* An error occurred while executing JavaScript supplied by the user.
*/
class JavascriptError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* The target for mouse interaction is not in the browser’s viewport and cannot
* be brought into that viewport.
*/
class MoveTargetOutOfBoundsError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* An attempt was made to operate on a modal dialog when one was not open.
*/
class NoSuchAlertError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* Indicates a named cookie could not be found in the cookie jar for the
* currently selected document.
*/
class NoSuchCookieError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* An element could not be located on the page using the given search
* parameters.
*/
class NoSuchElementError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* A ShadowRoot could not be located on the element
*/
class NoSuchShadowRootError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* A request to switch to a frame could not be satisfied because the frame
* could not be found.
*/
class NoSuchFrameError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* A request to switch to a window could not be satisfied because the window
* could not be found.
*/
class NoSuchWindowError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* A script did not complete before its timeout expired.
*/
class ScriptTimeoutError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* A new session could not be created.
*/
class SessionNotCreatedError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* An element command failed because the referenced element is no longer
* attached to the DOM.
*/
class StaleElementReferenceError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* An operation did not complete before its timeout expired.
*/
class TimeoutError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* A request to set a cookie’s value could not be satisfied.
*/
class UnableToSetCookieError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* A screen capture operation was not possible.
*/
class UnableToCaptureScreenError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* A modal dialog was open, blocking this operation.
*/
class UnexpectedAlertOpenError extends WebDriverError {
/**
* @param {string=} opt_error the error message, if any.
* @param {string=} opt_text the text of the open dialog, if available.
*/
constructor(opt_error, opt_text) {
super(opt_error)
/** @private {(string|undefined)} */
this.text_ = opt_text
}
/**
* @return {(string|undefined)} The text displayed with the unhandled alert,
* if available.
*/
getAlertText() {
return this.text_
}
}
/**
* A command could not be executed because the remote end is not aware of it.
*/
class UnknownCommandError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* The requested command matched a known URL but did not match an method for
* that URL.
*/
class UnknownMethodError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
/**
* Reports an unsupported operation.
*/
class UnsupportedOperationError extends WebDriverError {
/** @param {string=} opt_error the error message, if any. */
constructor(opt_error) {
super(opt_error)
}
}
// TODO(jleyba): Define UnknownError as an alias of WebDriverError?
/**
* Enum of legacy error codes.
* TODO: remove this when all code paths have been switched to the new error
* types.
* @deprecated
* @enum {number}
*/
const ErrorCode = {
SUCCESS: 0,
NO_SUCH_SESSION: 6,
NO_SUCH_ELEMENT: 7,
NO_SUCH_FRAME: 8,
UNKNOWN_COMMAND: 9,
UNSUPPORTED_OPERATION: 9,
STALE_ELEMENT_REFERENCE: 10,
ELEMENT_NOT_VISIBLE: 11,
INVALID_ELEMENT_STATE: 12,
UNKNOWN_ERROR: 13,
ELEMENT_NOT_SELECTABLE: 15,
JAVASCRIPT_ERROR: 17,
XPATH_LOOKUP_ERROR: 19,
TIMEOUT: 21,
NO_SUCH_WINDOW: 23,
INVALID_COOKIE_DOMAIN: 24,
UNABLE_TO_SET_COOKIE: 25,
UNEXPECTED_ALERT_OPEN: 26,
NO_SUCH_ALERT: 27,
SCRIPT_TIMEOUT: 28,
INVALID_ELEMENT_COORDINATES: 29,
IME_NOT_AVAILABLE: 30,
IME_ENGINE_ACTIVATION_FAILED: 31,
INVALID_SELECTOR_ERROR: 32,
SESSION_NOT_CREATED: 33,
MOVE_TARGET_OUT_OF_BOUNDS: 34,
SQL_DATABASE_ERROR: 35,
INVALID_XPATH_SELECTOR: 51,
INVALID_XPATH_SELECTOR_RETURN_TYPE: 52,
ELEMENT_NOT_INTERACTABLE: 60,
INVALID_ARGUMENT: 61,
NO_SUCH_COOKIE: 62,
UNABLE_TO_CAPTURE_SCREEN: 63,
ELEMENT_CLICK_INTERCEPTED: 64,
METHOD_NOT_ALLOWED: 405,
}
const LEGACY_ERROR_CODE_TO_TYPE = new Map([
[ErrorCode.NO_SUCH_SESSION, NoSuchSessionError],
[ErrorCode.NO_SUCH_ELEMENT, NoSuchElementError],
[ErrorCode.NO_SUCH_FRAME, NoSuchFrameError],
[ErrorCode.UNSUPPORTED_OPERATION, UnsupportedOperationError],
[ErrorCode.STALE_ELEMENT_REFERENCE, StaleElementReferenceError],
[ErrorCode.INVALID_ELEMENT_STATE, InvalidElementStateError],
[ErrorCode.UNKNOWN_ERROR, WebDriverError],
[ErrorCode.ELEMENT_NOT_SELECTABLE, ElementNotSelectableError],
[ErrorCode.JAVASCRIPT_ERROR, JavascriptError],
[ErrorCode.XPATH_LOOKUP_ERROR, InvalidSelectorError],
[ErrorCode.TIMEOUT, TimeoutError],
[ErrorCode.NO_SUCH_WINDOW, NoSuchWindowError],
[ErrorCode.INVALID_COOKIE_DOMAIN, InvalidCookieDomainError],
[ErrorCode.UNABLE_TO_SET_COOKIE, UnableToSetCookieError],
[ErrorCode.UNEXPECTED_ALERT_OPEN, UnexpectedAlertOpenError],
[ErrorCode.NO_SUCH_ALERT, NoSuchAlertError],
[ErrorCode.SCRIPT_TIMEOUT, ScriptTimeoutError],
[ErrorCode.INVALID_ELEMENT_COORDINATES, InvalidCoordinatesError],
[ErrorCode.INVALID_SELECTOR_ERROR, InvalidSelectorError],
[ErrorCode.SESSION_NOT_CREATED, SessionNotCreatedError],
[ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS, MoveTargetOutOfBoundsError],
[ErrorCode.INVALID_XPATH_SELECTOR, InvalidSelectorError],
[ErrorCode.INVALID_XPATH_SELECTOR_RETURN_TYPE, InvalidSelectorError],
[ErrorCode.ELEMENT_NOT_INTERACTABLE, ElementNotInteractableError],
[ErrorCode.INVALID_ARGUMENT, InvalidArgumentError],
[ErrorCode.NO_SUCH_COOKIE, NoSuchCookieError],
[ErrorCode.UNABLE_TO_CAPTURE_SCREEN, UnableToCaptureScreenError],
[ErrorCode.ELEMENT_CLICK_INTERCEPTED, ElementClickInterceptedError],
[ErrorCode.METHOD_NOT_ALLOWED, UnsupportedOperationError],
])
const ERROR_CODE_TO_TYPE = new Map([
['unknown error', WebDriverError],
['detached shadow root', DetachedShadowRootError],
['element click intercepted', ElementClickInterceptedError],
['element not interactable', ElementNotInteractableError],
['element not selectable', ElementNotSelectableError],
['insecure certificate', InsecureCertificateError],
['invalid argument', InvalidArgumentError],
['invalid cookie domain', InvalidCookieDomainError],
['invalid coordinates', InvalidCoordinatesError],
['invalid element state', InvalidElementStateError],
['invalid selector', InvalidSelectorError],
['invalid session id', NoSuchSessionError],
['javascript error', JavascriptError],
['move target out of bounds', MoveTargetOutOfBoundsError],
['no such alert', NoSuchAlertError],
['no such cookie', NoSuchCookieError],
['no such element', NoSuchElementError],
['no such frame', NoSuchFrameError],
['no such shadow root', NoSuchShadowRootError],
['no such window', NoSuchWindowError],
['script timeout', ScriptTimeoutError],
['session not created', SessionNotCreatedError],
['stale element reference', StaleElementReferenceError],
['timeout', TimeoutError],
['unable to set cookie', UnableToSetCookieError],
['unable to capture screen', UnableToCaptureScreenError],
['unexpected alert open', UnexpectedAlertOpenError],
['unknown command', UnknownCommandError],
['unknown method', UnknownMethodError],
['unsupported operation', UnsupportedOperationError],
])
const TYPE_TO_ERROR_CODE = new Map()
ERROR_CODE_TO_TYPE.forEach((value, key) => {
TYPE_TO_ERROR_CODE.set(value, key)
})
/**
* @param {*} err The error to encode.
* @return {{error: string, message: string}} the encoded error.
*/
function encodeError(err) {
let type = WebDriverError
if (err instanceof WebDriverError && TYPE_TO_ERROR_CODE.has(err.constructor)) {
type = err.constructor
}
let message = err instanceof Error ? err.message : err + ''
let code = /** @type {string} */ (TYPE_TO_ERROR_CODE.get(type))
return { error: code, message: message }
}
/**
* Tests if the given value is a valid error response object according to the
* W3C WebDriver spec.
*
* @param {?} data The value to test.
* @return {boolean} Whether the given value data object is a valid error
* response.
* @see https://w3c.github.io/webdriver/webdriver-spec.html#protocol
*/
function isErrorResponse(data) {
return isObject(data) && typeof data.error === 'string'
}
/**
* Throws an error coded from the W3C protocol. A generic error will be thrown
* if the provided `data` is not a valid encoded error.
*
* @param {{error: string, message: string}} data The error data to decode.
* @throws {WebDriverError} the decoded error.
* @see https://w3c.github.io/webdriver/webdriver-spec.html#protocol
*/
function throwDecodedError(data) {
if (isErrorResponse(data)) {
let ctor = ERROR_CODE_TO_TYPE.get(data.error) || WebDriverError
let err = new ctor(data.message)
// TODO(jleyba): remove whichever case is excluded from the final W3C spec.
if (typeof data.stacktrace === 'string') {
err.remoteStacktrace = data.stacktrace
} else if (typeof data.stackTrace === 'string') {
err.remoteStacktrace = data.stackTrace
}
throw err
}
throw new WebDriverError('Unknown error: ' + JSON.stringify(data))
}
/**
* Checks a legacy response from the Selenium 2.0 wire protocol for an error.
* @param {*} responseObj the response object to check.
* @return {*} responseObj the original response if it does not define an error.
* @throws {WebDriverError} if the response object defines an error.
*/
function checkLegacyResponse(responseObj) {
// Handle the legacy Selenium error response format.
if (isObject(responseObj) && typeof responseObj.status === 'number' && responseObj.status !== 0) {
const { status, value } = responseObj
let ctor = LEGACY_ERROR_CODE_TO_TYPE.get(status) || WebDriverError
if (!value || typeof value !== 'object') {
throw new ctor(value + '')
} else {
let message = value['message'] + ''
if (ctor !== UnexpectedAlertOpenError) {
throw new ctor(message)
}
let text = ''
if (value['alert'] && typeof value['alert']['text'] === 'string') {
text = value['alert']['text']
}
throw new UnexpectedAlertOpenError(message, text)
}
}
return responseObj
}
// PUBLIC API
module.exports = {
ErrorCode,
WebDriverError,
DetachedShadowRootError,
ElementClickInterceptedError,
ElementNotInteractableError,
ElementNotSelectableError,
InsecureCertificateError,
InvalidArgumentError,
InvalidCookieDomainError,
InvalidCoordinatesError,
InvalidElementStateError,
InvalidSelectorError,
JavascriptError,
MoveTargetOutOfBoundsError,
NoSuchAlertError,
NoSuchCookieError,
NoSuchElementError,
NoSuchFrameError,
NoSuchShadowRootError,
NoSuchSessionError,
NoSuchWindowError,
ScriptTimeoutError,
SessionNotCreatedError,
StaleElementReferenceError,
TimeoutError,
UnableToSetCookieError,
UnableToCaptureScreenError,
UnexpectedAlertOpenError,
UnknownCommandError,
UnknownMethodError,
UnsupportedOperationError,
checkLegacyResponse,
encodeError,
isErrorResponse,
throwDecodedError,
}

View File

@@ -0,0 +1,577 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Defines an environment agnostic {@linkplain cmd.Executor
* command executor} that communicates with a remote end using JSON over HTTP.
*
* Clients should implement the {@link Client} interface, which is used by
* the {@link Executor} to send commands to the remote end.
*/
'use strict'
const path = require('node:path')
const cmd = require('./command')
const error = require('./error')
const logging = require('./logging')
const promise = require('./promise')
const { Session } = require('./session')
const webElement = require('./webelement')
const { isObject } = require('./util')
const log_ = logging.getLogger(`${logging.Type.DRIVER}.http`)
const getAttribute = requireAtom('get-attribute.js', '//javascript/node/selenium-webdriver/lib/atoms:get-attribute.js')
const isDisplayed = requireAtom('is-displayed.js', '//javascript/node/selenium-webdriver/lib/atoms:is-displayed.js')
const findElements = requireAtom('find-elements.js', '//javascript/node/selenium-webdriver/lib/atoms:find-elements.js')
/**
* @param {string} module
* @param {string} bazelTarget
* @return {!Function}
*/
function requireAtom(module, bazelTarget) {
try {
return require('./atoms/' + module)
} catch (ex) {
try {
const file = bazelTarget.slice(2).replace(':', '/')
log_.log(`../../../bazel-bin/${file}`)
return require(path.resolve(`../../../bazel-bin/${file}`))
} catch (ex2) {
log_.severe(ex2)
throw Error(
`Failed to import atoms module ${module}. If running in dev mode, you` +
` need to run \`bazel build ${bazelTarget}\` from the project` +
`root: ${ex}`,
)
}
}
}
/**
* Converts a headers map to a HTTP header block string.
* @param {!Map<string, string>} headers The map to convert.
* @return {string} The headers as a string.
*/
function headersToString(headers) {
const ret = []
headers.forEach(function (value, name) {
ret.push(`${name.toLowerCase()}: ${value}`)
})
return ret.join('\n')
}
/**
* Represents a HTTP request message. This class is a "partial" request and only
* defines the path on the server to send a request to. It is each client's
* responsibility to build the full URL for the final request.
* @final
*/
class Request {
/**
* @param {string} method The HTTP method to use for the request.
* @param {string} path The path on the server to send the request to.
* @param {Object=} opt_data This request's non-serialized JSON payload data.
*/
constructor(method, path, opt_data) {
this.method = /** string */ method
this.path = /** string */ path
this.data = /** Object */ opt_data
this.headers = /** !Map<string, string> */ new Map([['Accept', 'application/json; charset=utf-8']])
}
/** @override */
toString() {
let ret = `${this.method} ${this.path} HTTP/1.1\n`
ret += headersToString(this.headers) + '\n\n'
if (this.data) {
ret += JSON.stringify(this.data)
}
return ret
}
}
/**
* Represents a HTTP response message.
* @final
*/
class Response {
/**
* @param {number} status The response code.
* @param {!Object<string>} headers The response headers. All header names
* will be converted to lowercase strings for consistent lookups.
* @param {string} body The response body.
*/
constructor(status, headers, body) {
this.status = /** number */ status
this.body = /** string */ body
this.headers = /** !Map<string, string>*/ new Map()
for (let header in headers) {
this.headers.set(header.toLowerCase(), headers[header])
}
}
/** @override */
toString() {
let ret = `HTTP/1.1 ${this.status}\n${headersToString(this.headers)}\n\n`
if (this.body) {
ret += this.body
}
return ret
}
}
/** @enum {!Function} */
const Atom = {
GET_ATTRIBUTE: getAttribute,
IS_DISPLAYED: isDisplayed,
FIND_ELEMENTS: findElements,
}
function post(path) {
return resource('POST', path)
}
function del(path) {
return resource('DELETE', path)
}
function get(path) {
return resource('GET', path)
}
function resource(method, path) {
return { method: method, path: path }
}
/** @typedef {{method: string, path: string}} */
var CommandSpec
/** @typedef {function(!cmd.Command): !cmd.Command} */
var CommandTransformer
class InternalTypeError extends TypeError {}
/**
* @param {!cmd.Command} command The initial command.
* @param {Atom} atom The name of the atom to execute.
* @param params
* @return {!Command} The transformed command to execute.
*/
function toExecuteAtomCommand(command, atom, name, ...params) {
if (typeof atom !== 'function') {
throw new InternalTypeError('atom is not a function: ' + typeof atom)
}
return new cmd.Command(cmd.Name.EXECUTE_SCRIPT)
.setParameter('sessionId', command.getParameter('sessionId'))
.setParameter('script', `/* ${name} */return (${atom}).apply(null, arguments)`)
.setParameter(
'args',
params.map((param) => command.getParameter(param)),
)
}
/** @const {!Map<string, (CommandSpec|CommandTransformer)>} */
const W3C_COMMAND_MAP = new Map([
// Session management.
[cmd.Name.NEW_SESSION, post('/session')],
[cmd.Name.QUIT, del('/session/:sessionId')],
// Server status.
[cmd.Name.GET_SERVER_STATUS, get('/status')],
// timeouts
[cmd.Name.GET_TIMEOUT, get('/session/:sessionId/timeouts')],
[cmd.Name.SET_TIMEOUT, post('/session/:sessionId/timeouts')],
// Navigation.
[cmd.Name.GET_CURRENT_URL, get('/session/:sessionId/url')],
[cmd.Name.GET, post('/session/:sessionId/url')],
[cmd.Name.GO_BACK, post('/session/:sessionId/back')],
[cmd.Name.GO_FORWARD, post('/session/:sessionId/forward')],
[cmd.Name.REFRESH, post('/session/:sessionId/refresh')],
// Page inspection.
[cmd.Name.GET_PAGE_SOURCE, get('/session/:sessionId/source')],
[cmd.Name.GET_TITLE, get('/session/:sessionId/title')],
// Script execution.
[cmd.Name.EXECUTE_SCRIPT, post('/session/:sessionId/execute/sync')],
[cmd.Name.EXECUTE_ASYNC_SCRIPT, post('/session/:sessionId/execute/async')],
// Frame selection.
[cmd.Name.SWITCH_TO_FRAME, post('/session/:sessionId/frame')],
[cmd.Name.SWITCH_TO_FRAME_PARENT, post('/session/:sessionId/frame/parent')],
// Window management.
[cmd.Name.GET_CURRENT_WINDOW_HANDLE, get('/session/:sessionId/window')],
[cmd.Name.CLOSE, del('/session/:sessionId/window')],
[cmd.Name.SWITCH_TO_WINDOW, post('/session/:sessionId/window')],
[cmd.Name.SWITCH_TO_NEW_WINDOW, post('/session/:sessionId/window/new')],
[cmd.Name.GET_WINDOW_HANDLES, get('/session/:sessionId/window/handles')],
[cmd.Name.GET_WINDOW_RECT, get('/session/:sessionId/window/rect')],
[cmd.Name.SET_WINDOW_RECT, post('/session/:sessionId/window/rect')],
[cmd.Name.MAXIMIZE_WINDOW, post('/session/:sessionId/window/maximize')],
[cmd.Name.MINIMIZE_WINDOW, post('/session/:sessionId/window/minimize')],
[cmd.Name.FULLSCREEN_WINDOW, post('/session/:sessionId/window/fullscreen')],
// Actions.
[cmd.Name.ACTIONS, post('/session/:sessionId/actions')],
[cmd.Name.CLEAR_ACTIONS, del('/session/:sessionId/actions')],
[cmd.Name.PRINT_PAGE, post('/session/:sessionId/print')],
// Locating elements.
[cmd.Name.GET_ACTIVE_ELEMENT, get('/session/:sessionId/element/active')],
[cmd.Name.FIND_ELEMENT, post('/session/:sessionId/element')],
[cmd.Name.FIND_ELEMENTS, post('/session/:sessionId/elements')],
[
cmd.Name.FIND_ELEMENTS_RELATIVE,
(cmd) => {
return toExecuteAtomCommand(cmd, Atom.FIND_ELEMENTS, 'findElements', 'args')
},
],
[cmd.Name.FIND_CHILD_ELEMENT, post('/session/:sessionId/element/:id/element')],
[cmd.Name.FIND_CHILD_ELEMENTS, post('/session/:sessionId/element/:id/elements')],
// Element interaction.
[cmd.Name.GET_ELEMENT_TAG_NAME, get('/session/:sessionId/element/:id/name')],
[cmd.Name.GET_DOM_ATTRIBUTE, get('/session/:sessionId/element/:id/attribute/:name')],
[
cmd.Name.GET_ELEMENT_ATTRIBUTE,
(cmd) => {
return toExecuteAtomCommand(cmd, Atom.GET_ATTRIBUTE, 'getAttribute', 'id', 'name')
},
],
[cmd.Name.GET_ELEMENT_PROPERTY, get('/session/:sessionId/element/:id/property/:name')],
[cmd.Name.GET_ELEMENT_VALUE_OF_CSS_PROPERTY, get('/session/:sessionId/element/:id/css/:propertyName')],
[cmd.Name.GET_ELEMENT_RECT, get('/session/:sessionId/element/:id/rect')],
[cmd.Name.CLEAR_ELEMENT, post('/session/:sessionId/element/:id/clear')],
[cmd.Name.CLICK_ELEMENT, post('/session/:sessionId/element/:id/click')],
[cmd.Name.SEND_KEYS_TO_ELEMENT, post('/session/:sessionId/element/:id/value')],
[cmd.Name.GET_ELEMENT_TEXT, get('/session/:sessionId/element/:id/text')],
[cmd.Name.GET_COMPUTED_ROLE, get('/session/:sessionId/element/:id/computedrole')],
[cmd.Name.GET_COMPUTED_LABEL, get('/session/:sessionId/element/:id/computedlabel')],
[cmd.Name.IS_ELEMENT_ENABLED, get('/session/:sessionId/element/:id/enabled')],
[cmd.Name.IS_ELEMENT_SELECTED, get('/session/:sessionId/element/:id/selected')],
[
cmd.Name.IS_ELEMENT_DISPLAYED,
(cmd) => {
return toExecuteAtomCommand(cmd, Atom.IS_DISPLAYED, 'isDisplayed', 'id')
},
],
// Cookie management.
[cmd.Name.GET_ALL_COOKIES, get('/session/:sessionId/cookie')],
[cmd.Name.ADD_COOKIE, post('/session/:sessionId/cookie')],
[cmd.Name.DELETE_ALL_COOKIES, del('/session/:sessionId/cookie')],
[cmd.Name.GET_COOKIE, get('/session/:sessionId/cookie/:name')],
[cmd.Name.DELETE_COOKIE, del('/session/:sessionId/cookie/:name')],
// Alert management.
[cmd.Name.ACCEPT_ALERT, post('/session/:sessionId/alert/accept')],
[cmd.Name.DISMISS_ALERT, post('/session/:sessionId/alert/dismiss')],
[cmd.Name.GET_ALERT_TEXT, get('/session/:sessionId/alert/text')],
[cmd.Name.SET_ALERT_TEXT, post('/session/:sessionId/alert/text')],
// Screenshots.
[cmd.Name.SCREENSHOT, get('/session/:sessionId/screenshot')],
[cmd.Name.TAKE_ELEMENT_SCREENSHOT, get('/session/:sessionId/element/:id/screenshot')],
// Shadow Root
[cmd.Name.GET_SHADOW_ROOT, get('/session/:sessionId/element/:id/shadow')],
[cmd.Name.FIND_ELEMENT_FROM_SHADOWROOT, post('/session/:sessionId/shadow/:id/element')],
[cmd.Name.FIND_ELEMENTS_FROM_SHADOWROOT, post('/session/:sessionId/shadow/:id/elements')],
// Log extensions.
[cmd.Name.GET_LOG, post('/session/:sessionId/se/log')],
[cmd.Name.GET_AVAILABLE_LOG_TYPES, get('/session/:sessionId/se/log/types')],
// Server Extensions
[cmd.Name.UPLOAD_FILE, post('/session/:sessionId/se/file')],
// Virtual Authenticator
[cmd.Name.ADD_VIRTUAL_AUTHENTICATOR, post('/session/:sessionId/webauthn/authenticator')],
[cmd.Name.REMOVE_VIRTUAL_AUTHENTICATOR, del('/session/:sessionId/webauthn/authenticator/:authenticatorId')],
[cmd.Name.ADD_CREDENTIAL, post('/session/:sessionId/webauthn/authenticator/:authenticatorId/credential')],
[cmd.Name.GET_CREDENTIALS, get('/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials')],
[
cmd.Name.REMOVE_CREDENTIAL,
del('/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials/:credentialId'),
],
[cmd.Name.REMOVE_ALL_CREDENTIALS, del('/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials')],
[cmd.Name.SET_USER_VERIFIED, post('/session/:sessionId/webauthn/authenticator/:authenticatorId/uv')],
[cmd.Name.GET_DOWNLOADABLE_FILES, get('/session/:sessionId/se/files')],
[cmd.Name.DOWNLOAD_FILE, post(`/session/:sessionId/se/files`)],
[cmd.Name.DELETE_DOWNLOADABLE_FILES, del(`/session/:sessionId/se/files`)],
])
/**
* Handles sending HTTP messages to a remote end.
*
* @interface
*/
class Client {
/**
* Sends a request to the server. The client will automatically follow any
* redirects returned by the server, fulfilling the returned promise with the
* final response.
*
* @param {!Request} httpRequest The request to send.
* @return {!Promise<Response>} A promise that will be fulfilled with the
* server's response.
*/
send(httpRequest) {}
}
/**
* @param {Map<string, CommandSpec>} customCommands
* A map of custom command definitions.
* @param {!cmd.Command} command The command to resolve.
* @return {!Request} A promise that will resolve with the
* command to execute.
*/
function buildRequest(customCommands, command) {
log_.finest(() => `Translating command: ${command.getName()}`)
let spec = customCommands && customCommands.get(command.getName())
if (spec) {
return toHttpRequest(spec)
}
spec = W3C_COMMAND_MAP.get(command.getName())
if (typeof spec === 'function') {
log_.finest(() => `Transforming command for W3C: ${command.getName()}`)
let newCommand = spec(command)
return buildRequest(customCommands, newCommand)
} else if (spec) {
return toHttpRequest(spec)
}
throw new error.UnknownCommandError('Unrecognized command: ' + command.getName())
/**
* @param {CommandSpec} resource
* @return {!Request}
*/
function toHttpRequest(resource) {
log_.finest(() => `Building HTTP request: ${JSON.stringify(resource)}`)
let parameters = command.getParameters()
let path = buildPath(resource.path, parameters)
return new Request(resource.method, path, parameters)
}
}
const CLIENTS = /** !WeakMap<!Executor, !(Client|IThenable<!Client>)> */ new WeakMap()
/**
* A command executor that communicates with the server using JSON over HTTP.
*
* By default, each instance of this class will use the legacy wire protocol
* from [Selenium project][json]. The executor will automatically switch to the
* [W3C wire protocol][w3c] if the remote end returns a compliant response to
* a new session command.
*
* [json]: https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol
* [w3c]: https://w3c.github.io/webdriver/webdriver-spec.html
*
* @implements {cmd.Executor}
*/
class Executor {
/**
* @param {!(Client|IThenable<!Client>)} client The client to use for sending
* requests to the server, or a promise-like object that will resolve
* to the client.
*/
constructor(client) {
CLIENTS.set(this, client)
/** @private {Map<string, CommandSpec>} */
this.customCommands_ = null
/** @private {!logging.Logger} */
this.log_ = logging.getLogger(`${logging.Type.DRIVER}.http.Executor`)
}
/**
* Defines a new command for use with this executor. When a command is sent,
* the {@code path} will be preprocessed using the command's parameters; any
* path segments prefixed with ":" will be replaced by the parameter of the
* same name. For example, given "/person/:name" and the parameters
* "{name: 'Bob'}", the final command path will be "/person/Bob".
*
* @param {string} name The command name.
* @param {string} method The HTTP method to use when sending this command.
* @param {string} path The path to send the command to, relative to
* the WebDriver server's command root and of the form
* "/path/:variable/segment".
*/
defineCommand(name, method, path) {
if (!this.customCommands_) {
this.customCommands_ = new Map()
}
this.customCommands_.set(name, { method, path })
}
/** @override */
async execute(command) {
let request = buildRequest(this.customCommands_, command)
this.log_.finer(() => `>>> ${request.method} ${request.path}`)
let client = CLIENTS.get(this)
if (promise.isPromise(client)) {
client = await client
CLIENTS.set(this, client)
}
let response = await client.send(request)
this.log_.finer(() => `>>>\n${request}\n<<<\n${response}`)
let httpResponse = /** @type {!Response} */ (response)
let { isW3C, value } = parseHttpResponse(command, httpResponse)
if (command.getName() === cmd.Name.NEW_SESSION) {
if (!value || !value.sessionId) {
throw new error.WebDriverError(`Unable to parse new session response: ${response.body}`)
}
// The remote end is a W3C compliant server if there is no `status`
// field in the response.
if (command.getName() === cmd.Name.NEW_SESSION) {
this.w3c = this.w3c || isW3C
}
// No implementations use the `capabilities` key yet...
let capabilities = value.capabilities || value.value
return new Session(/** @type {{sessionId: string}} */ (value).sessionId, capabilities)
}
return typeof value === 'undefined' ? null : value
}
}
/**
* @param {string} str .
* @return {?} .
*/
function tryParse(str) {
try {
return JSON.parse(str)
/*eslint no-unused-vars: "off"*/
} catch (ignored) {
// Do nothing.
}
}
/**
* Callback used to parse {@link Response} objects from a
* {@link HttpClient}.
*
* @param {!cmd.Command} command The command the response is for.
* @param {!Response} httpResponse The HTTP response to parse.
* @return {{isW3C: boolean, value: ?}} An object describing the parsed
* response. This object will have two fields: `isW3C` indicates whether
* the response looks like it came from a remote end that conforms with the
* W3C WebDriver spec, and `value`, the actual response value.
* @throws {WebDriverError} If the HTTP response is an error.
*/
function parseHttpResponse(command, httpResponse) {
if (httpResponse.status < 200) {
// This should never happen, but throw the raw response so users report it.
throw new error.WebDriverError(`Unexpected HTTP response:\n${httpResponse}`)
}
let parsed = tryParse(httpResponse.body)
if (parsed && typeof parsed === 'object') {
let value = parsed.value
let isW3C = isObject(value) && typeof parsed.status === 'undefined'
if (!isW3C) {
error.checkLegacyResponse(parsed)
// Adjust legacy new session responses to look like W3C to simplify
// later processing.
if (command.getName() === cmd.Name.NEW_SESSION) {
value = parsed
}
} else if (httpResponse.status > 399) {
error.throwDecodedError(value)
}
return { isW3C, value }
}
if (parsed !== undefined) {
return { isW3C: false, value: parsed }
}
let value = httpResponse.body.replace(/\r\n/g, '\n')
// 404 represents an unknown command; anything else > 399 is a generic unknown
// error.
if (httpResponse.status === 404) {
throw new error.UnsupportedOperationError(command.getName() + ': ' + value)
} else if (httpResponse.status >= 400) {
throw new error.WebDriverError(value)
}
return { isW3C: false, value: value || null }
}
/**
* Builds a fully qualified path using the given set of command parameters. Each
* path segment prefixed with ':' will be replaced by the value of the
* corresponding parameter. All parameters spliced into the path will be
* removed from the parameter map.
* @param {string} path The original resource path.
* @param {!Object<*>} parameters The parameters object to splice into the path.
* @return {string} The modified path.
*/
function buildPath(path, parameters) {
let pathParameters = path.match(/\/:(\w+)\b/g)
if (pathParameters) {
for (let i = 0; i < pathParameters.length; ++i) {
let key = pathParameters[i].substring(2) // Trim the /:
if (key in parameters) {
let value = parameters[key]
if (webElement.isId(value)) {
// When inserting a WebElement into the URL, only use its ID value,
// not the full JSON.
value = webElement.extractId(value)
}
path = path.replace(pathParameters[i], '/' + value)
delete parameters[key]
} else {
throw new error.InvalidArgumentError('Missing required parameter: ' + key)
}
}
}
return path
}
// PUBLIC API
module.exports = {
Executor,
Client,
Request,
Response,
// Exported for testing.
buildPath,
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,653 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
/**
* @fileoverview Defines WebDriver's logging system. The logging system is
* broken into major components: local and remote logging.
*
* The local logging API, which is anchored by the {@linkplain Logger} class is
* similar to Java's logging API. Loggers, retrieved by
* {@linkplain #getLogger getLogger(name)}, use hierarchical, dot-delimited
* namespaces (e.g. "" > "webdriver" > "webdriver.logging"). Recorded log
* messages are represented by the {@linkplain Entry} class. You can capture log
* records by {@linkplain Logger#addHandler attaching} a handler function to the
* desired logger. For convenience, you can quickly enable logging to the
* console by simply calling {@linkplain #installConsoleHandler
* installConsoleHandler}.
*
* The [remote logging API](https://github.com/SeleniumHQ/selenium/wiki/Logging)
* allows you to retrieve logs from a remote WebDriver server. This API uses the
* {@link Preferences} class to define desired log levels prior to creating
* a WebDriver session:
*
* var prefs = new logging.Preferences();
* prefs.setLevel(logging.Type.BROWSER, logging.Level.DEBUG);
*
* var caps = Capabilities.chrome();
* caps.setLoggingPrefs(prefs);
* // ...
*
* Remote log entries, also represented by the {@link Entry} class, may be
* retrieved via {@link webdriver.WebDriver.Logs}:
*
* driver.manage().logs().get(logging.Type.BROWSER)
* .then(function(entries) {
* entries.forEach(function(entry) {
* console.log('[%s] %s', entry.level.name, entry.message);
* });
* });
*
* **NOTE:** Only a few browsers support the remote logging API (notably
* Firefox and Chrome). Firefox supports basic logging functionality, while
* Chrome exposes robust
* [performance logging](https://chromedriver.chromium.org/logging)
* options. Remote logging is still considered a non-standard feature, and the
* APIs exposed by this module for it are non-frozen. This module will be
* updated, possibly breaking backwards-compatibility, once logging is
* officially defined by the
* [W3C WebDriver spec](http://www.w3.org/TR/webdriver/).
*/
/**
* Defines a message level that may be used to control logging output.
*
* @final
*/
class Level {
/**
* @param {string} name the level's name.
* @param {number} level the level's numeric value.
*/
constructor(name, level) {
if (level < 0) {
throw new TypeError('Level must be >= 0')
}
/** @private {string} */
this.name_ = name
/** @private {number} */
this.value_ = level
}
/** This logger's name. */
get name() {
return this.name_
}
/** The numeric log level. */
get value() {
return this.value_
}
/** @override */
toString() {
return this.name
}
}
/**
* Indicates no log messages should be recorded.
* @const
*/
Level.OFF = new Level('OFF', Infinity)
/**
* Log messages with a level of `1000` or higher.
* @const
*/
Level.SEVERE = new Level('SEVERE', 1000)
/**
* Log messages with a level of `900` or higher.
* @const
*/
Level.WARNING = new Level('WARNING', 900)
/**
* Log messages with a level of `800` or higher.
* @const
*/
Level.INFO = new Level('INFO', 800)
/**
* Log messages with a level of `700` or higher.
* @const
*/
Level.DEBUG = new Level('DEBUG', 700)
/**
* Log messages with a level of `500` or higher.
* @const
*/
Level.FINE = new Level('FINE', 500)
/**
* Log messages with a level of `400` or higher.
* @const
*/
Level.FINER = new Level('FINER', 400)
/**
* Log messages with a level of `300` or higher.
* @const
*/
Level.FINEST = new Level('FINEST', 300)
/**
* Indicates all log messages should be recorded.
* @const
*/
Level.ALL = new Level('ALL', 0)
const ALL_LEVELS = /** !Set<Level> */ new Set([
Level.OFF,
Level.SEVERE,
Level.WARNING,
Level.INFO,
Level.DEBUG,
Level.FINE,
Level.FINER,
Level.FINEST,
Level.ALL,
])
const LEVELS_BY_NAME = /** !Map<string, !Level> */ new Map([
[Level.OFF.name, Level.OFF],
[Level.SEVERE.name, Level.SEVERE],
[Level.WARNING.name, Level.WARNING],
[Level.INFO.name, Level.INFO],
[Level.DEBUG.name, Level.DEBUG],
[Level.FINE.name, Level.FINE],
[Level.FINER.name, Level.FINER],
[Level.FINEST.name, Level.FINEST],
[Level.ALL.name, Level.ALL],
])
/**
* Converts a level name or value to a {@link Level} value. If the name/value
* is not recognized, {@link Level.ALL} will be returned.
*
* @param {(number|string)} nameOrValue The log level name, or value, to
* convert.
* @return {!Level} The converted level.
*/
function getLevel(nameOrValue) {
if (typeof nameOrValue === 'string') {
return LEVELS_BY_NAME.get(nameOrValue) || Level.ALL
}
if (typeof nameOrValue !== 'number') {
throw new TypeError('not a string or number')
}
for (let level of ALL_LEVELS) {
if (nameOrValue >= level.value) {
return level
}
}
return Level.ALL
}
/**
* Describes a single log entry.
*
* @final
*/
class Entry {
/**
* @param {(!Level|string|number)} level The entry level.
* @param {string} message The log message.
* @param {number=} opt_timestamp The time this entry was generated, in
* milliseconds since 0:00:00, January 1, 1970 UTC. If omitted, the
* current time will be used.
* @param {string=} opt_type The log type, if known.
*/
constructor(level, message, opt_timestamp, opt_type) {
this.level = level instanceof Level ? level : getLevel(level)
this.message = message
this.timestamp = typeof opt_timestamp === 'number' ? opt_timestamp : Date.now()
this.type = opt_type || ''
}
/**
* @return {{level: string, message: string, timestamp: number,
* type: string}} The JSON representation of this entry.
*/
toJSON() {
return {
level: this.level.name,
message: this.message,
timestamp: this.timestamp,
type: this.type,
}
}
}
/**
* An object used to log debugging messages. Loggers use a hierarchical,
* dot-separated naming scheme. For instance, "foo" is considered the parent of
* the "foo.bar" and an ancestor of "foo.bar.baz".
*
* Each logger may be assigned a {@linkplain #setLevel log level}, which
* controls which level of messages will be reported to the
* {@linkplain #addHandler handlers} attached to this instance. If a log level
* is not explicitly set on a logger, it will inherit its parent.
*
* This class should never be directly instantiated. Instead, users should
* obtain logger references using the {@linkplain ./logging.getLogger()
* getLogger()} function.
*
* @final
*/
class Logger {
/**
* @param {string} name the name of this logger.
* @param {Level=} opt_level the initial level for this logger.
*/
constructor(name, opt_level) {
/** @private {string} */
this.name_ = name
/** @private {Level} */
this.level_ = opt_level || null
/** @private {Logger} */
this.parent_ = null
/** @private {Set<function(!Entry)>} */
this.handlers_ = null
}
/** @return {string} the name of this logger. */
getName() {
return this.name_
}
/**
* @param {Level} level the new level for this logger, or `null` if the logger
* should inherit its level from its parent logger.
*/
setLevel(level) {
this.level_ = level
}
/** @return {Level} the log level for this logger. */
getLevel() {
return this.level_
}
/**
* @return {!Level} the effective level for this logger.
*/
getEffectiveLevel() {
let logger = this
let level
do {
level = logger.level_
logger = logger.parent_
} while (logger && !level)
return level || Level.OFF
}
/**
* @param {!Level} level the level to check.
* @return {boolean} whether messages recorded at the given level are loggable
* by this instance.
*/
isLoggable(level) {
return level.value !== Level.OFF.value && level.value >= this.getEffectiveLevel().value
}
/**
* Adds a handler to this logger. The handler will be invoked for each message
* logged with this instance, or any of its descendants.
*
* @param {function(!Entry)} handler the handler to add.
*/
addHandler(handler) {
if (!this.handlers_) {
this.handlers_ = new Set()
}
this.handlers_.add(handler)
}
/**
* Removes a handler from this logger.
*
* @param {function(!Entry)} handler the handler to remove.
* @return {boolean} whether a handler was successfully removed.
*/
removeHandler(handler) {
if (!this.handlers_) {
return false
}
return this.handlers_.delete(handler)
}
/**
* Logs a message at the given level. The message may be defined as a string
* or as a function that will return the message. If a function is provided,
* it will only be invoked if this logger's
* {@linkplain #getEffectiveLevel() effective log level} includes the given
* `level`.
*
* @param {!Level} level the level at which to log the message.
* @param {(string|function(): string)} loggable the message to log, or a
* function that will return the message.
*/
log(level, loggable) {
if (!this.isLoggable(level)) {
return
}
let message = '[' + this.name_ + '] ' + (typeof loggable === 'function' ? loggable() : loggable)
let entry = new Entry(level, message, Date.now())
for (let logger = this; logger; logger = logger.parent_) {
if (logger.handlers_) {
for (let handler of logger.handlers_) {
handler(entry)
}
}
}
}
/**
* Logs a message at the {@link Level.SEVERE} log level.
* @param {(string|function(): string)} loggable the message to log, or a
* function that will return the message.
*/
severe(loggable) {
this.log(Level.SEVERE, loggable)
}
/**
* Logs a message at the {@link Level.WARNING} log level.
* @param {(string|function(): string)} loggable the message to log, or a
* function that will return the message.
*/
warning(loggable) {
this.log(Level.WARNING, loggable)
}
/**
* Logs a message at the {@link Level.INFO} log level.
* @param {(string|function(): string)} loggable the message to log, or a
* function that will return the message.
*/
info(loggable) {
this.log(Level.INFO, loggable)
}
/**
* Logs a message at the {@link Level.DEBUG} log level.
* @param {(string|function(): string)} loggable the message to log, or a
* function that will return the message.
*/
debug(loggable) {
this.log(Level.DEBUG, loggable)
}
/**
* Logs a message at the {@link Level.FINE} log level.
* @param {(string|function(): string)} loggable the message to log, or a
* function that will return the message.
*/
fine(loggable) {
this.log(Level.FINE, loggable)
}
/**
* Logs a message at the {@link Level.FINER} log level.
* @param {(string|function(): string)} loggable the message to log, or a
* function that will return the message.
*/
finer(loggable) {
this.log(Level.FINER, loggable)
}
/**
* Logs a message at the {@link Level.FINEST} log level.
* @param {(string|function(): string)} loggable the message to log, or a
* function that will return the message.
*/
finest(loggable) {
this.log(Level.FINEST, loggable)
}
}
/**
* Maintains a collection of loggers.
*
* @final
*/
class LogManager {
constructor() {
/** @private {!Map<string, !Logger>} */
this.loggers_ = new Map()
this.root_ = new Logger('', Level.OFF)
}
/**
* Retrieves a named logger, creating it in the process. This function will
* implicitly create the requested logger, and any of its parents, if they
* do not yet exist.
*
* @param {string} name the logger's name.
* @return {!Logger} the requested logger.
*/
getLogger(name) {
if (!name) {
return this.root_
}
let parent = this.root_
for (let i = name.indexOf('.'); i != -1; i = name.indexOf('.', i + 1)) {
let parentName = name.substr(0, i)
parent = this.createLogger_(parentName, parent)
}
return this.createLogger_(name, parent)
}
/**
* Creates a new logger.
*
* @param {string} name the logger's name.
* @param {!Logger} parent the logger's parent.
* @return {!Logger} the new logger.
* @private
*/
createLogger_(name, parent) {
if (this.loggers_.has(name)) {
return /** @type {!Logger} */ (this.loggers_.get(name))
}
let logger = new Logger(name, null)
logger.parent_ = parent
this.loggers_.set(name, logger)
return logger
}
}
const logManager = new LogManager()
/**
* Retrieves a named logger, creating it in the process. This function will
* implicitly create the requested logger, and any of its parents, if they
* do not yet exist.
*
* The log level will be unspecified for newly created loggers. Use
* {@link Logger#setLevel(level)} to explicitly set a level.
*
* @param {string} name the logger's name.
* @return {!Logger} the requested logger.
*/
function getLogger(name) {
return logManager.getLogger(name)
}
/**
* Pads a number to ensure it has a minimum of two digits.
*
* @param {number} n the number to be padded.
* @return {string} the padded number.
*/
function pad(n) {
if (n >= 10) {
return '' + n
} else {
return '0' + n
}
}
/**
* Logs all messages to the Console API.
* @param {!Entry} entry the entry to log.
*/
function consoleHandler(entry) {
if (typeof console === 'undefined' || !console) {
return
}
var timestamp = new Date(entry.timestamp)
var msg =
'[' +
timestamp.getUTCFullYear() +
'-' +
pad(timestamp.getUTCMonth() + 1) +
'-' +
pad(timestamp.getUTCDate()) +
'T' +
pad(timestamp.getUTCHours()) +
':' +
pad(timestamp.getUTCMinutes()) +
':' +
pad(timestamp.getUTCSeconds()) +
'Z] ' +
'[' +
entry.level.name +
'] ' +
entry.message
var level = entry.level.value
if (level >= Level.SEVERE.value) {
console.error(msg)
} else if (level >= Level.WARNING.value) {
console.warn(msg)
} else {
console.log(msg)
}
}
/**
* Adds the console handler to the given logger. The console handler will log
* all messages using the JavaScript Console API.
*
* @param {Logger=} opt_logger The logger to add the handler to; defaults
* to the root logger.
*/
function addConsoleHandler(opt_logger) {
let logger = opt_logger || logManager.root_
logger.addHandler(consoleHandler)
}
/**
* Removes the console log handler from the given logger.
*
* @param {Logger=} opt_logger The logger to remove the handler from; defaults
* to the root logger.
* @see exports.addConsoleHandler
*/
function removeConsoleHandler(opt_logger) {
let logger = opt_logger || logManager.root_
logger.removeHandler(consoleHandler)
}
/**
* Installs the console log handler on the root logger.
*/
function installConsoleHandler() {
addConsoleHandler(logManager.root_)
}
/**
* Common log types.
* @enum {string}
*/
const Type = {
/** Logs originating from the browser. */
BROWSER: 'browser',
/** Logs from a WebDriver client. */
CLIENT: 'client',
/** Logs from a WebDriver implementation. */
DRIVER: 'driver',
/** Logs related to performance. */
PERFORMANCE: 'performance',
/** Logs from the remote server. */
SERVER: 'server',
}
/**
* Describes the log preferences for a WebDriver session.
*
* @final
*/
class Preferences {
constructor() {
/** @private {!Map<string, !Level>} */
this.prefs_ = new Map()
}
/**
* Sets the desired logging level for a particular log type.
* @param {(string|Type)} type The log type.
* @param {(!Level|string|number)} level The desired log level.
* @throws {TypeError} if `type` is not a `string`.
*/
setLevel(type, level) {
if (typeof type !== 'string') {
throw TypeError('specified log type is not a string: ' + typeof type)
}
this.prefs_.set(type, level instanceof Level ? level : getLevel(level))
}
/**
* Converts this instance to its JSON representation.
* @return {!Object<string, string>} The JSON representation of this set of
* preferences.
*/
toJSON() {
let json = {}
for (let key of this.prefs_.keys()) {
json[key] = this.prefs_.get(key).name
}
return json
}
}
// PUBLIC API
module.exports = {
Entry: Entry,
Level: Level,
LogManager: LogManager,
Logger: Logger,
Preferences: Preferences,
Type: Type,
addConsoleHandler: addConsoleHandler,
getLevel: getLevel,
getLogger: getLogger,
installConsoleHandler: installConsoleHandler,
removeConsoleHandler: removeConsoleHandler,
}

View File

@@ -0,0 +1,59 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const crypto = require('node:crypto')
class PinnedScript {
constructor(script) {
this.scriptSource_ = script
this.scriptHandle_ = crypto.randomUUID().replace(/-/gi, '')
}
get handle() {
return this.scriptHandle_
}
get source() {
return this.scriptSource_
}
get scriptId() {
return this.scriptId_
}
set scriptId(id) {
this.scriptId_ = id
}
creationScript() {
return `function __webdriver_${this.scriptHandle_}(arguments) { ${this.scriptSource_} }`
}
executionScript() {
return `return __webdriver_${this.scriptHandle_}(arguments)`
}
removalScript() {
return `__webdriver_${this.scriptHandle_} = undefined`
}
}
// PUBLIC API
module.exports = {
PinnedScript,
}

View File

@@ -0,0 +1,266 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Defines a handful of utility functions to simplify working
* with promises.
*/
'use strict'
const { isObject, isPromise } = require('./util')
/**
* Creates a promise that will be resolved at a set time in the future.
* @param {number} ms The amount of time, in milliseconds, to wait before
* resolving the promise.
* @return {!Promise<void>} The promise.
*/
function delayed(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
/**
* Wraps a function that expects a node-style callback as its final
* argument. This callback expects two arguments: an error value (which will be
* null if the call succeeded), and the success value as the second argument.
* The callback will the resolve or reject the returned promise, based on its
* arguments.
* @param {!Function} fn The function to wrap.
* @param {...?} args The arguments to apply to the function, excluding the
* final callback.
* @return {!Thenable} A promise that will be resolved with the
* result of the provided function's callback.
*/
function checkedNodeCall(fn, ...args) {
return new Promise(function (fulfill, reject) {
try {
fn(...args, function (error, value) {
error ? reject(error) : fulfill(value)
})
} catch (ex) {
reject(ex)
}
})
}
/**
* Registers a listener to invoke when a promise is resolved, regardless
* of whether the promise's value was successfully computed. This function
* is synonymous with the {@code finally} clause in a synchronous API:
*
* // Synchronous API:
* try {
* doSynchronousWork();
* } finally {
* cleanUp();
* }
*
* // Asynchronous promise API:
* doAsynchronousWork().finally(cleanUp);
*
* __Note:__ similar to the {@code finally} clause, if the registered
* callback returns a rejected promise or throws an error, it will silently
* replace the rejection error (if any) from this promise:
*
* try {
* throw Error('one');
* } finally {
* throw Error('two'); // Hides Error: one
* }
*
* let p = Promise.reject(Error('one'));
* promise.finally(p, function() {
* throw Error('two'); // Hides Error: one
* });
*
* @param {!IThenable<?>} promise The promise to add the listener to.
* @param {function(): (R|IThenable<R>)} callback The function to call when
* the promise is resolved.
* @return {!Promise<R>} A promise that will be resolved with the callback
* result.
* @template R
*/
async function thenFinally(promise, callback) {
try {
await Promise.resolve(promise)
return callback()
} catch (e) {
await callback()
throw e
}
}
/**
* Calls a function for each element in an array and inserts the result into a
* new array, which is used as the fulfillment value of the promise returned
* by this function.
*
* If the return value of the mapping function is a promise, this function
* will wait for it to be fulfilled before inserting it into the new array.
*
* If the mapping function throws or returns a rejected promise, the
* promise returned by this function will be rejected with the same reason.
* Only the first failure will be reported; all subsequent errors will be
* silently ignored.
*
* @param {!(Array<TYPE>|IThenable<!Array<TYPE>>)} array The array to iterate
* over, or a promise that will resolve to said array.
* @param {function(this: SELF, TYPE, number, !Array<TYPE>): ?} fn The
* function to call for each element in the array. This function should
* expect three arguments (the element, the index, and the array itself.
* @param {SELF=} self The object to be used as the value of 'this' within `fn`.
* @template TYPE, SELF
*/
async function map(array, fn, self = undefined) {
const v = await Promise.resolve(array)
if (!Array.isArray(v)) {
throw TypeError('not an array')
}
const arr = /** @type {!Array} */ (v)
const values = []
for (const [index, item] of arr.entries()) {
values.push(await Promise.resolve(fn.call(self, item, index, arr)))
}
return values
}
/**
* Calls a function for each element in an array, and if the function returns
* true adds the element to a new array.
*
* If the return value of the filter function is a promise, this function
* will wait for it to be fulfilled before determining whether to insert the
* element into the new array.
*
* If the filter function throws or returns a rejected promise, the promise
* returned by this function will be rejected with the same reason. Only the
* first failure will be reported; all subsequent errors will be silently
* ignored.
*
* @param {!(Array<TYPE>|IThenable<!Array<TYPE>>)} array The array to iterate
* over, or a promise that will resolve to said array.
* @param {function(this: SELF, TYPE, number, !Array<TYPE>): (
* boolean|IThenable<boolean>)} fn The function
* to call for each element in the array.
* @param {SELF=} self The object to be used as the value of 'this' within `fn`.
* @template TYPE, SELF
*/
async function filter(array, fn, self = undefined) {
const v = await Promise.resolve(array)
if (!Array.isArray(v)) {
throw TypeError('not an array')
}
const arr = /** @type {!Array} */ (v)
const values = []
for (const [index, item] of arr.entries()) {
const isConditionTrue = await Promise.resolve(fn.call(self, item, index, arr))
if (isConditionTrue) {
values.push(item)
}
}
return values
}
/**
* Returns a promise that will be resolved with the input value in a
* fully-resolved state. If the value is an array, each element will be fully
* resolved. Likewise, if the value is an object, all keys will be fully
* resolved. In both cases, all nested arrays and objects will also be
* fully resolved. All fields are resolved in place; the returned promise will
* resolve on {@code value} and not a copy.
*
* Warning: This function makes no checks against objects that contain
* cyclical references:
*
* var value = {};
* value['self'] = value;
* promise.fullyResolved(value); // Stack overflow.
*
* @param {*} value The value to fully resolve.
* @return {!Thenable} A promise for a fully resolved version
* of the input value.
*/
async function fullyResolved(value) {
value = await Promise.resolve(value)
if (Array.isArray(value)) {
return fullyResolveKeys(/** @type {!Array} */ (value))
}
if (isObject(value)) {
return fullyResolveKeys(/** @type {!Object} */ (value))
}
if (typeof value === 'function') {
return fullyResolveKeys(/** @type {!Object} */ (value))
}
return value
}
/**
* @param {!(Array|Object)} obj the object to resolve.
* @return {!Thenable} A promise that will be resolved with the
* input object once all of its values have been fully resolved.
*/
async function fullyResolveKeys(obj) {
const isArray = Array.isArray(obj)
const numKeys = isArray ? obj.length : Object.keys(obj).length
if (!numKeys) {
return obj
}
async function forEachProperty(obj, fn) {
for (let key in obj) {
await fn(obj[key], key)
}
}
async function forEachElement(arr, fn) {
for (let i = 0; i < arr.length; i++) {
await fn(arr[i], i)
}
}
const forEachKey = isArray ? forEachElement : forEachProperty
await forEachKey(obj, async function (partialValue, key) {
if (!Array.isArray(partialValue) && (!partialValue || typeof partialValue !== 'object')) {
return
}
obj[key] = await fullyResolved(partialValue)
})
return obj
}
// PUBLIC API
module.exports = {
checkedNodeCall,
delayed,
filter,
finally: thenFinally,
fullyResolved,
isPromise,
map,
}

View File

@@ -0,0 +1,222 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Defines functions for configuring a webdriver proxy:
*
* const proxy = require('selenium-webdriver/proxy');
* const {Capabilities} = require('selenium-webdriver');
*
* let capabilities = new Capabilities();
* capabilities.setProxy(proxy.manual({http: 'host:1234'});
*/
'use strict'
/**
* Supported {@linkplain Config proxy configuration} types.
*
* @enum {string}
*/
const Type = {
AUTODETECT: 'autodetect',
DIRECT: 'direct',
MANUAL: 'manual',
PAC: 'pac',
SYSTEM: 'system',
}
/**
* Describes how a proxy should be configured for a WebDriver session.
* @record
*/
function Config() {}
/**
* The proxy type.
* @type {Type}
*/
Config.prototype.proxyType
/**
* Describes how to configure a PAC proxy.
* @record
* @extends {Config}
*/
function PacConfig() {}
/**
* URL for the PAC file to use.
*
* @type {string}
*/
PacConfig.prototype.proxyAutoconfigUrl
/**
* Record object that defines a manual proxy configuration. Manual
* configurations can be easily created using either the
* {@link ./proxy.manual proxy.manual()} or {@link ./proxy.socks proxy.socks()}
* factory method.
*
* @record
* @extends {Config}
*/
function ManualConfig() {}
/**
* The proxy host for FTP requests.
*
* @type {(string|undefined)}
*/
ManualConfig.prototype.ftpProxy
/**
* The proxy host for HTTP requests.
*
* @type {(string|undefined)}
*/
ManualConfig.prototype.httpProxy
/**
* An array of hosts which should bypass all proxies.
*
* @type {(Array<string>|undefined)}
*/
ManualConfig.prototype.noProxy
/**
* The proxy host for HTTPS requests.
*
* @type {(string|undefined)}
*/
ManualConfig.prototype.sslProxy
/**
* Defines the host and port for the SOCKS proxy to use.
*
* @type {(number|undefined)}
*/
ManualConfig.prototype.socksProxy
/**
* Defines the SOCKS proxy version. Must be a number in the range [0, 255].
*
* @type {(number|undefined)}
*/
ManualConfig.prototype.socksVersion
// PUBLIC API
/** @const */ exports.Config = Config
/** @const */ exports.ManualConfig = ManualConfig
/** @const */ exports.PacConfig = PacConfig
/** @const */ exports.Type = Type
/**
* Configures WebDriver to bypass all browser proxies.
* @return {!Config} A new proxy configuration object.
*/
function direct() {
return { proxyType: Type.DIRECT }
}
/**
* Manually configures the browser proxy. The following options are
* supported:
*
* - `ftp`: Proxy host to use for FTP requests
* - `http`: Proxy host to use for HTTP requests
* - `https`: Proxy host to use for HTTPS requests
* - `bypass`: A list of hosts requests should directly connect to,
* bypassing any other proxies for that request. May be specified as a
* comma separated string, or a list of strings.
*
* Behavior is undefined for FTP, HTTP, and HTTPS requests if the
* corresponding key is omitted from the configuration options.
*
* @param {{ftp: (string|undefined),
* http: (string|undefined),
* https: (string|undefined),
* bypass: (Array<string>|undefined)}} options Proxy
* configuration options.
* @return {!ManualConfig} A new proxy configuration object.
*/
function manual({ ftp, http, https, bypass }) {
return {
proxyType: Type.MANUAL,
ftpProxy: ftp,
httpProxy: http,
sslProxy: https,
noProxy: bypass,
}
}
/**
* Creates a proxy configuration for a socks proxy.
*
* __Example:__
*
* const {Capabilities} = require('selenium-webdriver');
* const proxy = require('selenium-webdriver/lib/proxy');
*
* let capabilities = new Capabilities();
* capabilities.setProxy(proxy.socks('localhost:1234'));
*
* // Or, to include authentication.
* capabilities.setProxy(proxy.socks('bob:password@localhost:1234'));
*
*
* @param {string} socksProxy The proxy host, in the form `hostname:port`.
* @param {number=} socksVersion The SOCKS proxy version.
* @return {!ManualConfig} A new proxy configuration object.
* @see https://en.wikipedia.org/wiki/SOCKS
*/
function socks(socksProxy, socksVersion = undefined) {
return /** @type {!Config} */ ({
proxyType: Type.MANUAL,
socksProxy,
socksVersion,
})
}
/**
* Configures WebDriver to configure the browser proxy using the PAC file at
* the given URL.
* @param {string} proxyAutoconfigUrl URL for the PAC proxy to use.
* @return {!PacConfig} A new proxy configuration object.
*/
function pac(proxyAutoconfigUrl) {
return { proxyType: Type.PAC, proxyAutoconfigUrl }
}
/**
* Configures WebDriver to use the current system's proxy.
* @return {!Config} A new proxy configuration object.
*/
function system() {
return { proxyType: Type.SYSTEM }
}
// PUBLIC API
module.exports = {
system,
pac,
socks,
manual,
direct,
}

View File

@@ -0,0 +1,63 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
const logInspector = require('../bidi/logInspector')
class Script {
#driver
#logInspector
constructor(driver) {
this.#driver = driver
}
// This should be done in the constructor.
// But since it needs to call async methods we cannot do that in the constructor.
// We can have a separate async method that initialises the Script instance.
// However, that pattern does not allow chaining the methods as we would like the user to use it.
// Since it involves awaiting to get the instance and then another await to call the method.
// Using this allows the user to do this "await driver.script().addJavaScriptErrorHandler(callback)"
async #init() {
if (this.#logInspector !== undefined) {
return
}
this.#logInspector = await logInspector(this.#driver)
}
async addJavaScriptErrorHandler(callback) {
await this.#init()
return await this.#logInspector.onJavascriptException(callback)
}
async removeJavaScriptErrorHandler(id) {
await this.#init()
await this.#logInspector.removeCallback(id)
}
async addConsoleMessageHandler(callback) {
await this.#init()
return this.#logInspector.onConsoleEntry(callback)
}
async removeConsoleMessageHandler(id) {
await this.#init()
await this.#logInspector.removeCallback(id)
}
}
module.exports = Script

View File

@@ -0,0 +1,502 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/*
* Licensed to the Software Freedom Conservancy (SFC) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The SFC licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
'use strict'
const { By } = require('./by')
const error = require('./error')
/**
* ISelect interface makes a protocol for all kind of select elements (standard html and custom
* model)
*
* @interface
*/
// eslint-disable-next-line no-unused-vars
class ISelect {
/**
* @return {!Promise<boolean>} Whether this select element supports selecting multiple options at the same time? This
* is done by checking the value of the "multiple" attribute.
*/
isMultiple() {}
/**
* @return {!Promise<!Array<!WebElement>>} All options belonging to this select tag
*/
getOptions() {}
/**
* @return {!Promise<!Array<!WebElement>>} All selected options belonging to this select tag
*/
getAllSelectedOptions() {}
/**
* @return {!Promise<!WebElement>} The first selected option in this select tag (or the currently selected option in a
* normal select)
*/
getFirstSelectedOption() {}
/**
* Select all options that display text matching the argument. That is, when given "Bar" this
* would select an option like:
*
* &lt;option value="foo"&gt;Bar&lt;/option&gt;
*
* @param {string} text The visible text to match against
* @return {Promise<void>}
*/
selectByVisibleText(text) {} // eslint-disable-line
/**
* Select all options that have a value matching the argument. That is, when given "foo" this
* would select an option like:
*
* &lt;option value="foo"&gt;Bar&lt;/option&gt;
*
* @param {string} value The value to match against
* @return {Promise<void>}
*/
selectByValue(value) {} // eslint-disable-line
/**
* Select the option at the given index. This is done by examining the "index" attribute of an
* element, and not merely by counting.
*
* @param {Number} index The option at this index will be selected
* @return {Promise<void>}
*/
selectByIndex(index) {} // eslint-disable-line
/**
* Clear all selected entries. This is only valid when the SELECT supports multiple selections.
*
* @return {Promise<void>}
*/
deselectAll() {}
/**
* Deselect all options that display text matching the argument. That is, when given "Bar" this
* would deselect an option like:
*
* &lt;option value="foo"&gt;Bar&lt;/option&gt;
*
* @param {string} text The visible text to match against
* @return {Promise<void>}
*/
deselectByVisibleText(text) {} // eslint-disable-line
/**
* Deselect all options that have a value matching the argument. That is, when given "foo" this
* would deselect an option like:
*
* @param {string} value The value to match against
* @return {Promise<void>}
*/
deselectByValue(value) {} // eslint-disable-line
/**
* Deselect the option at the given index. This is done by examining the "index" attribute of an
* element, and not merely by counting.
*
* @param {Number} index The option at this index will be deselected
* @return {Promise<void>}
*/
deselectByIndex(index) {} // eslint-disable-line
}
/**
* @implements ISelect
*/
class Select {
/**
* Create an Select Element
* @param {WebElement} element Select WebElement.
*/
constructor(element) {
if (element === null) {
throw new Error(`Element must not be null. Please provide a valid <select> element.`)
}
this.element = element
this.element.getAttribute('tagName').then(function (tagName) {
if (tagName.toLowerCase() !== 'select') {
throw new Error(`Select only works on <select> elements`)
}
})
this.element.getAttribute('multiple').then((multiple) => {
this.multiple = multiple !== null && multiple !== 'false'
})
}
/**
*
* Select option with specified index.
*
* <example>
<select id="selectbox">
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</select>
const selectBox = await driver.findElement(By.id("selectbox"));
await selectObject.selectByIndex(1);
* </example>
*
* @param index
*/
async selectByIndex(index) {
if (index < 0) {
throw new Error('Index needs to be 0 or any other positive number')
}
let options = await this.element.findElements(By.tagName('option'))
if (options.length === 0) {
throw new Error("Select element doesn't contain any option element")
}
if (options.length - 1 < index) {
throw new Error(
`Option with index "${index}" not found. Select element only contains ${options.length - 1} option elements`,
)
}
for (let option of options) {
if ((await option.getAttribute('index')) === index.toString()) {
await this.setSelected(option)
}
}
}
/**
*
* Select option by specific value.
*
* <example>
<select id="selectbox">
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</select>
const selectBox = await driver.findElement(By.id("selectbox"));
await selectObject.selectByVisibleText("Option 2");
* </example>
*
*
* @param {string} value value of option element to be selected
*/
async selectByValue(value) {
let matched = false
let isMulti = await this.isMultiple()
let options = await this.element.findElements(By.xpath('.//option[@value = ' + escapeQuotes(value) + ']'))
for (let option of options) {
await this.setSelected(option)
if (!isMulti) {
return
}
matched = true
}
if (!matched) {
throw new Error(`Cannot locate option with value: ${value}`)
}
}
/**
*
* Select option with displayed text matching the argument.
*
* <example>
<select id="selectbox">
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</select>
const selectBox = await driver.findElement(By.id("selectbox"));
await selectObject.selectByVisibleText("Option 2");
* </example>
*
* @param {String|Number} text text of option element to get selected
*
*/
async selectByVisibleText(text) {
text = typeof text === 'number' ? text.toString() : text
const xpath = './/option[normalize-space(.) = ' + escapeQuotes(text) + ']'
const options = await this.element.findElements(By.xpath(xpath))
for (let option of options) {
await this.setSelected(option)
if (!(await this.isMultiple())) {
return
}
}
let matched = Array.isArray(options) && options.length > 0
if (!matched && text.includes(' ')) {
const subStringWithoutSpace = getLongestSubstringWithoutSpace(text)
let candidates
if ('' === subStringWithoutSpace) {
candidates = await this.element.findElements(By.tagName('option'))
} else {
const xpath = './/option[contains(., ' + escapeQuotes(subStringWithoutSpace) + ')]'
candidates = await this.element.findElements(By.xpath(xpath))
}
const trimmed = text.trim()
for (let option of candidates) {
const optionText = await option.getText()
if (trimmed === optionText.trim()) {
await this.setSelected(option)
if (!(await this.isMultiple())) {
return
}
matched = true
}
}
}
if (!matched) {
throw new Error(`Cannot locate option with text: ${text}`)
}
}
/**
* Returns a list of all options belonging to this select tag
* @returns {!Promise<!Array<!WebElement>>}
*/
async getOptions() {
return await this.element.findElements({ tagName: 'option' })
}
/**
* Returns a boolean value if the select tag is multiple
* @returns {Promise<boolean>}
*/
async isMultiple() {
return this.multiple
}
/**
* Returns a list of all selected options belonging to this select tag
*
* @returns {Promise<void>}
*/
async getAllSelectedOptions() {
const opts = await this.getOptions()
const results = []
for (let options of opts) {
if (await options.isSelected()) {
results.push(options)
}
}
return results
}
/**
* Returns first Selected Option
* @returns {Promise<Element>}
*/
async getFirstSelectedOption() {
return (await this.getAllSelectedOptions())[0]
}
/**
* Deselects all selected options
* @returns {Promise<void>}
*/
async deselectAll() {
if (!this.isMultiple()) {
throw new Error('You may only deselect all options of a multi-select')
}
const options = await this.getOptions()
for (let option of options) {
if (await option.isSelected()) {
await option.click()
}
}
}
/**
*
* @param {string|Number}text text of option to deselect
* @returns {Promise<void>}
*/
async deselectByVisibleText(text) {
if (!(await this.isMultiple())) {
throw new Error('You may only deselect options of a multi-select')
}
/**
* convert value into string
*/
text = typeof text === 'number' ? text.toString() : text
const optionElement = await this.element.findElement(
By.xpath('.//option[normalize-space(.) = ' + escapeQuotes(text) + ']'),
)
if (await optionElement.isSelected()) {
await optionElement.click()
}
}
/**
*
* @param {Number} index index of option element to deselect
* Deselect the option at the given index.
* This is done by examining the "index"
* attribute of an element, and not merely by counting.
* @returns {Promise<void>}
*/
async deselectByIndex(index) {
if (!(await this.isMultiple())) {
throw new Error('You may only deselect options of a multi-select')
}
if (index < 0) {
throw new Error('Index needs to be 0 or any other positive number')
}
let options = await this.element.findElements(By.tagName('option'))
if (options.length === 0) {
throw new Error("Select element doesn't contain any option element")
}
if (options.length - 1 < index) {
throw new Error(
`Option with index "${index}" not found. Select element only contains ${options.length - 1} option elements`,
)
}
for (let option of options) {
if ((await option.getAttribute('index')) === index.toString()) {
if (await option.isSelected()) {
await option.click()
}
}
}
}
/**
*
* @param {String} value value of an option to deselect
* @returns {Promise<void>}
*/
async deselectByValue(value) {
if (!(await this.isMultiple())) {
throw new Error('You may only deselect options of a multi-select')
}
let matched = false
let options = await this.element.findElements(By.xpath('.//option[@value = ' + escapeQuotes(value) + ']'))
if (options.length === 0) {
throw new Error(`Cannot locate option with value: ${value}`)
}
for (let option of options) {
if (await option.isSelected()) {
await option.click()
}
matched = true
}
if (!matched) {
throw new Error(`Cannot locate option with value: ${value}`)
}
}
async setSelected(option) {
if (!(await option.isSelected())) {
if (!(await option.isEnabled())) {
throw new error.UnsupportedOperationError(`You may not select a disabled option`)
}
await option.click()
}
}
}
function escapeQuotes(toEscape) {
if (toEscape.includes(`"`) && toEscape.includes(`'`)) {
const quoteIsLast = toEscape.lastIndexOf(`"`) === toEscape.length - 1
const substrings = toEscape.split(`"`)
// Remove the last element if it's an empty string
if (substrings[substrings.length - 1] === '') {
substrings.pop()
}
let result = 'concat('
for (let i = 0; i < substrings.length; i++) {
result += `"${substrings[i]}"`
result += i === substrings.length - 1 ? (quoteIsLast ? `, '"')` : `)`) : `, '"', `
}
return result
}
if (toEscape.includes('"')) {
return `'${toEscape}'`
}
// Otherwise return the quoted string
return `"${toEscape}"`
}
function getLongestSubstringWithoutSpace(text) {
let words = text.split(' ')
let longestString = ''
for (let word of words) {
if (word.length > longestString.length) {
longestString = word
}
}
return longestString
}
module.exports = { Select, escapeQuotes }

View File

@@ -0,0 +1,77 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
const { Capabilities } = require('./capabilities')
/**
* Contains information about a single WebDriver session.
*/
class Session {
/**
* @param {string} id The session ID.
* @param {!./capabilities.Capabilities} capabilities
* The session capabilities.
*/
constructor(id, capabilities) {
/** @private {string} */
this.id_ = id
/** @private {!Capabilities} */
this.caps_ =
capabilities instanceof Capabilities
? /** @type {!Capabilities} */ (capabilities)
: new Capabilities(capabilities)
}
/**
* @return {string} This session's ID.
*/
getId() {
return this.id_
}
/**
* @return {!Capabilities} This session's capabilities.
*/
getCapabilities() {
return this.caps_
}
/**
* Retrieves the value of a specific capability.
* @param {string} key The capability to retrieve.
* @return {*} The capability value.
*/
getCapability(key) {
return this.caps_.get(key)
}
/**
* Returns the JSON representation of this object, which is just the string
* session ID.
* @return {string} The JSON representation of this Session.
*/
toJSON() {
return this.getId()
}
}
// PUBLIC API
module.exports = { Session }

View File

@@ -0,0 +1,37 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
/**
* @fileoverview Defines well-known symbols used within the selenium-webdriver
* library.
*/
module.exports = {
/**
* The serialize symbol specifies a method that returns an object's serialized
* representation. If an object's serialized form is not immediately
* available, the serialize method will return a promise that will be resolved
* with the serialized form.
*
* Note that the described method is analogous to objects that define a
* `toJSON()` method, except the serialized result may be a promise, or
* another object with a promised property.
*/
serialize: Symbol('serialize'),
}

View File

@@ -0,0 +1,431 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Defines common conditions for use with
* {@link webdriver.WebDriver#wait WebDriver wait}.
*
* Sample usage:
*
* driver.get('http://www.google.com/ncr');
*
* var query = driver.wait(until.elementLocated(By.name('q')));
* query.sendKeys('webdriver\n');
*
* driver.wait(until.titleIs('webdriver - Google Search'));
*
* To define a custom condition, simply call WebDriver.wait with a function
* that will eventually return a truthy-value (neither null, undefined, false,
* 0, or the empty string):
*
* driver.wait(function() {
* return driver.getTitle().then(function(title) {
* return title === 'webdriver - Google Search';
* });
* }, 1000);
*/
'use strict'
const by = require('./by')
const error = require('./error')
const webdriver = require('./webdriver')
const Condition = webdriver.Condition
const WebElementCondition = webdriver.WebElementCondition
/**
* Creates a condition that will wait until the input driver is able to switch
* to the designated frame. The target frame may be specified as
*
* 1. a numeric index into
* [window.frames](https://developer.mozilla.org/en-US/docs/Web/API/Window.frames)
* for the currently selected frame.
* 2. a {@link ./webdriver.WebElement}, which must reference a FRAME or IFRAME
* element on the current page.
* 3. a locator which may be used to first locate a FRAME or IFRAME on the
* current page before attempting to switch to it.
*
* Upon successful resolution of this condition, the driver will be left
* focused on the new frame.
*
* @param {!(number|./webdriver.WebElement|By|
* function(!./webdriver.WebDriver): !./webdriver.WebElement)} frame
* The frame identifier.
* @return {!Condition<boolean>} A new condition.
*/
function ableToSwitchToFrame(frame) {
let condition
if (typeof frame === 'number' || frame instanceof webdriver.WebElement) {
condition = (driver) => attemptToSwitchFrames(driver, frame)
} else {
condition = function (driver) {
let locator = /** @type {!(By|Function)} */ (frame)
return driver.findElements(locator).then(function (els) {
if (els.length) {
return attemptToSwitchFrames(driver, els[0])
}
})
}
}
return new Condition('to be able to switch to frame', condition)
function attemptToSwitchFrames(driver, frame) {
return driver
.switchTo()
.frame(frame)
.then(
function () {
return true
},
function (e) {
if (!(e instanceof error.NoSuchFrameError)) {
throw e
}
},
)
}
}
/**
* Creates a condition that waits for an alert to be opened. Upon success, the
* returned promise will be fulfilled with the handle for the opened alert.
*
* @return {!Condition<!./webdriver.Alert>} The new condition.
*/
function alertIsPresent() {
return new Condition('for alert to be present', function (driver) {
return driver
.switchTo()
.alert()
.catch(function (e) {
if (
!(
e instanceof error.NoSuchAlertError ||
// XXX: Workaround for GeckoDriver error `TypeError: can't convert null
// to object`. For more details, see
// https://github.com/SeleniumHQ/selenium/pull/2137
(e instanceof error.WebDriverError && e.message === `can't convert null to object`)
)
) {
throw e
}
})
})
}
/**
* Creates a condition that will wait for the current page's title to match the
* given value.
*
* @param {string} title The expected page title.
* @return {!Condition<boolean>} The new condition.
*/
function titleIs(title) {
return new Condition('for title to be ' + JSON.stringify(title), function (driver) {
return driver.getTitle().then(function (t) {
return t === title
})
})
}
/**
* Creates a condition that will wait for the current page's title to contain
* the given substring.
*
* @param {string} substr The substring that should be present in the page
* title.
* @return {!Condition<boolean>} The new condition.
*/
function titleContains(substr) {
return new Condition('for title to contain ' + JSON.stringify(substr), function (driver) {
return driver.getTitle().then(function (title) {
return title.indexOf(substr) !== -1
})
})
}
/**
* Creates a condition that will wait for the current page's title to match the
* given regular expression.
*
* @param {!RegExp} regex The regular expression to test against.
* @return {!Condition<boolean>} The new condition.
*/
function titleMatches(regex) {
return new Condition('for title to match ' + regex, function (driver) {
return driver.getTitle().then(function (title) {
return regex.test(title)
})
})
}
/**
* Creates a condition that will wait for the current page's url to match the
* given value.
*
* @param {string} url The expected page url.
* @return {!Condition<boolean>} The new condition.
*/
function urlIs(url) {
return new Condition('for URL to be ' + JSON.stringify(url), function (driver) {
return driver.getCurrentUrl().then(function (u) {
return u === url
})
})
}
/**
* Creates a condition that will wait for the current page's url to contain
* the given substring.
*
* @param {string} substrUrl The substring that should be present in the current
* URL.
* @return {!Condition<boolean>} The new condition.
*/
function urlContains(substrUrl) {
return new Condition('for URL to contain ' + JSON.stringify(substrUrl), function (driver) {
return driver.getCurrentUrl().then(function (url) {
return url && url.includes(substrUrl)
})
})
}
/**
* Creates a condition that will wait for the current page's url to match the
* given regular expression.
*
* @param {!RegExp} regex The regular expression to test against.
* @return {!Condition<boolean>} The new condition.
*/
function urlMatches(regex) {
return new Condition('for URL to match ' + regex, function (driver) {
return driver.getCurrentUrl().then(function (url) {
return regex.test(url)
})
})
}
/**
* Creates a condition that will loop until an element is
* {@link ./webdriver.WebDriver#findElement found} with the given locator.
*
* @param {!(By|Function)} locator The locator to use.
* @return {!WebElementCondition} The new condition.
*/
function elementLocated(locator) {
locator = by.checkedLocator(locator)
let locatorStr = typeof locator === 'function' ? 'by function()' : locator + ''
return new WebElementCondition('for element to be located ' + locatorStr, function (driver) {
return driver.findElements(locator).then(function (elements) {
return elements[0]
})
})
}
/**
* Creates a condition that will loop until at least one element is
* {@link ./webdriver.WebDriver#findElement found} with the given locator.
*
* @param {!(By|Function)} locator The locator to use.
* @return {!Condition<!Array<!./webdriver.WebElement>>} The new
* condition.
*/
function elementsLocated(locator) {
locator = by.checkedLocator(locator)
let locatorStr = typeof locator === 'function' ? 'by function()' : locator + ''
return new Condition('for at least one element to be located ' + locatorStr, function (driver) {
return driver.findElements(locator).then(function (elements) {
return elements.length > 0 ? elements : null
})
})
}
/**
* Creates a condition that will wait for the given element to become stale. An
* element is considered stale once it is removed from the DOM, or a new page
* has loaded.
*
* @param {!./webdriver.WebElement} element The element that should become stale.
* @return {!Condition<boolean>} The new condition.
*/
function stalenessOf(element) {
return new Condition('element to become stale', function () {
return element.getTagName().then(
function () {
return false
},
function (e) {
if (e instanceof error.StaleElementReferenceError) {
return true
}
throw e
},
)
})
}
/**
* Creates a condition that will wait for the given element to become visible.
*
* @param {!./webdriver.WebElement} element The element to test.
* @return {!WebElementCondition} The new condition.
* @see ./webdriver.WebDriver#isDisplayed
*/
function elementIsVisible(element) {
return new WebElementCondition('until element is visible', function () {
return element.isDisplayed().then((v) => (v ? element : null))
})
}
/**
* Creates a condition that will wait for the given element to be in the DOM,
* yet not visible to the user.
*
* @param {!./webdriver.WebElement} element The element to test.
* @return {!WebElementCondition} The new condition.
* @see ./webdriver.WebDriver#isDisplayed
*/
function elementIsNotVisible(element) {
return new WebElementCondition('until element is not visible', function () {
return element.isDisplayed().then((v) => (v ? null : element))
})
}
/**
* Creates a condition that will wait for the given element to be enabled.
*
* @param {!./webdriver.WebElement} element The element to test.
* @return {!WebElementCondition} The new condition.
* @see webdriver.WebDriver#isEnabled
*/
function elementIsEnabled(element) {
return new WebElementCondition('until element is enabled', function () {
return element.isEnabled().then((v) => (v ? element : null))
})
}
/**
* Creates a condition that will wait for the given element to be disabled.
*
* @param {!./webdriver.WebElement} element The element to test.
* @return {!WebElementCondition} The new condition.
* @see webdriver.WebDriver#isEnabled
*/
function elementIsDisabled(element) {
return new WebElementCondition('until element is disabled', function () {
return element.isEnabled().then((v) => (v ? null : element))
})
}
/**
* Creates a condition that will wait for the given element to be selected.
* @param {!./webdriver.WebElement} element The element to test.
* @return {!WebElementCondition} The new condition.
* @see webdriver.WebDriver#isSelected
*/
function elementIsSelected(element) {
return new WebElementCondition('until element is selected', function () {
return element.isSelected().then((v) => (v ? element : null))
})
}
/**
* Creates a condition that will wait for the given element to be deselected.
*
* @param {!./webdriver.WebElement} element The element to test.
* @return {!WebElementCondition} The new condition.
* @see webdriver.WebDriver#isSelected
*/
function elementIsNotSelected(element) {
return new WebElementCondition('until element is not selected', function () {
return element.isSelected().then((v) => (v ? null : element))
})
}
/**
* Creates a condition that will wait for the given element's
* {@link webdriver.WebDriver#getText visible text} to match the given
* {@code text} exactly.
*
* @param {!./webdriver.WebElement} element The element to test.
* @param {string} text The expected text.
* @return {!WebElementCondition} The new condition.
* @see webdriver.WebDriver#getText
*/
function elementTextIs(element, text) {
return new WebElementCondition('until element text is', function () {
return element.getText().then((t) => (t === text ? element : null))
})
}
/**
* Creates a condition that will wait for the given element's
* {@link webdriver.WebDriver#getText visible text} to contain the given
* substring.
*
* @param {!./webdriver.WebElement} element The element to test.
* @param {string} substr The substring to search for.
* @return {!WebElementCondition} The new condition.
* @see webdriver.WebDriver#getText
*/
function elementTextContains(element, substr) {
return new WebElementCondition('until element text contains', function () {
return element.getText().then((t) => (t.indexOf(substr) != -1 ? element : null))
})
}
/**
* Creates a condition that will wait for the given element's
* {@link webdriver.WebDriver#getText visible text} to match a regular
* expression.
*
* @param {!./webdriver.WebElement} element The element to test.
* @param {!RegExp} regex The regular expression to test against.
* @return {!WebElementCondition} The new condition.
* @see webdriver.WebDriver#getText
*/
function elementTextMatches(element, regex) {
return new WebElementCondition('until element text matches', function () {
return element.getText().then((t) => (regex.test(t) ? element : null))
})
}
// PUBLIC API
module.exports = {
elementTextMatches,
elementTextContains,
elementTextIs,
elementIsNotSelected,
elementIsSelected,
elementIsDisabled,
ableToSwitchToFrame,
elementIsEnabled,
elementIsNotVisible,
elementIsVisible,
stalenessOf,
elementsLocated,
elementLocated,
urlMatches,
urlContains,
urlIs,
titleMatches,
titleContains,
alertIsPresent,
titleIs,
}

View File

@@ -0,0 +1,50 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
/**
* Determines whether a {@code value} should be treated as an object.
* @param {?} value The value to test.
* @returns {boolean} Whether the value is an object.
*/
function isObject(value) {
return Object.prototype.toString.call(value) === '[object Object]'
}
/**
* Determines whether a {@code value} should be treated as a promise.
* Any object whose "then" property is a function will be considered a promise.
*
* @param {?} value The value to test.
* @return {boolean} Whether the value is a promise.
*/
function isPromise(value) {
try {
// Use array notation so the Closure compiler does not obfuscate away our
// contract.
return (typeof value === 'object' || typeof value === 'function') && typeof value['then'] === 'function'
/*eslint no-unused-vars: "off"*/
} catch (ex) {
return false
}
}
module.exports = {
isObject,
isPromise,
}

View File

@@ -0,0 +1,236 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
/**
* Protocol for virtual authenticators
* @enum {string}
*/
const Protocol = {
CTAP2: 'ctap2',
U2F: 'ctap1/u2f',
}
/**
* AuthenticatorTransport values
* @enum {string}
*/
const Transport = {
BLE: 'ble',
USB: 'usb',
NFC: 'nfc',
INTERNAL: 'internal',
}
/**
* Options for the creation of virtual authenticators.
* @see http://w3c.github.io/webauthn/#sctn-automation
*/
class VirtualAuthenticatorOptions {
/**
* Constructor to initialise VirtualAuthenticatorOptions object.
*/
constructor() {
this._protocol = Protocol['CTAP2']
this._transport = Transport['USB']
this._hasResidentKey = false
this._hasUserVerification = false
this._isUserConsenting = true
this._isUserVerified = false
}
getProtocol() {
return this._protocol
}
setProtocol(protocol) {
this._protocol = protocol
}
getTransport() {
return this._transport
}
setTransport(transport) {
this._transport = transport
}
getHasResidentKey() {
return this._hasResidentKey
}
setHasResidentKey(value) {
this._hasResidentKey = value
}
getHasUserVerification() {
return this._hasUserVerification
}
setHasUserVerification(value) {
this._hasUserVerification = value
}
getIsUserConsenting() {
return this._isUserConsenting
}
setIsUserConsenting(value) {
this._isUserConsenting = value
}
getIsUserVerified() {
return this._isUserVerified
}
setIsUserVerified(value) {
this._isUserVerified = value
}
toDict() {
return {
protocol: this.getProtocol(),
transport: this.getTransport(),
hasResidentKey: this.getHasResidentKey(),
hasUserVerification: this.getHasUserVerification(),
isUserConsenting: this.getIsUserConsenting(),
isUserVerified: this.getIsUserVerified(),
}
}
}
/**
* A credential stored in a virtual authenticator.
* @see https://w3c.github.io/webauthn/#credential-parameters
*/
class Credential {
constructor(credentialId, isResidentCredential, rpId, userHandle, privateKey, signCount) {
this._id = credentialId
this._isResidentCredential = isResidentCredential
this._rpId = rpId
this._userHandle = userHandle
this._privateKey = privateKey
this._signCount = signCount
}
static createResidentCredential(id, rpId, userHandle, privateKey, signCount) {
return new Credential(id, true, rpId, userHandle, privateKey, signCount)
}
static createNonResidentCredential(id, rpId, privateKey, signCount) {
return new Credential(id, false, rpId, null, privateKey, signCount)
}
id() {
return this._id
}
isResidentCredential() {
return this._isResidentCredential
}
rpId() {
return this._rpId
}
userHandle() {
if (this._userHandle != null) {
return this._userHandle
}
return null
}
privateKey() {
return this._privateKey
}
signCount() {
return this._signCount
}
/**
* Creates a resident (i.e. stateless) credential.
* @param id Unique base64 encoded string.
* @param rpId Relying party identifier.
* @param userHandle userHandle associated to the credential. Must be Base64 encoded string.
* @param privateKey Base64 encoded PKCS
* @param signCount initial value for a signature counter.
* @deprecated This method has been made static. Call it with class name. Example, Credential.createResidentCredential()
* @returns A resident credential
*/
createResidentCredential(id, rpId, userHandle, privateKey, signCount) {
return new Credential(id, true, rpId, userHandle, privateKey, signCount)
}
/**
* Creates a non-resident (i.e. stateless) credential.
* @param id Unique base64 encoded string.
* @param rpId Relying party identifier.
* @param privateKey Base64 encoded PKCS
* @param signCount initial value for a signature counter.
* @deprecated This method has been made static. Call it with class name. Example, Credential.createNonResidentCredential()
* @returns A non-resident credential
*/
createNonResidentCredential(id, rpId, privateKey, signCount) {
return new Credential(id, false, rpId, null, privateKey, signCount)
}
toDict() {
let credentialData = {
credentialId: Buffer.from(this._id).toString('base64url'),
isResidentCredential: this._isResidentCredential,
rpId: this._rpId,
privateKey: Buffer.from(this._privateKey, 'binary').toString('base64url'),
signCount: this._signCount,
}
if (this.userHandle() != null) {
credentialData['userHandle'] = Buffer.from(this._userHandle).toString('base64url')
}
return credentialData
}
/**
* Creates a credential from a map.
*/
fromDict(data) {
let id = new Uint8Array(Buffer.from(data['credentialId'], 'base64url'))
let isResidentCredential = data['isResidentCredential']
let rpId = data['rpId']
let privateKey = Buffer.from(data['privateKey'], 'base64url').toString('binary')
let signCount = data['signCount']
let userHandle
if ('userHandle' in data) {
userHandle = new Uint8Array(Buffer.from(data['userHandle'], 'base64url'))
} else {
userHandle = null
}
return new Credential(id, isResidentCredential, rpId, userHandle, privateKey, signCount)
}
}
// PUBLIC API
module.exports = {
Credential,
VirtualAuthenticatorOptions,
Transport,
Protocol,
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
const { isObject } = require('./util')
/**
* @fileoverview Defines some common methods used for WebElements.
*/
const LEGACY_ELEMENT_ID_KEY = 'ELEMENT'
const ELEMENT_ID_KEY = 'element-6066-11e4-a52e-4f735466cecf'
/**
* Contains logic about WebElements.
*/
/**
* @param {?} obj the object to test.
* @return {boolean} whether the object is a valid encoded WebElement ID.
*/
function isId(obj) {
return isObject(obj) && (typeof obj[ELEMENT_ID_KEY] === 'string' || typeof obj[LEGACY_ELEMENT_ID_KEY] === 'string')
}
/**
* Extracts the encoded WebElement ID from the object.
*
* @param {?} obj The object to extract the ID from.
* @return {string} the extracted ID.
* @throws {TypeError} if the object is not a valid encoded ID.
*/
function extractId(obj) {
if (isObject(obj)) {
if (typeof obj[ELEMENT_ID_KEY] === 'string') {
return obj[ELEMENT_ID_KEY]
} else if (typeof obj[LEGACY_ELEMENT_ID_KEY] === 'string') {
return obj[LEGACY_ELEMENT_ID_KEY]
}
}
throw new TypeError('object is not a WebElement ID')
}
// PUBLIC API
module.exports = {
isId,
extractId,
}

View File

@@ -0,0 +1,114 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
const os = require('node:os')
function getLoInterface() {
let name
if (process.platform === 'darwin') {
name = 'lo0'
} else if (process.platform === 'linux') {
name = 'lo'
}
return name ? os.networkInterfaces()[name] : null
}
/**
* Queries the system network interfaces for an IP address.
* @param {boolean} loopback Whether to find a loopback address.
* @param {string} family The IP family (IPv4 or IPv6). Defaults to IPv4.
* @return {(string|undefined)} The located IP address or undefined.
*/
function getIPAddress(loopback, family) {
let interfaces
if (loopback) {
const lo = getLoInterface()
interfaces = lo ? [lo] : null
}
interfaces = interfaces || os.networkInterfaces()
for (let key in interfaces) {
if (!Object.prototype.hasOwnProperty.call(interfaces, key)) {
continue
}
for (let ipAddress of interfaces[key]) {
if ((ipAddress.family === family || `IPv${ipAddress.family}` === family) && ipAddress.internal === loopback) {
return ipAddress.address
}
}
}
return undefined
}
// PUBLIC API
/**
* Retrieves the external IP address for this host.
* @param {string=} family The IP family to retrieve. Defaults to "IPv4".
* @return {(string|undefined)} The IP address or undefined if not available.
*/
function getAddress(family = 'IPv4') {
return getIPAddress(false, family)
}
/**
* Retrieves a loopback address for this machine.
* @param {string=} family The IP family to retrieve. Defaults to "IPv4".
* @return {(string|undefined)} The IP address or undefined if not available.
*/
function getLoopbackAddress(family = 'IPv4') {
return getIPAddress(true, family)
}
/**
* Splits a hostport string, e.g. "www.example.com:80", into its component
* parts.
*
* @param {string} hostport The string to split.
* @return {{host: string, port: ?number}} A host and port. If no port is
* present in the argument `hostport`, port is null.
*/
function splitHostAndPort(hostport) {
let lastIndex = hostport.lastIndexOf(':')
if (lastIndex < 0) {
return { host: hostport, port: null }
}
let firstIndex = hostport.indexOf(':')
if (firstIndex != lastIndex && !hostport.includes('[')) {
// Multiple colons but no brackets, so assume the string is an IPv6 address
// with no port (e.g. "1234:5678:9:0:1234:5678:9:0").
return { host: hostport, port: null }
}
let host = hostport.slice(0, lastIndex)
if (host.startsWith('[') && host.endsWith(']')) {
host = host.slice(1, -1)
}
let port = parseInt(hostport.slice(lastIndex + 1), 10)
return { host, port }
}
// PUBLIC API
module.exports = {
splitHostAndPort,
getLoopbackAddress,
getAddress,
}

View File

@@ -0,0 +1,76 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
const net = require('node:net')
/**
* Tests if a port is free.
* @param {number} port The port to test.
* @param {string=} opt_host The bound host to test the {@code port} against.
* Defaults to {@code INADDR_ANY}.
* @return {!Promise<boolean>} A promise that will resolve with whether the port
* is free.
*/
function isFree(port, opt_host) {
return new Promise((resolve, reject) => {
const server = net.createServer().on('error', function (e) {
if (e.code === 'EADDRINUSE' || e.code === 'EACCES') {
resolve(false)
} else {
reject(e)
}
})
server.listen(port, opt_host, function () {
server.close(() => resolve(true))
})
})
}
/**
* @param {string=} opt_host The bound host to test the {@code port} against.
* Defaults to {@code INADDR_ANY}.
* @return {!Promise<number>} A promise that will resolve to a free port. If a
* port cannot be found, the promise will be rejected.
*/
function findFreePort(opt_host) {
return new Promise((resolve, reject) => {
const server = net.createServer()
server.on('listening', function () {
resolve(server.address().port)
server.close()
})
server.on('error', (e) => {
if (e.code === 'EADDRINUSE' || e.code === 'EACCES') {
resolve('Unable to find a free port')
} else {
reject(e)
}
})
// By providing 0 we let the operative system find an arbitrary port
server.listen(0, opt_host)
})
}
// PUBLIC API
module.exports = {
findFreePort,
isFree,
}

View File

@@ -0,0 +1,64 @@
{
"name": "selenium-webdriver",
"version": "4.22.0",
"description": "The official WebDriver JavaScript bindings from the Selenium project",
"license": "Apache-2.0",
"keywords": [
"automation",
"selenium",
"testing",
"webdriver",
"webdriverjs"
],
"homepage": "https://github.com/SeleniumHQ/selenium/tree/trunk/javascript/node/selenium-webdriver#readme",
"bugs": {
"url": "https://github.com/SeleniumHQ/selenium/issues"
},
"main": "./index",
"repository": {
"type": "git",
"url": "https://github.com/SeleniumHQ/selenium.git"
},
"engines": {
"node": ">= 14.21.0"
},
"dependencies": {
"jszip": "^3.10.1",
"tmp": "^0.2.3",
"ws": ">=8.16.0"
},
"devDependencies": {
"@bazel/runfiles": "^5.8.1",
"@eslint/js": "^9.1.1",
"clean-jsdoc-theme": "^4.3.0",
"eslint": "^9.1.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-mocha": "^10.4.3",
"eslint-plugin-n": "^17.2.1",
"eslint-plugin-no-only-tests": "^3.1.0",
"eslint-plugin-prettier": "^5.1.3",
"express": "^4.19.2",
"globals": "^15.0.0",
"jsdoc": "^4.0.3",
"mocha": "^10.4.0",
"mocha-junit-reporter": "^2.2.1",
"multer": "^1.4.5-lts.1",
"prettier": "^3.2.5",
"serve-index": "^1.9.1",
"sinon": "^17.0.1"
},
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "npm run lint && mocha -t 600000 --recursive test",
"test-jasmine": "bazel test //javascript/node/selenium-webdriver:tests",
"generate-docs": "jsdoc --configure jsdoc_conf.json --verbose"
},
"mocha": {
"recursive": true,
"timeout": 600000
},
"publishConfig": {
"registry": "https://registry.npmjs.org/"
}
}

View File

@@ -0,0 +1,32 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Proxy module alias.
*
* var webdriver = require('selenium-webdriver'),
* proxy = require('selenium-webdriver/proxy');
*
* var driver = new webdriver.Builder()
* .withCapabilities(webdriver.Capabilities.chrome())
* .setProxy(proxy.manual({http: 'host:1234'}))
* .build();
*/
'use strict'
module.exports = require('./lib/proxy')

View File

@@ -0,0 +1,621 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
const url = require('node:url')
const httpUtil = require('../http/util')
const io = require('../io')
const { exec } = require('../io/exec')
const { Zip } = require('../io/zip')
const cmd = require('../lib/command')
const input = require('../lib/input')
const net = require('../net')
const portprober = require('../net/portprober')
const logging = require('../lib/logging')
const { getJavaPath, formatSpawnArgs } = require('./util')
/**
* @typedef {(string|!Array<string|number|!stream.Stream|null|undefined>)}
*/
let StdIoOptions // eslint-disable-line
/**
* @typedef {(string|!IThenable<string>)}
*/
let CommandLineFlag // eslint-disable-line
/**
* A record object that defines the configuration options for a DriverService
* instance.
*
* @record
*/
function ServiceOptions() {}
/**
* Whether the service should only be accessed on this host's loopback address.
*
* @type {(boolean|undefined)}
*/
ServiceOptions.prototype.loopback
/**
* The host name to access the server on. If this option is specified, the
* {@link #loopback} option will be ignored.
*
* @type {(string|undefined)}
*/
ServiceOptions.prototype.hostname
/**
* The port to start the server on (must be > 0). If the port is provided as a
* promise, the service will wait for the promise to resolve before starting.
*
* @type {(number|!IThenable<number>)}
*/
ServiceOptions.prototype.port
/**
* The arguments to pass to the service. If a promise is provided, the service
* will wait for it to resolve before starting.
*
* @type {!(Array<CommandLineFlag>|IThenable<!Array<CommandLineFlag>>)}
*/
ServiceOptions.prototype.args
/**
* The base path on the server for the WebDriver wire protocol (e.g. '/wd/hub').
* Defaults to '/'.
*
* @type {(string|undefined|null)}
*/
ServiceOptions.prototype.path
/**
* The environment variables that should be visible to the server process.
* Defaults to inheriting the current process's environment.
*
* @type {(Object<string, string>|undefined)}
*/
ServiceOptions.prototype.env
/**
* IO configuration for the spawned server process. For more information, refer
* to the documentation of `child_process.spawn`.
*
* @type {(StdIoOptions|undefined)}
* @see https://nodejs.org/dist/latest-v4.x/docs/api/child_process.html#child_process_options_stdio
*/
ServiceOptions.prototype.stdio
/**
* Manages the life and death of a native executable WebDriver server.
*
* It is expected that the driver server implements the
* https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol.
* Furthermore, the managed server should support multiple concurrent sessions,
* so that this class may be reused for multiple clients.
*/
class DriverService {
/**
* @param {string} executable Path to the executable to run.
* @param {!ServiceOptions} options Configuration options for the service.
*/
constructor(executable, options) {
/** @private @const */
this.log_ = logging.getLogger(`${logging.Type.DRIVER}.DriverService`)
/** @private {string} */
this.executable_ = executable
/** @private {boolean} */
this.loopbackOnly_ = !!options.loopback
/** @private {(string|undefined)} */
this.hostname_ = options.hostname
/** @private {(number|!IThenable<number>)} */
this.port_ = options.port
/**
* @private {!(Array<CommandLineFlag>|
* IThenable<!Array<CommandLineFlag>>)}
*/
this.args_ = options.args
/** @private {string} */
this.path_ = options.path || '/'
/** @private {!Object<string, string>} */
this.env_ = options.env || process.env
/**
* @private {(string|!Array<string|number|!stream.Stream|null|undefined>)}
*/
this.stdio_ = options.stdio || 'ignore'
/**
* A promise for the managed subprocess, or null if the server has not been
* started yet. This promise will never be rejected.
* @private {Promise<!exec.Command>}
*/
this.command_ = null
/**
* Promise that resolves to the server's address or null if the server has
* not been started. This promise will be rejected if the server terminates
* before it starts accepting WebDriver requests.
* @private {Promise<string>}
*/
this.address_ = null
}
getExecutable() {
return this.executable_
}
setExecutable(value) {
this.executable_ = value
}
/**
* @return {!Promise<string>} A promise that resolves to the server's address.
* @throws {Error} If the server has not been started.
*/
address() {
if (this.address_) {
return this.address_
}
throw Error('Server has not been started.')
}
/**
* Returns whether the underlying process is still running. This does not take
* into account whether the process is in the process of shutting down.
* @return {boolean} Whether the underlying service process is running.
*/
isRunning() {
return !!this.address_
}
/**
* Starts the server if it is not already running.
* @param {number=} opt_timeoutMs How long to wait, in milliseconds, for the
* server to start accepting requests. Defaults to 30 seconds.
* @return {!Promise<string>} A promise that will resolve to the server's base
* URL when it has started accepting requests. If the timeout expires
* before the server has started, the promise will be rejected.
*/
start(opt_timeoutMs) {
if (this.address_) {
return this.address_
}
const timeout = opt_timeoutMs || DriverService.DEFAULT_START_TIMEOUT_MS
const self = this
let resolveCommand
this.command_ = new Promise((resolve) => (resolveCommand = resolve))
this.address_ = new Promise((resolveAddress, rejectAddress) => {
resolveAddress(
Promise.resolve(this.port_).then((port) => {
if (port <= 0) {
throw Error('Port must be > 0: ' + port)
}
return resolveCommandLineFlags(this.args_).then((args) => {
const command = exec(self.executable_, {
args: args,
env: self.env_,
stdio: self.stdio_,
})
resolveCommand(command)
const earlyTermination = command.result().then(function (result) {
const error =
result.code == null
? Error('Server was killed with ' + result.signal)
: Error('Server terminated early with status ' + result.code)
rejectAddress(error)
self.address_ = null
self.command_ = null
throw error
})
let hostname = self.hostname_
if (!hostname) {
hostname = (!self.loopbackOnly_ && net.getAddress()) || net.getLoopbackAddress()
}
const serverUrl = url.format({
protocol: 'http',
hostname: hostname,
port: port + '',
pathname: self.path_,
})
return new Promise((fulfill, reject) => {
let cancelToken = earlyTermination.catch((e) => reject(Error(e.message)))
httpUtil.waitForServer(serverUrl, timeout, cancelToken).then(
(_) => fulfill(serverUrl),
(err) => {
if (err instanceof httpUtil.CancellationError) {
fulfill(serverUrl)
} else {
reject(err)
}
},
)
})
})
}),
)
})
return this.address_
}
/**
* Stops the service if it is not currently running. This function will kill
* the server immediately. To synchronize with the active control flow, use
* {@link #stop()}.
* @return {!Promise} A promise that will be resolved when the server has been
* stopped.
*/
kill() {
if (!this.address_ || !this.command_) {
return Promise.resolve() // Not currently running.
}
let cmd = this.command_
this.address_ = null
this.command_ = null
return cmd.then((c) => c.kill('SIGTERM'))
}
}
/**
* @param {!(Array<CommandLineFlag>|IThenable<!Array<CommandLineFlag>>)} args
* @return {!Promise<!Array<string>>}
*/
function resolveCommandLineFlags(args) {
// Resolve the outer array, then the individual flags.
return Promise.resolve(args).then(/** !Array<CommandLineFlag> */ (args) => Promise.all(args))
}
/**
* The default amount of time, in milliseconds, to wait for the server to
* start.
* @const {number}
*/
DriverService.DEFAULT_START_TIMEOUT_MS = 30 * 1000
/**
* Creates {@link DriverService} objects that manage a WebDriver server in a
* child process.
*/
DriverService.Builder = class {
/**
* @param {string} exe Path to the executable to use. This executable must
* accept the `--port` flag for defining the port to start the server on.
* @throws {Error} If the provided executable path does not exist.
*/
constructor(exe) {
/** @private @const {string} */
this.exe_ = exe
/** @private {!ServiceOptions} */
this.options_ = {
args: [],
port: 0,
env: null,
stdio: 'ignore',
}
}
/**
* Define additional command line arguments to use when starting the server.
*
* @param {...CommandLineFlag} var_args The arguments to include.
* @return {!THIS} A self reference.
* @this {THIS}
* @template THIS
*/
addArguments(...arguments_) {
this.options_.args = this.options_.args.concat(arguments_)
return this
}
/**
* Sets the host name to access the server on. If specified, the
* {@linkplain #setLoopback() loopback} setting will be ignored.
*
* @param {string} hostname
* @return {!DriverService.Builder} A self reference.
*/
setHostname(hostname) {
this.options_.hostname = hostname
return this
}
/**
* Sets whether the service should be accessed at this host's loopback
* address.
*
* @param {boolean} loopback
* @return {!DriverService.Builder} A self reference.
*/
setLoopback(loopback) {
this.options_.loopback = loopback
return this
}
/**
* Sets the base path for WebDriver REST commands (e.g. "/wd/hub").
* By default, the driver will accept commands relative to "/".
*
* @param {?string} basePath The base path to use, or `null` to use the
* default.
* @return {!DriverService.Builder} A self reference.
*/
setPath(basePath) {
this.options_.path = basePath
return this
}
/**
* Sets the port to start the server on.
*
* @param {number} port The port to use, or 0 for any free port.
* @return {!DriverService.Builder} A self reference.
* @throws {Error} If an invalid port is specified.
*/
setPort(port) {
if (port < 0) {
throw Error(`port must be >= 0: ${port}`)
}
this.options_.port = port
return this
}
/**
* Defines the environment to start the server under. This setting will be
* inherited by every browser session started by the server. By default, the
* server will inherit the enviroment of the current process.
*
* @param {(Map<string, string>|Object<string, string>|null)} env The desired
* environment to use, or `null` if the server should inherit the
* current environment.
* @return {!DriverService.Builder} A self reference.
*/
setEnvironment(env) {
if (env instanceof Map) {
let tmp = {}
env.forEach((value, key) => (tmp[key] = value))
env = tmp
}
this.options_.env = env
return this
}
/**
* IO configuration for the spawned server process. For more information,
* refer to the documentation of `child_process.spawn`.
*
* @param {StdIoOptions} config The desired IO configuration.
* @return {!DriverService.Builder} A self reference.
* @see https://nodejs.org/dist/latest-v4.x/docs/api/child_process.html#child_process_options_stdio
*/
setStdio(config) {
this.options_.stdio = config
return this
}
/**
* Creates a new DriverService using this instance's current configuration.
*
* @return {!DriverService} A new driver service.
*/
build() {
let port = this.options_.port || portprober.findFreePort()
let args = Promise.resolve(port).then((port) => {
return this.options_.args.concat('--port=' + port)
})
let options =
/** @type {!ServiceOptions} */
(Object.assign({}, this.options_, { args, port }))
return new DriverService(this.exe_, options)
}
}
/**
* Manages the life and death of the
* <a href="https://www.selenium.dev/downloads/">
* standalone Selenium server</a>.
*/
class SeleniumServer extends DriverService {
/**
* @param {string} jar Path to the Selenium server jar.
* @param {SeleniumServer.Options=} opt_options Configuration options for the
* server.
* @throws {Error} If the path to the Selenium jar is not specified or if an
* invalid port is specified.
*/
constructor(jar, opt_options) {
if (!jar) {
throw Error('Path to the Selenium jar not specified')
}
const options = opt_options || {}
if (options.port < 0) {
throw Error('Port must be >= 0: ' + options.port)
}
let port = options.port || portprober.findFreePort()
let args = Promise.all([port, options.jvmArgs || [], options.args || []]).then((resolved) => {
let port = resolved[0]
let jvmArgs = resolved[1]
let args = resolved[2]
const fullArgsList = jvmArgs.concat('-jar', jar, '-port', port).concat(args)
return formatSpawnArgs(jar, fullArgsList)
})
const java = getJavaPath()
super(java, {
loopback: options.loopback,
port: port,
args: args,
path: '/wd/hub',
env: options.env,
stdio: options.stdio,
})
}
}
/**
* A record object describing configuration options for a {@link SeleniumServer}
* instance.
*
* @record
*/
SeleniumServer.Options = class {
constructor() {
/**
* Whether the server should only be accessible on this host's loopback
* address.
*
* @type {(boolean|undefined)}
*/
this.loopback
/**
* The port to start the server on (must be > 0). If the port is provided as
* a promise, the service will wait for the promise to resolve before
* starting.
*
* @type {(number|!IThenable<number>)}
*/
this.port
/**
* The arguments to pass to the service. If a promise is provided,
* the service will wait for it to resolve before starting.
*
* @type {!(Array<string>|IThenable<!Array<string>>)}
*/
this.args
/**
* The arguments to pass to the JVM. If a promise is provided,
* the service will wait for it to resolve before starting.
*
* @type {(!Array<string>|!IThenable<!Array<string>>|undefined)}
*/
this.jvmArgs
/**
* The environment variables that should be visible to the server
* process. Defaults to inheriting the current process's environment.
*
* @type {(!Object<string, string>|undefined)}
*/
this.env
/**
* IO configuration for the spawned server process. If unspecified, IO will
* be ignored.
*
* @type {(string|!Array<string|number|!stream.Stream|null|undefined>|
* undefined)}
* @see <https://nodejs.org/dist/latest-v8.x/docs/api/child_process.html#child_process_options_stdio>
*/
this.stdio
}
}
/**
* A {@link webdriver.FileDetector} that may be used when running
* against a remote
* [Selenium server](https://www.selenium.dev/downloads/).
*
* When a file path on the local machine running this script is entered with
* {@link webdriver.WebElement#sendKeys WebElement#sendKeys}, this file detector
* will transfer the specified file to the Selenium server's host; the sendKeys
* command will be updated to use the transferred file's path.
*
* __Note:__ This class depends on a non-standard command supported on the
* Java Selenium server. The file detector will fail if used with a server that
* only supports standard WebDriver commands (such as the ChromeDriver).
*
* @final
*/
class FileDetector extends input.FileDetector {
/**
* Prepares a `file` for use with the remote browser. If the provided path
* does not reference a normal file (i.e. it does not exist or is a
* directory), then the promise returned by this method will be resolved with
* the original file path. Otherwise, this method will upload the file to the
* remote server, which will return the file's path on the remote system so
* it may be referenced in subsequent commands.
*
* @override
*/
handleFile(driver, file) {
return io.stat(file).then(
function (stats) {
if (stats.isDirectory()) {
return file // Not a valid file, return original input.
}
let zip = new Zip()
return zip
.addFile(file)
.then(() => zip.toBuffer())
.then((buf) => buf.toString('base64'))
.then((encodedZip) => {
let command = new cmd.Command(cmd.Name.UPLOAD_FILE).setParameter('file', encodedZip)
return driver.execute(command)
})
},
function (err) {
if (err.code === 'ENOENT') {
return file // Not a file; return original input.
}
throw err
},
)
}
}
// PUBLIC API
module.exports = {
DriverService,
FileDetector,
SeleniumServer,
// Exported for API docs.
ServiceOptions,
}

View File

@@ -0,0 +1,87 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
const path = require('node:path')
const cp = require('node:child_process')
const logging = require('../lib/logging')
/**
* returns path to java or 'java' string if JAVA_HOME does not exist in env obj
* @returns {string}
*/
function getJavaPath() {
return process.env['JAVA_HOME'] ? path.join(process.env['JAVA_HOME'], 'bin/java') : 'java'
}
/**
* @param {string} seleniumStandalonePath path to standalone server
* @returns {boolean}
*/
function isSelenium3x(seleniumStandalonePath) {
const javaPath = getJavaPath()
const execRes = cp.execFileSync(javaPath, ['-jar', seleniumStandalonePath, '--version'])
return execRes.toString().trim().startsWith('Selenium server version: 3')
}
/**
* @param {string} seleniumStandalonePath path to standalone server
* @param {Array.<string>} args spawn arguments array
* returns formatted args based on selenium standalone server version
* @returns {Array.<string>}
*/
function formatSpawnArgs(seleniumStandalonePath, args) {
if (isSelenium3x(seleniumStandalonePath)) {
logging
.getLogger(logging.Type.SERVER)
.warning('Deprecation: Support for Standalone Server 3.x will be removed soon. Please update to version 4.x')
return args
}
const standaloneArg = 'standalone'
const port3xArgFormat = '-port'
const port4xArgFormat = '--port'
let formattedArgs = Array.from(args)
const standaloneArgIndex = formattedArgs.findIndex((arg) => arg === seleniumStandalonePath)
const v3portArgFormat = formattedArgs.findIndex((arg) => arg === port3xArgFormat)
// old v3x port arg format was -port, new v4x port arg format is --port
if (v3portArgFormat !== -1) {
formattedArgs[v3portArgFormat] = port4xArgFormat
}
// 'standalone' arg should be right after jar file path
// in case if it is already in place - returns args
if (formattedArgs[standaloneArgIndex + 1] === standaloneArg) return formattedArgs
// insert 'standalone' right after jar file path
formattedArgs.splice(standaloneArgIndex + 1, 0, standaloneArg)
return formattedArgs
}
// PUBLIC API
module.exports = {
getJavaPath,
isSelenium3x,
formatSpawnArgs,
}

View File

@@ -0,0 +1,140 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Defines a WebDriver client for Safari.
*
* @module selenium-webdriver/safari
*/
'use strict'
const http = require('./http')
const remote = require('./remote')
const webdriver = require('./lib/webdriver')
const { Browser, Capabilities } = require('./lib/capabilities')
const { getBinaryPaths } = require('./common/driverFinder')
/**
* Creates {@link remote.DriverService} instances that manage
* a [safaridriver] server in a child process.
*
* [safaridriver]: https://developer.apple.com/library/prerelease/content/releasenotes/General/WhatsNewInSafari/Articles/Safari_10_0.html#//apple_ref/doc/uid/TP40014305-CH11-DontLinkElementID_28
*/
class ServiceBuilder extends remote.DriverService.Builder {
/**
* @param {string=} opt_exe Path to the server executable to use. If omitted,
* the builder will attempt to locate the safaridriver on the system PATH.
*/
constructor(opt_exe) {
super(opt_exe)
this.setLoopback(true) // Required.
}
}
const OPTIONS_CAPABILITY_KEY = 'safari:options'
const TECHNOLOGY_PREVIEW_OPTIONS_KEY = 'technologyPreview'
/**
* Configuration options specific to the {@link Driver SafariDriver}.
*/
class Options extends Capabilities {
/**
* @param {(Capabilities|Map<string, ?>|Object)=} other Another set of
* capabilities to initialize this instance from.
*/
constructor(other = undefined) {
super(other)
/** @private {!Object} */
this.options_ = this.get(OPTIONS_CAPABILITY_KEY) || {}
this.set(OPTIONS_CAPABILITY_KEY, this.options_)
this.setBrowserName(Browser.SAFARI)
}
/**
* Instruct the SafariDriver to use the Safari Technology Preview if true.
* Otherwise, use the release version of Safari. Defaults to using the release version of Safari.
*
* @param {boolean} useTechnologyPreview
* @return {!Options} A self reference.
*/
setTechnologyPreview(useTechnologyPreview) {
this.options_[TECHNOLOGY_PREVIEW_OPTIONS_KEY] = !!useTechnologyPreview
return this
}
}
/**
* @param {(Capabilities|Object<string, *>)=} o The options object
* @return {boolean}
*/
function useTechnologyPreview(o) {
if (o instanceof Capabilities) {
let options = o.get(OPTIONS_CAPABILITY_KEY)
return !!(options && options[TECHNOLOGY_PREVIEW_OPTIONS_KEY])
}
if (o && typeof o === 'object') {
return !!o[TECHNOLOGY_PREVIEW_OPTIONS_KEY]
}
return false
}
const SAFARIDRIVER_TECHNOLOGY_PREVIEW_EXE = '/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver'
/**
* A WebDriver client for Safari. This class should never be instantiated
* directly; instead, use the {@linkplain ./builder.Builder Builder}:
*
* var driver = new Builder()
* .forBrowser('safari')
* .build();
*
*/
class Driver extends webdriver.WebDriver {
/**
* Creates a new Safari session.
*
* @param {(Options|Capabilities)=} options The configuration options.
* @return {!Driver} A new driver instance.
*/
static createSession(options) {
let caps = options || new Options()
let exe
if (useTechnologyPreview(caps.get(OPTIONS_CAPABILITY_KEY))) {
exe = SAFARIDRIVER_TECHNOLOGY_PREVIEW_EXE
}
let service = new ServiceBuilder(exe).build()
if (!service.getExecutable()) {
service.setExecutable(getBinaryPaths(caps).driverPath)
}
let executor = new http.Executor(service.start().then((url) => new http.HttpClient(url)))
return /** @type {!Driver} */ (super.createSession(executor, caps, () => service.kill()))
}
}
// Public API
exports.Driver = Driver
exports.Options = Options
exports.ServiceBuilder = ServiceBuilder

View File

@@ -0,0 +1,516 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Provides extensions for
* [Jasmine](https://jasmine.github.io) and [Mocha](https://mochajs.org).
*
* You may conditionally suppress a test function using the exported
* "ignore" function. If the provided predicate returns true, the attached
* test case will be skipped:
*
* test.ignore(maybe()).it('is flaky', function() {
* if (Math.random() < 0.5) throw Error();
* });
*
* function maybe() { return Math.random() < 0.5; }
*/
'use strict'
const { isatty } = require('node:tty')
const chrome = require('../chrome')
const edge = require('../edge')
const firefox = require('../firefox')
const ie = require('../ie')
const remote = require('../remote')
const safari = require('../safari')
const { Browser } = require('../lib/capabilities')
const { Builder } = require('../index')
const { getBinaryPaths } = require('../common/driverFinder')
/**
* Describes a browser targeted by a {@linkplain suite test suite}.
* @record
*/
function TargetBrowser() {}
/**
* The {@linkplain Browser name} of the targeted browser.
* @type {string}
*/
TargetBrowser.prototype.name
/**
* The specific version of the targeted browser, if any.
* @type {(string|undefined)}
*/
TargetBrowser.prototype.version
/**
* The specific {@linkplain ../lib/capabilities.Platform platform} for the
* targeted browser, if any.
* @type {(string|undefined)}.
*/
TargetBrowser.prototype.platform
/** @suppress {checkTypes} */
function color(c, s) {
return isatty(process.stdout) ? `\u001b[${c}m${s}\u001b[0m` : s
}
function green(s) {
return color(32, s)
}
function cyan(s) {
return color(36, s)
}
function info(msg) {
console.info(`${green('[INFO]')} ${msg}`)
}
function warn(msg) {
console.warn(`${cyan('[WARNING]')} ${msg}`)
}
/**
* Extracts the browsers for a test suite to target from the `SELENIUM_BROWSER`
* environment variable.
*
* @return {{name: string, version: string, platform: string}}[] the browsers to target.
*/
function getBrowsersToTestFromEnv() {
let browsers = process.env['SELENIUM_BROWSER']
if (!browsers) {
return []
}
return browsers.split(',').map((spec) => {
const parts = spec.split(/:/, 3)
let name = parts[0]
if (name === 'ie') {
name = Browser.INTERNET_EXPLORER
} else if (name === 'edge') {
name = Browser.EDGE
}
let version = parts[1]
let platform = parts[2]
return { name, version, platform }
})
}
/**
* @return {!Array<!TargetBrowser>} the browsers available for testing on this
* system.
*/
function getAvailableBrowsers() {
info(`Searching for WebDriver executables installed on the current system...`)
let targets = [
[getBinaryPaths(new chrome.Options()), Browser.CHROME],
[getBinaryPaths(new edge.Options()), Browser.EDGE],
[getBinaryPaths(new firefox.Options()), Browser.FIREFOX],
]
if (process.platform === 'win32') {
targets.push([getBinaryPaths(new ie.Options()), Browser.INTERNET_EXPLORER])
}
if (process.platform === 'darwin') {
targets.push([getBinaryPaths(new safari.Options()), Browser.SAFARI])
}
let availableBrowsers = []
for (let pair of targets) {
const driverPath = pair[0].driverPath
const browserPath = pair[0].browserPath
const name = pair[1]
const capabilities = pair[2]
if (driverPath.length > 0 && browserPath && browserPath.length > 0) {
info(`... located ${name}`)
availableBrowsers.push({ name, capabilities })
}
}
if (availableBrowsers.length === 0) {
warn(`Unable to locate any WebDriver executables for testing`)
}
return availableBrowsers
}
let wasInit
let targetBrowsers
let seleniumJar
let seleniumUrl
let seleniumServer
/**
* Initializes this module by determining which browsers a
* {@linkplain ./index.suite test suite} should run against. The default
* behavior is to run tests against every browser with a WebDriver executables
* (chromedriver, firefoxdriver, etc.) are installed on the system by `PATH`.
*
* Specific browsers can be selected at runtime by setting the
* `SELENIUM_BROWSER` environment variable. This environment variable has the
* same semantics as with the WebDriver {@link ../index.Builder Builder},
* except you may use a comma-delimited list to run against multiple browsers:
*
* SELENIUM_BROWSER=chrome,firefox mocha --recursive tests/
*
* The `SELENIUM_REMOTE_URL` environment variable may be set to configure tests
* to run against an externally managed (usually remote) Selenium server. When
* set, the WebDriver builder provided by each
* {@linkplain TestEnvironment#builder TestEnvironment} will automatically be
* configured to use this server instead of starting a browser driver locally.
*
* The `SELENIUM_SERVER_JAR` environment variable may be set to the path of a
* standalone Selenium server on the local machine that should be used for
* WebDriver sessions. When set, the WebDriver builder provided by each
* {@linkplain TestEnvironment} will automatically be configured to use the
* started server instead of using a browser driver directly. It should only be
* necessary to set the `SELENIUM_SERVER_JAR` when testing locally against
* browsers not natively supported by the WebDriver
* {@link ../index.Builder Builder}.
*
* When either of the `SELENIUM_REMOTE_URL` or `SELENIUM_SERVER_JAR` environment
* variables are set, the `SELENIUM_BROWSER` variable must also be set.
*
* @param {boolean=} force whether to force this module to re-initialize and
* scan `process.env` again to determine which browsers to run tests
* against.
*/
function init(force = false) {
if (wasInit && !force) {
return
}
wasInit = true
// If force re-init, kill the current server if there is one.
if (seleniumServer) {
seleniumServer.kill()
seleniumServer = null
}
seleniumJar = process.env['SELENIUM_SERVER_JAR']
seleniumUrl = process.env['SELENIUM_REMOTE_URL']
if (seleniumJar) {
info(`Using Selenium server jar: ${seleniumJar}`)
}
if (seleniumUrl) {
info(`Using Selenium remote end: ${seleniumUrl}`)
}
if (seleniumJar && seleniumUrl) {
throw Error(
'Ambiguous test configuration: both SELENIUM_REMOTE_URL' +
' && SELENIUM_SERVER_JAR environment variables are set',
)
}
const envBrowsers = getBrowsersToTestFromEnv()
if ((seleniumJar || seleniumUrl) && envBrowsers.length === 0) {
throw Error(
'Ambiguous test configuration: when either the SELENIUM_REMOTE_URL or' +
' SELENIUM_SERVER_JAR environment variable is set, the' +
' SELENIUM_BROWSER variable must also be set.',
)
}
targetBrowsers = envBrowsers.length > 0 ? envBrowsers : getAvailableBrowsers()
info(`Running tests against [${targetBrowsers.map((b) => b.name).join(', ')}]`)
after(function () {
if (seleniumServer) {
return seleniumServer.kill()
}
})
}
const TARGET_MAP = /** !WeakMap<!Environment, !TargetBrowser> */ new WeakMap()
const URL_MAP = /** !WeakMap<!Environment, ?(string|remote.SeleniumServer)> */ new WeakMap()
/**
* Defines the environment a {@linkplain suite test suite} is running against.
* @final
*/
class Environment {
/**
* @param {!TargetBrowser} browser the browser targetted in this environment.
* @param {?(string|remote.SeleniumServer)=} url remote URL of an existing
* Selenium server to test against.
*/
constructor(browser, url = undefined) {
browser = /** @type {!TargetBrowser} */ (Object.seal(Object.assign({}, browser)))
TARGET_MAP.set(this, browser)
URL_MAP.set(this, url || null)
}
/** @return {!TargetBrowser} the target browser for this test environment. */
get browser() {
return TARGET_MAP.get(this)
}
/**
* Returns a predicate function that will suppress tests in this environment
* if the {@linkplain #browser current browser} is in the list of
* `browsersToIgnore`.
*
* @param {...(string|!Browser)} browsersToIgnore the browsers that should
* be ignored.
* @return {function(): boolean} a new predicate function.
*/
browsers(...browsersToIgnore) {
return () => browsersToIgnore.indexOf(this.browser.name) !== -1
}
/**
* @return {!Builder} a new WebDriver builder configured to target this
* environment's {@linkplain #browser browser}.
*/
builder() {
const browser = this.browser
const urlOrServer = URL_MAP.get(this)
const builder = new Builder()
builder.disableEnvironmentOverrides()
const realBuild = builder.build
builder.build = function () {
builder.forBrowser(browser.name, browser.version, browser.platform)
if (browser.capabilities) {
builder.getCapabilities().merge(browser.capabilities)
}
if (browser.name === 'firefox') {
builder.setCapability('moz:debuggerAddress', true)
}
// Enable BiDi for supporting browsers.
if (browser.name === Browser.FIREFOX || browser.name === Browser.CHROME || browser.name === Browser.EDGE) {
builder.setCapability('webSocketUrl', true)
}
if (typeof urlOrServer === 'string') {
builder.usingServer(urlOrServer)
} else if (urlOrServer) {
builder.usingServer(urlOrServer.address())
}
return realBuild.call(builder)
}
return builder
}
}
/**
* Configuration options for a {@linkplain ./index.suite test suite}.
* @record
*/
function SuiteOptions() {}
/**
* The browsers to run the test suite against.
* @type {!Array<!(Browser|TargetBrowser)>}
*/
SuiteOptions.prototype.browsers
let inSuite = false
/**
* Defines a test suite by calling the provided function once for each of the
* target browsers. If a suite is not limited to a specific set of browsers in
* the provided {@linkplain ./index.SuiteOptions suite options}, the suite will
* be configured to run against each of the {@linkplain ./index.init runtime
* target browsers}.
*
* Sample usage:
*
* const {By, Key, until} = require('selenium-webdriver');
* const {suite} = require('selenium-webdriver/testing');
*
* suite(function(env) {
* describe('Google Search', function() {
* let driver;
*
* before(async function() {
* driver = await env.builder().build();
* });
*
* after(() => driver.quit());
*
* it('demo', async function() {
* await driver.get('http://www.google.com/ncr');
*
* let q = await driver.findElement(By.name('q'));
* await q.sendKeys('webdriver', Key.RETURN);
* await driver.wait(
* until.titleIs('webdriver - Google Search'), 1000);
* });
* });
* });
*
* By default, this example suite will run against every WebDriver-enabled
* browser on the current system. Alternatively, the `SELENIUM_BROWSER`
* environment variable may be used to run against a specific browser:
*
* SELENIUM_BROWSER=firefox mocha -t 120000 example_test.js
*
* @param {function(!Environment)} fn the function to call to build the test
* suite.
* @param {SuiteOptions=} options configuration options.
*/
function suite(fn, options = undefined) {
if (inSuite) {
throw Error('Calls to suite() may not be nested')
}
try {
init()
inSuite = true
const suiteBrowsers = new Map()
if (options && options.browsers) {
for (let browser of options.browsers) {
if (typeof browser === 'string') {
suiteBrowsers.set(browser, { name: browser })
} else {
suiteBrowsers.set(browser.name, browser)
}
}
}
for (let browser of targetBrowsers) {
if (suiteBrowsers.size > 0 && !suiteBrowsers.has(browser.name)) {
continue
}
describe(`[${browser.name}]`, function () {
if (!seleniumUrl && seleniumJar && !seleniumServer) {
seleniumServer = new remote.SeleniumServer(seleniumJar)
const startTimeout = 65 * 1000
function startSelenium() {
if (typeof this.timeout === 'function') {
this.timeout(startTimeout) // For mocha.
}
info(`Starting selenium server ${seleniumJar}`)
return seleniumServer.start(60 * 1000)
}
const /** !Function */ beforeHook = global.beforeAll || global.before
beforeHook(startSelenium, startTimeout)
}
fn(new Environment(browser, seleniumUrl || seleniumServer))
})
}
} finally {
inSuite = false
}
}
/**
* Returns an object with wrappers for the standard mocha/jasmine test
* functions: `describe` and `it`, which will redirect to `xdescribe` and `xit`,
* respectively, if provided predicate function returns false.
*
* Sample usage:
*
* const {Browser} = require('selenium-webdriver');
* const {suite, ignore} = require('selenium-webdriver/testing');
*
* suite(function(env) {
*
* // Skip tests the current environment targets Chrome.
* ignore(env.browsers(Browser.CHROME)).
* describe('something', async function() {
* let driver = await env.builder().build();
* // etc.
* });
* });
*
* @param {function(): boolean} predicateFn A predicate to call to determine
* if the test should be suppressed. This function MUST be synchronous.
* @return {{describe: !Function, it: !Function}} an object with wrapped
* versions of the `describe` and `it` test functions.
*/
function ignore(predicateFn) {
const isJasmine = global.jasmine && typeof global.jasmine === 'object'
const hooks = {
describe: getTestHook('describe'),
xdescribe: getTestHook('xdescribe'),
it: getTestHook('it'),
xit: getTestHook('xit'),
}
hooks.fdescribe = isJasmine ? getTestHook('fdescribe') : hooks.describe.only
hooks.fit = isJasmine ? getTestHook('fit') : hooks.it.only
let describe = wrap(hooks.xdescribe, hooks.describe)
let fdescribe = wrap(hooks.xdescribe, hooks.fdescribe)
//eslint-disable-next-line no-only-tests/no-only-tests
describe.only = fdescribe
let it = wrap(hooks.xit, hooks.it)
let fit = wrap(hooks.xit, hooks.fit)
//eslint-disable-next-line no-only-tests/no-only-tests
it.only = fit
return { describe, it }
function wrap(onSkip, onRun) {
return function (...args) {
if (predicateFn()) {
onSkip(...args)
} else {
onRun(...args)
}
}
}
}
/**
* @param {string} name
* @return {!Function}
* @throws {TypeError}
*/
function getTestHook(name) {
let fn = global[name]
let type = typeof fn
if (type !== 'function') {
throw TypeError(
`Expected global.${name} to be a function, but is ${type}.` +
' This can happen if you try using this module when running with' +
' node directly instead of using jasmine or mocha',
)
}
return fn
}
// PUBLIC API
module.exports = {
Environment,
SuiteOptions,
init,
ignore,
suite,
}