Merge branch 'main' into screenshot
This commit is contained in:
commit
4630f20f69
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
|
|
@ -24,6 +24,7 @@ body:
|
||||||
## Make a minimal reproduction
|
## Make a minimal reproduction
|
||||||
To file the report, you will need a GitHub repository with a minimal (but complete) example and simple/clear steps on how to reproduce the bug.
|
To file the report, you will need a GitHub repository with a minimal (but complete) example and simple/clear steps on how to reproduce the bug.
|
||||||
The simpler you can make it, the more likely we are to successfully verify and fix the bug. You can create a new project with `npm init playwright@latest new-project` and then add the test code there.
|
The simpler you can make it, the more likely we are to successfully verify and fix the bug. You can create a new project with `npm init playwright@latest new-project` and then add the test code there.
|
||||||
|
Please make sure you only include the code and the dependencies absolutely necessary for your repro. Due to the security considerations, we can only run the code we trust. Major web frameworks are Ok to use, but smaller convenience libraries are not.
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
|
|
|
||||||
2
.github/workflows/tests_service.yml
vendored
2
.github/workflows/tests_service.yml
vendored
|
|
@ -53,7 +53,7 @@ jobs:
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- name: Download blob report artifact
|
- name: Download blob report artifact
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: all-blob-reports
|
name: all-blob-reports
|
||||||
path: all-blob-reports
|
path: all-blob-reports
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -34,3 +34,4 @@ test-results
|
||||||
/tests/installation/.registry.json
|
/tests/installation/.registry.json
|
||||||
.cache/
|
.cache/
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
playwright.env
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# 🎭 Playwright
|
# 🎭 Playwright
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||||
|
|
||||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||||
|
|
||||||
|
|
@ -8,9 +8,9 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
||||||
|
|
||||||
| | Linux | macOS | Windows |
|
| | Linux | macOS | Windows |
|
||||||
| :--- | :---: | :---: | :---: |
|
| :--- | :---: | :---: | :---: |
|
||||||
| Chromium <!-- GEN:chromium-version -->128.0.6613.36<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| Chromium <!-- GEN:chromium-version -->129.0.6668.29<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
| WebKit <!-- GEN:webkit-version -->18.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| WebKit <!-- GEN:webkit-version -->18.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
| Firefox <!-- GEN:firefox-version -->129.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| Firefox <!-- GEN:firefox-version -->130.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
|
|
||||||
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.
|
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.
|
||||||
|
|
||||||
|
|
|
||||||
17
SUPPORT.md
Normal file
17
SUPPORT.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Support
|
||||||
|
|
||||||
|
## How to file issues and get help
|
||||||
|
|
||||||
|
This project uses GitHub issues to track bugs and feature requests. Please search the [existing issues][gh-issues] before filing new ones to avoid duplicates. For new issues, file your bug or feature request as a new issue using corresponding template.
|
||||||
|
|
||||||
|
For help and questions about using this project, please see the [docs site for Playwright][docs].
|
||||||
|
|
||||||
|
Join our community [Discord Server][discord-server] to connect with other developers using Playwright and ask questions in our 'help-playwright' forum.
|
||||||
|
|
||||||
|
## Microsoft Support Policy
|
||||||
|
|
||||||
|
Support for Playwright is limited to the resources listed above.
|
||||||
|
|
||||||
|
[gh-issues]: https://github.com/microsoft/playwright/issues/
|
||||||
|
[docs]: https://playwright.dev/
|
||||||
|
[discord-server]: https://aka.ms/playwright/discord
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
REMOTE_URL="https://github.com/mozilla/gecko-dev"
|
REMOTE_URL="https://github.com/mozilla/gecko-dev"
|
||||||
BASE_BRANCH="release"
|
BASE_BRANCH="release"
|
||||||
BASE_REVISION="4c9a3f8e2db68ae0a8fcf6bbf0574e3c0549ff49"
|
BASE_REVISION="cf0397e3ba298868fdca53f894da5b0d239dc09e"
|
||||||
|
|
|
||||||
|
|
@ -602,6 +602,8 @@ class NetworkObserver {
|
||||||
proxyFilter.onProxyFilterResult(defaultProxyInfo);
|
proxyFilter.onProxyFilterResult(defaultProxyInfo);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this._targetRegistry.shouldBustHTTPAuthCacheForProxy(proxy))
|
||||||
|
Services.obs.notifyObservers(null, "net:clear-active-logins");
|
||||||
proxyFilter.onProxyFilterResult(protocolProxyService.newProxyInfo(
|
proxyFilter.onProxyFilterResult(protocolProxyService.newProxyInfo(
|
||||||
proxy.type,
|
proxy.type,
|
||||||
proxy.host,
|
proxy.host,
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ class TargetRegistry {
|
||||||
this._browserToTarget = new Map();
|
this._browserToTarget = new Map();
|
||||||
this._browserIdToTarget = new Map();
|
this._browserIdToTarget = new Map();
|
||||||
|
|
||||||
|
this._proxiesWithClashingAuthCacheKeys = new Set();
|
||||||
this._browserProxy = null;
|
this._browserProxy = null;
|
||||||
|
|
||||||
// Cleanup containers from previous runs (if any)
|
// Cleanup containers from previous runs (if any)
|
||||||
|
|
@ -234,12 +235,50 @@ class TargetRegistry {
|
||||||
onOpenWindow(win);
|
onOpenWindow(win);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Firefox uses nsHttpAuthCache to cache authentication to the proxy.
|
||||||
|
// If we're provided with a single proxy with a multiple different authentications, then
|
||||||
|
// we should clear the nsHttpAuthCache on every request.
|
||||||
|
shouldBustHTTPAuthCacheForProxy(proxy) {
|
||||||
|
return this._proxiesWithClashingAuthCacheKeys.has(proxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateProxiesWithSameAuthCacheAndDifferentCredentials() {
|
||||||
|
const proxyIdToCredentials = new Map();
|
||||||
|
const allProxies = [...this._browserContextIdToBrowserContext.values()].map(bc => bc._proxy).filter(Boolean);
|
||||||
|
if (this._browserProxy)
|
||||||
|
allProxies.push(this._browserProxy);
|
||||||
|
const proxyAuthCacheKeyAndProxy = allProxies.map(proxy => [
|
||||||
|
JSON.stringify({
|
||||||
|
type: proxy.type,
|
||||||
|
host: proxy.host,
|
||||||
|
port: proxy.port,
|
||||||
|
}),
|
||||||
|
proxy,
|
||||||
|
]);
|
||||||
|
this._proxiesWithClashingAuthCacheKeys.clear();
|
||||||
|
|
||||||
|
proxyAuthCacheKeyAndProxy.sort(([cacheKey1], [cacheKey2]) => cacheKey1 < cacheKey2 ? -1 : 1);
|
||||||
|
for (let i = 0; i < proxyAuthCacheKeyAndProxy.length - 1; ++i) {
|
||||||
|
const [cacheKey1, proxy1] = proxyAuthCacheKeyAndProxy[i];
|
||||||
|
const [cacheKey2, proxy2] = proxyAuthCacheKeyAndProxy[i + 1];
|
||||||
|
if (cacheKey1 !== cacheKey2)
|
||||||
|
continue;
|
||||||
|
if (proxy1.username === proxy2.username && proxy1.password === proxy2.password)
|
||||||
|
continue;
|
||||||
|
// `proxy1` and `proxy2` have the same caching key, but serve different credentials.
|
||||||
|
// We have to bust HTTP Auth Cache everytime there's a request that will use either of the proxies.
|
||||||
|
this._proxiesWithClashingAuthCacheKeys.add(proxy1);
|
||||||
|
this._proxiesWithClashingAuthCacheKeys.add(proxy2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async cancelDownload(options) {
|
async cancelDownload(options) {
|
||||||
this._downloadInterceptor.cancelDownload(options.uuid);
|
this._downloadInterceptor.cancelDownload(options.uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
setBrowserProxy(proxy) {
|
setBrowserProxy(proxy) {
|
||||||
this._browserProxy = proxy;
|
this._browserProxy = proxy;
|
||||||
|
this._updateProxiesWithSameAuthCacheAndDifferentCredentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
getProxyInfo(channel) {
|
getProxyInfo(channel) {
|
||||||
|
|
@ -906,12 +945,14 @@ class BrowserContext {
|
||||||
}
|
}
|
||||||
this._registry._browserContextIdToBrowserContext.delete(this.browserContextId);
|
this._registry._browserContextIdToBrowserContext.delete(this.browserContextId);
|
||||||
this._registry._userContextIdToBrowserContext.delete(this.userContextId);
|
this._registry._userContextIdToBrowserContext.delete(this.userContextId);
|
||||||
|
this._registry._updateProxiesWithSameAuthCacheAndDifferentCredentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
setProxy(proxy) {
|
setProxy(proxy) {
|
||||||
// Clear AuthCache.
|
// Clear AuthCache.
|
||||||
Services.obs.notifyObservers(null, "net:clear-active-logins");
|
Services.obs.notifyObservers(null, "net:clear-active-logins");
|
||||||
this._proxy = proxy;
|
this._proxy = proxy;
|
||||||
|
this._registry._updateProxiesWithSameAuthCacheAndDifferentCredentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
setIgnoreHTTPSErrors(ignoreHTTPSErrors) {
|
setIgnoreHTTPSErrors(ignoreHTTPSErrors) {
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ class JugglerFrameChild extends JSWindowActorChild {
|
||||||
|
|
||||||
const agents = topBrowingContextToAgents.get(this.browsingContext);
|
const agents = topBrowingContextToAgents.get(this.browsingContext);
|
||||||
// The agents are already re-bound to a new actor.
|
// The agents are already re-bound to a new actor.
|
||||||
if (agents.actor !== this)
|
if (agents?.actor !== this)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
topBrowingContextToAgents.delete(this.browsingContext);
|
topBrowingContextToAgents.delete(this.browsingContext);
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ class nsScreencastService::Session : public rtc::VideoSinkInterface<webrtc::Vide
|
||||||
capability.height = 960;
|
capability.height = 960;
|
||||||
capability.maxFPS = ScreencastEncoder::fps;
|
capability.maxFPS = ScreencastEncoder::fps;
|
||||||
capability.videoType = webrtc::VideoType::kI420;
|
capability.videoType = webrtc::VideoType::kI420;
|
||||||
int error = mCaptureModule->StartCapture(capability);
|
int error = mCaptureModule->StartCaptureCounted(capability);
|
||||||
if (error) {
|
if (error) {
|
||||||
fprintf(stderr, "StartCapture error %d\n", error);
|
fprintf(stderr, "StartCapture error %d\n", error);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -152,7 +152,7 @@ class nsScreencastService::Session : public rtc::VideoSinkInterface<webrtc::Vide
|
||||||
mCaptureModule->DeRegisterCaptureDataCallback(this);
|
mCaptureModule->DeRegisterCaptureDataCallback(this);
|
||||||
else
|
else
|
||||||
mCaptureModule->DeRegisterRawFrameCallback(this);
|
mCaptureModule->DeRegisterRawFrameCallback(this);
|
||||||
mCaptureModule->StopCapture();
|
mCaptureModule->StopCaptureCounted();
|
||||||
if (mEncoder) {
|
if (mEncoder) {
|
||||||
mEncoder->finish([this, protect = RefPtr{this}] {
|
mEncoder->finish([this, protect = RefPtr{this}] {
|
||||||
NS_DispatchToMainThread(NS_NewRunnableFunction(
|
NS_DispatchToMainThread(NS_NewRunnableFunction(
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,3 +1,3 @@
|
||||||
REMOTE_URL="https://github.com/WebKit/WebKit.git"
|
REMOTE_URL="https://github.com/WebKit/WebKit.git"
|
||||||
BASE_BRANCH="main"
|
BASE_BRANCH="main"
|
||||||
BASE_REVISION="a47deb713746fa2f228e8450a52ed0ecafc5309d"
|
BASE_REVISION="f371dbc2bb4292037ed394e2162150a16ef977fc"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -60,7 +60,7 @@ An object with all the response HTTP headers associated with this response.
|
||||||
- `name` <[string]> Name of the header.
|
- `name` <[string]> Name of the header.
|
||||||
- `value` <[string]> Value of the header.
|
- `value` <[string]> Value of the header.
|
||||||
|
|
||||||
An array with all the request HTTP headers associated with this response. Header names are not lower-cased.
|
An array with all the response HTTP headers associated with this response. Header names are not lower-cased.
|
||||||
Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times.
|
Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times.
|
||||||
|
|
||||||
## async method: APIResponse.json
|
## async method: APIResponse.json
|
||||||
|
|
|
||||||
|
|
@ -297,8 +297,10 @@ testing frameworks should explicitly create [`method: Browser.newContext`] follo
|
||||||
|
|
||||||
## async method: Browser.removeAllListeners
|
## async method: Browser.removeAllListeners
|
||||||
* since: v1.47
|
* since: v1.47
|
||||||
|
* langs: js
|
||||||
|
|
||||||
Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners.
|
Removes all the listeners of the given type (or all registered listeners if no type given).
|
||||||
|
Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners.
|
||||||
|
|
||||||
### param: Browser.removeAllListeners.type
|
### param: Browser.removeAllListeners.type
|
||||||
* since: v1.47
|
* since: v1.47
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ BrowserContexts provide a way to operate multiple independent browser sessions.
|
||||||
If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser
|
If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser
|
||||||
context.
|
context.
|
||||||
|
|
||||||
Playwright allows creating "incognito" browser contexts with [`method: Browser.newContext`] method. "Incognito" browser
|
Playwright allows creating isolated non-persistent browser contexts with [`method: Browser.newContext`] method. Non-persistent browser
|
||||||
contexts don't write any browsing data to disk.
|
contexts don't write any browsing data to disk.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|
@ -1017,8 +1017,10 @@ Returns all open pages in the context.
|
||||||
|
|
||||||
## async method: BrowserContext.removeAllListeners
|
## async method: BrowserContext.removeAllListeners
|
||||||
* since: v1.47
|
* since: v1.47
|
||||||
|
* langs: js
|
||||||
|
|
||||||
Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners.
|
Removes all the listeners of the given type (or all registered listeners if no type given).
|
||||||
|
Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners.
|
||||||
|
|
||||||
### param: BrowserContext.removeAllListeners.type
|
### param: BrowserContext.removeAllListeners.type
|
||||||
* since: v1.47
|
* since: v1.47
|
||||||
|
|
|
||||||
|
|
@ -866,7 +866,7 @@ await handle.SelectOptionAsync(new[] {
|
||||||
### option: ElementHandle.selectOption.force = %%-input-force-%%
|
### option: ElementHandle.selectOption.force = %%-input-force-%%
|
||||||
* since: v1.13
|
* since: v1.13
|
||||||
|
|
||||||
### option: ElementHandle.selectOption.noWaitAfter = %%-input-no-wait-after-%%
|
### option: ElementHandle.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
|
|
||||||
### option: ElementHandle.selectOption.timeout = %%-input-timeout-%%
|
### option: ElementHandle.selectOption.timeout = %%-input-timeout-%%
|
||||||
|
|
|
||||||
|
|
@ -1543,7 +1543,7 @@ await frame.SelectOptionAsync("select#colors", new[] { "red", "green", "blue" })
|
||||||
### option: Frame.selectOption.force = %%-input-force-%%
|
### option: Frame.selectOption.force = %%-input-force-%%
|
||||||
* since: v1.13
|
* since: v1.13
|
||||||
|
|
||||||
### option: Frame.selectOption.noWaitAfter = %%-input-no-wait-after-%%
|
### option: Frame.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
|
|
||||||
### option: Frame.selectOption.strict = %%-input-strict-%%
|
### option: Frame.selectOption.strict = %%-input-strict-%%
|
||||||
|
|
|
||||||
|
|
@ -2055,7 +2055,7 @@ await element.SelectOptionAsync(new[] { "red", "green", "blue" });
|
||||||
### option: Locator.selectOption.force = %%-input-force-%%
|
### option: Locator.selectOption.force = %%-input-force-%%
|
||||||
* since: v1.14
|
* since: v1.14
|
||||||
|
|
||||||
### option: Locator.selectOption.noWaitAfter = %%-input-no-wait-after-%%
|
### option: Locator.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
|
||||||
* since: v1.14
|
* since: v1.14
|
||||||
|
|
||||||
### option: Locator.selectOption.timeout = %%-input-timeout-%%
|
### option: Locator.selectOption.timeout = %%-input-timeout-%%
|
||||||
|
|
|
||||||
|
|
@ -3341,8 +3341,23 @@ By default, after calling the handler Playwright will wait until the overlay bec
|
||||||
|
|
||||||
## async method: Page.removeAllListeners
|
## async method: Page.removeAllListeners
|
||||||
* since: v1.47
|
* since: v1.47
|
||||||
|
* langs: js
|
||||||
|
|
||||||
Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners.
|
Removes all the listeners of the given type (or all registered listeners if no type given).
|
||||||
|
Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners.
|
||||||
|
|
||||||
|
**Usage**
|
||||||
|
|
||||||
|
```js
|
||||||
|
page.on('request', async request => {
|
||||||
|
const response = await request.response();
|
||||||
|
const body = await response.body();
|
||||||
|
console.log(body.byteLength);
|
||||||
|
});
|
||||||
|
await page.goto('https://playwright.dev', { waitUntil: 'domcontentloaded' });
|
||||||
|
// Waits for all the reported 'request' events to resolve.
|
||||||
|
await page.removeAllListeners('request', { behavior: 'wait' });
|
||||||
|
```
|
||||||
|
|
||||||
### param: Page.removeAllListeners.type
|
### param: Page.removeAllListeners.type
|
||||||
* since: v1.47
|
* since: v1.47
|
||||||
|
|
@ -3711,7 +3726,7 @@ await page.SelectOptionAsync("select#colors", new[] { "red", "green", "blue" });
|
||||||
### option: Page.selectOption.force = %%-input-force-%%
|
### option: Page.selectOption.force = %%-input-force-%%
|
||||||
* since: v1.13
|
* since: v1.13
|
||||||
|
|
||||||
### option: Page.selectOption.noWaitAfter = %%-input-no-wait-after-%%
|
### option: Page.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
|
|
||||||
### option: Page.selectOption.strict = %%-input-strict-%%
|
### option: Page.selectOption.strict = %%-input-strict-%%
|
||||||
|
|
|
||||||
|
|
@ -288,10 +288,15 @@ Returns the matching [Response] object, or `null` if the response was not receiv
|
||||||
## method: Request.serviceWorker
|
## method: Request.serviceWorker
|
||||||
* since: v1.24
|
* since: v1.24
|
||||||
* langs: js
|
* langs: js
|
||||||
* deprecated: Requests made by a Service Worker are not reported in Playwright.
|
|
||||||
- returns: <[null]|[Worker]>
|
- returns: <[null]|[Worker]>
|
||||||
|
|
||||||
This method will always return `null`.
|
The Service [Worker] that is performing the request.
|
||||||
|
|
||||||
|
**Details**
|
||||||
|
|
||||||
|
This method is Chromium only. It's safe to call when using other browsers, but it will always be `null`.
|
||||||
|
|
||||||
|
Requests originated in a Service Worker do not have a [`method: Request.frame`] available.
|
||||||
|
|
||||||
## async method: Request.sizes
|
## async method: Request.sizes
|
||||||
* since: v1.15
|
* since: v1.15
|
||||||
|
|
|
||||||
|
|
@ -769,12 +769,6 @@ Actual picture of each page will be scaled down if necessary to fit the specifie
|
||||||
|
|
||||||
Network proxy settings to use with this context. Defaults to none.
|
Network proxy settings to use with this context. Defaults to none.
|
||||||
|
|
||||||
:::note
|
|
||||||
For Chromium on Windows the browser needs to be launched with the global proxy for this option to work. If all
|
|
||||||
contexts override the proxy, global proxy will be never used and can be any string, for example
|
|
||||||
`launch({ proxy: { server: 'http://per-context' } })`.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## context-option-strict
|
## context-option-strict
|
||||||
- `strictSelectors` <[boolean]>
|
- `strictSelectors` <[boolean]>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,7 @@ Use Linux when running your tests on CI as it is cheaper. Developers can use wha
|
||||||
|
|
||||||
### Lint your tests
|
### Lint your tests
|
||||||
|
|
||||||
Linting the tests helps catching errors early. Use [`@typescript-eslint/no-floating-promises`](https://typescript-eslint.io/rules/no-floating-promises/) [ESLint](https://eslint.org) rule to make sure there are no missing awaits before the asynchronous calls to the Playwright API.
|
We recommend TypeScript and linting with ESLint for your tests to catch errors early. Use [`@typescript-eslint/no-floating-promises`](https://typescript-eslint.io/rules/no-floating-promises/) [ESLint](https://eslint.org) rule to make sure there are no missing awaits before the asynchronous calls to the Playwright API. On your CI you can run `tsc --noEmit` to ensure that functions are called with the right signature.
|
||||||
|
|
||||||
### Use parallelism and sharding
|
### Use parallelism and sharding
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -461,7 +461,7 @@ Playwright's Firefox version matches the recent [Firefox Stable](https://www.moz
|
||||||
|
|
||||||
### WebKit
|
### WebKit
|
||||||
|
|
||||||
Playwright's WebKit is derived from the latest WebKit main branch sources, often before these updates are incorporated into Apple Safari and other WebKit-based browsers. This gives a lot of lead time to react on the potential browser update issues. Playwright doesn't work with the branded version of Safari since it relies on patches. Instead, you can test using the most recent WebKit build. Note that avialability of certain features, which depend heavily on the underlying platform, may vary between operating systems.
|
Playwright's WebKit is derived from the latest WebKit main branch sources, often before these updates are incorporated into Apple Safari and other WebKit-based browsers. This gives a lot of lead time to react on the potential browser update issues. Playwright doesn't work with the branded version of Safari since it relies on patches. Instead, you can test using the most recent WebKit build. Note that availability of certain features, which depend heavily on the underlying platform, may vary between operating systems.
|
||||||
|
|
||||||
## Install behind a firewall or a proxy
|
## Install behind a firewall or a proxy
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -432,6 +432,10 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# Force a non-shallow checkout, so that we can reference $GITHUB_BASE_REF.
|
||||||
|
# See https://github.com/actions/checkout for more details.
|
||||||
|
fetch-depth: 0
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,95 @@ int status = await page.EvaluateAsync<int>(@"async () => {
|
||||||
}");
|
}");
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Different environments
|
||||||
|
|
||||||
|
Evaluated scripts run in the browser environment, while your test runs in a testing environments. This means you cannot use variables from your test in the page and vice versa. Instead, you should pass them explicitly as an argument.
|
||||||
|
|
||||||
|
The following snippet is **WRONG** because it uses the variable directly:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const data = 'some data';
|
||||||
|
const result = await page.evaluate(() => {
|
||||||
|
// WRONG: there is no "data" in the web page.
|
||||||
|
window.myApp.use(data);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
String data = "some data";
|
||||||
|
Object result = page.evaluate("() => {\n" +
|
||||||
|
" // WRONG: there is no 'data' in the web page.\n" +
|
||||||
|
" window.myApp.use(data);\n" +
|
||||||
|
"}");
|
||||||
|
```
|
||||||
|
|
||||||
|
```python async
|
||||||
|
data = "some data"
|
||||||
|
result = await page.evaluate("""() => {
|
||||||
|
// WRONG: there is no "data" in the web page.
|
||||||
|
window.myApp.use(data)
|
||||||
|
}""")
|
||||||
|
```
|
||||||
|
|
||||||
|
```python sync
|
||||||
|
data = "some data"
|
||||||
|
result = page.evaluate("""() => {
|
||||||
|
// WRONG: there is no "data" in the web page.
|
||||||
|
window.myApp.use(data)
|
||||||
|
}""")
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var data = "some data";
|
||||||
|
var result = await page.EvaluateAsync(@"() => {
|
||||||
|
// WRONG: there is no 'data' in the web page.
|
||||||
|
window.myApp.use(data);
|
||||||
|
}");
|
||||||
|
```
|
||||||
|
|
||||||
|
The following snippet is **CORRECT** because it passes the value explicitly as an argument:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const data = 'some data';
|
||||||
|
// Pass |data| as a parameter.
|
||||||
|
const result = await page.evaluate(data => {
|
||||||
|
window.myApp.use(data);
|
||||||
|
}, data);
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
String data = "some data";
|
||||||
|
// Pass |data| as a parameter.
|
||||||
|
Object result = page.evaluate("data => {\n" +
|
||||||
|
" window.myApp.use(data);\n" +
|
||||||
|
"}", data);
|
||||||
|
```
|
||||||
|
|
||||||
|
```python async
|
||||||
|
data = "some data"
|
||||||
|
# Pass |data| as a parameter.
|
||||||
|
result = await page.evaluate("""data => {
|
||||||
|
window.myApp.use(data)
|
||||||
|
}""", data)
|
||||||
|
```
|
||||||
|
|
||||||
|
```python sync
|
||||||
|
data = "some data"
|
||||||
|
# Pass |data| as a parameter.
|
||||||
|
result = page.evaluate("""data => {
|
||||||
|
window.myApp.use(data)
|
||||||
|
}""", data)
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var data = "some data";
|
||||||
|
// Pass |data| as a parameter.
|
||||||
|
var result = await page.EvaluateAsync("data => { window.myApp.use(data); }", data);
|
||||||
|
```
|
||||||
|
|
||||||
## Evaluation Argument
|
## Evaluation Argument
|
||||||
|
|
||||||
Playwright evaluation methods like [`method: Page.evaluate`] take a single optional argument. This argument can be a mix of [Serializable] values and [JSHandle] or [ElementHandle] instances. Handles are automatically converted to the value they represent.
|
Playwright evaluation methods like [`method: Page.evaluate`] take a single optional argument. This argument can be a mix of [Serializable] values and [JSHandle] instances. Handles are automatically converted to the value they represent.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// A primitive value.
|
// A primitive value.
|
||||||
|
|
@ -86,7 +172,7 @@ await page.evaluate(object => object.foo, { foo: 'bar' });
|
||||||
const button = await page.evaluateHandle('window.button');
|
const button = await page.evaluateHandle('window.button');
|
||||||
await page.evaluate(button => button.textContent, button);
|
await page.evaluate(button => button.textContent, button);
|
||||||
|
|
||||||
// Alternative notation using elementHandle.evaluate.
|
// Alternative notation using JSHandle.evaluate.
|
||||||
await button.evaluate((button, from) => button.textContent.substring(from), 5);
|
await button.evaluate((button, from) => button.textContent.substring(from), 5);
|
||||||
|
|
||||||
// Object with multiple handles.
|
// Object with multiple handles.
|
||||||
|
|
@ -109,7 +195,7 @@ await page.evaluate(
|
||||||
([b1, b2]) => b1.textContent + b2.textContent,
|
([b1, b2]) => b1.textContent + b2.textContent,
|
||||||
[button1, button2]);
|
[button1, button2]);
|
||||||
|
|
||||||
// Any non-cyclic mix of serializables and handles works.
|
// Any mix of serializables and handles works.
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
x => x.button1.textContent + x.list[0].textContent + String(x.foo),
|
x => x.button1.textContent + x.list[0].textContent + String(x.foo),
|
||||||
{ button1, list: [button2], foo: null });
|
{ button1, list: [button2], foo: null });
|
||||||
|
|
@ -131,7 +217,7 @@ page.evaluate("object => object.foo", obj);
|
||||||
ElementHandle button = page.evaluateHandle("window.button");
|
ElementHandle button = page.evaluateHandle("window.button");
|
||||||
page.evaluate("button => button.textContent", button);
|
page.evaluate("button => button.textContent", button);
|
||||||
|
|
||||||
// Alternative notation using elementHandle.evaluate.
|
// Alternative notation using JSHandle.evaluate.
|
||||||
button.evaluate("(button, from) => button.textContent.substring(from)", 5);
|
button.evaluate("(button, from) => button.textContent.substring(from)", 5);
|
||||||
|
|
||||||
// Object with multiple handles.
|
// Object with multiple handles.
|
||||||
|
|
@ -156,7 +242,7 @@ page.evaluate(
|
||||||
"([b1, b2]) => b1.textContent + b2.textContent",
|
"([b1, b2]) => b1.textContent + b2.textContent",
|
||||||
Arrays.asList(button1, button2));
|
Arrays.asList(button1, button2));
|
||||||
|
|
||||||
// Any non-cyclic mix of serializables and handles works.
|
// Any mix of serializables and handles works.
|
||||||
Map<String, Object> arg = new HashMap<>();
|
Map<String, Object> arg = new HashMap<>();
|
||||||
arg.put("button1", button1);
|
arg.put("button1", button1);
|
||||||
arg.put("list", Arrays.asList(button2));
|
arg.put("list", Arrays.asList(button2));
|
||||||
|
|
@ -180,7 +266,7 @@ await page.evaluate('object => object.foo', { 'foo': 'bar' })
|
||||||
button = await page.evaluate_handle('button')
|
button = await page.evaluate_handle('button')
|
||||||
await page.evaluate('button => button.textContent', button)
|
await page.evaluate('button => button.textContent', button)
|
||||||
|
|
||||||
# Alternative notation using elementHandle.evaluate.
|
# Alternative notation using JSHandle.evaluate.
|
||||||
await button.evaluate('(button, from) => button.textContent.substring(from)', 5)
|
await button.evaluate('(button, from) => button.textContent.substring(from)', 5)
|
||||||
|
|
||||||
# Object with multiple handles.
|
# Object with multiple handles.
|
||||||
|
|
@ -203,7 +289,7 @@ await page.evaluate("""
|
||||||
([b1, b2]) => b1.textContent + b2.textContent""",
|
([b1, b2]) => b1.textContent + b2.textContent""",
|
||||||
[button1, button2])
|
[button1, button2])
|
||||||
|
|
||||||
# Any non-cyclic mix of serializables and handles works.
|
# Any mix of serializables and handles works.
|
||||||
await page.evaluate("""
|
await page.evaluate("""
|
||||||
x => x.button1.textContent + x.list[0].textContent + String(x.foo)""",
|
x => x.button1.textContent + x.list[0].textContent + String(x.foo)""",
|
||||||
{ 'button1': button1, 'list': [button2], 'foo': None })
|
{ 'button1': button1, 'list': [button2], 'foo': None })
|
||||||
|
|
@ -223,7 +309,7 @@ page.evaluate('object => object.foo', { 'foo': 'bar' })
|
||||||
button = page.evaluate_handle('window.button')
|
button = page.evaluate_handle('window.button')
|
||||||
page.evaluate('button => button.textContent', button)
|
page.evaluate('button => button.textContent', button)
|
||||||
|
|
||||||
# Alternative notation using elementHandle.evaluate.
|
# Alternative notation using JSHandle.evaluate.
|
||||||
button.evaluate('(button, from) => button.textContent.substring(from)', 5)
|
button.evaluate('(button, from) => button.textContent.substring(from)', 5)
|
||||||
|
|
||||||
# Object with multiple handles.
|
# Object with multiple handles.
|
||||||
|
|
@ -245,7 +331,7 @@ page.evaluate("""
|
||||||
([b1, b2]) => b1.textContent + b2.textContent""",
|
([b1, b2]) => b1.textContent + b2.textContent""",
|
||||||
[button1, button2])
|
[button1, button2])
|
||||||
|
|
||||||
# Any non-cyclic mix of serializables and handles works.
|
# Any mix of serializables and handles works.
|
||||||
page.evaluate("""
|
page.evaluate("""
|
||||||
x => x.button1.textContent + x.list[0].textContent + String(x.foo)""",
|
x => x.button1.textContent + x.list[0].textContent + String(x.foo)""",
|
||||||
{ 'button1': button1, 'list': [button2], 'foo': None })
|
{ 'button1': button1, 'list': [button2], 'foo': None })
|
||||||
|
|
@ -265,7 +351,7 @@ await page.EvaluateAsync<object>("object => object.foo", new { foo = "bar" });
|
||||||
var button = await page.EvaluateHandleAsync("window.button");
|
var button = await page.EvaluateHandleAsync("window.button");
|
||||||
await page.EvaluateAsync<IJSHandle>("button => button.textContent", button);
|
await page.EvaluateAsync<IJSHandle>("button => button.textContent", button);
|
||||||
|
|
||||||
// Alternative notation using elementHandle.EvaluateAsync.
|
// Alternative notation using JSHandle.EvaluateAsync.
|
||||||
await button.EvaluateAsync<string>("(button, from) => button.textContent.substring(from)", 5);
|
await button.EvaluateAsync<string>("(button, from) => button.textContent.substring(from)", 5);
|
||||||
|
|
||||||
// Object with multiple handles.
|
// Object with multiple handles.
|
||||||
|
|
@ -282,93 +368,69 @@ await page.EvaluateAsync("({ button1, button2 }) => button1.textContent + button
|
||||||
// Note the required parenthesis.
|
// Note the required parenthesis.
|
||||||
await page.EvaluateAsync("([b1, b2]) => b1.textContent + b2.textContent", new[] { button1, button2 });
|
await page.EvaluateAsync("([b1, b2]) => b1.textContent + b2.textContent", new[] { button1, button2 });
|
||||||
|
|
||||||
// Any non-cyclic mix of serializables and handles works.
|
// Any mix of serializables and handles works.
|
||||||
await page.EvaluateAsync("x => x.button1.textContent + x.list[0].textContent + String(x.foo)", new { button1, list = new[] { button2 }, foo = null as object });
|
await page.EvaluateAsync("x => x.button1.textContent + x.list[0].textContent + String(x.foo)", new { button1, list = new[] { button2 }, foo = null as object });
|
||||||
```
|
```
|
||||||
|
|
||||||
Right:
|
## Init scripts
|
||||||
|
|
||||||
|
Sometimes it is convenient to evaluate something in the page before it starts loading. For example, you might want to setup some mocks or test data.
|
||||||
|
|
||||||
|
In this case, use [`method: Page.addInitScript`] or [`method: BrowserContext.addInitScript`]. In the example below, we will replace `Math.random()` with a constant value.
|
||||||
|
|
||||||
|
First, create a `preload.js` file that contains the mock.
|
||||||
|
|
||||||
|
```js browser
|
||||||
|
// preload.js
|
||||||
|
Math.random = () => 42;
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, add init script to the page.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const data = { text: 'some data', value: 1 };
|
import { test, expect } from '@playwright/test';
|
||||||
// Pass |data| as a parameter.
|
import path from 'path';
|
||||||
const result = await page.evaluate(data => {
|
|
||||||
window.myApp.use(data);
|
|
||||||
}, data);
|
|
||||||
```
|
|
||||||
|
|
||||||
```java
|
test.beforeEach(async ({ page }) => {
|
||||||
Map<String, Object> data = new HashMap<>();
|
// Add script for every test in the beforeEach hook.
|
||||||
data.put("text", "some data");
|
// Make sure to correctly resolve the script path.
|
||||||
data.put("value", 1);
|
await page.addInitScript({ path: path.resolve(__dirname, '../mocks/preload.js') });
|
||||||
// Pass |data| as a parameter.
|
|
||||||
Object result = page.evaluate("data => {\n" +
|
|
||||||
" window.myApp.use(data);\n" +
|
|
||||||
"}", data);
|
|
||||||
```
|
|
||||||
|
|
||||||
```python async
|
|
||||||
data = { 'text': 'some data', 'value': 1 }
|
|
||||||
# Pass |data| as a parameter.
|
|
||||||
result = await page.evaluate("""data => {
|
|
||||||
window.myApp.use(data)
|
|
||||||
}""", data)
|
|
||||||
```
|
|
||||||
|
|
||||||
```python sync
|
|
||||||
data = { 'text': 'some data', 'value': 1 }
|
|
||||||
# Pass |data| as a parameter.
|
|
||||||
result = page.evaluate("""data => {
|
|
||||||
window.myApp.use(data)
|
|
||||||
}""", data)
|
|
||||||
```
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
var data = new { text = "some data", value = 1};
|
|
||||||
// Pass data as a parameter
|
|
||||||
var result = await page.EvaluateAsync("data => { window.myApp.use(data); }", data);
|
|
||||||
```
|
|
||||||
|
|
||||||
Wrong:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const data = { text: 'some data', value: 1 };
|
|
||||||
const result = await page.evaluate(() => {
|
|
||||||
// There is no |data| in the web page.
|
|
||||||
window.myApp.use(data);
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
```java
|
```java
|
||||||
Map<String, Object> data = new HashMap<>();
|
// In your test, assuming the "preload.js" file is in the "mocks" directory.
|
||||||
data.put("text", "some data");
|
page.addInitScript(Paths.get("mocks/preload.js"));
|
||||||
data.put("value", 1);
|
|
||||||
Object result = page.evaluate("() => {\n" +
|
|
||||||
" // There is no |data| in the web page.\n" +
|
|
||||||
" window.myApp.use(data);\n" +
|
|
||||||
"}");
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```python async
|
```python async
|
||||||
data = { 'text': 'some data', 'value': 1 }
|
# In your test, assuming the "preload.js" file is in the "mocks" directory.
|
||||||
result = await page.evaluate("""() => {
|
await page.add_init_script(path="mocks/preload.js")
|
||||||
// There is no |data| in the web page.
|
|
||||||
window.myApp.use(data)
|
|
||||||
}""")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```python sync
|
```python sync
|
||||||
data = { 'text': 'some data', 'value': 1 }
|
# In your test, assuming the "preload.js" file is in the "mocks" directory.
|
||||||
result = page.evaluate("""() => {
|
page.add_init_script(path="mocks/preload.js")
|
||||||
// There is no |data| in the web page.
|
|
||||||
window.myApp.use(data)
|
|
||||||
}""")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
var data = new { text = "some data", value = 1};
|
// In your test, assuming the "preload.js" file is in the "mocks" directory.
|
||||||
// Pass data as a parameter
|
await Page.AddInitScriptAsync(scriptPath: "mocks/preload.js");
|
||||||
var result = await page.EvaluateAsync(@"data => {
|
```
|
||||||
// There is no |data| in the web page.
|
|
||||||
window.myApp.use(data);
|
######
|
||||||
}");
|
* langs: js
|
||||||
|
|
||||||
|
Alternatively, you can pass a function instead of creating a preload script file. This is more convenient for short or one-off scripts. You can also pass an argument this way.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
// Add script for every test in the beforeEach hook.
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
const value = 42;
|
||||||
|
await page.addInitScript(value => {
|
||||||
|
Math.random = () => value;
|
||||||
|
}, value);
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -288,7 +288,7 @@ await Expect(page.GetByText("Strawberry")).ToBeVisibleAsync();
|
||||||
|
|
||||||
```java
|
```java
|
||||||
// Get the response from the HAR file
|
// Get the response from the HAR file
|
||||||
page.routeFromHAR("./hars/fruit.har", new RouteFromHAROptions()
|
page.routeFromHAR(Path.of("./hars/fruit.har"), new RouteFromHAROptions()
|
||||||
.setUrl("*/**/api/v1/fruits")
|
.setUrl("*/**/api/v1/fruits")
|
||||||
.setUpdate(true)
|
.setUpdate(true)
|
||||||
);
|
);
|
||||||
|
|
@ -386,7 +386,7 @@ await page.ExpectByTextAsync("Playwright", new() { Exact = true }).ToBeVisibleAs
|
||||||
// Replay API requests from HAR.
|
// Replay API requests from HAR.
|
||||||
// Either use a matching response from the HAR,
|
// Either use a matching response from the HAR,
|
||||||
// or abort the request if nothing matches.
|
// or abort the request if nothing matches.
|
||||||
page.routeFromHAR("./hars/fruit.har", new RouteFromHAROptions()
|
page.routeFromHAR(Path.of("./hars/fruit.har"), new RouteFromHAROptions()
|
||||||
.setUrl("*/**/api/v1/fruits")
|
.setUrl("*/**/api/v1/fruits")
|
||||||
.setUpdate(false)
|
.setUpdate(false)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ const browser = await chromium.launch({
|
||||||
Browser browser = chromium.launch(new BrowserType.LaunchOptions()
|
Browser browser = chromium.launch(new BrowserType.LaunchOptions()
|
||||||
.setProxy(new Proxy("http://myproxy.com:3128")
|
.setProxy(new Proxy("http://myproxy.com:3128")
|
||||||
.setUsername('usr')
|
.setUsername('usr')
|
||||||
.setPassword('pwd'));
|
.setPassword('pwd')));
|
||||||
```
|
```
|
||||||
|
|
||||||
```python async
|
```python async
|
||||||
|
|
@ -179,60 +179,48 @@ await using var browser = await BrowserType.LaunchAsync(new()
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
When specifying proxy for each context individually, **Chromium on Windows** needs a hint that proxy will be set. This is done via passing a non-empty proxy server to the browser itself. Here is an example of a context-specific proxy:
|
Its also possible to specify it per context:
|
||||||
|
|
||||||
```js tab=js-test title="playwright.config.ts"
|
```js tab=js-test title="example.spec.ts"
|
||||||
import { defineConfig } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
export default defineConfig({
|
|
||||||
use: {
|
test('should use custom proxy on a new context', async ({ browser }) => {
|
||||||
launchOptions: {
|
const context = await browser.newContext({
|
||||||
// Browser proxy option is required for Chromium on Windows.
|
|
||||||
proxy: { server: 'per-context' }
|
|
||||||
},
|
|
||||||
proxy: {
|
proxy: {
|
||||||
server: 'http://myproxy.com:3128',
|
server: 'http://myproxy.com:3128',
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
await context.close();
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
```js tab=js-library
|
```js tab=js-library
|
||||||
const browser = await chromium.launch({
|
const browser = await chromium.launch();
|
||||||
// Browser proxy option is required for Chromium on Windows.
|
|
||||||
proxy: { server: 'per-context' }
|
|
||||||
});
|
|
||||||
const context = await browser.newContext({
|
const context = await browser.newContext({
|
||||||
proxy: { server: 'http://myproxy.com:3128' }
|
proxy: { server: 'http://myproxy.com:3128' }
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
```java
|
```java
|
||||||
Browser browser = chromium.launch(new BrowserType.LaunchOptions()
|
Browser browser = chromium.launch();
|
||||||
// Browser proxy option is required for Chromium on Windows.
|
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
|
||||||
.setProxy(new Proxy("per-context"));
|
.setProxy(new Proxy("http://myproxy.com:3128")));
|
||||||
BrowserContext context = chromium.launch(new Browser.NewContextOptions()
|
|
||||||
.setProxy(new Proxy("http://myproxy.com:3128"));
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```python async
|
```python async
|
||||||
# Browser proxy option is required for Chromium on Windows.
|
browser = await chromium.launch()
|
||||||
browser = await chromium.launch(proxy={"server": "per-context"})
|
|
||||||
context = await browser.new_context(proxy={"server": "http://myproxy.com:3128"})
|
context = await browser.new_context(proxy={"server": "http://myproxy.com:3128"})
|
||||||
```
|
```
|
||||||
|
|
||||||
```python sync
|
```python sync
|
||||||
# Browser proxy option is required for Chromium on Windows.
|
browser = chromium.launch()
|
||||||
browser = chromium.launch(proxy={"server": "per-context"})
|
|
||||||
context = browser.new_context(proxy={"server": "http://myproxy.com:3128"})
|
context = browser.new_context(proxy={"server": "http://myproxy.com:3128"})
|
||||||
```
|
```
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
var proxy = new Proxy { Server = "per-context" };
|
await using var browser = await BrowserType.LaunchAsync();
|
||||||
await using var browser = await BrowserType.LaunchAsync(new()
|
|
||||||
{
|
|
||||||
// Browser proxy option is required for Chromium on Windows.
|
|
||||||
Proxy = proxy
|
|
||||||
});
|
|
||||||
await using var context = await browser.NewContextAsync(new()
|
await using var context = await browser.NewContextAsync(new()
|
||||||
{
|
{
|
||||||
Proxy = new Proxy { Server = "http://myproxy.com:3128" },
|
Proxy = new Proxy { Server = "http://myproxy.com:3128" },
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,37 @@ title: "Release notes"
|
||||||
toc_max_heading_level: 2
|
toc_max_heading_level: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Version 1.47
|
||||||
|
|
||||||
|
### Network Tab improvements
|
||||||
|
|
||||||
|
The Network tab in the trace viewer now allows searching and filtering by asset type:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
And for fonts, it now shows a nice preview:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
|
||||||
|
- The `mcr.microsoft.com/playwright-dotnet:v1.47.0` now serves a Playwright image based on Ubuntu 24.04 Noble.
|
||||||
|
To use the 22.04 jammy-based image, please use `mcr.microsoft.com/playwright-dotnet:v1.47.0-jammy` instead.
|
||||||
|
- TLS client certificates can now be passed from memory by passing [`option: cert`] and [`option: key`] as byte arrays instead of file paths.
|
||||||
|
- [`option: noWaitAfter`] in [`method: Locator.selectOption`] was deprecated.
|
||||||
|
- We've seen reports of WebGL in Webkit misbehaving on GitHub Actions `macos-13`. We recommend upgrading GitHub Actions to `macos-14`.
|
||||||
|
|
||||||
|
### Browser Versions
|
||||||
|
|
||||||
|
- Chromium 129.0.6668.29
|
||||||
|
- Mozilla Firefox 130.0
|
||||||
|
- WebKit 18.0
|
||||||
|
|
||||||
|
This version was also tested against the following stable channels:
|
||||||
|
|
||||||
|
- Google Chrome 128
|
||||||
|
- Microsoft Edge 128
|
||||||
|
|
||||||
## Version 1.46
|
## Version 1.46
|
||||||
|
|
||||||
### TLS Client Certificates
|
### TLS Client Certificates
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,37 @@ title: "Release notes"
|
||||||
toc_max_heading_level: 2
|
toc_max_heading_level: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Version 1.47
|
||||||
|
|
||||||
|
### Network Tab improvements
|
||||||
|
|
||||||
|
The Network tab in the trace viewer now allows searching and filtering by asset type:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
And for fonts, it now shows a nice preview:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
|
||||||
|
- The `mcr.microsoft.com/playwright-java:v1.47.0` now serves a Playwright image based on Ubuntu 24.04 Noble.
|
||||||
|
To use the 22.02 jammy-based image, please use `mcr.microsoft.com/playwright-java:v1.47.0-jammy` instead.
|
||||||
|
- TLS client certificates can now be passed from memory by passing [`option: cert`] and [`option: key`] as byte arrays instead of file paths.
|
||||||
|
- [`option: noWaitAfter`] in [`method: Locator.selectOption`] was deprecated.
|
||||||
|
- We've seen reports of WebGL in Webkit misbehaving on GitHub Actions `macos-13`. We recommend upgrading GitHub Actions to `macos-14`.
|
||||||
|
|
||||||
|
### Browser Versions
|
||||||
|
|
||||||
|
- Chromium 129.0.6668.29
|
||||||
|
- Mozilla Firefox 130.0
|
||||||
|
- WebKit 18.0
|
||||||
|
|
||||||
|
This version was also tested against the following stable channels:
|
||||||
|
|
||||||
|
- Google Chrome 128
|
||||||
|
- Microsoft Edge 128
|
||||||
|
|
||||||
## Version 1.46
|
## Version 1.46
|
||||||
|
|
||||||
### TLS Client Certificates
|
### TLS Client Certificates
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,67 @@ toc_max_heading_level: 2
|
||||||
|
|
||||||
import LiteYouTube from '@site/src/components/LiteYouTube';
|
import LiteYouTube from '@site/src/components/LiteYouTube';
|
||||||
|
|
||||||
|
## Version 1.47
|
||||||
|
|
||||||
|
### Network Tab improvements
|
||||||
|
|
||||||
|
The Network tab in the UI mode and trace viewer now allows searching and filtering by asset type:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
And for fonts, it now shows a nice preview:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
### `--tsconfig` CLI option
|
||||||
|
|
||||||
|
By default, Playwright will look up the closest tsconfig for each imported file using a heuristic. You can now specify a single tsconfig file in the command line, and Playwright will use it for all imported files, not only test files:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Pass a specific tsconfig
|
||||||
|
npx playwright test --tsconfig tsconfig.test.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### [APIRequestContext] now accepts [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) and `string` as query parameters
|
||||||
|
|
||||||
|
You can now pass [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) and `string` as query parameters to [APIRequestContext]:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
test('query params', async ({ request }) => {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
searchParams.set('userId', 1);
|
||||||
|
const response = await request.get(
|
||||||
|
'https://jsonplaceholder.typicode.com/posts',
|
||||||
|
{
|
||||||
|
params: searchParams // or as a string: 'userId=1'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
|
||||||
|
- The `mcr.microsoft.com/playwright:v1.47.0` now serves a Playwright image based on Ubuntu 24.04 Noble.
|
||||||
|
To use the 22.04 jammy-based image, please use `mcr.microsoft.com/playwright:v1.47.0-jammy` instead.
|
||||||
|
- New option [`option: behavior`] in [`method: Page.removeAllListeners`], [`method: Browser.removeAllListeners`] and [`method: BrowserContext.removeAllListeners`] to wait for ongoing listeners to complete.
|
||||||
|
- TLS client certificates can now be passed from memory by passing [`option: cert`] and [`option: key`] as buffers instead of file paths.
|
||||||
|
- Attachments with a `text/html` content type can now be opened in a new tab in the HTML report. This is useful for including third-party reports or other HTML content in the Playwright test report and distributing it to your team.
|
||||||
|
- [`option: noWaitAfter`] in [`method: Locator.selectOption`] was deprecated.
|
||||||
|
- We've seen reports of WebGL in Webkit misbehaving on GitHub Actions `macos-13`. We recommend upgrading GitHub Actions to `macos-14`.
|
||||||
|
|
||||||
|
### Browser Versions
|
||||||
|
|
||||||
|
- Chromium 129.0.6668.29
|
||||||
|
- Mozilla Firefox 130.0
|
||||||
|
- WebKit 18.0
|
||||||
|
|
||||||
|
This version was also tested against the following stable channels:
|
||||||
|
|
||||||
|
- Google Chrome 128
|
||||||
|
- Microsoft Edge 128
|
||||||
|
|
||||||
## Version 1.46
|
## Version 1.46
|
||||||
|
|
||||||
<LiteYouTube
|
<LiteYouTube
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,37 @@ title: "Release notes"
|
||||||
toc_max_heading_level: 2
|
toc_max_heading_level: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Version 1.47
|
||||||
|
|
||||||
|
### Network Tab improvements
|
||||||
|
|
||||||
|
The Network tab in the trace viewer now allows searching and filtering by asset type:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
And for fonts, it now shows a nice preview:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
|
||||||
|
- The `mcr.microsoft.com/playwright-python:v1.47.0` now serves a Playwright image based on Ubuntu 24.04 Noble.
|
||||||
|
To use the 22.04 jammy-based image, please use `mcr.microsoft.com/playwright-python:v1.47.0-jammy` instead.
|
||||||
|
- TLS client certificates can now be passed from memory by passing [`option: cert`] and [`option: key`] as bytes instead of file paths.
|
||||||
|
- [`option: noWaitAfter`] in [`method: Locator.selectOption`] was deprecated.
|
||||||
|
- We've seen reports of WebGL in Webkit misbehaving on GitHub Actions `macos-13`. We recommend upgrading GitHub Actions to `macos-14`.
|
||||||
|
|
||||||
|
### Browser Versions
|
||||||
|
|
||||||
|
- Chromium 129.0.6668.29
|
||||||
|
- Mozilla Firefox 130.0
|
||||||
|
- WebKit 18.0
|
||||||
|
|
||||||
|
This version was also tested against the following stable channels:
|
||||||
|
|
||||||
|
- Google Chrome 128
|
||||||
|
- Microsoft Edge 128
|
||||||
|
|
||||||
## Version 1.46
|
## Version 1.46
|
||||||
|
|
||||||
### TLS Client Certificates
|
### TLS Client Certificates
|
||||||
|
|
|
||||||
35
docs/src/test-api/class-testinfoerror-matcherresult.md
Normal file
35
docs/src/test-api/class-testinfoerror-matcherresult.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# class: TestInfoErrorMatcherResult
|
||||||
|
* since: v1.48
|
||||||
|
* langs: js
|
||||||
|
|
||||||
|
Matcher-specific details for the error thrown during the `expect` call.
|
||||||
|
|
||||||
|
## property: TestInfoErrorMatcherResult.actual
|
||||||
|
* since: v1.48
|
||||||
|
- type: ?<[unknown]>
|
||||||
|
|
||||||
|
Actual value.
|
||||||
|
|
||||||
|
## property: TestInfoErrorMatcherResult.expected
|
||||||
|
* since: v1.48
|
||||||
|
- type: ?<[unknown]>
|
||||||
|
|
||||||
|
Expected value.
|
||||||
|
|
||||||
|
## property: TestInfoErrorMatcherResult.name
|
||||||
|
* since: v1.48
|
||||||
|
- type: ?<[string]>
|
||||||
|
|
||||||
|
Matcher name.
|
||||||
|
|
||||||
|
## property: TestInfoErrorMatcherResult.pass
|
||||||
|
* since: v1.48
|
||||||
|
- type: <[string]>
|
||||||
|
|
||||||
|
Whether the matcher passed.
|
||||||
|
|
||||||
|
## property: TestInfoErrorMatcherResult.timeout
|
||||||
|
* since: v1.48
|
||||||
|
- type: ?<[int]>
|
||||||
|
|
||||||
|
Timeout that was used during matching.
|
||||||
|
|
@ -4,6 +4,12 @@
|
||||||
|
|
||||||
Information about an error thrown during test execution.
|
Information about an error thrown during test execution.
|
||||||
|
|
||||||
|
## property: TestInfoError.matcherResult
|
||||||
|
* since: v1.48
|
||||||
|
- type: ?<[TestInfoErrorMatcherResult]>
|
||||||
|
|
||||||
|
Matcher result details.
|
||||||
|
|
||||||
## property: TestInfoError.message
|
## property: TestInfoError.message
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: ?<[string]>
|
- type: ?<[string]>
|
||||||
|
|
|
||||||
|
|
@ -454,10 +454,6 @@ test('example test', async ({ slowFixture }) => {
|
||||||
|
|
||||||
## Fixtures-options
|
## Fixtures-options
|
||||||
|
|
||||||
:::note
|
|
||||||
Overriding custom fixtures in the config file has changed in version 1.18. [Learn more](./release-notes#breaking-change-custom-config-options).
|
|
||||||
:::
|
|
||||||
|
|
||||||
Playwright Test supports running multiple test projects that can be separately configured. You can use "option" fixtures to make your configuration options declarative and type-checked. Learn more about [parametrizing tests](./test-parameterize.md).
|
Playwright Test supports running multiple test projects that can be separately configured. You can use "option" fixtures to make your configuration options declarative and type-checked. Learn more about [parametrizing tests](./test-parameterize.md).
|
||||||
|
|
||||||
Below we'll create a `defaultItem` option in addition to the `todoPage` fixture from other examples. This option will be set in configuration file. Note the tuple syntax and `{ option: true }` argument.
|
Below we'll create a `defaultItem` option in addition to the `todoPage` fixture from other examples. This option will be set in configuration file. Note the tuple syntax and `{ option: true }` argument.
|
||||||
|
|
@ -555,6 +551,30 @@ export default defineConfig<MyOptions>({
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Array as an option value**
|
||||||
|
|
||||||
|
If the value of your option is an array, for example `[{ name: 'Alice' }, { name: 'Bob' }]`, you'll need to wrap it into an extra array when providing the value. This is best illustrated with an example.
|
||||||
|
|
||||||
|
```js
|
||||||
|
type Person = { name: string };
|
||||||
|
const test = base.extend<{ persons: Person[] }>({
|
||||||
|
// Declare the option, default value is an empty array.
|
||||||
|
persons: [[], { option: true }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Option value is an array of persons.
|
||||||
|
const actualPersons = [{ name: 'Alice' }, { name: 'Bob' }];
|
||||||
|
test.use({
|
||||||
|
// CORRECT: Wrap the value into an array and pass the scope.
|
||||||
|
persons: [actualPersons, { scope: 'test' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
test.use({
|
||||||
|
// WRONG: passing an array value directly will not work.
|
||||||
|
persons: actualPersons,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## Execution order
|
## Execution order
|
||||||
|
|
||||||
Each fixture has a setup and teardown phase separated by the `await use()` call in the fixture. Setup is executed before the fixture is used by the test/hook, and teardown is executed when the fixture will not be used by the test/hook anymore.
|
Each fixture has a setup and teardown phase separated by the `await use()` call in the fixture. Setup is executed before the fixture is used by the test/hook, and teardown is executed when the fixture will not be used by the test/hook anymore.
|
||||||
|
|
@ -702,3 +722,64 @@ export const test = base.extend({
|
||||||
}, { title: 'my fixture' }],
|
}, { title: 'my fixture' }],
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Adding global beforeEach/afterEach hooks
|
||||||
|
|
||||||
|
[`method: Test.beforeEach`] and [`method: Test.afterEach`] hooks run before/after each test declared in the same file and same [`method: Test.describe`] block (if any). If you want to declare hooks that run before/after each test globally, you can declare them as auto fixtures like this:
|
||||||
|
|
||||||
|
```ts title="fixtures.ts"
|
||||||
|
import { test as base } from '@playwright/test';
|
||||||
|
|
||||||
|
export const test = base.extend<{ forEachTest: void }>({
|
||||||
|
forEachTest: [async ({ page }, use) => {
|
||||||
|
// This code runs before every test.
|
||||||
|
await page.goto('http://localhost:8000');
|
||||||
|
await use();
|
||||||
|
// This code runs after every test.
|
||||||
|
console.log('Last URL:', page.url());
|
||||||
|
}, { auto: true }], // automatically starts for every test.
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
And then import the fixtures in all your tests:
|
||||||
|
|
||||||
|
```ts title="mytest.spec.ts"
|
||||||
|
import { test } from './fixtures';
|
||||||
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('basic', async ({ page }) => {
|
||||||
|
expect(page).toHaveURL('http://localhost:8000');
|
||||||
|
await page.goto('https://playwright.dev');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding global beforeAll/afterAll hooks
|
||||||
|
|
||||||
|
[`method: Test.beforeAll`] and [`method: Test.afterAll`] hooks run before/after all tests declared in the same file and same [`method: Test.describe`] block (if any), once per worker process. If you want to declare hooks
|
||||||
|
that run before/after all tests in every file, you can declare them as auto fixtures with `scope: 'worker'` as follows:
|
||||||
|
|
||||||
|
```ts title="fixtures.ts"
|
||||||
|
import { test as base } from '@playwright/test';
|
||||||
|
|
||||||
|
export const test = base.extend<{}, { forEachWorker: void }>({
|
||||||
|
forEachWorker: [async ({}, use) => {
|
||||||
|
// This code runs before all the tests in the worker process.
|
||||||
|
console.log(`Starting test worker ${test.info().workerIndex}`);
|
||||||
|
await use();
|
||||||
|
// This code runs after all the tests in the worker process.
|
||||||
|
console.log(`Stopping test worker ${test.info().workerIndex}`);
|
||||||
|
}, { scope: 'worker', auto: true }], // automatically starts for every worker.
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
And then import the fixtures in all your tests:
|
||||||
|
|
||||||
|
```ts title="mytest.spec.ts"
|
||||||
|
import { test } from './fixtures';
|
||||||
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('basic', async ({ }) => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
Note that the fixtures will still run once per [worker process](./test-parallel.md#worker-processes), but you don't need to redeclare them in every file.
|
||||||
|
|
|
||||||
35
docs/src/test-reporter-api/class-testerror-matcherresult.md
Normal file
35
docs/src/test-reporter-api/class-testerror-matcherresult.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# class: TestErrorMatcherResult
|
||||||
|
* since: v1.48
|
||||||
|
* langs: js
|
||||||
|
|
||||||
|
Matcher-specific details for the error thrown during the `expect` call.
|
||||||
|
|
||||||
|
## property: TestErrorMatcherResult.actual
|
||||||
|
* since: v1.48
|
||||||
|
- type: ?<[unknown]>
|
||||||
|
|
||||||
|
Actual value.
|
||||||
|
|
||||||
|
## property: TestErrorMatcherResult.expected
|
||||||
|
* since: v1.48
|
||||||
|
- type: ?<[unknown]>
|
||||||
|
|
||||||
|
Expected value.
|
||||||
|
|
||||||
|
## property: TestErrorMatcherResult.name
|
||||||
|
* since: v1.48
|
||||||
|
- type: ?<[string]>
|
||||||
|
|
||||||
|
Matcher name.
|
||||||
|
|
||||||
|
## property: TestErrorMatcherResult.pass
|
||||||
|
* since: v1.48
|
||||||
|
- type: <[string]>
|
||||||
|
|
||||||
|
Whether the matcher passed.
|
||||||
|
|
||||||
|
## property: TestErrorMatcherResult.timeout
|
||||||
|
* since: v1.48
|
||||||
|
- type: ?<[int]>
|
||||||
|
|
||||||
|
Timeout that was used during matching.
|
||||||
|
|
@ -4,6 +4,12 @@
|
||||||
|
|
||||||
Information about an error thrown during test execution.
|
Information about an error thrown during test execution.
|
||||||
|
|
||||||
|
## property: TestError.matcherResult
|
||||||
|
* since: v1.48
|
||||||
|
- type: ?<[TestErrorMatcherResult]>
|
||||||
|
|
||||||
|
Matcher result details.
|
||||||
|
|
||||||
## property: TestError.message
|
## property: TestError.message
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: ?<[string]>
|
- type: ?<[string]>
|
||||||
|
|
|
||||||
|
|
@ -80,14 +80,14 @@ By default, Playwright will look up a closest tsconfig for each imported file by
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Playwright will choose tsconfig automatically
|
# Playwright will choose tsconfig automatically
|
||||||
npx playwrigh test
|
npx playwright test
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, you can specify a single tsconfig file to use in the command line, and Playwright will use it for all imported files, not only test files.
|
Alternatively, you can specify a single tsconfig file to use in the command line, and Playwright will use it for all imported files, not only test files.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Pass a specific tsconfig
|
# Pass a specific tsconfig
|
||||||
npx playwrigh test --tsconfig=tsconfig.test.json
|
npx playwright test --tsconfig=tsconfig.test.json
|
||||||
```
|
```
|
||||||
|
|
||||||
## Manually compile tests with TypeScript
|
## Manually compile tests with TypeScript
|
||||||
|
|
|
||||||
129
package-lock.json
generated
129
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-internal",
|
"name": "playwright-internal",
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "playwright-internal",
|
"name": "playwright-internal",
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
@ -38,10 +38,11 @@
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"@zip.js/zip.js": "^2.7.29",
|
"@zip.js/zip.js": "^2.7.29",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
|
"chromium-bidi": "^0.6.4",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"concurrently": "^6.2.1",
|
"concurrently": "^6.2.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.4.5",
|
||||||
"electron": "^30.1.2",
|
"electron": "^30.1.2",
|
||||||
"esbuild": "^0.18.11",
|
"esbuild": "^0.18.11",
|
||||||
"eslint": "^8.55.0",
|
"eslint": "^8.55.0",
|
||||||
|
|
@ -2828,6 +2829,20 @@
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chromium-bidi": {
|
||||||
|
"version": "0.6.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.5.tgz",
|
||||||
|
"integrity": "sha512-RuLrmzYrxSb0s9SgpB+QN5jJucPduZQ/9SIe76MDxYJuecPW5mxMdacJ1f4EtgiV+R0p3sCkznTMvH0MPGFqjA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"mitt": "3.0.1",
|
||||||
|
"urlpattern-polyfill": "10.0.0",
|
||||||
|
"zod": "3.23.8"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"devtools-protocol": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cliui": {
|
"node_modules/cliui": {
|
||||||
"version": "7.0.4",
|
"version": "7.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
|
||||||
|
|
@ -3258,6 +3273,13 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/devtools-protocol": {
|
||||||
|
"version": "0.0.1349977",
|
||||||
|
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1349977.tgz",
|
||||||
|
"integrity": "sha512-5JcwlDKinshGSm+4AVLFCkokJUAKTgjmiorNmrGgYYKix1h8Ts9/fplQeK1xg/rACYw1JlEM2PwIEvny5QswKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/dezalgo": {
|
"node_modules/dezalgo": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
||||||
|
|
@ -3293,15 +3315,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.3.1",
|
"version": "16.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
||||||
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
|
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/motdotla/dotenv?sponsor=1"
|
"url": "https://dotenvx.com"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron": {
|
"node_modules/electron": {
|
||||||
|
|
@ -5454,6 +5476,12 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mitt": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/mkdirp": {
|
"node_modules/mkdirp": {
|
||||||
"version": "0.5.6",
|
"version": "0.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||||
|
|
@ -6820,9 +6848,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte": {
|
"node_modules/svelte": {
|
||||||
"version": "4.2.9",
|
"version": "4.2.19",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz",
|
||||||
"integrity": "sha512-hsoB/WZGEPFXeRRLPhPrbRz67PhP6sqYgvwcAs+gWdSQSvNDw+/lTeUJSWe5h2xC97Fz/8QxAOqItwBzNJPU8w==",
|
"integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.1",
|
"@ampproject/remapping": "^2.2.1",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||||
|
|
@ -7120,6 +7148,12 @@
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/urlpattern-polyfill": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/util-extend": {
|
"node_modules/util-extend": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz",
|
||||||
|
|
@ -7905,6 +7939,15 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "3.23.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||||
|
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/html-reporter": {
|
"packages/html-reporter": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -7912,10 +7955,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright": {
|
"packages/playwright": {
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.47.0-next"
|
"playwright-core": "1.48.0-next"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -7929,11 +7972,11 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-browser-chromium": {
|
"packages/playwright-browser-chromium": {
|
||||||
"name": "@playwright/browser-chromium",
|
"name": "@playwright/browser-chromium",
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.47.0-next"
|
"playwright-core": "1.48.0-next"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|
@ -7941,11 +7984,11 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-browser-firefox": {
|
"packages/playwright-browser-firefox": {
|
||||||
"name": "@playwright/browser-firefox",
|
"name": "@playwright/browser-firefox",
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.47.0-next"
|
"playwright-core": "1.48.0-next"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|
@ -7953,22 +7996,22 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-browser-webkit": {
|
"packages/playwright-browser-webkit": {
|
||||||
"name": "@playwright/browser-webkit",
|
"name": "@playwright/browser-webkit",
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.47.0-next"
|
"playwright-core": "1.48.0-next"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-chromium": {
|
"packages/playwright-chromium": {
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.47.0-next"
|
"playwright-core": "1.48.0-next"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -7978,7 +8021,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-core": {
|
"packages/playwright-core": {
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright-core": "cli.js"
|
"playwright-core": "cli.js"
|
||||||
|
|
@ -7989,11 +8032,11 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-core": {
|
"packages/playwright-ct-core": {
|
||||||
"name": "@playwright/experimental-ct-core",
|
"name": "@playwright/experimental-ct-core",
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.47.0-next",
|
"playwright": "1.48.0-next",
|
||||||
"playwright-core": "1.47.0-next",
|
"playwright-core": "1.48.0-next",
|
||||||
"vite": "^5.2.8"
|
"vite": "^5.2.8"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -8002,10 +8045,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-react": {
|
"packages/playwright-ct-react": {
|
||||||
"name": "@playwright/experimental-ct-react",
|
"name": "@playwright/experimental-ct-react",
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.47.0-next",
|
"@playwright/experimental-ct-core": "1.48.0-next",
|
||||||
"@vitejs/plugin-react": "^4.2.1"
|
"@vitejs/plugin-react": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -8017,10 +8060,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-react17": {
|
"packages/playwright-ct-react17": {
|
||||||
"name": "@playwright/experimental-ct-react17",
|
"name": "@playwright/experimental-ct-react17",
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.47.0-next",
|
"@playwright/experimental-ct-core": "1.48.0-next",
|
||||||
"@vitejs/plugin-react": "^4.2.1"
|
"@vitejs/plugin-react": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -8032,10 +8075,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-solid": {
|
"packages/playwright-ct-solid": {
|
||||||
"name": "@playwright/experimental-ct-solid",
|
"name": "@playwright/experimental-ct-solid",
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.47.0-next",
|
"@playwright/experimental-ct-core": "1.48.0-next",
|
||||||
"vite-plugin-solid": "^2.7.0"
|
"vite-plugin-solid": "^2.7.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -8050,17 +8093,17 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-svelte": {
|
"packages/playwright-ct-svelte": {
|
||||||
"name": "@playwright/experimental-ct-svelte",
|
"name": "@playwright/experimental-ct-svelte",
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.47.0-next",
|
"@playwright/experimental-ct-core": "1.48.0-next",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"svelte": "^4.2.8"
|
"svelte": "^4.2.19"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|
@ -8068,10 +8111,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-vue": {
|
"packages/playwright-ct-vue": {
|
||||||
"name": "@playwright/experimental-ct-vue",
|
"name": "@playwright/experimental-ct-vue",
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.47.0-next",
|
"@playwright/experimental-ct-core": "1.48.0-next",
|
||||||
"@vitejs/plugin-vue": "^4.2.1"
|
"@vitejs/plugin-vue": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -8083,10 +8126,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-vue2": {
|
"packages/playwright-ct-vue2": {
|
||||||
"name": "@playwright/experimental-ct-vue2",
|
"name": "@playwright/experimental-ct-vue2",
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.47.0-next",
|
"@playwright/experimental-ct-core": "1.48.0-next",
|
||||||
"@vitejs/plugin-vue2": "^2.2.0"
|
"@vitejs/plugin-vue2": "^2.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -8135,11 +8178,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-firefox": {
|
"packages/playwright-firefox": {
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.47.0-next"
|
"playwright-core": "1.48.0-next"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -8150,10 +8193,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-test": {
|
"packages/playwright-test": {
|
||||||
"name": "@playwright/test",
|
"name": "@playwright/test",
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.47.0-next"
|
"playwright": "1.48.0-next"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -8163,11 +8206,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-webkit": {
|
"packages/playwright-webkit": {
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.47.0-next"
|
"playwright-core": "1.48.0-next"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-internal",
|
"name": "playwright-internal",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"description": "A high-level API to automate web browsers",
|
"description": "A high-level API to automate web browsers",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
"webview2test": "playwright test --config=tests/webview2/playwright.config.ts",
|
"webview2test": "playwright test --config=tests/webview2/playwright.config.ts",
|
||||||
"itest": "playwright test --config=tests/installation/playwright.config.ts",
|
"itest": "playwright test --config=tests/installation/playwright.config.ts",
|
||||||
"stest": "playwright test --config=tests/stress/playwright.config.ts",
|
"stest": "playwright test --config=tests/stress/playwright.config.ts",
|
||||||
|
"biditest": "playwright test --config=tests/bidi/playwright.config.ts",
|
||||||
"test-html-reporter": "playwright test --config=packages/html-reporter",
|
"test-html-reporter": "playwright test --config=packages/html-reporter",
|
||||||
"test-web": "playwright test --config=packages/web",
|
"test-web": "playwright test --config=packages/web",
|
||||||
"ttest": "node ./tests/playwright-test/stable-test-runner/node_modules/@playwright/test/cli test --config=tests/playwright-test/playwright.config.ts",
|
"ttest": "node ./tests/playwright-test/stable-test-runner/node_modules/@playwright/test/cli test --config=tests/playwright-test/playwright.config.ts",
|
||||||
|
|
@ -76,10 +77,11 @@
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"@zip.js/zip.js": "^2.7.29",
|
"@zip.js/zip.js": "^2.7.29",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
|
"chromium-bidi": "^0.6.4",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"concurrently": "^6.2.1",
|
"concurrently": "^6.2.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.4.5",
|
||||||
"electron": "^30.1.2",
|
"electron": "^30.1.2",
|
||||||
"esbuild": "^0.18.11",
|
"esbuild": "^0.18.11",
|
||||||
"eslint": "^8.55.0",
|
"eslint": "^8.55.0",
|
||||||
|
|
|
||||||
|
|
@ -75,11 +75,16 @@ export const AttachmentLink: React.FunctionComponent<{
|
||||||
attachment: TestAttachment,
|
attachment: TestAttachment,
|
||||||
href?: string,
|
href?: string,
|
||||||
linkName?: string,
|
linkName?: string,
|
||||||
}> = ({ attachment, href, linkName }) => {
|
openInNewTab?: boolean,
|
||||||
|
}> = ({ attachment, href, linkName, openInNewTab }) => {
|
||||||
return <TreeItem title={<span>
|
return <TreeItem title={<span>
|
||||||
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
|
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
|
||||||
{attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>}
|
{attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>}
|
||||||
{!attachment.path && <span>{linkifyText(attachment.name)}</span>}
|
{!attachment.path && (
|
||||||
|
openInNewTab
|
||||||
|
? <a href={URL.createObjectURL(new Blob([attachment.body!], { type: attachment.contentType }))} target='_blank' rel='noreferrer' onClick={e => e.stopPropagation()}>{attachment.name}</a>
|
||||||
|
: <span>{linkifyText(attachment.name)}</span>
|
||||||
|
)}
|
||||||
</span>} loadChildren={attachment.body ? () => {
|
</span>} loadChildren={attachment.body ? () => {
|
||||||
return [<div key={1} className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>];
|
return [<div key={1} className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>];
|
||||||
} : undefined} depth={0} style={{ lineHeight: '32px' }}></TreeItem>;
|
} : undefined} depth={0} style={{ lineHeight: '32px' }}></TreeItem>;
|
||||||
|
|
|
||||||
|
|
@ -67,15 +67,16 @@ export const TestResultView: React.FC<{
|
||||||
anchor: 'video' | 'diff' | '',
|
anchor: 'video' | 'diff' | '',
|
||||||
}> = ({ result, anchor }) => {
|
}> = ({ result, anchor }) => {
|
||||||
|
|
||||||
const { screenshots, videos, traces, otherAttachments, diffs } = React.useMemo(() => {
|
const { screenshots, videos, traces, otherAttachments, diffs, htmls } = React.useMemo(() => {
|
||||||
const attachments = result?.attachments || [];
|
const attachments = result?.attachments || [];
|
||||||
const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/')));
|
const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/')));
|
||||||
const videos = attachments.filter(a => a.name === 'video');
|
const videos = attachments.filter(a => a.name === 'video');
|
||||||
const traces = attachments.filter(a => a.name === 'trace');
|
const traces = attachments.filter(a => a.name === 'trace');
|
||||||
|
const htmls = attachments.filter(a => a.contentType.startsWith('text/html'));
|
||||||
const otherAttachments = new Set<TestAttachment>(attachments);
|
const otherAttachments = new Set<TestAttachment>(attachments);
|
||||||
[...screenshots, ...videos, ...traces].forEach(a => otherAttachments.delete(a));
|
[...screenshots, ...videos, ...traces, ...htmls].forEach(a => otherAttachments.delete(a));
|
||||||
const diffs = groupImageDiffs(screenshots);
|
const diffs = groupImageDiffs(screenshots);
|
||||||
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs };
|
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, htmls };
|
||||||
}, [result]);
|
}, [result]);
|
||||||
|
|
||||||
const videoRef = React.useRef<HTMLDivElement>(null);
|
const videoRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -135,7 +136,10 @@ export const TestResultView: React.FC<{
|
||||||
</div>)}
|
</div>)}
|
||||||
</AutoChip>}
|
</AutoChip>}
|
||||||
|
|
||||||
{!!otherAttachments.size && <AutoChip header='Attachments'>
|
{!!(otherAttachments.size + htmls.length) && <AutoChip header='Attachments'>
|
||||||
|
{[...htmls].map((a, i) => (
|
||||||
|
<AttachmentLink key={`html-link-${i}`} attachment={a} openInNewTab />)
|
||||||
|
)}
|
||||||
{[...otherAttachments].map((a, i) => <AttachmentLink key={`attachment-link-${i}`} attachment={a}></AttachmentLink>)}
|
{[...otherAttachments].map((a, i) => <AttachmentLink key={`attachment-link-${i}`} attachment={a}></AttachmentLink>)}
|
||||||
</AutoChip>}
|
</AutoChip>}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/browser-chromium",
|
"name": "@playwright/browser-chromium",
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"description": "Playwright package that automatically installs Chromium",
|
"description": "Playwright package that automatically installs Chromium",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -27,6 +27,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.47.0-next"
|
"playwright-core": "1.48.0-next"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/browser-firefox",
|
"name": "@playwright/browser-firefox",
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"description": "Playwright package that automatically installs Firefox",
|
"description": "Playwright package that automatically installs Firefox",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -27,6 +27,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.47.0-next"
|
"playwright-core": "1.48.0-next"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/browser-webkit",
|
"name": "@playwright/browser-webkit",
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"description": "Playwright package that automatically installs WebKit",
|
"description": "Playwright package that automatically installs WebKit",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -27,6 +27,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.47.0-next"
|
"playwright-core": "1.48.0-next"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-chromium",
|
"name": "playwright-chromium",
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"description": "A high-level API to automate Chromium",
|
"description": "A high-level API to automate Chromium",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,6 +30,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.47.0-next"
|
"playwright-core": "1.48.0-next"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ This project incorporates components from the projects listed below. The origina
|
||||||
- concat-map@0.0.1 (https://github.com/substack/node-concat-map)
|
- concat-map@0.0.1 (https://github.com/substack/node-concat-map)
|
||||||
- debug@4.3.4 (https://github.com/debug-js/debug)
|
- debug@4.3.4 (https://github.com/debug-js/debug)
|
||||||
- define-lazy-prop@2.0.0 (https://github.com/sindresorhus/define-lazy-prop)
|
- define-lazy-prop@2.0.0 (https://github.com/sindresorhus/define-lazy-prop)
|
||||||
|
- dotenv@16.4.5 (https://github.com/motdotla/dotenv)
|
||||||
- end-of-stream@1.4.4 (https://github.com/mafintosh/end-of-stream)
|
- end-of-stream@1.4.4 (https://github.com/mafintosh/end-of-stream)
|
||||||
- escape-string-regexp@2.0.0 (https://github.com/sindresorhus/escape-string-regexp)
|
- escape-string-regexp@2.0.0 (https://github.com/sindresorhus/escape-string-regexp)
|
||||||
- extract-zip@2.0.1 (https://github.com/maxogden/extract-zip)
|
- extract-zip@2.0.1 (https://github.com/maxogden/extract-zip)
|
||||||
|
|
@ -472,6 +473,34 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||||
=========================================
|
=========================================
|
||||||
END OF define-lazy-prop@2.0.0 AND INFORMATION
|
END OF define-lazy-prop@2.0.0 AND INFORMATION
|
||||||
|
|
||||||
|
%% dotenv@16.4.5 NOTICES AND INFORMATION BEGIN HERE
|
||||||
|
=========================================
|
||||||
|
Copyright (c) 2015, Scott Motte
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
=========================================
|
||||||
|
END OF dotenv@16.4.5 AND INFORMATION
|
||||||
|
|
||||||
%% end-of-stream@1.4.4 NOTICES AND INFORMATION BEGIN HERE
|
%% end-of-stream@1.4.4 NOTICES AND INFORMATION BEGIN HERE
|
||||||
=========================================
|
=========================================
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
@ -1514,6 +1543,6 @@ END OF yazl@2.5.1 AND INFORMATION
|
||||||
|
|
||||||
SUMMARY BEGIN HERE
|
SUMMARY BEGIN HERE
|
||||||
=========================================
|
=========================================
|
||||||
Total Packages: 45
|
Total Packages: 46
|
||||||
=========================================
|
=========================================
|
||||||
END OF SUMMARY
|
END OF SUMMARY
|
||||||
|
|
@ -3,21 +3,21 @@
|
||||||
"browsers": [
|
"browsers": [
|
||||||
{
|
{
|
||||||
"name": "chromium",
|
"name": "chromium",
|
||||||
"revision": "1131",
|
"revision": "1134",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"browserVersion": "128.0.6613.36"
|
"browserVersion": "129.0.6668.29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "chromium-tip-of-tree",
|
"name": "chromium-tip-of-tree",
|
||||||
"revision": "1250",
|
"revision": "1256",
|
||||||
"installByDefault": false,
|
"installByDefault": false,
|
||||||
"browserVersion": "129.0.6658.0"
|
"browserVersion": "130.0.6695.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "firefox",
|
"name": "firefox",
|
||||||
"revision": "1462",
|
"revision": "1463",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"browserVersion": "129.0"
|
"browserVersion": "130.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "firefox-beta",
|
"name": "firefox-beta",
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "webkit",
|
"name": "webkit",
|
||||||
"revision": "2062",
|
"revision": "2070",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"revisionOverrides": {
|
"revisionOverrides": {
|
||||||
"mac10.14": "1446",
|
"mac10.14": "1446",
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
"colors": "1.4.0",
|
"colors": "1.4.0",
|
||||||
"commander": "8.3.0",
|
"commander": "8.3.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
"graceful-fs": "4.2.10",
|
"graceful-fs": "4.2.10",
|
||||||
"https-proxy-agent": "5.0.0",
|
"https-proxy-agent": "5.0.0",
|
||||||
"jpeg-js": "0.4.4",
|
"jpeg-js": "0.4.4",
|
||||||
|
|
@ -198,6 +199,17 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "16.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
||||||
|
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/escape-string-regexp": {
|
"node_modules/escape-string-regexp": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
|
||||||
|
|
@ -560,6 +572,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
|
||||||
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="
|
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="
|
||||||
},
|
},
|
||||||
|
"dotenv": {
|
||||||
|
"version": "16.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
||||||
|
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg=="
|
||||||
|
},
|
||||||
"escape-string-regexp": {
|
"escape-string-regexp": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
"colors": "1.4.0",
|
"colors": "1.4.0",
|
||||||
"commander": "8.3.0",
|
"commander": "8.3.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
"graceful-fs": "4.2.10",
|
"graceful-fs": "4.2.10",
|
||||||
"https-proxy-agent": "5.0.0",
|
"https-proxy-agent": "5.0.0",
|
||||||
"jpeg-js": "0.4.4",
|
"jpeg-js": "0.4.4",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ export const colors = colorsLibrary;
|
||||||
import debugLibrary from 'debug';
|
import debugLibrary from 'debug';
|
||||||
export const debug = debugLibrary;
|
export const debug = debugLibrary;
|
||||||
|
|
||||||
|
import dotenvLibrary from 'dotenv';
|
||||||
|
export const dotenv = dotenvLibrary;
|
||||||
|
|
||||||
export { getProxyForUrl } from 'proxy-from-env';
|
export { getProxyForUrl } from 'proxy-from-env';
|
||||||
|
|
||||||
export { HttpsProxyAgent } from 'https-proxy-agent';
|
export { HttpsProxyAgent } from 'https-proxy-agent';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-core",
|
"name": "playwright-core",
|
||||||
"version": "1.47.0-next",
|
"version": "1.48.0-next",
|
||||||
"description": "A high-level API to automate web browsers",
|
"description": "A high-level API to automate web browsers",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import fs from 'fs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { Command } from '../utilsBundle';
|
import type { Command } from '../utilsBundle';
|
||||||
import { program } from '../utilsBundle';
|
import { program, dotenv } from '../utilsBundle';
|
||||||
export { program } from '../utilsBundle';
|
export { program } from '../utilsBundle';
|
||||||
import { runDriver, runServer, printApiJson, launchBrowserServer } from './driver';
|
import { runDriver, runServer, printApiJson, launchBrowserServer } from './driver';
|
||||||
import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer';
|
import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer';
|
||||||
|
|
@ -561,6 +561,7 @@ async function open(options: Options, url: string | undefined, language: string)
|
||||||
async function codegen(options: Options & { target: string, output?: string, testIdAttribute?: string }, url: string | undefined) {
|
async function codegen(options: Options & { target: string, output?: string, testIdAttribute?: string }, url: string | undefined) {
|
||||||
const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options;
|
const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options;
|
||||||
const { context, launchOptions, contextOptions } = await launchContext(options, !!process.env.PWTEST_CLI_HEADLESS, process.env.PWTEST_CLI_EXECUTABLE_PATH);
|
const { context, launchOptions, contextOptions } = await launchContext(options, !!process.env.PWTEST_CLI_HEADLESS, process.env.PWTEST_CLI_EXECUTABLE_PATH);
|
||||||
|
dotenv.config({ path: 'playwright.env' });
|
||||||
await context._enableRecorder({
|
await context._enableRecorder({
|
||||||
language,
|
language,
|
||||||
launchOptions,
|
launchOptions,
|
||||||
|
|
@ -570,7 +571,6 @@ async function codegen(options: Options & { target: string, output?: string, tes
|
||||||
mode: 'recording',
|
mode: 'recording',
|
||||||
testIdAttributeName,
|
testIdAttributeName,
|
||||||
outputFile: outputFile ? path.resolve(outputFile) : undefined,
|
outputFile: outputFile ? path.resolve(outputFile) : undefined,
|
||||||
handleSIGINT: false,
|
|
||||||
});
|
});
|
||||||
await openPage(context, url);
|
await openPage(context, url);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -481,7 +481,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||||
mode?: 'recording' | 'inspecting',
|
mode?: 'recording' | 'inspecting',
|
||||||
testIdAttributeName?: string,
|
testIdAttributeName?: string,
|
||||||
outputFile?: string,
|
outputFile?: string,
|
||||||
handleSIGINT?: boolean,
|
|
||||||
}) {
|
}) {
|
||||||
await this._channel.recorderSupplementEnable(params);
|
await this._channel.recorderSupplementEnable(params);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ import { Selectors, SelectorsOwner } from './selectors';
|
||||||
export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
||||||
readonly _android: Android;
|
readonly _android: Android;
|
||||||
readonly _electron: Electron;
|
readonly _electron: Electron;
|
||||||
|
readonly _bidiChromium: BrowserType;
|
||||||
|
readonly _bidiFirefox: BrowserType;
|
||||||
readonly chromium: BrowserType;
|
readonly chromium: BrowserType;
|
||||||
readonly firefox: BrowserType;
|
readonly firefox: BrowserType;
|
||||||
readonly webkit: BrowserType;
|
readonly webkit: BrowserType;
|
||||||
|
|
@ -45,6 +47,10 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
||||||
this.webkit._playwright = this;
|
this.webkit._playwright = this;
|
||||||
this._android = Android.from(initializer.android);
|
this._android = Android.from(initializer.android);
|
||||||
this._electron = Electron.from(initializer.electron);
|
this._electron = Electron.from(initializer.electron);
|
||||||
|
this._bidiChromium = BrowserType.from(initializer.bidiChromium);
|
||||||
|
this._bidiChromium._playwright = this;
|
||||||
|
this._bidiFirefox = BrowserType.from(initializer.bidiFirefox);
|
||||||
|
this._bidiFirefox._playwright = this;
|
||||||
this.devices = this._connection.localUtils()?.devices ?? {};
|
this.devices = this._connection.localUtils()?.devices ?? {};
|
||||||
this.selectors = new Selectors();
|
this.selectors = new Selectors();
|
||||||
this.errors = { TimeoutError };
|
this.errors = { TimeoutError };
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export type WaitForEventOptions = Function | { predicate?: Function, timeout?: n
|
||||||
export type WaitForFunctionOptions = { timeout?: number, polling?: 'raf' | number };
|
export type WaitForFunctionOptions = { timeout?: number, polling?: 'raf' | number };
|
||||||
|
|
||||||
export type SelectOption = { value?: string, label?: string, index?: number, valueOrLabel?: string };
|
export type SelectOption = { value?: string, label?: string, index?: number, valueOrLabel?: string };
|
||||||
export type SelectOptionOptions = { force?: boolean, timeout?: number, noWaitAfter?: boolean };
|
export type SelectOptionOptions = { force?: boolean, timeout?: number };
|
||||||
export type FilePayload = { name: string, mimeType: string, buffer: Buffer };
|
export type FilePayload = { name: string, mimeType: string, buffer: Buffer };
|
||||||
export type StorageState = {
|
export type StorageState = {
|
||||||
cookies: channels.NetworkCookie[],
|
cookies: channels.NetworkCookie[],
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,8 @@ scheme.PlaywrightInitializer = tObject({
|
||||||
chromium: tChannel(['BrowserType']),
|
chromium: tChannel(['BrowserType']),
|
||||||
firefox: tChannel(['BrowserType']),
|
firefox: tChannel(['BrowserType']),
|
||||||
webkit: tChannel(['BrowserType']),
|
webkit: tChannel(['BrowserType']),
|
||||||
|
bidiChromium: tChannel(['BrowserType']),
|
||||||
|
bidiFirefox: tChannel(['BrowserType']),
|
||||||
android: tChannel(['Android']),
|
android: tChannel(['Android']),
|
||||||
electron: tChannel(['Electron']),
|
electron: tChannel(['Electron']),
|
||||||
utils: tOptional(tChannel(['LocalUtils'])),
|
utils: tOptional(tChannel(['LocalUtils'])),
|
||||||
|
|
@ -961,7 +963,6 @@ scheme.BrowserContextRecorderSupplementEnableParams = tObject({
|
||||||
device: tOptional(tString),
|
device: tOptional(tString),
|
||||||
saveStorage: tOptional(tString),
|
saveStorage: tOptional(tString),
|
||||||
outputFile: tOptional(tString),
|
outputFile: tOptional(tString),
|
||||||
handleSIGINT: tOptional(tBoolean),
|
|
||||||
omitCallTracking: tOptional(tBoolean),
|
omitCallTracking: tOptional(tBoolean),
|
||||||
});
|
});
|
||||||
scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({}));
|
scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({}));
|
||||||
|
|
@ -1637,7 +1638,6 @@ scheme.FrameSelectOptionParams = tObject({
|
||||||
}))),
|
}))),
|
||||||
force: tOptional(tBoolean),
|
force: tOptional(tBoolean),
|
||||||
timeout: tOptional(tNumber),
|
timeout: tOptional(tNumber),
|
||||||
noWaitAfter: tOptional(tBoolean),
|
|
||||||
});
|
});
|
||||||
scheme.FrameSelectOptionResult = tObject({
|
scheme.FrameSelectOptionResult = tObject({
|
||||||
values: tArray(tString),
|
values: tArray(tString),
|
||||||
|
|
@ -2001,7 +2001,6 @@ scheme.ElementHandleSelectOptionParams = tObject({
|
||||||
}))),
|
}))),
|
||||||
force: tOptional(tBoolean),
|
force: tOptional(tBoolean),
|
||||||
timeout: tOptional(tNumber),
|
timeout: tOptional(tNumber),
|
||||||
noWaitAfter: tOptional(tBoolean),
|
|
||||||
});
|
});
|
||||||
scheme.ElementHandleSelectOptionResult = tObject({
|
scheme.ElementHandleSelectOptionResult = tObject({
|
||||||
values: tArray(tString),
|
values: tArray(tString),
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
[playwright.ts]
|
[playwright.ts]
|
||||||
./android/
|
./android/
|
||||||
|
./bidi/
|
||||||
./chromium/
|
./chromium/
|
||||||
./electron/
|
./electron/
|
||||||
./firefox/
|
./firefox/
|
||||||
|
|
|
||||||
11
packages/playwright-core/src/server/bidi/DEPS.list
Normal file
11
packages/playwright-core/src/server/bidi/DEPS.list
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[*]
|
||||||
|
../../utils/
|
||||||
|
../
|
||||||
|
../isomorphic/
|
||||||
|
./third_party/
|
||||||
|
|
||||||
|
[bidiOverCdp.ts]
|
||||||
|
***
|
||||||
|
|
||||||
|
[bidiChromium.ts]
|
||||||
|
../chromium/chromiumSwitches.ts
|
||||||
329
packages/playwright-core/src/server/bidi/bidiBrowser.ts
Normal file
329
packages/playwright-core/src/server/bidi/bidiBrowser.ts
Normal file
|
|
@ -0,0 +1,329 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as channels from '@protocol/channels';
|
||||||
|
import type { RegisteredListener } from '../../utils/eventsHelper';
|
||||||
|
import { eventsHelper } from '../../utils/eventsHelper';
|
||||||
|
import type { BrowserOptions } from '../browser';
|
||||||
|
import { Browser } from '../browser';
|
||||||
|
import { assertBrowserContextIsNotOwned, BrowserContext } from '../browserContext';
|
||||||
|
import type { SdkObject } from '../instrumentation';
|
||||||
|
import * as network from '../network';
|
||||||
|
import type { InitScript, Page, PageDelegate } from '../page';
|
||||||
|
import type { ConnectionTransport } from '../transport';
|
||||||
|
import type * as types from '../types';
|
||||||
|
import type { BidiSession } from './bidiConnection';
|
||||||
|
import { BidiConnection } from './bidiConnection';
|
||||||
|
import { bidiBytesValueToString } from './bidiNetworkManager';
|
||||||
|
import { BidiPage } from './bidiPage';
|
||||||
|
import * as bidi from './third_party/bidiProtocol';
|
||||||
|
|
||||||
|
export class BidiBrowser extends Browser {
|
||||||
|
private readonly _connection: BidiConnection;
|
||||||
|
readonly _browserSession: BidiSession;
|
||||||
|
private _bidiSessionInfo!: bidi.Session.NewResult;
|
||||||
|
readonly _contexts = new Map<string, BidiBrowserContext>();
|
||||||
|
readonly _bidiPages = new Map<bidi.BrowsingContext.BrowsingContext, BidiPage>();
|
||||||
|
private readonly _eventListeners: RegisteredListener[];
|
||||||
|
|
||||||
|
static async connect(parent: SdkObject, transport: ConnectionTransport, options: BrowserOptions): Promise<BidiBrowser> {
|
||||||
|
const browser = new BidiBrowser(parent, transport, options);
|
||||||
|
if ((options as any).__testHookOnConnectToBrowser)
|
||||||
|
await (options as any).__testHookOnConnectToBrowser();
|
||||||
|
|
||||||
|
let proxy: bidi.Session.ManualProxyConfiguration | undefined;
|
||||||
|
if (options.proxy) {
|
||||||
|
proxy = {
|
||||||
|
proxyType: 'manual',
|
||||||
|
};
|
||||||
|
const url = new URL(options.proxy.server); // Validate proxy server.
|
||||||
|
switch (url.protocol) {
|
||||||
|
case 'http:':
|
||||||
|
proxy.httpProxy = url.host;
|
||||||
|
break;
|
||||||
|
case 'https:':
|
||||||
|
proxy.httpsProxy = url.host;
|
||||||
|
break;
|
||||||
|
case 'socks4:':
|
||||||
|
proxy.socksProxy = url.host;
|
||||||
|
proxy.socksVersion = 4;
|
||||||
|
break;
|
||||||
|
case 'socks5:':
|
||||||
|
proxy.socksProxy = url.host;
|
||||||
|
proxy.socksVersion = 5;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid proxy server protocol: ' + options.proxy.server);
|
||||||
|
}
|
||||||
|
if (options.proxy.bypass)
|
||||||
|
proxy.noProxy = options.proxy.bypass.split(',');
|
||||||
|
// TODO: support authentication.
|
||||||
|
}
|
||||||
|
|
||||||
|
browser._bidiSessionInfo = await browser._browserSession.send('session.new', {
|
||||||
|
capabilities: {
|
||||||
|
alwaysMatch: {
|
||||||
|
acceptInsecureCerts: false,
|
||||||
|
proxy,
|
||||||
|
unhandledPromptBehavior: {
|
||||||
|
default: bidi.Session.UserPromptHandlerType.Ignore,
|
||||||
|
},
|
||||||
|
webSocketUrl: true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await browser._browserSession.send('session.subscribe', {
|
||||||
|
events: [
|
||||||
|
'browsingContext',
|
||||||
|
'network',
|
||||||
|
'log',
|
||||||
|
'script',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(parent: SdkObject, transport: ConnectionTransport, options: BrowserOptions) {
|
||||||
|
super(parent, options);
|
||||||
|
this._connection = new BidiConnection(transport, this._onDisconnect.bind(this), options.protocolLogger, options.browserLogsCollector);
|
||||||
|
this._browserSession = this._connection.browserSession;
|
||||||
|
this._eventListeners = [
|
||||||
|
eventsHelper.addEventListener(this._browserSession, 'browsingContext.contextCreated', this._onBrowsingContextCreated.bind(this)),
|
||||||
|
eventsHelper.addEventListener(this._browserSession, 'script.realmDestroyed', this._onScriptRealmDestroyed.bind(this)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDisconnect() {
|
||||||
|
this._didClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
async doCreateNewContext(options: channels.BrowserNewContextParams): Promise<BrowserContext> {
|
||||||
|
const { userContext } = await this._browserSession.send('browser.createUserContext', {});
|
||||||
|
const context = new BidiBrowserContext(this, userContext, options);
|
||||||
|
await context._initialize();
|
||||||
|
this._contexts.set(userContext, context);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
contexts(): BrowserContext[] {
|
||||||
|
return Array.from(this._contexts.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
version(): string {
|
||||||
|
return this._bidiSessionInfo.capabilities.browserVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
userAgent(): string {
|
||||||
|
return this._bidiSessionInfo.capabilities.userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnected(): boolean {
|
||||||
|
return !this._connection.isClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onBrowsingContextCreated(event: bidi.BrowsingContext.Info) {
|
||||||
|
if (event.parent) {
|
||||||
|
const parentFrameId = event.parent;
|
||||||
|
for (const page of this._bidiPages.values()) {
|
||||||
|
const parentFrame = page._page._frameManager.frame(parentFrameId);
|
||||||
|
if (!parentFrame)
|
||||||
|
continue;
|
||||||
|
page._session.addFrameBrowsingContext(event.context);
|
||||||
|
page._page._frameManager.frameAttached(event.context, parentFrameId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let context = this._contexts.get(event.userContext);
|
||||||
|
if (!context)
|
||||||
|
context = this._defaultContext as BidiBrowserContext;
|
||||||
|
if (!context)
|
||||||
|
return;
|
||||||
|
const session = this._connection.createMainFrameBrowsingContextSession(event.context);
|
||||||
|
const opener = event.originalOpener && this._bidiPages.get(event.originalOpener);
|
||||||
|
const page = new BidiPage(context, session, opener || null);
|
||||||
|
this._bidiPages.set(event.context, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onBrowsingContextDestroyed(event: bidi.BrowsingContext.Info) {
|
||||||
|
if (event.parent) {
|
||||||
|
this._browserSession.removeFrameBrowsingContext(event.context);
|
||||||
|
const parentFrameId = event.parent;
|
||||||
|
for (const page of this._bidiPages.values()) {
|
||||||
|
const parentFrame = page._page._frameManager.frame(parentFrameId);
|
||||||
|
if (!parentFrame)
|
||||||
|
continue;
|
||||||
|
page._page._frameManager.frameDetached(event.context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bidiPage = this._bidiPages.get(event.context);
|
||||||
|
if (!bidiPage)
|
||||||
|
return;
|
||||||
|
bidiPage.didClose();
|
||||||
|
this._bidiPages.delete(event.context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onScriptRealmDestroyed(event: bidi.Script.RealmDestroyedParameters) {
|
||||||
|
for (const page of this._bidiPages.values()) {
|
||||||
|
if (page._onRealmDestroyed(event))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BidiBrowserContext extends BrowserContext {
|
||||||
|
declare readonly _browser: BidiBrowser;
|
||||||
|
|
||||||
|
constructor(browser: BidiBrowser, browserContextId: string | undefined, options: channels.BrowserNewContextParams) {
|
||||||
|
super(browser, options, browserContextId);
|
||||||
|
this._authenticateProxyViaHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
pages(): Page[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async newPageDelegate(): Promise<PageDelegate> {
|
||||||
|
assertBrowserContextIsNotOwned(this);
|
||||||
|
const { context } = await this._browser._browserSession.send('browsingContext.create', {
|
||||||
|
type: bidi.BrowsingContext.CreateType.Window,
|
||||||
|
userContext: this._browserContextId,
|
||||||
|
});
|
||||||
|
return this._browser._bidiPages.get(context)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
async doGetCookies(urls: string[]): Promise<channels.NetworkCookie[]> {
|
||||||
|
const { cookies } = await this._browser._browserSession.send('storage.getCookies',
|
||||||
|
{ partition: { type: 'storageKey', userContext: this._browserContextId } });
|
||||||
|
return network.filterCookies(cookies.map((c: bidi.Network.Cookie) => {
|
||||||
|
const copy: channels.NetworkCookie = {
|
||||||
|
name: c.name,
|
||||||
|
value: bidiBytesValueToString(c.value),
|
||||||
|
domain: c.domain,
|
||||||
|
path: c.path,
|
||||||
|
httpOnly: c.httpOnly,
|
||||||
|
secure: c.secure,
|
||||||
|
expires: c.expiry ?? -1,
|
||||||
|
sameSite: c.sameSite ? fromBidiSameSite(c.sameSite) : 'None',
|
||||||
|
};
|
||||||
|
return copy;
|
||||||
|
}), urls);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addCookies(cookies: channels.SetNetworkCookie[]) {
|
||||||
|
cookies = network.rewriteCookies(cookies);
|
||||||
|
const promises = cookies.map((c: channels.SetNetworkCookie) => {
|
||||||
|
const cookie: bidi.Storage.PartialCookie = {
|
||||||
|
name: c.name,
|
||||||
|
value: { type: 'string', value: c.value },
|
||||||
|
domain: c.domain!,
|
||||||
|
path: c.path,
|
||||||
|
httpOnly: c.httpOnly,
|
||||||
|
secure: c.secure,
|
||||||
|
sameSite: c.sameSite && toBidiSameSite(c.sameSite),
|
||||||
|
expiry: (c.expires === -1 || c.expires === undefined) ? undefined : Math.round(c.expires),
|
||||||
|
};
|
||||||
|
return this._browser._browserSession.send('storage.setCookie',
|
||||||
|
{ cookie, partition: { type: 'storageKey', userContext: this._browserContextId } });
|
||||||
|
});
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
async doClearCookies() {
|
||||||
|
await this._browser._browserSession.send('storage.deleteCookies',
|
||||||
|
{ partition: { type: 'storageKey', userContext: this._browserContextId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async doGrantPermissions(origin: string, permissions: string[]) {
|
||||||
|
}
|
||||||
|
|
||||||
|
async doClearPermissions() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async setGeolocation(geolocation?: types.Geolocation): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async setExtraHTTPHeaders(headers: types.HeadersArray): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async setUserAgent(userAgent: string | undefined): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async setOffline(offline: boolean): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async doAddInitScript(initScript: InitScript) {
|
||||||
|
// for (const page of this.pages())
|
||||||
|
// await (page._delegate as WKPage)._updateBootstrapScript();
|
||||||
|
}
|
||||||
|
|
||||||
|
async doRemoveNonInternalInitScripts() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async doUpdateRequestInterception(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
onClosePersistent() {}
|
||||||
|
|
||||||
|
override async clearCache(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async doClose(reason: string | undefined) {
|
||||||
|
// TODO: implement for persistent context
|
||||||
|
if (!this._browserContextId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await this._browser._browserSession.send('browser.removeUserContext', {
|
||||||
|
userContext: this._browserContextId
|
||||||
|
});
|
||||||
|
this._browser._contexts.delete(this._browserContextId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelDownload(uuid: string) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromBidiSameSite(sameSite: bidi.Network.SameSite): channels.NetworkCookie['sameSite'] {
|
||||||
|
switch (sameSite) {
|
||||||
|
case 'strict': return 'Strict';
|
||||||
|
case 'lax': return 'Lax';
|
||||||
|
case 'none': return 'None';
|
||||||
|
}
|
||||||
|
return 'None';
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBidiSameSite(sameSite: channels.SetNetworkCookie['sameSite']): bidi.Network.SameSite {
|
||||||
|
switch (sameSite) {
|
||||||
|
case 'Strict': return bidi.Network.SameSite.Strict;
|
||||||
|
case 'Lax': return bidi.Network.SameSite.Lax;
|
||||||
|
case 'None': return bidi.Network.SameSite.None;
|
||||||
|
}
|
||||||
|
return bidi.Network.SameSite.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Network {
|
||||||
|
export const enum SameSite {
|
||||||
|
Strict = 'strict',
|
||||||
|
Lax = 'lax',
|
||||||
|
None = 'none',
|
||||||
|
}
|
||||||
|
}
|
||||||
161
packages/playwright-core/src/server/bidi/bidiChromium.ts
Normal file
161
packages/playwright-core/src/server/bidi/bidiChromium.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import os from 'os';
|
||||||
|
import { assert, wrapInASCIIBox } from '../../utils';
|
||||||
|
import type { Env } from '../../utils/processLauncher';
|
||||||
|
import type { BrowserOptions } from '../browser';
|
||||||
|
import { BrowserReadyState, BrowserType, kNoXServerRunningError } from '../browserType';
|
||||||
|
import { chromiumSwitches } from '../chromium/chromiumSwitches';
|
||||||
|
import type { SdkObject } from '../instrumentation';
|
||||||
|
import type { ProtocolError } from '../protocolError';
|
||||||
|
import type { ConnectionTransport } from '../transport';
|
||||||
|
import type * as types from '../types';
|
||||||
|
import { BidiBrowser } from './bidiBrowser';
|
||||||
|
import { kBrowserCloseMessageId } from './bidiConnection';
|
||||||
|
|
||||||
|
export class BidiChromium extends BrowserType {
|
||||||
|
constructor(parent: SdkObject) {
|
||||||
|
super(parent, 'bidi');
|
||||||
|
this._useBidi = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
override async connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BidiBrowser> {
|
||||||
|
// Chrome doesn't support Bidi, we create Bidi over CDP which is used by Chrome driver.
|
||||||
|
// bidiOverCdp depends on chromium-bidi which we only have in devDependencies, so
|
||||||
|
// we load bidiOverCdp dynamically.
|
||||||
|
const bidiTransport = await require('./bidiOverCdp').connectBidiOverCdp(transport);
|
||||||
|
(transport as any)[kBidiOverCdpWrapper] = bidiTransport;
|
||||||
|
return BidiBrowser.connect(this.attribution.playwright, bidiTransport, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
override doRewriteStartupLog(error: ProtocolError): ProtocolError {
|
||||||
|
if (!error.logs)
|
||||||
|
return error;
|
||||||
|
if (error.logs.includes('Missing X server'))
|
||||||
|
error.logs = '\n' + wrapInASCIIBox(kNoXServerRunningError, 1);
|
||||||
|
// These error messages are taken from Chromium source code as of July, 2020:
|
||||||
|
// https://github.com/chromium/chromium/blob/70565f67e79f79e17663ad1337dc6e63ee207ce9/content/browser/zygote_host/zygote_host_impl_linux.cc
|
||||||
|
if (!error.logs.includes('crbug.com/357670') && !error.logs.includes('No usable sandbox!') && !error.logs.includes('crbug.com/638180'))
|
||||||
|
return error;
|
||||||
|
error.logs = [
|
||||||
|
`Chromium sandboxing failed!`,
|
||||||
|
`================================`,
|
||||||
|
`To avoid the sandboxing issue, do either of the following:`,
|
||||||
|
` - (preferred): Configure your environment to support sandboxing`,
|
||||||
|
` - (alternative): Launch Chromium without sandbox using 'chromiumSandbox: false' option`,
|
||||||
|
`================================`,
|
||||||
|
``,
|
||||||
|
].join('\n');
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
override amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
override attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void {
|
||||||
|
const bidiTransport = (transport as any)[kBidiOverCdpWrapper];
|
||||||
|
if (bidiTransport)
|
||||||
|
transport = bidiTransport;
|
||||||
|
transport.send({ method: 'browser.close', params: {}, id: kBrowserCloseMessageId });
|
||||||
|
}
|
||||||
|
|
||||||
|
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
|
||||||
|
const chromeArguments = this._innerDefaultArgs(options);
|
||||||
|
chromeArguments.push(`--user-data-dir=${userDataDir}`);
|
||||||
|
chromeArguments.push('--remote-debugging-port=0');
|
||||||
|
if (isPersistent)
|
||||||
|
chromeArguments.push('about:blank');
|
||||||
|
else
|
||||||
|
chromeArguments.push('--no-startup-window');
|
||||||
|
return chromeArguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
override readyState(options: types.LaunchOptions): BrowserReadyState | undefined {
|
||||||
|
assert(options.useWebSocket);
|
||||||
|
return new ChromiumReadyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _innerDefaultArgs(options: types.LaunchOptions): string[] {
|
||||||
|
const { args = [], proxy } = options;
|
||||||
|
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
|
||||||
|
if (userDataDirArg)
|
||||||
|
throw this._createUserDataDirArgMisuseError('--user-data-dir');
|
||||||
|
if (args.find(arg => arg.startsWith('--remote-debugging-pipe')))
|
||||||
|
throw new Error('Playwright manages remote debugging connection itself.');
|
||||||
|
if (args.find(arg => !arg.startsWith('-')))
|
||||||
|
throw new Error('Arguments can not specify page to be opened');
|
||||||
|
const chromeArguments = [...chromiumSwitches];
|
||||||
|
|
||||||
|
if (os.platform() === 'darwin') {
|
||||||
|
// See https://github.com/microsoft/playwright/issues/7362
|
||||||
|
chromeArguments.push('--enable-use-zoom-for-dsf=false');
|
||||||
|
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1407025.
|
||||||
|
if (options.headless)
|
||||||
|
chromeArguments.push('--use-angle');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.devtools)
|
||||||
|
chromeArguments.push('--auto-open-devtools-for-tabs');
|
||||||
|
if (options.headless) {
|
||||||
|
if (process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW)
|
||||||
|
chromeArguments.push('--headless=new');
|
||||||
|
else
|
||||||
|
chromeArguments.push('--headless=old');
|
||||||
|
|
||||||
|
chromeArguments.push(
|
||||||
|
'--hide-scrollbars',
|
||||||
|
'--mute-audio',
|
||||||
|
'--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (options.chromiumSandbox !== true)
|
||||||
|
chromeArguments.push('--no-sandbox');
|
||||||
|
if (proxy) {
|
||||||
|
const proxyURL = new URL(proxy.server);
|
||||||
|
const isSocks = proxyURL.protocol === 'socks5:';
|
||||||
|
// https://www.chromium.org/developers/design-documents/network-settings
|
||||||
|
if (isSocks && !this.attribution.playwright.options.socksProxyPort) {
|
||||||
|
// https://www.chromium.org/developers/design-documents/network-stack/socks-proxy
|
||||||
|
chromeArguments.push(`--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE ${proxyURL.hostname}"`);
|
||||||
|
}
|
||||||
|
chromeArguments.push(`--proxy-server=${proxy.server}`);
|
||||||
|
const proxyBypassRules = [];
|
||||||
|
// https://source.chromium.org/chromium/chromium/src/+/master:net/docs/proxy.md;l=548;drc=71698e610121078e0d1a811054dcf9fd89b49578
|
||||||
|
if (this.attribution.playwright.options.socksProxyPort)
|
||||||
|
proxyBypassRules.push('<-loopback>');
|
||||||
|
if (proxy.bypass)
|
||||||
|
proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t));
|
||||||
|
if (!process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK && !proxyBypassRules.includes('<-loopback>'))
|
||||||
|
proxyBypassRules.push('<-loopback>');
|
||||||
|
if (proxyBypassRules.length > 0)
|
||||||
|
chromeArguments.push(`--proxy-bypass-list=${proxyBypassRules.join(';')}`);
|
||||||
|
}
|
||||||
|
chromeArguments.push(...args);
|
||||||
|
return chromeArguments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChromiumReadyState extends BrowserReadyState {
|
||||||
|
override onBrowserOutput(message: string): void {
|
||||||
|
const match = message.match(/DevTools listening on (.*)/);
|
||||||
|
if (match)
|
||||||
|
this._wsEndpoint.resolve(match[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const kBidiOverCdpWrapper = Symbol('kBidiConnectionWrapper');
|
||||||
232
packages/playwright-core/src/server/bidi/bidiConnection.ts
Normal file
232
packages/playwright-core/src/server/bidi/bidiConnection.ts
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { assert } from '../../utils';
|
||||||
|
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||||
|
import type { RecentLogsCollector } from '../../utils/debugLogger';
|
||||||
|
import { debugLogger } from '../../utils/debugLogger';
|
||||||
|
import type { ProtocolLogger } from '../types';
|
||||||
|
import { helper } from '../helper';
|
||||||
|
import { ProtocolError } from '../protocolError';
|
||||||
|
import type * as bidi from './third_party/bidiProtocol';
|
||||||
|
import type * as bidiCommands from './third_party/bidiCommands';
|
||||||
|
|
||||||
|
// BidiPlaywright uses this special id to issue Browser.close command which we
|
||||||
|
// should ignore.
|
||||||
|
export const kBrowserCloseMessageId = 0;
|
||||||
|
|
||||||
|
export class BidiConnection {
|
||||||
|
private readonly _transport: ConnectionTransport;
|
||||||
|
private readonly _onDisconnect: () => void;
|
||||||
|
private readonly _protocolLogger: ProtocolLogger;
|
||||||
|
private readonly _browserLogsCollector: RecentLogsCollector;
|
||||||
|
_browserDisconnectedLogs: string | undefined;
|
||||||
|
private _lastId = 0;
|
||||||
|
private _closed = false;
|
||||||
|
readonly browserSession: BidiSession;
|
||||||
|
readonly _browsingContextToSession = new Map<string, BidiSession>();
|
||||||
|
|
||||||
|
constructor(transport: ConnectionTransport, onDisconnect: () => void, protocolLogger: ProtocolLogger, browserLogsCollector: RecentLogsCollector) {
|
||||||
|
this._transport = transport;
|
||||||
|
this._onDisconnect = onDisconnect;
|
||||||
|
this._protocolLogger = protocolLogger;
|
||||||
|
this._browserLogsCollector = browserLogsCollector;
|
||||||
|
this.browserSession = new BidiSession(this, '', (message: any) => {
|
||||||
|
this.rawSend(message);
|
||||||
|
});
|
||||||
|
this._transport.onmessage = this._dispatchMessage.bind(this);
|
||||||
|
// onclose should be set last, since it can be immediately called.
|
||||||
|
this._transport.onclose = this._onClose.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextMessageId(): number {
|
||||||
|
return ++this._lastId;
|
||||||
|
}
|
||||||
|
|
||||||
|
rawSend(message: ProtocolRequest) {
|
||||||
|
this._protocolLogger('send', message);
|
||||||
|
this._transport.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dispatchMessage(message: ProtocolResponse) {
|
||||||
|
this._protocolLogger('receive', message);
|
||||||
|
const object = message as bidi.Message;
|
||||||
|
// Bidi messages do not have a common session identifier, so we
|
||||||
|
// route them based on BrowsingContext.
|
||||||
|
if (object.type === 'event') {
|
||||||
|
// Route page events to the right session.
|
||||||
|
let context;
|
||||||
|
if ('context' in object.params)
|
||||||
|
context = object.params.context;
|
||||||
|
else if (object.method === 'log.entryAdded')
|
||||||
|
context = object.params.source?.context;
|
||||||
|
if (context) {
|
||||||
|
const session = this._browsingContextToSession.get(context);
|
||||||
|
if (session) {
|
||||||
|
session.dispatchMessage(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (message.id) {
|
||||||
|
// Find caller session.
|
||||||
|
for (const session of this._browsingContextToSession.values()) {
|
||||||
|
if (session.hasCallback(message.id)) {
|
||||||
|
session.dispatchMessage(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.browserSession.dispatchMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onClose(reason?: string) {
|
||||||
|
this._closed = true;
|
||||||
|
this._transport.onmessage = undefined;
|
||||||
|
this._transport.onclose = undefined;
|
||||||
|
this._browserDisconnectedLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs(), reason);
|
||||||
|
this.browserSession.dispose();
|
||||||
|
this._onDisconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
isClosed() {
|
||||||
|
return this._closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (!this._closed)
|
||||||
|
this._transport.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
createMainFrameBrowsingContextSession(bowsingContextId: bidi.BrowsingContext.BrowsingContext): BidiSession {
|
||||||
|
const result = new BidiSession(this, bowsingContextId, message => this.rawSend(message));
|
||||||
|
this._browsingContextToSession.set(bowsingContextId, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type BidiEvents = {
|
||||||
|
[K in bidi.Event['method']]: Extract<bidi.Event, {method: K}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class BidiSession extends EventEmitter {
|
||||||
|
readonly connection: BidiConnection;
|
||||||
|
readonly sessionId: string;
|
||||||
|
|
||||||
|
private _disposed = false;
|
||||||
|
private readonly _rawSend: (message: any) => void;
|
||||||
|
private readonly _callbacks = new Map<number, { resolve: (o: any) => void, reject: (e: ProtocolError) => void, error: ProtocolError }>();
|
||||||
|
private _crashed: boolean = false;
|
||||||
|
private readonly _browsingContexts = new Set<string>();
|
||||||
|
|
||||||
|
override on: <T extends keyof BidiEvents | symbol>(event: T, listener: (payload: T extends symbol ? any : BidiEvents[T extends keyof BidiEvents ? T : never]['params']) => void) => this;
|
||||||
|
override addListener: <T extends keyof BidiEvents | symbol>(event: T, listener: (payload: T extends symbol ? any : BidiEvents[T extends keyof BidiEvents ? T : never]['params']) => void) => this;
|
||||||
|
override off: <T extends keyof BidiEvents | symbol>(event: T, listener: (payload: T extends symbol ? any : BidiEvents[T extends keyof BidiEvents ? T : never]['params']) => void) => this;
|
||||||
|
override removeListener: <T extends keyof BidiEvents | symbol>(event: T, listener: (payload: T extends symbol ? any : BidiEvents[T extends keyof BidiEvents ? T : never]['params']) => void) => this;
|
||||||
|
override once: <T extends keyof BidiEvents | symbol>(event: T, listener: (payload: T extends symbol ? any : BidiEvents[T extends keyof BidiEvents ? T : never]['params']) => void) => this;
|
||||||
|
|
||||||
|
constructor(connection: BidiConnection, sessionId: string, rawSend: (message: any) => void) {
|
||||||
|
super();
|
||||||
|
this.setMaxListeners(0);
|
||||||
|
this.connection = connection;
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
this._rawSend = rawSend;
|
||||||
|
|
||||||
|
this.on = super.on;
|
||||||
|
this.off = super.removeListener;
|
||||||
|
this.addListener = super.addListener;
|
||||||
|
this.removeListener = super.removeListener;
|
||||||
|
this.once = super.once;
|
||||||
|
}
|
||||||
|
|
||||||
|
addFrameBrowsingContext(context: string) {
|
||||||
|
this._browsingContexts.add(context);
|
||||||
|
this.connection._browsingContextToSession.set(context, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFrameBrowsingContext(context: string) {
|
||||||
|
this._browsingContexts.delete(context);
|
||||||
|
this.connection._browsingContextToSession.delete(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async send<T extends keyof bidiCommands.Commands>(
|
||||||
|
method: T,
|
||||||
|
params?: bidiCommands.Commands[T]['params']
|
||||||
|
): Promise<bidiCommands.Commands[T]['returnType']> {
|
||||||
|
if (this._crashed || this._disposed || this.connection._browserDisconnectedLogs)
|
||||||
|
throw new ProtocolError(this._crashed ? 'crashed' : 'closed', undefined, this.connection._browserDisconnectedLogs);
|
||||||
|
const id = this.connection.nextMessageId();
|
||||||
|
const messageObj = { id, method, params };
|
||||||
|
this._rawSend(messageObj);
|
||||||
|
return new Promise<bidiCommands.Commands[T]['returnType']>((resolve, reject) => {
|
||||||
|
this._callbacks.set(id, { resolve, reject, error: new ProtocolError('error', method) });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMayFail<T extends keyof bidiCommands.Commands>(method: T, params?: bidiCommands.Commands[T]['params']): Promise<bidiCommands.Commands[T]['returnType'] | void> {
|
||||||
|
return this.send(method, params).catch(error => debugLogger.log('error', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
markAsCrashed() {
|
||||||
|
this._crashed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDisposed(): boolean {
|
||||||
|
return this._disposed;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this._disposed = true;
|
||||||
|
this.connection._browsingContextToSession.delete(this.sessionId);
|
||||||
|
for (const context of this._browsingContexts)
|
||||||
|
this.connection._browsingContextToSession.delete(context);
|
||||||
|
this._browsingContexts.clear();
|
||||||
|
for (const callback of this._callbacks.values()) {
|
||||||
|
callback.error.type = this._crashed ? 'crashed' : 'closed';
|
||||||
|
callback.error.logs = this.connection._browserDisconnectedLogs;
|
||||||
|
callback.reject(callback.error);
|
||||||
|
}
|
||||||
|
this._callbacks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCallback(id: number): boolean {
|
||||||
|
return this._callbacks.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchMessage(message: any) {
|
||||||
|
const object = message as bidi.Message;
|
||||||
|
if (object.id === kBrowserCloseMessageId)
|
||||||
|
return;
|
||||||
|
if (object.id && this._callbacks.has(object.id)) {
|
||||||
|
const callback = this._callbacks.get(object.id)!;
|
||||||
|
this._callbacks.delete(object.id);
|
||||||
|
if (object.type === 'error') {
|
||||||
|
callback.error.setMessage(object.error + '\nMessage: ' + object.message);
|
||||||
|
callback.reject(callback.error);
|
||||||
|
} else if (object.type === 'success') {
|
||||||
|
callback.resolve(object.result);
|
||||||
|
} else {
|
||||||
|
callback.error.setMessage('Internal error, unexpected response type: ' + JSON.stringify(object));
|
||||||
|
callback.reject(callback.error);
|
||||||
|
}
|
||||||
|
} else if (object.id) {
|
||||||
|
// Response might come after session has been disposed and rejected all callbacks.
|
||||||
|
assert(this.isDisposed());
|
||||||
|
} else {
|
||||||
|
Promise.resolve().then(() => this.emit(object.method, object.params));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
167
packages/playwright-core/src/server/bidi/bidiExecutionContext.ts
Normal file
167
packages/playwright-core/src/server/bidi/bidiExecutionContext.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { parseEvaluationResultValue } from '../isomorphic/utilityScriptSerializers';
|
||||||
|
import * as js from '../javascript';
|
||||||
|
import type { BidiSession } from './bidiConnection';
|
||||||
|
import { BidiDeserializer } from './third_party/bidiDeserializer';
|
||||||
|
import * as bidi from './third_party/bidiProtocol';
|
||||||
|
import { BidiSerializer } from './third_party/bidiSerializer';
|
||||||
|
|
||||||
|
export class BidiExecutionContext implements js.ExecutionContextDelegate {
|
||||||
|
private readonly _session: BidiSession;
|
||||||
|
private readonly _target: bidi.Script.Target;
|
||||||
|
|
||||||
|
constructor(session: BidiSession, realmInfo: bidi.Script.RealmInfo) {
|
||||||
|
this._session = session;
|
||||||
|
if (realmInfo.type === 'window') {
|
||||||
|
// Simple realm does not seem to work for Window contexts.
|
||||||
|
this._target = {
|
||||||
|
context: realmInfo.context,
|
||||||
|
sandbox: realmInfo.sandbox,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this._target = {
|
||||||
|
realm: realmInfo.realm
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async rawEvaluateJSON(expression: string): Promise<any> {
|
||||||
|
const response = await this._session.send('script.evaluate', {
|
||||||
|
expression,
|
||||||
|
target: this._target,
|
||||||
|
serializationOptions: {
|
||||||
|
maxObjectDepth: 10,
|
||||||
|
maxDomDepth: 10,
|
||||||
|
},
|
||||||
|
awaitPromise: true,
|
||||||
|
userActivation: true,
|
||||||
|
});
|
||||||
|
if (response.type === 'success')
|
||||||
|
return BidiDeserializer.deserialize(response.result);
|
||||||
|
if (response.type === 'exception')
|
||||||
|
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
|
||||||
|
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> {
|
||||||
|
const response = await this._session.send('script.evaluate', {
|
||||||
|
expression,
|
||||||
|
target: this._target,
|
||||||
|
resultOwnership: bidi.Script.ResultOwnership.Root, // Necessary for the handle to be returned.
|
||||||
|
serializationOptions: { maxObjectDepth: 0, maxDomDepth: 0 },
|
||||||
|
awaitPromise: true,
|
||||||
|
userActivation: true,
|
||||||
|
});
|
||||||
|
if (response.type === 'success') {
|
||||||
|
if ('handle' in response.result)
|
||||||
|
return response.result.handle!;
|
||||||
|
throw new js.JavaScriptErrorInEvaluate('Cannot get handle: ' + JSON.stringify(response.result));
|
||||||
|
}
|
||||||
|
if (response.type === 'exception')
|
||||||
|
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
|
||||||
|
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
rawCallFunctionNoReply(func: Function, ...args: any[]) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async evaluateWithArguments(functionDeclaration: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
||||||
|
const response = await this._session.send('script.callFunction', {
|
||||||
|
functionDeclaration,
|
||||||
|
target: this._target,
|
||||||
|
arguments: [
|
||||||
|
{ handle: utilityScript._objectId! },
|
||||||
|
...values.map(BidiSerializer.serialize),
|
||||||
|
...objectIds.map(handle => ({ handle })),
|
||||||
|
],
|
||||||
|
resultOwnership: returnByValue ? undefined : bidi.Script.ResultOwnership.Root, // Necessary for the handle to be returned.
|
||||||
|
serializationOptions: returnByValue ? {} : { maxObjectDepth: 0, maxDomDepth: 0 },
|
||||||
|
awaitPromise: true,
|
||||||
|
userActivation: true,
|
||||||
|
});
|
||||||
|
if (response.type === 'exception')
|
||||||
|
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
|
||||||
|
if (response.type === 'success') {
|
||||||
|
if (returnByValue)
|
||||||
|
return parseEvaluationResultValue(BidiDeserializer.deserialize(response.result));
|
||||||
|
const objectId = 'handle' in response.result ? response.result.handle : undefined ;
|
||||||
|
return utilityScript._context.createHandle({ objectId, ...response.result });
|
||||||
|
}
|
||||||
|
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProperties(context: js.ExecutionContext, objectId: js.ObjectId): Promise<Map<string, js.JSHandle>> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
createHandle(context: js.ExecutionContext, jsRemoteObject: js.RemoteObject): js.JSHandle {
|
||||||
|
const remoteObject: bidi.Script.RemoteValue = jsRemoteObject as bidi.Script.RemoteValue;
|
||||||
|
return new js.JSHandle(context, remoteObject.type, renderPreview(remoteObject), jsRemoteObject.objectId, remoteObjectValue(remoteObject));
|
||||||
|
}
|
||||||
|
|
||||||
|
async releaseHandle(objectId: js.ObjectId): Promise<void> {
|
||||||
|
await this._session.send('script.disown', {
|
||||||
|
target: this._target,
|
||||||
|
handles: [objectId],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
objectCount(objectId: js.ObjectId): Promise<number> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async rawCallFunction(functionDeclaration: string, arg: bidi.Script.LocalValue): Promise<bidi.Script.RemoteValue> {
|
||||||
|
const response = await this._session.send('script.callFunction', {
|
||||||
|
functionDeclaration,
|
||||||
|
target: this._target,
|
||||||
|
arguments: [arg],
|
||||||
|
resultOwnership: bidi.Script.ResultOwnership.Root, // Necessary for the handle to be returned.
|
||||||
|
serializationOptions: { maxObjectDepth: 0, maxDomDepth: 0 },
|
||||||
|
awaitPromise: true,
|
||||||
|
userActivation: true,
|
||||||
|
});
|
||||||
|
if (response.type === 'exception')
|
||||||
|
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
|
||||||
|
if (response.type === 'success')
|
||||||
|
return response.result;
|
||||||
|
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPreview(remoteObject: bidi.Script.RemoteValue): string | undefined {
|
||||||
|
if (remoteObject.type === 'undefined')
|
||||||
|
return 'undefined';
|
||||||
|
if (remoteObject.type === 'null')
|
||||||
|
return 'null';
|
||||||
|
if ('value' in remoteObject)
|
||||||
|
return String(remoteObject.value);
|
||||||
|
return `<${remoteObject.type}>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function remoteObjectValue(remoteObject: bidi.Script.RemoteValue): any {
|
||||||
|
if (remoteObject.type === 'undefined')
|
||||||
|
return undefined;
|
||||||
|
if (remoteObject.type === 'null')
|
||||||
|
return null;
|
||||||
|
if (remoteObject.type === 'number' && typeof remoteObject.value === 'string')
|
||||||
|
return js.parseUnserializableValue(remoteObject.value);
|
||||||
|
if ('value' in remoteObject)
|
||||||
|
return remoteObject.value;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
101
packages/playwright-core/src/server/bidi/bidiFirefox.ts
Normal file
101
packages/playwright-core/src/server/bidi/bidiFirefox.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
import { assert, wrapInASCIIBox } from '../../utils';
|
||||||
|
import type { Env } from '../../utils/processLauncher';
|
||||||
|
import type { BrowserOptions } from '../browser';
|
||||||
|
import { BrowserReadyState, BrowserType, kNoXServerRunningError } from '../browserType';
|
||||||
|
import type { SdkObject } from '../instrumentation';
|
||||||
|
import type { ProtocolError } from '../protocolError';
|
||||||
|
import type { ConnectionTransport } from '../transport';
|
||||||
|
import type * as types from '../types';
|
||||||
|
import { BidiBrowser } from './bidiBrowser';
|
||||||
|
import { kBrowserCloseMessageId } from './bidiConnection';
|
||||||
|
|
||||||
|
export class BidiFirefox extends BrowserType {
|
||||||
|
constructor(parent: SdkObject) {
|
||||||
|
super(parent, 'bidi');
|
||||||
|
this._useBidi = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
override async connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BidiBrowser> {
|
||||||
|
return BidiBrowser.connect(this.attribution.playwright, transport, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
override doRewriteStartupLog(error: ProtocolError): ProtocolError {
|
||||||
|
if (!error.logs)
|
||||||
|
return error;
|
||||||
|
// https://github.com/microsoft/playwright/issues/6500
|
||||||
|
if (error.logs.includes(`as root in a regular user's session is not supported.`))
|
||||||
|
error.logs = '\n' + wrapInASCIIBox(`Firefox is unable to launch if the $HOME folder isn't owned by the current user.\nWorkaround: Set the HOME=/root environment variable${process.env.GITHUB_ACTION ? ' in your GitHub Actions workflow file' : ''} when running Playwright.`, 1);
|
||||||
|
if (error.logs.includes('no DISPLAY environment variable specified'))
|
||||||
|
error.logs = '\n' + wrapInASCIIBox(kNoXServerRunningError, 1);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
override amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {
|
||||||
|
if (!path.isAbsolute(os.homedir()))
|
||||||
|
throw new Error(`Cannot launch Firefox with relative home directory. Did you set ${os.platform() === 'win32' ? 'USERPROFILE' : 'HOME'} to a relative path?`);
|
||||||
|
if (os.platform() === 'linux') {
|
||||||
|
// Always remove SNAP_NAME and SNAP_INSTANCE_NAME env variables since they
|
||||||
|
// confuse Firefox: in our case, builds never come from SNAP.
|
||||||
|
// See https://github.com/microsoft/playwright/issues/20555
|
||||||
|
return { ...env, SNAP_NAME: undefined, SNAP_INSTANCE_NAME: undefined };
|
||||||
|
}
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
override attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void {
|
||||||
|
transport.send({ method: 'browser.close', params: {}, id: kBrowserCloseMessageId });
|
||||||
|
}
|
||||||
|
|
||||||
|
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
|
||||||
|
const { args = [], headless } = options;
|
||||||
|
const userDataDirArg = args.find(arg => arg.startsWith('-profile') || arg.startsWith('--profile'));
|
||||||
|
if (userDataDirArg)
|
||||||
|
throw this._createUserDataDirArgMisuseError('--profile');
|
||||||
|
const firefoxArguments = ['--remote-debugging-port=0'];
|
||||||
|
if (headless)
|
||||||
|
firefoxArguments.push('--headless');
|
||||||
|
else
|
||||||
|
firefoxArguments.push('--foreground');
|
||||||
|
firefoxArguments.push(`--profile`, userDataDir);
|
||||||
|
firefoxArguments.push(...args);
|
||||||
|
// TODO: make ephemeral context work without this argument.
|
||||||
|
firefoxArguments.push('about:blank');
|
||||||
|
// if (isPersistent)
|
||||||
|
// firefoxArguments.push('about:blank');
|
||||||
|
// else
|
||||||
|
// firefoxArguments.push('-silent');
|
||||||
|
return firefoxArguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
override readyState(options: types.LaunchOptions): BrowserReadyState | undefined {
|
||||||
|
assert(options.useWebSocket);
|
||||||
|
return new FirefoxReadyState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FirefoxReadyState extends BrowserReadyState {
|
||||||
|
override onBrowserOutput(message: string): void {
|
||||||
|
// Bidi WebSocket in Firefox.
|
||||||
|
const match = message.match(/WebDriver BiDi listening on (ws:\/\/.*)$/);
|
||||||
|
if (match)
|
||||||
|
this._wsEndpoint.resolve(match[1] + '/session');
|
||||||
|
}
|
||||||
|
}
|
||||||
149
packages/playwright-core/src/server/bidi/bidiInput.ts
Normal file
149
packages/playwright-core/src/server/bidi/bidiInput.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as input from '../input';
|
||||||
|
import type * as types from '../types';
|
||||||
|
import type { BidiSession } from './bidiConnection';
|
||||||
|
import * as bidi from './third_party/bidiProtocol';
|
||||||
|
import { getBidiKeyValue } from './third_party/bidiKeyboard';
|
||||||
|
|
||||||
|
export class RawKeyboardImpl implements input.RawKeyboard {
|
||||||
|
private _session: BidiSession;
|
||||||
|
|
||||||
|
constructor(session: BidiSession) {
|
||||||
|
this._session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSession(session: BidiSession) {
|
||||||
|
this._session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
async keydown(modifiers: Set<types.KeyboardModifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise<void> {
|
||||||
|
const actions: bidi.Input.KeySourceAction[] = [];
|
||||||
|
actions.push({ type: 'keyDown', value: getBidiKeyValue(key) });
|
||||||
|
// TODO: add modifiers?
|
||||||
|
await this._performActions(actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async keyup(modifiers: Set<types.KeyboardModifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number): Promise<void> {
|
||||||
|
const actions: bidi.Input.KeySourceAction[] = [];
|
||||||
|
actions.push({ type: 'keyUp', value: getBidiKeyValue(key) });
|
||||||
|
await this._performActions(actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendText(text: string): Promise<void> {
|
||||||
|
const actions: bidi.Input.KeySourceAction[] = [];
|
||||||
|
for (const char of text) {
|
||||||
|
const value = getBidiKeyValue(char);
|
||||||
|
actions.push({ type: 'keyDown', value });
|
||||||
|
actions.push({ type: 'keyUp', value });
|
||||||
|
}
|
||||||
|
await this._performActions(actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _performActions(actions: bidi.Input.KeySourceAction[]) {
|
||||||
|
await this._session.send('input.performActions', {
|
||||||
|
context: this._session.sessionId,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'key',
|
||||||
|
id: 'pw_keyboard',
|
||||||
|
actions,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RawMouseImpl implements input.RawMouse {
|
||||||
|
private readonly _session: BidiSession;
|
||||||
|
|
||||||
|
constructor(session: BidiSession) {
|
||||||
|
this._session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
async move(x: number, y: number, button: types.MouseButton | 'none', buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, forClick: boolean): Promise<void> {
|
||||||
|
// TODO: bidi throws when x/y are not integers.
|
||||||
|
x = Math.round(x);
|
||||||
|
y = Math.round(y);
|
||||||
|
await this._performActions([{ type: 'pointerMove', x, y }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(x: number, y: number, button: types.MouseButton, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, clickCount: number): Promise<void> {
|
||||||
|
await this._performActions([{ type: 'pointerDown', button: toBidiButton(button) }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async up(x: number, y: number, button: types.MouseButton, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, clickCount: number): Promise<void> {
|
||||||
|
await this._performActions([{ type: 'pointerUp', button: toBidiButton(button) }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async click(x: number, y: number, options: { delay?: number, button?: types.MouseButton, clickCount?: number } = {}) {
|
||||||
|
x = Math.round(x);
|
||||||
|
y = Math.round(y);
|
||||||
|
const button = toBidiButton(options.button || 'left');
|
||||||
|
const { delay = null, clickCount = 1 } = options;
|
||||||
|
const actions: bidi.Input.PointerSourceAction[] = [];
|
||||||
|
actions.push({ type: 'pointerMove', x, y });
|
||||||
|
for (let cc = 1; cc <= clickCount; ++cc) {
|
||||||
|
actions.push({ type: 'pointerDown', button });
|
||||||
|
if (delay)
|
||||||
|
actions.push({ type: 'pause', duration: delay });
|
||||||
|
actions.push({ type: 'pointerUp', button });
|
||||||
|
if (delay && cc < clickCount)
|
||||||
|
actions.push({ type: 'pause', duration: delay });
|
||||||
|
}
|
||||||
|
await this._performActions(actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async wheel(x: number, y: number, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, deltaX: number, deltaY: number): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _performActions(actions: bidi.Input.PointerSourceAction[]) {
|
||||||
|
await this._session.send('input.performActions', {
|
||||||
|
context: this._session.sessionId,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'pointer',
|
||||||
|
id: 'pw_mouse',
|
||||||
|
parameters: {
|
||||||
|
pointerType: bidi.Input.PointerType.Mouse,
|
||||||
|
},
|
||||||
|
actions,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RawTouchscreenImpl implements input.RawTouchscreen {
|
||||||
|
private readonly _session: BidiSession;
|
||||||
|
|
||||||
|
constructor(session: BidiSession) {
|
||||||
|
this._session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tap(x: number, y: number, modifiers: Set<types.KeyboardModifier>) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBidiButton(button: string): number {
|
||||||
|
switch (button) {
|
||||||
|
case 'left': return 0;
|
||||||
|
case 'right': return 2;
|
||||||
|
case 'middle': return 1;
|
||||||
|
}
|
||||||
|
throw new Error('Unknown button: ' + button);
|
||||||
|
}
|
||||||
316
packages/playwright-core/src/server/bidi/bidiNetworkManager.ts
Normal file
316
packages/playwright-core/src/server/bidi/bidiNetworkManager.ts
Normal file
|
|
@ -0,0 +1,316 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { RegisteredListener } from '../../utils/eventsHelper';
|
||||||
|
import { eventsHelper } from '../../utils/eventsHelper';
|
||||||
|
import type { Page } from '../page';
|
||||||
|
import * as network from '../network';
|
||||||
|
import type * as frames from '../frames';
|
||||||
|
import type * as types from '../types';
|
||||||
|
import * as bidi from './third_party/bidiProtocol';
|
||||||
|
import type { BidiSession } from './bidiConnection';
|
||||||
|
|
||||||
|
|
||||||
|
export class BidiNetworkManager {
|
||||||
|
private readonly _session: BidiSession;
|
||||||
|
private readonly _requests: Map<string, BidiRequest>;
|
||||||
|
private readonly _page: Page;
|
||||||
|
private readonly _eventListeners: RegisteredListener[];
|
||||||
|
private readonly _onNavigationResponseStarted: (params: bidi.Network.ResponseStartedParameters) => void;
|
||||||
|
private _userRequestInterceptionEnabled: boolean = false;
|
||||||
|
private _protocolRequestInterceptionEnabled: boolean = false;
|
||||||
|
private _credentials: types.Credentials | undefined;
|
||||||
|
private _intercepId: bidi.Network.Intercept | undefined;
|
||||||
|
|
||||||
|
constructor(bidiSession: BidiSession, page: Page, onNavigationResponseStarted: (params: bidi.Network.ResponseStartedParameters) => void) {
|
||||||
|
this._session = bidiSession;
|
||||||
|
this._requests = new Map();
|
||||||
|
this._page = page;
|
||||||
|
this._onNavigationResponseStarted = onNavigationResponseStarted;
|
||||||
|
this._eventListeners = [
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'network.beforeRequestSent', this._onBeforeRequestSent.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'network.responseStarted', this._onResponseStarted.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'network.responseCompleted', this._onResponseCompleted.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'network.fetchError', this._onFetchError.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'network.authRequired', this._onAuthRequired.bind(this)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
eventsHelper.removeEventListeners(this._eventListeners);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onBeforeRequestSent(param: bidi.Network.BeforeRequestSentParameters) {
|
||||||
|
if (param.request.url.startsWith('data:'))
|
||||||
|
return;
|
||||||
|
const redirectedFrom = param.redirectCount ? (this._requests.get(param.request.request) || null) : null;
|
||||||
|
const frame = redirectedFrom ? redirectedFrom.request.frame() : (param.context ? this._page._frameManager.frame(param.context) : null);
|
||||||
|
if (!frame)
|
||||||
|
return;
|
||||||
|
if (redirectedFrom)
|
||||||
|
this._requests.delete(redirectedFrom._id);
|
||||||
|
let route;
|
||||||
|
if (param.intercepts) {
|
||||||
|
// We do not support intercepting redirects.
|
||||||
|
if (redirectedFrom) {
|
||||||
|
this._session.sendMayFail('network.continueRequest', {
|
||||||
|
request: param.request.request,
|
||||||
|
headers: redirectedFrom._originalRequestRoute?._alreadyContinuedHeaders,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
route = new BidiRouteImpl(this._session, param.request.request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const request = new BidiRequest(frame, redirectedFrom, param, route);
|
||||||
|
this._requests.set(request._id, request);
|
||||||
|
this._page._frameManager.requestStarted(request.request, route);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onResponseStarted(params: bidi.Network.ResponseStartedParameters) {
|
||||||
|
const request = this._requests.get(params.request.request);
|
||||||
|
if (!request)
|
||||||
|
return;
|
||||||
|
const getResponseBody = async () => {
|
||||||
|
throw new Error(`Response body is not available for requests in Bidi`);
|
||||||
|
};
|
||||||
|
const timings = params.request.timings;
|
||||||
|
const startTime = timings.requestTime;
|
||||||
|
function relativeToStart(time: number): number {
|
||||||
|
if (!time)
|
||||||
|
return -1;
|
||||||
|
return (time - startTime) / 1000;
|
||||||
|
}
|
||||||
|
const timing: network.ResourceTiming = {
|
||||||
|
startTime: startTime / 1000,
|
||||||
|
requestStart: relativeToStart(timings.requestStart),
|
||||||
|
responseStart: relativeToStart(timings.responseStart),
|
||||||
|
domainLookupStart: relativeToStart(timings.dnsStart),
|
||||||
|
domainLookupEnd: relativeToStart(timings.dnsEnd),
|
||||||
|
connectStart: relativeToStart(timings.connectStart),
|
||||||
|
secureConnectionStart: relativeToStart(timings.tlsStart),
|
||||||
|
connectEnd: relativeToStart(timings.connectEnd),
|
||||||
|
};
|
||||||
|
const response = new network.Response(request.request, params.response.status, params.response.statusText, fromBidiHeaders(params.response.headers), timing, getResponseBody, false);
|
||||||
|
response._serverAddrFinished();
|
||||||
|
response._securityDetailsFinished();
|
||||||
|
// "raw" headers are the same as "provisional" headers in Bidi.
|
||||||
|
response.setRawResponseHeaders(null);
|
||||||
|
response.setResponseHeadersSize(params.response.headersSize);
|
||||||
|
this._page._frameManager.requestReceivedResponse(response);
|
||||||
|
if (params.navigation)
|
||||||
|
this._onNavigationResponseStarted(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onResponseCompleted(params: bidi.Network.ResponseCompletedParameters) {
|
||||||
|
const request = this._requests.get(params.request.request);
|
||||||
|
if (!request)
|
||||||
|
return;
|
||||||
|
const response = request.request._existingResponse()!;
|
||||||
|
// TODO: body size is the encoded size
|
||||||
|
response.setTransferSize(params.response.bodySize);
|
||||||
|
response.setEncodedBodySize(params.response.bodySize);
|
||||||
|
|
||||||
|
// Keep redirected requests in the map for future reference as redirectedFrom.
|
||||||
|
const isRedirected = response.status() >= 300 && response.status() <= 399;
|
||||||
|
const responseEndTime = params.request.timings.responseEnd / 1000 - response.timing().startTime;
|
||||||
|
if (isRedirected) {
|
||||||
|
response._requestFinished(responseEndTime);
|
||||||
|
} else {
|
||||||
|
this._requests.delete(request._id);
|
||||||
|
response._requestFinished(responseEndTime);
|
||||||
|
}
|
||||||
|
response._setHttpVersion(params.response.protocol);
|
||||||
|
this._page._frameManager.reportRequestFinished(request.request, response);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onFetchError(params: bidi.Network.FetchErrorParameters) {
|
||||||
|
const request = this._requests.get(params.request.request);
|
||||||
|
if (!request)
|
||||||
|
return;
|
||||||
|
this._requests.delete(request._id);
|
||||||
|
const response = request.request._existingResponse();
|
||||||
|
if (response) {
|
||||||
|
response.setTransferSize(null);
|
||||||
|
response.setEncodedBodySize(null);
|
||||||
|
response._requestFinished(-1);
|
||||||
|
}
|
||||||
|
request.request._setFailureText(params.errorText);
|
||||||
|
// TODO: support canceled flag
|
||||||
|
this._page._frameManager.requestFailed(request.request, params.errorText === 'NS_BINDING_ABORTED');
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onAuthRequired(params: bidi.Network.AuthRequiredParameters) {
|
||||||
|
const isBasic = params.response.authChallenges?.some(challenge => challenge.scheme.startsWith('Basic'));
|
||||||
|
const credentials = this._page._browserContext._options.httpCredentials;
|
||||||
|
if (isBasic && credentials) {
|
||||||
|
this._session.sendMayFail('network.continueWithAuth', {
|
||||||
|
request: params.request.request,
|
||||||
|
action: 'provideCredentials',
|
||||||
|
credentials: {
|
||||||
|
type: 'password',
|
||||||
|
username: credentials.username,
|
||||||
|
password: credentials.password,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._session.sendMayFail('network.continueWithAuth', {
|
||||||
|
request: params.request.request,
|
||||||
|
action: 'default',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setRequestInterception(value: boolean) {
|
||||||
|
this._userRequestInterceptionEnabled = value;
|
||||||
|
await this._updateProtocolRequestInterception();
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCredentials(credentials: types.Credentials | undefined) {
|
||||||
|
this._credentials = credentials;
|
||||||
|
await this._updateProtocolRequestInterception();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _updateProtocolRequestInterception(initial?: boolean) {
|
||||||
|
const enabled = this._userRequestInterceptionEnabled || !!this._credentials;
|
||||||
|
if (enabled === this._protocolRequestInterceptionEnabled)
|
||||||
|
return;
|
||||||
|
this._protocolRequestInterceptionEnabled = enabled;
|
||||||
|
if (initial && !enabled)
|
||||||
|
return;
|
||||||
|
const cachePromise = this._session.send('network.setCacheBehavior', { cacheBehavior: enabled ? 'bypass' : 'default' });
|
||||||
|
let interceptPromise = Promise.resolve<any>(undefined);
|
||||||
|
if (enabled) {
|
||||||
|
interceptPromise = this._session.send('network.addIntercept', {
|
||||||
|
phases: [bidi.Network.InterceptPhase.AuthRequired, bidi.Network.InterceptPhase.BeforeRequestSent],
|
||||||
|
urlPatterns: [{ type: 'pattern' }],
|
||||||
|
// urlPatterns: [{ type: 'string', pattern: '*' }],
|
||||||
|
}).then(r => {
|
||||||
|
this._intercepId = r.intercept;
|
||||||
|
});
|
||||||
|
} else if (this._intercepId) {
|
||||||
|
interceptPromise = this._session.send('network.removeIntercept', { intercept: this._intercepId });
|
||||||
|
this._intercepId = undefined;
|
||||||
|
}
|
||||||
|
await Promise.all([cachePromise, interceptPromise]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BidiRequest {
|
||||||
|
readonly request: network.Request;
|
||||||
|
readonly _id: string;
|
||||||
|
private _redirectedTo: BidiRequest | undefined;
|
||||||
|
// Only first request in the chain can be intercepted, so this will
|
||||||
|
// store the first and only Route in the chain (if any).
|
||||||
|
_originalRequestRoute: BidiRouteImpl | undefined;
|
||||||
|
|
||||||
|
constructor(frame: frames.Frame, redirectedFrom: BidiRequest | null, payload: bidi.Network.BeforeRequestSentParameters, route: BidiRouteImpl | undefined) {
|
||||||
|
this._id = payload.request.request;
|
||||||
|
if (redirectedFrom)
|
||||||
|
redirectedFrom._redirectedTo = this;
|
||||||
|
// TODO: missing in the spec?
|
||||||
|
const postDataBuffer = null;
|
||||||
|
this.request = new network.Request(frame._page._browserContext, frame, null, redirectedFrom ? redirectedFrom.request : null, payload.navigation ?? undefined,
|
||||||
|
payload.request.url, 'other', payload.request.method, postDataBuffer, fromBidiHeaders(payload.request.headers));
|
||||||
|
// "raw" headers are the same as "provisional" headers in Bidi.
|
||||||
|
this.request.setRawRequestHeaders(null);
|
||||||
|
this.request._setBodySize(payload.request.bodySize || 0);
|
||||||
|
this._originalRequestRoute = route ?? redirectedFrom?._originalRequestRoute;
|
||||||
|
route?._setRequest(this.request);
|
||||||
|
}
|
||||||
|
|
||||||
|
_finalRequest(): BidiRequest {
|
||||||
|
let request: BidiRequest = this;
|
||||||
|
while (request._redirectedTo)
|
||||||
|
request = request._redirectedTo;
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BidiRouteImpl implements network.RouteDelegate {
|
||||||
|
private _requestId: bidi.Network.Request;
|
||||||
|
private _session: BidiSession;
|
||||||
|
private _request!: network.Request;
|
||||||
|
_alreadyContinuedHeaders: bidi.Network.Header[] | undefined;
|
||||||
|
|
||||||
|
constructor(session: BidiSession, requestId: bidi.Network.Request) {
|
||||||
|
this._session = session;
|
||||||
|
this._requestId = requestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setRequest(request: network.Request) {
|
||||||
|
this._request = request;
|
||||||
|
}
|
||||||
|
|
||||||
|
async continue(overrides: types.NormalizedContinueOverrides) {
|
||||||
|
// Firefox does not update content-length header.
|
||||||
|
let headers = overrides.headers || this._request.headers();
|
||||||
|
if (overrides.postData && headers) {
|
||||||
|
headers = headers.map(header => {
|
||||||
|
if (header.name.toLowerCase() === 'content-length')
|
||||||
|
return { name: header.name, value: overrides.postData!.byteLength.toString() };
|
||||||
|
return header;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this._alreadyContinuedHeaders = toBidiHeaders(headers);
|
||||||
|
await this._session.sendMayFail('network.continueRequest', {
|
||||||
|
request: this._requestId,
|
||||||
|
url: overrides.url,
|
||||||
|
method: overrides.method,
|
||||||
|
// TODO: cookies!
|
||||||
|
headers: this._alreadyContinuedHeaders,
|
||||||
|
body: overrides.postData ? { type: 'base64', value: Buffer.from(overrides.postData).toString('base64') } : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fulfill(response: types.NormalizedFulfillResponse) {
|
||||||
|
const base64body = response.isBase64 ? response.body : Buffer.from(response.body).toString('base64');
|
||||||
|
await this._session.sendMayFail('network.provideResponse', {
|
||||||
|
request: this._requestId,
|
||||||
|
statusCode: response.status,
|
||||||
|
reasonPhrase: network.statusText(response.status),
|
||||||
|
headers: toBidiHeaders(response.headers),
|
||||||
|
body: { type: 'base64', value: base64body },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async abort(errorCode: string) {
|
||||||
|
await this._session.sendMayFail('network.failRequest', {
|
||||||
|
request: this._requestId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromBidiHeaders(bidiHeaders: bidi.Network.Header[]): types.HeadersArray {
|
||||||
|
const result: types.HeadersArray = [];
|
||||||
|
for (const { name, value } of bidiHeaders)
|
||||||
|
result.push({ name, value: bidiBytesValueToString(value) });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBidiHeaders(headers: types.HeadersArray): bidi.Network.Header[] {
|
||||||
|
return headers.map(({ name, value }) => ({ name, value: { type: 'string', value } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bidiBytesValueToString(value: bidi.Network.BytesValue): string {
|
||||||
|
if (value.type === 'string')
|
||||||
|
return value.value;
|
||||||
|
if (value.type === 'base64')
|
||||||
|
return Buffer.from(value.type, 'base64').toString('binary');
|
||||||
|
return 'unknown value type: ' + (value as any).type;
|
||||||
|
|
||||||
|
}
|
||||||
100
packages/playwright-core/src/server/bidi/bidiOverCdp.ts
Normal file
100
packages/playwright-core/src/server/bidi/bidiOverCdp.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as bidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/BidiMapper';
|
||||||
|
import * as bidiCdpConnection from 'chromium-bidi/lib/cjs/cdp/CdpConnection';
|
||||||
|
import type * as bidiTransport from 'chromium-bidi/lib/cjs/utils/transport';
|
||||||
|
import type { ChromiumBidi } from 'chromium-bidi/lib/cjs/protocol/protocol';
|
||||||
|
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||||
|
import { debugLogger } from '../../utils/debugLogger';
|
||||||
|
|
||||||
|
const bidiServerLogger = (prefix: string, ...args: unknown[]): void => {
|
||||||
|
debugLogger.log(prefix as any, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function connectBidiOverCdp(cdp: ConnectionTransport): Promise<ConnectionTransport> {
|
||||||
|
let server: bidiMapper.BidiServer | undefined = undefined;
|
||||||
|
const bidiTransport = new BidiTransportImpl();
|
||||||
|
const bidiConnection = new BidiConnection(bidiTransport, () => server?.close());
|
||||||
|
const cdpTransportImpl = new CdpTransportImpl(cdp);
|
||||||
|
const cdpConnection = new bidiCdpConnection.MapperCdpConnection(cdpTransportImpl, bidiServerLogger);
|
||||||
|
// Make sure onclose event is propagated.
|
||||||
|
cdp.onclose = () => bidiConnection.onclose?.();
|
||||||
|
server = await bidiMapper.BidiServer.createAndStart(
|
||||||
|
bidiTransport,
|
||||||
|
cdpConnection,
|
||||||
|
await cdpConnection.createBrowserSession(),
|
||||||
|
/* selfTargetId= */ '',
|
||||||
|
undefined,
|
||||||
|
bidiServerLogger);
|
||||||
|
return bidiConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BidiTransportImpl implements bidiMapper.BidiTransport {
|
||||||
|
_handler?: (message: ChromiumBidi.Command) => Promise<void> | void;
|
||||||
|
_bidiConnection!: BidiConnection;
|
||||||
|
|
||||||
|
setOnMessage(handler: (message: ChromiumBidi.Command) => Promise<void> | void) {
|
||||||
|
this._handler = handler;
|
||||||
|
}
|
||||||
|
sendMessage(message: ChromiumBidi.Message): Promise<void> | void {
|
||||||
|
return this._bidiConnection.onmessage?.(message as any);
|
||||||
|
}
|
||||||
|
close() {
|
||||||
|
this._bidiConnection.onclose?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BidiConnection implements ConnectionTransport {
|
||||||
|
private _bidiTransport: BidiTransportImpl;
|
||||||
|
private _closeCallback: () => void;
|
||||||
|
|
||||||
|
constructor(bidiTransport: BidiTransportImpl, closeCallback: () => void) {
|
||||||
|
this._bidiTransport = bidiTransport;
|
||||||
|
this._bidiTransport._bidiConnection = this;
|
||||||
|
this._closeCallback = closeCallback;
|
||||||
|
}
|
||||||
|
send(s: ProtocolRequest): void {
|
||||||
|
this._bidiTransport._handler?.(s as any);
|
||||||
|
}
|
||||||
|
close(): void {
|
||||||
|
this._closeCallback();
|
||||||
|
}
|
||||||
|
onmessage?: ((message: ProtocolResponse) => void) | undefined;
|
||||||
|
onclose?: ((reason?: string) => void) | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CdpTransportImpl implements bidiTransport.Transport {
|
||||||
|
private _connection: ConnectionTransport;
|
||||||
|
private _handler?: (message: string) => Promise<void> | void;
|
||||||
|
_bidiConnection!: BidiConnection;
|
||||||
|
|
||||||
|
constructor(connection: ConnectionTransport) {
|
||||||
|
this._connection = connection;
|
||||||
|
this._connection.onmessage = message => {
|
||||||
|
this._handler?.(JSON.stringify(message));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
setOnMessage(handler: (message: string) => Promise<void> | void) {
|
||||||
|
this._handler = handler;
|
||||||
|
}
|
||||||
|
sendMessage(message: string): Promise<void> | void {
|
||||||
|
return this._connection.send(JSON.parse(message));
|
||||||
|
}
|
||||||
|
close(): void {
|
||||||
|
this._connection.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
518
packages/playwright-core/src/server/bidi/bidiPage.ts
Normal file
518
packages/playwright-core/src/server/bidi/bidiPage.ts
Normal file
|
|
@ -0,0 +1,518 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { RegisteredListener } from '../../utils/eventsHelper';
|
||||||
|
import { eventsHelper } from '../../utils/eventsHelper';
|
||||||
|
import { assert } from '../../utils';
|
||||||
|
import type * as accessibility from '../accessibility';
|
||||||
|
import * as dom from '../dom';
|
||||||
|
import * as dialog from '../dialog';
|
||||||
|
import type * as frames from '../frames';
|
||||||
|
import { type InitScript, Page, type PageDelegate } from '../page';
|
||||||
|
import type { Progress } from '../progress';
|
||||||
|
import type * as types from '../types';
|
||||||
|
import type { BidiBrowserContext } from './bidiBrowser';
|
||||||
|
import type { BidiSession } from './bidiConnection';
|
||||||
|
import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './bidiInput';
|
||||||
|
import * as bidi from './third_party/bidiProtocol';
|
||||||
|
import { BidiExecutionContext } from './bidiExecutionContext';
|
||||||
|
import { BidiNetworkManager } from './bidiNetworkManager';
|
||||||
|
import { BrowserContext } from '../browserContext';
|
||||||
|
|
||||||
|
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
||||||
|
|
||||||
|
export class BidiPage implements PageDelegate {
|
||||||
|
readonly rawMouse: RawMouseImpl;
|
||||||
|
readonly rawKeyboard: RawKeyboardImpl;
|
||||||
|
readonly rawTouchscreen: RawTouchscreenImpl;
|
||||||
|
readonly _page: Page;
|
||||||
|
private readonly _pagePromise: Promise<Page | Error>;
|
||||||
|
readonly _session: BidiSession;
|
||||||
|
readonly _opener: BidiPage | null;
|
||||||
|
private readonly _realmToContext: Map<string, dom.FrameExecutionContext>;
|
||||||
|
private _sessionListeners: RegisteredListener[] = [];
|
||||||
|
readonly _browserContext: BidiBrowserContext;
|
||||||
|
readonly _networkManager: BidiNetworkManager;
|
||||||
|
_initializedPage: Page | null = null;
|
||||||
|
|
||||||
|
constructor(browserContext: BidiBrowserContext, bidiSession: BidiSession, opener: BidiPage | null) {
|
||||||
|
this._session = bidiSession;
|
||||||
|
this._opener = opener;
|
||||||
|
this.rawKeyboard = new RawKeyboardImpl(bidiSession);
|
||||||
|
this.rawMouse = new RawMouseImpl(bidiSession);
|
||||||
|
this.rawTouchscreen = new RawTouchscreenImpl(bidiSession);
|
||||||
|
this._realmToContext = new Map();
|
||||||
|
this._page = new Page(this, browserContext);
|
||||||
|
this._browserContext = browserContext;
|
||||||
|
this._networkManager = new BidiNetworkManager(this._session, this._page, this._onNavigationResponseStarted.bind(this));
|
||||||
|
this._page.on(Page.Events.FrameDetached, (frame: frames.Frame) => this._removeContextsForFrame(frame, false));
|
||||||
|
this._sessionListeners = [
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'script.realmCreated', this._onRealmCreated.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'browsingContext.contextDestroyed', this._onBrowsingContextDestroyed.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationStarted', this._onNavigationStarted.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationAborted', this._onNavigationAborted.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationFailed', this._onNavigationFailed.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'browsingContext.fragmentNavigated', this._onFragmentNavigated.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'browsingContext.domContentLoaded', this._onDomContentLoaded.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'browsingContext.load', this._onLoad.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'browsingContext.userPromptOpened', this._onUserPromptOpened.bind(this)),
|
||||||
|
eventsHelper.addEventListener(bidiSession, 'log.entryAdded', this._onLogEntryAdded.bind(this)),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Initialize main frame.
|
||||||
|
this._pagePromise = this._initialize().finally(async () => {
|
||||||
|
await this._page.initOpener(this._opener);
|
||||||
|
}).then(() => {
|
||||||
|
this._initializedPage = this._page;
|
||||||
|
this._page.reportAsNew();
|
||||||
|
return this._page;
|
||||||
|
}).catch(e => {
|
||||||
|
this._page.reportAsNew(e);
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _initialize() {
|
||||||
|
// Initialize main frame.
|
||||||
|
this._onFrameAttached(this._session.sessionId, null);
|
||||||
|
await Promise.all([
|
||||||
|
this.updateHttpCredentials(),
|
||||||
|
this.updateRequestInterception(),
|
||||||
|
this._updateViewport(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
potentiallyUninitializedPage(): Page {
|
||||||
|
return this._page;
|
||||||
|
}
|
||||||
|
|
||||||
|
didClose() {
|
||||||
|
this._session.dispose();
|
||||||
|
eventsHelper.removeEventListeners(this._sessionListeners);
|
||||||
|
this._page._didClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
async pageOrError(): Promise<Page | Error> {
|
||||||
|
// TODO: Wait for first execution context to be created and maybe about:blank navigated.
|
||||||
|
return this._pagePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onFrameAttached(frameId: string, parentFrameId: string | null): frames.Frame {
|
||||||
|
return this._page._frameManager.frameAttached(frameId, parentFrameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removeContextsForFrame(frame: frames.Frame, notifyFrame: boolean) {
|
||||||
|
for (const [contextId, context] of this._realmToContext) {
|
||||||
|
if (context.frame === frame) {
|
||||||
|
this._realmToContext.delete(contextId);
|
||||||
|
if (notifyFrame)
|
||||||
|
frame._contextDestroyed(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onRealmCreated(realmInfo: bidi.Script.RealmInfo) {
|
||||||
|
if (this._realmToContext.has(realmInfo.realm))
|
||||||
|
return;
|
||||||
|
if (realmInfo.type !== 'window')
|
||||||
|
return;
|
||||||
|
const frame = this._page._frameManager.frame(realmInfo.context);
|
||||||
|
if (!frame)
|
||||||
|
return;
|
||||||
|
const delegate = new BidiExecutionContext(this._session, realmInfo);
|
||||||
|
let worldName: types.World;
|
||||||
|
if (!realmInfo.sandbox) {
|
||||||
|
worldName = 'main';
|
||||||
|
// Force creating utility world every time the main world is created (e.g. due to navigation).
|
||||||
|
this._touchUtilityWorld(realmInfo.context);
|
||||||
|
} else if (realmInfo.sandbox === UTILITY_WORLD_NAME) {
|
||||||
|
worldName = 'utility';
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
|
||||||
|
(context as any)[contextDelegateSymbol] = delegate;
|
||||||
|
frame._contextCreated(worldName, context);
|
||||||
|
this._realmToContext.set(realmInfo.realm, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _touchUtilityWorld(context: bidi.BrowsingContext.BrowsingContext) {
|
||||||
|
await this._session.sendMayFail('script.evaluate', {
|
||||||
|
expression: '1 + 1',
|
||||||
|
target: {
|
||||||
|
context,
|
||||||
|
sandbox: UTILITY_WORLD_NAME,
|
||||||
|
},
|
||||||
|
serializationOptions: {
|
||||||
|
maxObjectDepth: 10,
|
||||||
|
maxDomDepth: 10,
|
||||||
|
},
|
||||||
|
awaitPromise: true,
|
||||||
|
userActivation: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRealmDestroyed(params: bidi.Script.RealmDestroyedParameters): boolean {
|
||||||
|
const context = this._realmToContext.get(params.realm);
|
||||||
|
if (!context)
|
||||||
|
return false;
|
||||||
|
this._realmToContext.delete(params.realm);
|
||||||
|
context.frame._contextDestroyed(context);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: route the message directly to the browser
|
||||||
|
private _onBrowsingContextDestroyed(params: bidi.BrowsingContext.Info) {
|
||||||
|
this._browserContext._browser._onBrowsingContextDestroyed(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onNavigationStarted(params: bidi.BrowsingContext.NavigationInfo) {
|
||||||
|
const frameId = params.context;
|
||||||
|
this._page._frameManager.frameRequestedNavigation(frameId, params.navigation!);
|
||||||
|
|
||||||
|
const url = params.url.toLowerCase();
|
||||||
|
if (url.startsWith('file:') || url.startsWith('data:') || url === 'about:blank') {
|
||||||
|
// Navigation to file urls doesn't emit network events, so we fire 'commit' event right when navigation is started.
|
||||||
|
// Doing it in domcontentload would be too late as we'd clear frame tree.
|
||||||
|
const frame = this._page._frameManager.frame(frameId)!;
|
||||||
|
if (frame)
|
||||||
|
this._page._frameManager.frameCommittedNewDocumentNavigation(frameId, params.url, '', params.navigation!, /* initial */ false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: there is no separate event for committed navigation, so we approximate it with responseStarted.
|
||||||
|
private _onNavigationResponseStarted(params: bidi.Network.ResponseStartedParameters) {
|
||||||
|
const frameId = params.context!;
|
||||||
|
const frame = this._page._frameManager.frame(frameId);
|
||||||
|
assert(frame);
|
||||||
|
this._page._frameManager.frameCommittedNewDocumentNavigation(frameId, params.response.url, '', params.navigation!, /* initial */ false);
|
||||||
|
// if (!initial)
|
||||||
|
// this._firstNonInitialNavigationCommittedFulfill();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onDomContentLoaded(params: bidi.BrowsingContext.NavigationInfo) {
|
||||||
|
const frameId = params.context;
|
||||||
|
this._page._frameManager.frameLifecycleEvent(frameId, 'domcontentloaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onLoad(params: bidi.BrowsingContext.NavigationInfo) {
|
||||||
|
this._page._frameManager.frameLifecycleEvent(params.context, 'load');
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onNavigationAborted(params: bidi.BrowsingContext.NavigationInfo) {
|
||||||
|
this._page._frameManager.frameAbortedNavigation(params.context, 'Navigation aborted', params.navigation || undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onNavigationFailed(params: bidi.BrowsingContext.NavigationInfo) {
|
||||||
|
this._page._frameManager.frameAbortedNavigation(params.context, 'Navigation failed', params.navigation || undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onFragmentNavigated(params: bidi.BrowsingContext.NavigationInfo) {
|
||||||
|
this._page._frameManager.frameCommittedSameDocumentNavigation(params.context, params.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onUserPromptOpened(event: bidi.BrowsingContext.UserPromptOpenedParameters) {
|
||||||
|
this._page.emitOnContext(BrowserContext.Events.Dialog, new dialog.Dialog(
|
||||||
|
this._page,
|
||||||
|
event.type as dialog.DialogType,
|
||||||
|
event.message,
|
||||||
|
async (accept: boolean, userText?: string) => {
|
||||||
|
await this._session.send('browsingContext.handleUserPrompt', { context: event.context, accept, userText });
|
||||||
|
},
|
||||||
|
event.defaultValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onLogEntryAdded(params: bidi.Log.Entry) {
|
||||||
|
if (params.type !== 'console')
|
||||||
|
return;
|
||||||
|
const entry: bidi.Log.ConsoleLogEntry = params as bidi.Log.ConsoleLogEntry;
|
||||||
|
const context = this._realmToContext.get(params.source.realm);
|
||||||
|
if (!context)
|
||||||
|
return;
|
||||||
|
const callFrame = params.stackTrace?.callFrames[0];
|
||||||
|
const location = callFrame ?? { url: '', lineNumber: 1, columnNumber: 1 };
|
||||||
|
this._page._addConsoleMessage(entry.method, entry.args.map(arg => context.createHandle({ objectId: (arg as any).handle, ...arg })), location, params.text || undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> {
|
||||||
|
const { navigation } = await this._session.send('browsingContext.navigate', {
|
||||||
|
context: frame._id,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
return { newDocumentId: navigation || undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateExtraHTTPHeaders(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateEmulateMedia(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateEmulatedViewportSize(): Promise<void> {
|
||||||
|
await this._updateViewport();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUserAgent(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async bringToFront(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _updateViewport(): Promise<void> {
|
||||||
|
const options = this._browserContext._options;
|
||||||
|
const deviceSize = this._page.emulatedSize();
|
||||||
|
if (deviceSize === null)
|
||||||
|
return;
|
||||||
|
const viewportSize = deviceSize.viewport;
|
||||||
|
await this._session.send('browsingContext.setViewport', {
|
||||||
|
context: this._session.sessionId,
|
||||||
|
viewport: {
|
||||||
|
width: viewportSize.width,
|
||||||
|
height: viewportSize.height,
|
||||||
|
},
|
||||||
|
devicePixelRatio: options.deviceScaleFactor || 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateRequestInterception(): Promise<void> {
|
||||||
|
await this._networkManager.setRequestInterception(this._page.needsRequestInterception());
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOffline() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateHttpCredentials() {
|
||||||
|
await this._networkManager.setCredentials(this._browserContext._options.httpCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateFileChooserInterception() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async reload(): Promise<void> {
|
||||||
|
await this._session.send('browsingContext.reload', {
|
||||||
|
context: this._session.sessionId,
|
||||||
|
// ignoreCache: true,
|
||||||
|
wait: bidi.BrowsingContext.ReadinessState.Interactive,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack(): Promise<boolean> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
goForward(): Promise<boolean> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async addInitScript(initScript: InitScript): Promise<void> {
|
||||||
|
await this._updateBootstrapScript();
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeNonInternalInitScripts() {
|
||||||
|
await this._updateBootstrapScript();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _updateBootstrapScript(): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async closePage(runBeforeUnload: boolean): Promise<void> {
|
||||||
|
await this._session.send('browsingContext.close', {
|
||||||
|
context: this._session.sessionId,
|
||||||
|
promptUnload: runBeforeUnload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
|
||||||
|
const executionContext = toBidiExecutionContext(handle._context);
|
||||||
|
const contentWindow = await executionContext.rawCallFunction('e => e.contentWindow', { handle: handle._objectId });
|
||||||
|
if (contentWindow.type === 'window') {
|
||||||
|
const frameId = contentWindow.value.context;
|
||||||
|
const result = this._page._frameManager.frame(frameId);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOwnerFrame(handle: dom.ElementHandle): Promise<string | null> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
isElementHandle(remoteObject: bidi.Script.RemoteValue): boolean {
|
||||||
|
return remoteObject.type === 'node';
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
|
||||||
|
const box = await handle.evaluate(element => {
|
||||||
|
if (!(element instanceof Element))
|
||||||
|
return null;
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
||||||
|
});
|
||||||
|
if (!box)
|
||||||
|
return null;
|
||||||
|
const position = await this._framePosition(handle._frame);
|
||||||
|
if (!position)
|
||||||
|
return null;
|
||||||
|
box.x += position.x;
|
||||||
|
box.y += position.y;
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move to Frame.
|
||||||
|
private async _framePosition(frame: frames.Frame): Promise<types.Point | null> {
|
||||||
|
if (frame === this._page.mainFrame())
|
||||||
|
return { x: 0, y: 0 };
|
||||||
|
const element = await frame.frameElement();
|
||||||
|
const box = await element.boundingBox();
|
||||||
|
if (!box)
|
||||||
|
return null;
|
||||||
|
const style = await element.evaluateInUtility(([injected, iframe]) => injected.describeIFrameStyle(iframe as Element), {}).catch(e => 'error:notconnected' as const);
|
||||||
|
if (style === 'error:notconnected' || style === 'transformed')
|
||||||
|
return null;
|
||||||
|
// Content box is offset by border and padding widths.
|
||||||
|
box.x += style.left;
|
||||||
|
box.y += style.top;
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle<Element>, rect?: types.Rect): Promise<'error:notvisible' | 'error:notconnected' | 'done'> {
|
||||||
|
return await handle.evaluateInUtility(([injected, node]) => {
|
||||||
|
node.scrollIntoView({
|
||||||
|
block: 'center',
|
||||||
|
inline: 'center',
|
||||||
|
behavior: 'instant',
|
||||||
|
});
|
||||||
|
}, null).then(() => 'done' as const).catch(e => {
|
||||||
|
if (e instanceof Error && e.message.includes('Node is detached from document'))
|
||||||
|
return 'error:notconnected';
|
||||||
|
if (e instanceof Error && e.message.includes('Node does not have a layout object'))
|
||||||
|
return 'error:notvisible';
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setScreencastOptions(options: { width: number, height: number, quality: number } | null): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
rafCountForStablePosition(): number {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getContentQuads(handle: dom.ElementHandle<Element>): Promise<types.Quad[] | null | 'error:notconnected'> {
|
||||||
|
const quads = await handle.evaluateInUtility(([injected, node]) => {
|
||||||
|
if (!node.isConnected)
|
||||||
|
return 'error:notconnected';
|
||||||
|
const rects = node.getClientRects();
|
||||||
|
if (!rects)
|
||||||
|
return null;
|
||||||
|
return [...rects].map(rect => [
|
||||||
|
{ x: rect.left, y: rect.top },
|
||||||
|
{ x: rect.right, y: rect.top },
|
||||||
|
{ x: rect.right, y: rect.bottom },
|
||||||
|
{ x: rect.left, y: rect.bottom },
|
||||||
|
]);
|
||||||
|
}, null);
|
||||||
|
if (!quads || quads === 'error:notconnected')
|
||||||
|
return quads;
|
||||||
|
// TODO: consider transforming quads to support clicks in iframes.
|
||||||
|
const position = await this._framePosition(handle._frame);
|
||||||
|
if (!position)
|
||||||
|
return null;
|
||||||
|
quads.forEach(quad => quad.forEach(point => {
|
||||||
|
point.x += position.x;
|
||||||
|
point.y += position.y;
|
||||||
|
}));
|
||||||
|
return quads as types.Quad[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, paths: string[]): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
|
||||||
|
const fromContext = toBidiExecutionContext(handle._context);
|
||||||
|
const shared = await fromContext.rawCallFunction('x => x', { handle: handle._objectId });
|
||||||
|
// TODO: store sharedId in the handle.
|
||||||
|
if (!('sharedId' in shared))
|
||||||
|
throw new Error('Element is not a node');
|
||||||
|
const sharedId = shared.sharedId!;
|
||||||
|
const executionContext = toBidiExecutionContext(to);
|
||||||
|
const result = await executionContext.rawCallFunction('x => x', { sharedId });
|
||||||
|
if ('handle' in result)
|
||||||
|
return to.createHandle({ objectId: result.handle!, ...result }) as dom.ElementHandle<T>;
|
||||||
|
throw new Error('Failed to adopt element handle.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async inputActionEpilogue(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetForReuse(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle> {
|
||||||
|
const parent = frame.parentFrame();
|
||||||
|
if (!parent)
|
||||||
|
throw new Error('Frame has been detached.');
|
||||||
|
const parentContext = await parent._mainContext();
|
||||||
|
const list = await parentContext.evaluateHandle(() => { return [...document.querySelectorAll('iframe,frame')]; });
|
||||||
|
const length = await list.evaluate(list => list.length);
|
||||||
|
let foundElement = null;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const element = await list.evaluateHandle((list, i) => list[i], i);
|
||||||
|
const candidate = await element.contentFrame();
|
||||||
|
if (frame === candidate) {
|
||||||
|
foundElement = element;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
element.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.dispose();
|
||||||
|
if (!foundElement)
|
||||||
|
throw new Error('Frame has been detached.');
|
||||||
|
return foundElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldToggleStyleSheetToSyncAnimations(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
useMainWorldForSetContent(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBidiExecutionContext(executionContext: dom.FrameExecutionContext): BidiExecutionContext {
|
||||||
|
return (executionContext as any)[contextDelegateSymbol] as BidiExecutionContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextDelegateSymbol = Symbol('delegate');
|
||||||
202
packages/playwright-core/src/server/bidi/third_party/LICENSE
vendored
Normal file
202
packages/playwright-core/src/server/bidi/third_party/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
https://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 2017 Google Inc.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
https://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.
|
||||||
176
packages/playwright-core/src/server/bidi/third_party/bidiCommands.d.ts
vendored
Normal file
176
packages/playwright-core/src/server/bidi/third_party/bidiCommands.d.ts
vendored
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google Inc.
|
||||||
|
* Modifications copyright (c) Microsoft Corporation.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Bidi from './bidiProtocol';
|
||||||
|
|
||||||
|
export interface Commands {
|
||||||
|
'script.evaluate': {
|
||||||
|
params: Bidi.Script.EvaluateParameters;
|
||||||
|
returnType: Bidi.Script.EvaluateResult;
|
||||||
|
};
|
||||||
|
'script.callFunction': {
|
||||||
|
params: Bidi.Script.CallFunctionParameters;
|
||||||
|
returnType: Bidi.Script.EvaluateResult;
|
||||||
|
};
|
||||||
|
'script.disown': {
|
||||||
|
params: Bidi.Script.DisownParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'script.addPreloadScript': {
|
||||||
|
params: Bidi.Script.AddPreloadScriptParameters;
|
||||||
|
returnType: Bidi.Script.AddPreloadScriptResult;
|
||||||
|
};
|
||||||
|
'script.removePreloadScript': {
|
||||||
|
params: Bidi.Script.RemovePreloadScriptParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
'browser.close': {
|
||||||
|
params: Bidi.EmptyParams;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
'browser.createUserContext': {
|
||||||
|
params: Bidi.EmptyParams;
|
||||||
|
returnType: Bidi.Browser.CreateUserContextResult;
|
||||||
|
};
|
||||||
|
'browser.getUserContexts': {
|
||||||
|
params: Bidi.EmptyParams;
|
||||||
|
returnType: Bidi.Browser.GetUserContextsResult;
|
||||||
|
};
|
||||||
|
'browser.removeUserContext': {
|
||||||
|
params: {
|
||||||
|
userContext: Bidi.Browser.UserContext;
|
||||||
|
};
|
||||||
|
returnType: Bidi.Browser.RemoveUserContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
'browsingContext.activate': {
|
||||||
|
params: Bidi.BrowsingContext.ActivateParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'browsingContext.create': {
|
||||||
|
params: Bidi.BrowsingContext.CreateParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.CreateResult;
|
||||||
|
};
|
||||||
|
'browsingContext.close': {
|
||||||
|
params: Bidi.BrowsingContext.CloseParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'browsingContext.getTree': {
|
||||||
|
params: Bidi.BrowsingContext.GetTreeParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.GetTreeResult;
|
||||||
|
};
|
||||||
|
'browsingContext.locateNodes': {
|
||||||
|
params: Bidi.BrowsingContext.LocateNodesParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.LocateNodesResult;
|
||||||
|
};
|
||||||
|
'browsingContext.navigate': {
|
||||||
|
params: Bidi.BrowsingContext.NavigateParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.NavigateResult;
|
||||||
|
};
|
||||||
|
'browsingContext.reload': {
|
||||||
|
params: Bidi.BrowsingContext.ReloadParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.NavigateResult;
|
||||||
|
};
|
||||||
|
'browsingContext.print': {
|
||||||
|
params: Bidi.BrowsingContext.PrintParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.PrintResult;
|
||||||
|
};
|
||||||
|
'browsingContext.captureScreenshot': {
|
||||||
|
params: Bidi.BrowsingContext.CaptureScreenshotParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.CaptureScreenshotResult;
|
||||||
|
};
|
||||||
|
'browsingContext.handleUserPrompt': {
|
||||||
|
params: Bidi.BrowsingContext.HandleUserPromptParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'browsingContext.setViewport': {
|
||||||
|
params: Bidi.BrowsingContext.SetViewportParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'browsingContext.traverseHistory': {
|
||||||
|
params: Bidi.BrowsingContext.TraverseHistoryParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
'input.performActions': {
|
||||||
|
params: Bidi.Input.PerformActionsParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'input.releaseActions': {
|
||||||
|
params: Bidi.Input.ReleaseActionsParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'input.setFiles': {
|
||||||
|
params: Bidi.Input.SetFilesParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
'session.end': {
|
||||||
|
params: Bidi.EmptyParams;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'session.new': {
|
||||||
|
params: Bidi.Session.NewParameters;
|
||||||
|
returnType: Bidi.Session.NewResult;
|
||||||
|
};
|
||||||
|
'session.status': {
|
||||||
|
params: object;
|
||||||
|
returnType: Bidi.Session.StatusResult;
|
||||||
|
};
|
||||||
|
'session.subscribe': {
|
||||||
|
params: Bidi.Session.SubscriptionRequest;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'session.unsubscribe': {
|
||||||
|
params: Bidi.Session.SubscriptionRequest;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
'storage.deleteCookies': {
|
||||||
|
params: Bidi.Storage.DeleteCookiesParameters;
|
||||||
|
returnType: Bidi.Storage.DeleteCookiesResult;
|
||||||
|
};
|
||||||
|
'storage.getCookies': {
|
||||||
|
params: Bidi.Storage.GetCookiesParameters;
|
||||||
|
returnType: Bidi.Storage.GetCookiesResult;
|
||||||
|
};
|
||||||
|
'network.setCacheBehavior': {
|
||||||
|
params: Bidi.Network.SetCacheBehaviorParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'storage.setCookie': {
|
||||||
|
params: Bidi.Storage.SetCookieParameters;
|
||||||
|
returnType: Bidi.Storage.SetCookieParameters;
|
||||||
|
};
|
||||||
|
|
||||||
|
'network.addIntercept': {
|
||||||
|
params: Bidi.Network.AddInterceptParameters;
|
||||||
|
returnType: Bidi.Network.AddInterceptResult;
|
||||||
|
};
|
||||||
|
'network.removeIntercept': {
|
||||||
|
params: Bidi.Network.RemoveInterceptParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'network.continueRequest': {
|
||||||
|
params: Bidi.Network.ContinueRequestParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'network.continueWithAuth': {
|
||||||
|
params: Bidi.Network.ContinueWithAuthParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'network.failRequest': {
|
||||||
|
params: Bidi.Network.FailRequestParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'network.provideResponse': {
|
||||||
|
params: Bidi.Network.ProvideResponseParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
}
|
||||||
91
packages/playwright-core/src/server/bidi/third_party/bidiDeserializer.ts
vendored
Normal file
91
packages/playwright-core/src/server/bidi/third_party/bidiDeserializer.ts
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google Inc.
|
||||||
|
* Modifications copyright (c) Microsoft Corporation.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import type * as Bidi from './bidiProtocol';
|
||||||
|
|
||||||
|
/* eslint-disable object-curly-spacing */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class BidiDeserializer {
|
||||||
|
static deserialize(result: Bidi.Script.RemoteValue): any {
|
||||||
|
if (!result)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
switch (result.type) {
|
||||||
|
case 'array':
|
||||||
|
return result.value?.map(value => {
|
||||||
|
return BidiDeserializer.deserialize(value);
|
||||||
|
});
|
||||||
|
case 'set':
|
||||||
|
return result.value?.reduce((acc: Set<unknown>, value) => {
|
||||||
|
return acc.add(BidiDeserializer.deserialize(value));
|
||||||
|
}, new Set());
|
||||||
|
case 'object':
|
||||||
|
return result.value?.reduce((acc: Record<any, unknown>, tuple) => {
|
||||||
|
const {key, value} = BidiDeserializer._deserializeTuple(tuple);
|
||||||
|
acc[key as any] = value;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
case 'map':
|
||||||
|
return result.value?.reduce((acc: Map<unknown, unknown>, tuple) => {
|
||||||
|
const {key, value} = BidiDeserializer._deserializeTuple(tuple);
|
||||||
|
return acc.set(key, value);
|
||||||
|
}, new Map());
|
||||||
|
case 'promise':
|
||||||
|
return {};
|
||||||
|
case 'regexp':
|
||||||
|
return new RegExp(result.value.pattern, result.value.flags);
|
||||||
|
case 'date':
|
||||||
|
return new Date(result.value);
|
||||||
|
case 'undefined':
|
||||||
|
return undefined;
|
||||||
|
case 'null':
|
||||||
|
return null;
|
||||||
|
case 'number':
|
||||||
|
return BidiDeserializer._deserializeNumber(result.value);
|
||||||
|
case 'bigint':
|
||||||
|
return BigInt(result.value);
|
||||||
|
case 'boolean':
|
||||||
|
return Boolean(result.value);
|
||||||
|
case 'string':
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Deserialization of type ${result.type} not supported.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static _deserializeNumber(value: Bidi.Script.SpecialNumber | number): number {
|
||||||
|
switch (value) {
|
||||||
|
case '-0':
|
||||||
|
return -0;
|
||||||
|
case 'NaN':
|
||||||
|
return NaN;
|
||||||
|
case 'Infinity':
|
||||||
|
return Infinity;
|
||||||
|
case '-Infinity':
|
||||||
|
return -Infinity;
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static _deserializeTuple([serializedKey, serializedValue]: [
|
||||||
|
Bidi.Script.RemoteValue | string,
|
||||||
|
Bidi.Script.RemoteValue,
|
||||||
|
]): {key: unknown; value: unknown} {
|
||||||
|
const key =
|
||||||
|
typeof serializedKey === 'string'
|
||||||
|
? serializedKey
|
||||||
|
: BidiDeserializer.deserialize(serializedKey);
|
||||||
|
const value = BidiDeserializer.deserialize(serializedValue);
|
||||||
|
|
||||||
|
return {key, value};
|
||||||
|
}
|
||||||
|
}
|
||||||
231
packages/playwright-core/src/server/bidi/third_party/bidiKeyboard.ts
vendored
Normal file
231
packages/playwright-core/src/server/bidi/third_party/bidiKeyboard.ts
vendored
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google Inc.
|
||||||
|
* Modifications copyright (c) Microsoft Corporation.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable curly */
|
||||||
|
|
||||||
|
export const getBidiKeyValue = (key: string) => {
|
||||||
|
switch (key) {
|
||||||
|
case '\r':
|
||||||
|
case '\n':
|
||||||
|
key = 'Enter';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Measures the number of code points rather than UTF-16 code units.
|
||||||
|
if ([...key].length === 1) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
switch (key) {
|
||||||
|
case 'Cancel':
|
||||||
|
return '\uE001';
|
||||||
|
case 'Help':
|
||||||
|
return '\uE002';
|
||||||
|
case 'Backspace':
|
||||||
|
return '\uE003';
|
||||||
|
case 'Tab':
|
||||||
|
return '\uE004';
|
||||||
|
case 'Clear':
|
||||||
|
return '\uE005';
|
||||||
|
case 'Enter':
|
||||||
|
return '\uE007';
|
||||||
|
case 'Shift':
|
||||||
|
case 'ShiftLeft':
|
||||||
|
return '\uE008';
|
||||||
|
case 'Control':
|
||||||
|
case 'ControlLeft':
|
||||||
|
return '\uE009';
|
||||||
|
case 'Alt':
|
||||||
|
case 'AltLeft':
|
||||||
|
return '\uE00A';
|
||||||
|
case 'Pause':
|
||||||
|
return '\uE00B';
|
||||||
|
case 'Escape':
|
||||||
|
return '\uE00C';
|
||||||
|
case 'PageUp':
|
||||||
|
return '\uE00E';
|
||||||
|
case 'PageDown':
|
||||||
|
return '\uE00F';
|
||||||
|
case 'End':
|
||||||
|
return '\uE010';
|
||||||
|
case 'Home':
|
||||||
|
return '\uE011';
|
||||||
|
case 'ArrowLeft':
|
||||||
|
return '\uE012';
|
||||||
|
case 'ArrowUp':
|
||||||
|
return '\uE013';
|
||||||
|
case 'ArrowRight':
|
||||||
|
return '\uE014';
|
||||||
|
case 'ArrowDown':
|
||||||
|
return '\uE015';
|
||||||
|
case 'Insert':
|
||||||
|
return '\uE016';
|
||||||
|
case 'Delete':
|
||||||
|
return '\uE017';
|
||||||
|
case 'NumpadEqual':
|
||||||
|
return '\uE019';
|
||||||
|
case 'Numpad0':
|
||||||
|
return '\uE01A';
|
||||||
|
case 'Numpad1':
|
||||||
|
return '\uE01B';
|
||||||
|
case 'Numpad2':
|
||||||
|
return '\uE01C';
|
||||||
|
case 'Numpad3':
|
||||||
|
return '\uE01D';
|
||||||
|
case 'Numpad4':
|
||||||
|
return '\uE01E';
|
||||||
|
case 'Numpad5':
|
||||||
|
return '\uE01F';
|
||||||
|
case 'Numpad6':
|
||||||
|
return '\uE020';
|
||||||
|
case 'Numpad7':
|
||||||
|
return '\uE021';
|
||||||
|
case 'Numpad8':
|
||||||
|
return '\uE022';
|
||||||
|
case 'Numpad9':
|
||||||
|
return '\uE023';
|
||||||
|
case 'NumpadMultiply':
|
||||||
|
return '\uE024';
|
||||||
|
case 'NumpadAdd':
|
||||||
|
return '\uE025';
|
||||||
|
case 'NumpadSubtract':
|
||||||
|
return '\uE027';
|
||||||
|
case 'NumpadDecimal':
|
||||||
|
return '\uE028';
|
||||||
|
case 'NumpadDivide':
|
||||||
|
return '\uE029';
|
||||||
|
case 'F1':
|
||||||
|
return '\uE031';
|
||||||
|
case 'F2':
|
||||||
|
return '\uE032';
|
||||||
|
case 'F3':
|
||||||
|
return '\uE033';
|
||||||
|
case 'F4':
|
||||||
|
return '\uE034';
|
||||||
|
case 'F5':
|
||||||
|
return '\uE035';
|
||||||
|
case 'F6':
|
||||||
|
return '\uE036';
|
||||||
|
case 'F7':
|
||||||
|
return '\uE037';
|
||||||
|
case 'F8':
|
||||||
|
return '\uE038';
|
||||||
|
case 'F9':
|
||||||
|
return '\uE039';
|
||||||
|
case 'F10':
|
||||||
|
return '\uE03A';
|
||||||
|
case 'F11':
|
||||||
|
return '\uE03B';
|
||||||
|
case 'F12':
|
||||||
|
return '\uE03C';
|
||||||
|
case 'Meta':
|
||||||
|
case 'MetaLeft':
|
||||||
|
return '\uE03D';
|
||||||
|
case 'ShiftRight':
|
||||||
|
return '\uE050';
|
||||||
|
case 'ControlRight':
|
||||||
|
return '\uE051';
|
||||||
|
case 'AltRight':
|
||||||
|
return '\uE052';
|
||||||
|
case 'MetaRight':
|
||||||
|
return '\uE053';
|
||||||
|
case 'Digit0':
|
||||||
|
return '0';
|
||||||
|
case 'Digit1':
|
||||||
|
return '1';
|
||||||
|
case 'Digit2':
|
||||||
|
return '2';
|
||||||
|
case 'Digit3':
|
||||||
|
return '3';
|
||||||
|
case 'Digit4':
|
||||||
|
return '4';
|
||||||
|
case 'Digit5':
|
||||||
|
return '5';
|
||||||
|
case 'Digit6':
|
||||||
|
return '6';
|
||||||
|
case 'Digit7':
|
||||||
|
return '7';
|
||||||
|
case 'Digit8':
|
||||||
|
return '8';
|
||||||
|
case 'Digit9':
|
||||||
|
return '9';
|
||||||
|
case 'KeyA':
|
||||||
|
return 'a';
|
||||||
|
case 'KeyB':
|
||||||
|
return 'b';
|
||||||
|
case 'KeyC':
|
||||||
|
return 'c';
|
||||||
|
case 'KeyD':
|
||||||
|
return 'd';
|
||||||
|
case 'KeyE':
|
||||||
|
return 'e';
|
||||||
|
case 'KeyF':
|
||||||
|
return 'f';
|
||||||
|
case 'KeyG':
|
||||||
|
return 'g';
|
||||||
|
case 'KeyH':
|
||||||
|
return 'h';
|
||||||
|
case 'KeyI':
|
||||||
|
return 'i';
|
||||||
|
case 'KeyJ':
|
||||||
|
return 'j';
|
||||||
|
case 'KeyK':
|
||||||
|
return 'k';
|
||||||
|
case 'KeyL':
|
||||||
|
return 'l';
|
||||||
|
case 'KeyM':
|
||||||
|
return 'm';
|
||||||
|
case 'KeyN':
|
||||||
|
return 'n';
|
||||||
|
case 'KeyO':
|
||||||
|
return 'o';
|
||||||
|
case 'KeyP':
|
||||||
|
return 'p';
|
||||||
|
case 'KeyQ':
|
||||||
|
return 'q';
|
||||||
|
case 'KeyR':
|
||||||
|
return 'r';
|
||||||
|
case 'KeyS':
|
||||||
|
return 's';
|
||||||
|
case 'KeyT':
|
||||||
|
return 't';
|
||||||
|
case 'KeyU':
|
||||||
|
return 'u';
|
||||||
|
case 'KeyV':
|
||||||
|
return 'v';
|
||||||
|
case 'KeyW':
|
||||||
|
return 'w';
|
||||||
|
case 'KeyX':
|
||||||
|
return 'x';
|
||||||
|
case 'KeyY':
|
||||||
|
return 'y';
|
||||||
|
case 'KeyZ':
|
||||||
|
return 'z';
|
||||||
|
case 'Semicolon':
|
||||||
|
return ';';
|
||||||
|
case 'Equal':
|
||||||
|
return '=';
|
||||||
|
case 'Comma':
|
||||||
|
return ',';
|
||||||
|
case 'Minus':
|
||||||
|
return '-';
|
||||||
|
case 'Period':
|
||||||
|
return '.';
|
||||||
|
case 'Slash':
|
||||||
|
return '/';
|
||||||
|
case 'Backquote':
|
||||||
|
return '`';
|
||||||
|
case 'BracketLeft':
|
||||||
|
return '[';
|
||||||
|
case 'Backslash':
|
||||||
|
return '\\';
|
||||||
|
case 'BracketRight':
|
||||||
|
return ']';
|
||||||
|
case 'Quote':
|
||||||
|
return '"';
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown key: "${key}"`);
|
||||||
|
}
|
||||||
|
};
|
||||||
2204
packages/playwright-core/src/server/bidi/third_party/bidiProtocol.ts
vendored
Normal file
2204
packages/playwright-core/src/server/bidi/third_party/bidiProtocol.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
148
packages/playwright-core/src/server/bidi/third_party/bidiSerializer.ts
vendored
Normal file
148
packages/playwright-core/src/server/bidi/third_party/bidiSerializer.ts
vendored
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google Inc.
|
||||||
|
* Modifications copyright (c) Microsoft Corporation.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as Bidi from './bidiProtocol';
|
||||||
|
|
||||||
|
/* eslint-disable curly, indent */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class UnserializableError extends Error {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class BidiSerializer {
|
||||||
|
static serialize(arg: unknown): Bidi.Script.LocalValue {
|
||||||
|
switch (typeof arg) {
|
||||||
|
case 'symbol':
|
||||||
|
case 'function':
|
||||||
|
throw new UnserializableError(`Unable to serializable ${typeof arg}`);
|
||||||
|
case 'object':
|
||||||
|
return BidiSerializer._serializeObject(arg);
|
||||||
|
|
||||||
|
case 'undefined':
|
||||||
|
return {
|
||||||
|
type: 'undefined',
|
||||||
|
};
|
||||||
|
case 'number':
|
||||||
|
return BidiSerializer._serializeNumber(arg);
|
||||||
|
case 'bigint':
|
||||||
|
return {
|
||||||
|
type: 'bigint',
|
||||||
|
value: arg.toString(),
|
||||||
|
};
|
||||||
|
case 'string':
|
||||||
|
return {
|
||||||
|
type: 'string',
|
||||||
|
value: arg,
|
||||||
|
};
|
||||||
|
case 'boolean':
|
||||||
|
return {
|
||||||
|
type: 'boolean',
|
||||||
|
value: arg,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static _serializeNumber(arg: number): Bidi.Script.LocalValue {
|
||||||
|
let value: Bidi.Script.SpecialNumber | number;
|
||||||
|
if (Object.is(arg, -0)) {
|
||||||
|
value = '-0';
|
||||||
|
} else if (Object.is(arg, Infinity)) {
|
||||||
|
value = 'Infinity';
|
||||||
|
} else if (Object.is(arg, -Infinity)) {
|
||||||
|
value = '-Infinity';
|
||||||
|
} else if (Object.is(arg, NaN)) {
|
||||||
|
value = 'NaN';
|
||||||
|
} else {
|
||||||
|
value = arg;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'number',
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static _serializeObject(arg: object | null): Bidi.Script.LocalValue {
|
||||||
|
if (arg === null) {
|
||||||
|
return {
|
||||||
|
type: 'null',
|
||||||
|
};
|
||||||
|
} else if (Array.isArray(arg)) {
|
||||||
|
const parsedArray = arg.map(subArg => {
|
||||||
|
return BidiSerializer.serialize(subArg);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'array',
|
||||||
|
value: parsedArray,
|
||||||
|
};
|
||||||
|
} else if (isPlainObject(arg)) {
|
||||||
|
try {
|
||||||
|
JSON.stringify(arg);
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
error instanceof TypeError &&
|
||||||
|
error.message.startsWith('Converting circular structure to JSON')
|
||||||
|
) {
|
||||||
|
error.message += ' Recursive objects are not allowed.';
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedObject: Bidi.Script.MappingLocalValue = [];
|
||||||
|
for (const key in arg) {
|
||||||
|
parsedObject.push([BidiSerializer.serialize(key), BidiSerializer.serialize(arg[key])]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'object',
|
||||||
|
value: parsedObject,
|
||||||
|
};
|
||||||
|
} else if (isRegExp(arg)) {
|
||||||
|
return {
|
||||||
|
type: 'regexp',
|
||||||
|
value: {
|
||||||
|
pattern: arg.source,
|
||||||
|
flags: arg.flags,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (isDate(arg)) {
|
||||||
|
return {
|
||||||
|
type: 'date',
|
||||||
|
value: arg.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnserializableError(
|
||||||
|
'Custom object serialization not possible. Use plain objects instead.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const isPlainObject = (obj: unknown): obj is Record<any, unknown> => {
|
||||||
|
return typeof obj === 'object' && obj?.constructor === Object;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const isRegExp = (obj: unknown): obj is RegExp => {
|
||||||
|
return typeof obj === 'object' && obj?.constructor === RegExp;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const isDate = (obj: unknown): obj is Date => {
|
||||||
|
return typeof obj === 'object' && obj?.constructor === Date;
|
||||||
|
};
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as os from 'os';
|
|
||||||
import { TimeoutSettings } from '../common/timeoutSettings';
|
import { TimeoutSettings } from '../common/timeoutSettings';
|
||||||
import { createGuid, debugMode } from '../utils';
|
import { createGuid, debugMode } from '../utils';
|
||||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||||
|
|
@ -44,6 +43,7 @@ import { BrowserContextAPIRequestContext } from './fetch';
|
||||||
import type { Artifact } from './artifact';
|
import type { Artifact } from './artifact';
|
||||||
import { Clock } from './clock';
|
import { Clock } from './clock';
|
||||||
import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
|
import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
|
||||||
|
import { RecorderApp } from './recorder/recorderApp';
|
||||||
|
|
||||||
export abstract class BrowserContext extends SdkObject {
|
export abstract class BrowserContext extends SdkObject {
|
||||||
static Events = {
|
static Events = {
|
||||||
|
|
@ -131,19 +131,21 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
|
|
||||||
// When PWDEBUG=1, show inspector for each context.
|
// When PWDEBUG=1, show inspector for each context.
|
||||||
if (debugMode() === 'inspector')
|
if (debugMode() === 'inspector')
|
||||||
await Recorder.show(this, { pauseOnNextStatement: true });
|
await Recorder.show(this, RecorderApp.factory(this), { pauseOnNextStatement: true });
|
||||||
|
|
||||||
// When paused, show inspector.
|
// When paused, show inspector.
|
||||||
if (this._debugger.isPaused())
|
if (this._debugger.isPaused())
|
||||||
Recorder.showInspector(this);
|
Recorder.showInspector(this, RecorderApp.factory(this));
|
||||||
|
|
||||||
this._debugger.on(Debugger.Events.PausedStateChanged, () => {
|
this._debugger.on(Debugger.Events.PausedStateChanged, () => {
|
||||||
Recorder.showInspector(this);
|
if (this._debugger.isPaused())
|
||||||
|
Recorder.showInspector(this, RecorderApp.factory(this));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (debugMode() === 'console')
|
if (debugMode() === 'console')
|
||||||
await this.extendInjectedScript(consoleApiSource.source);
|
await this.extendInjectedScript(consoleApiSource.source);
|
||||||
if (this._options.serviceWorkers === 'block')
|
if (this._options.serviceWorkers === 'block')
|
||||||
await this.addInitScript(`\nnavigator.serviceWorker.register = async () => { console.warn('Service Worker registration blocked by Playwright'); };\n`);
|
await this.addInitScript(`\nif (navigator.serviceWorker) navigator.serviceWorker.register = async () => { console.warn('Service Worker registration blocked by Playwright'); };\n`);
|
||||||
|
|
||||||
if (this._options.permissions)
|
if (this._options.permissions)
|
||||||
await this.grantPermissions(this._options.permissions);
|
await this.grantPermissions(this._options.permissions);
|
||||||
|
|
@ -700,11 +702,8 @@ export function validateBrowserContextOptions(options: channels.BrowserNewContex
|
||||||
options.recordVideo.size!.width &= ~1;
|
options.recordVideo.size!.width &= ~1;
|
||||||
options.recordVideo.size!.height &= ~1;
|
options.recordVideo.size!.height &= ~1;
|
||||||
}
|
}
|
||||||
if (options.proxy) {
|
if (options.proxy)
|
||||||
if (!browserOptions.proxy && browserOptions.isChromium && os.platform() === 'win32')
|
|
||||||
throw new Error(`Browser needs to be launched with the global proxy. If all contexts override the proxy, global proxy will be never used and can be any string, for example "launch({ proxy: { server: 'http://per-context' } })"`);
|
|
||||||
options.proxy = normalizeProxySettings(options.proxy);
|
options.proxy = normalizeProxySettings(options.proxy);
|
||||||
}
|
|
||||||
verifyGeolocation(options.geolocation);
|
verifyGeolocation(options.geolocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import { ProgressController } from './progress';
|
||||||
import type * as types from './types';
|
import type * as types from './types';
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
import { DEFAULT_TIMEOUT, TimeoutSettings } from '../common/timeoutSettings';
|
import { DEFAULT_TIMEOUT, TimeoutSettings } from '../common/timeoutSettings';
|
||||||
import { debugMode } from '../utils';
|
import { debugMode, ManualPromise } from '../utils';
|
||||||
import { existsAsync } from '../utils/fileUtils';
|
import { existsAsync } from '../utils/fileUtils';
|
||||||
import { helper } from './helper';
|
import { helper } from './helper';
|
||||||
import { RecentLogsCollector } from '../utils/debugLogger';
|
import { RecentLogsCollector } from '../utils/debugLogger';
|
||||||
|
|
@ -44,14 +44,24 @@ export const kNoXServerRunningError = 'Looks like you launched a headed browser
|
||||||
'Set either \'headless: true\' or use \'xvfb-run <your-playwright-app>\' before running Playwright.\n\n<3 Playwright Team';
|
'Set either \'headless: true\' or use \'xvfb-run <your-playwright-app>\' before running Playwright.\n\n<3 Playwright Team';
|
||||||
|
|
||||||
|
|
||||||
export interface BrowserReadyState {
|
export abstract class BrowserReadyState {
|
||||||
onBrowserOutput(message: string): void;
|
protected readonly _wsEndpoint = new ManualPromise<string|undefined>();
|
||||||
onBrowserExit(): void;
|
|
||||||
waitUntilReady(): Promise<{ wsEndpoint?: string }>;
|
onBrowserExit(): void {
|
||||||
|
// Unblock launch when browser prematurely exits.
|
||||||
|
this._wsEndpoint.resolve(undefined);
|
||||||
|
}
|
||||||
|
async waitUntilReady(): Promise<{ wsEndpoint?: string }> {
|
||||||
|
const wsEndpoint = await this._wsEndpoint;
|
||||||
|
return { wsEndpoint };
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract onBrowserOutput(message: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class BrowserType extends SdkObject {
|
export abstract class BrowserType extends SdkObject {
|
||||||
private _name: BrowserName;
|
private _name: BrowserName;
|
||||||
|
_useBidi: boolean = false;
|
||||||
|
|
||||||
constructor(parent: SdkObject, browserName: BrowserName) {
|
constructor(parent: SdkObject, browserName: BrowserName) {
|
||||||
super(parent, 'browser-type');
|
super(parent, 'browser-type');
|
||||||
|
|
@ -69,6 +79,8 @@ export abstract class BrowserType extends SdkObject {
|
||||||
|
|
||||||
async launch(metadata: CallMetadata, options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise<Browser> {
|
async launch(metadata: CallMetadata, options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise<Browser> {
|
||||||
options = this._validateLaunchOptions(options);
|
options = this._validateLaunchOptions(options);
|
||||||
|
if (this._useBidi)
|
||||||
|
options.useWebSocket = true;
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
controller.setLogName('browser');
|
controller.setLogName('browser');
|
||||||
const browser = await controller.run(progress => {
|
const browser = await controller.run(progress => {
|
||||||
|
|
@ -82,6 +94,8 @@ export abstract class BrowserType extends SdkObject {
|
||||||
|
|
||||||
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise<BrowserContext> {
|
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise<BrowserContext> {
|
||||||
options = this._validateLaunchOptions(options);
|
options = this._validateLaunchOptions(options);
|
||||||
|
if (this._useBidi)
|
||||||
|
options.useWebSocket = true;
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
const persistent: channels.BrowserNewContextParams = { ...options };
|
const persistent: channels.BrowserNewContextParams = { ...options };
|
||||||
controller.setLogName('browser');
|
controller.setLogName('browser');
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import type { Env } from '../../utils/processLauncher';
|
||||||
import { gracefullyCloseSet } from '../../utils/processLauncher';
|
import { gracefullyCloseSet } from '../../utils/processLauncher';
|
||||||
import { kBrowserCloseMessageId } from './crConnection';
|
import { kBrowserCloseMessageId } from './crConnection';
|
||||||
import { BrowserType, kNoXServerRunningError } from '../browserType';
|
import { BrowserType, kNoXServerRunningError } from '../browserType';
|
||||||
import type { BrowserReadyState } from '../browserType';
|
import { BrowserReadyState } from '../browserType';
|
||||||
import type { ConnectionTransport, ProtocolRequest } from '../transport';
|
import type { ConnectionTransport, ProtocolRequest } from '../transport';
|
||||||
import { WebSocketTransport } from '../transport';
|
import { WebSocketTransport } from '../transport';
|
||||||
import { CRDevTools } from './crDevTools';
|
import { CRDevTools } from './crDevTools';
|
||||||
|
|
@ -110,12 +110,6 @@ export class Chromium extends BrowserType {
|
||||||
artifactsDir,
|
artifactsDir,
|
||||||
downloadsPath: options.downloadsPath || artifactsDir,
|
downloadsPath: options.downloadsPath || artifactsDir,
|
||||||
tracesDir: options.tracesDir || artifactsDir,
|
tracesDir: options.tracesDir || artifactsDir,
|
||||||
// On Windows context level proxies only work, if there isn't a global proxy
|
|
||||||
// set. This is currently a bug in the CR/Windows networking stack. By
|
|
||||||
// passing an arbitrary value we disable the check in PW land which warns
|
|
||||||
// users in normal (launch/launchServer) mode since otherwise connectOverCDP
|
|
||||||
// does not work at all with proxies on Windows.
|
|
||||||
proxy: { server: 'per-context' },
|
|
||||||
originalLaunchOptions: {},
|
originalLaunchOptions: {},
|
||||||
};
|
};
|
||||||
validateBrowserContextOptions(persistent, browserOptions);
|
validateBrowserContextOptions(persistent, browserOptions);
|
||||||
|
|
@ -358,21 +352,12 @@ export class Chromium extends BrowserType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChromiumReadyState implements BrowserReadyState {
|
class ChromiumReadyState extends BrowserReadyState {
|
||||||
private readonly _wsEndpoint = new ManualPromise<string|undefined>();
|
override onBrowserOutput(message: string): void {
|
||||||
|
|
||||||
onBrowserOutput(message: string): void {
|
|
||||||
const match = message.match(/DevTools listening on (.*)/);
|
const match = message.match(/DevTools listening on (.*)/);
|
||||||
if (match)
|
if (match)
|
||||||
this._wsEndpoint.resolve(match[1]);
|
this._wsEndpoint.resolve(match[1]);
|
||||||
}
|
}
|
||||||
onBrowserExit(): void {
|
|
||||||
this._wsEndpoint.resolve(undefined);
|
|
||||||
}
|
|
||||||
async waitUntilReady(): Promise<{ wsEndpoint?: string }> {
|
|
||||||
const wsEndpoint = await this._wsEndpoint;
|
|
||||||
return { wsEndpoint };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function urlToWSEndpoint(progress: Progress, endpointURL: string, headers: { [key: string]: string; }) {
|
async function urlToWSEndpoint(progress: Progress, endpointURL: string, headers: { [key: string]: string; }) {
|
||||||
|
|
|
||||||
|
|
@ -609,7 +609,7 @@ class RouteImpl implements network.RouteDelegate {
|
||||||
this._interceptionId = interceptionId;
|
this._interceptionId = interceptionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async continue(request: network.Request, overrides: types.NormalizedContinueOverrides): Promise<void> {
|
async continue(overrides: types.NormalizedContinueOverrides): Promise<void> {
|
||||||
this._alreadyContinuedParams = {
|
this._alreadyContinuedParams = {
|
||||||
requestId: this._interceptionId!,
|
requestId: this._interceptionId!,
|
||||||
url: overrides.url,
|
url: overrides.url,
|
||||||
|
|
|
||||||
|
|
@ -690,14 +690,15 @@ class FrameSession {
|
||||||
if (!frame || this._eventBelongsToStaleFrame(frame._id))
|
if (!frame || this._eventBelongsToStaleFrame(frame._id))
|
||||||
return;
|
return;
|
||||||
const delegate = new CRExecutionContext(this._client, contextPayload);
|
const delegate = new CRExecutionContext(this._client, contextPayload);
|
||||||
let worldName: types.World|null = null;
|
let worldName: types.World;
|
||||||
if (contextPayload.auxData && !!contextPayload.auxData.isDefault)
|
if (contextPayload.auxData && !!contextPayload.auxData.isDefault)
|
||||||
worldName = 'main';
|
worldName = 'main';
|
||||||
else if (contextPayload.name === UTILITY_WORLD_NAME)
|
else if (contextPayload.name === UTILITY_WORLD_NAME)
|
||||||
worldName = 'utility';
|
worldName = 'utility';
|
||||||
|
else
|
||||||
|
return;
|
||||||
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
|
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
|
||||||
(context as any)[contextDelegateSymbol] = delegate;
|
(context as any)[contextDelegateSymbol] = delegate;
|
||||||
if (worldName)
|
|
||||||
frame._contextCreated(worldName, context);
|
frame._contextCreated(worldName, context);
|
||||||
this._contextIdToContext.set(contextPayload.id, context);
|
this._contextIdToContext.set(contextPayload.id, context);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1131,17 +1131,21 @@ using Audits.issueAdded event.
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines commands and events for browser extensions. Available if the client
|
* Defines commands and events for browser extensions.
|
||||||
is connected using the --remote-debugging-pipe flag and
|
|
||||||
the --enable-unsafe-extension-debugging flag is set.
|
|
||||||
*/
|
*/
|
||||||
export module Extensions {
|
export module Extensions {
|
||||||
|
/**
|
||||||
|
* Storage areas.
|
||||||
|
*/
|
||||||
|
export type StorageArea = "session"|"local"|"sync"|"managed";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Installs an unpacked extension from the filesystem similar to
|
* Installs an unpacked extension from the filesystem similar to
|
||||||
--load-extension CLI flags. Returns extension ID once the extension
|
--load-extension CLI flags. Returns extension ID once the extension
|
||||||
has been installed.
|
has been installed. Available if the client is connected using the
|
||||||
|
--remote-debugging-pipe flag and the --enable-unsafe-extension-debugging
|
||||||
|
flag is set.
|
||||||
*/
|
*/
|
||||||
export type loadUnpackedParameters = {
|
export type loadUnpackedParameters = {
|
||||||
/**
|
/**
|
||||||
|
|
@ -1155,6 +1159,81 @@ has been installed.
|
||||||
*/
|
*/
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Gets data from extension storage in the given `storageArea`. If `keys` is
|
||||||
|
specified, these are used to filter the result.
|
||||||
|
*/
|
||||||
|
export type getStorageItemsParameters = {
|
||||||
|
/**
|
||||||
|
* ID of extension.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* StorageArea to retrieve data from.
|
||||||
|
*/
|
||||||
|
storageArea: StorageArea;
|
||||||
|
/**
|
||||||
|
* Keys to retrieve.
|
||||||
|
*/
|
||||||
|
keys?: string[];
|
||||||
|
}
|
||||||
|
export type getStorageItemsReturnValue = {
|
||||||
|
data: { [key: string]: string };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Removes `keys` from extension storage in the given `storageArea`.
|
||||||
|
*/
|
||||||
|
export type removeStorageItemsParameters = {
|
||||||
|
/**
|
||||||
|
* ID of extension.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* StorageArea to remove data from.
|
||||||
|
*/
|
||||||
|
storageArea: StorageArea;
|
||||||
|
/**
|
||||||
|
* Keys to remove.
|
||||||
|
*/
|
||||||
|
keys: string[];
|
||||||
|
}
|
||||||
|
export type removeStorageItemsReturnValue = {
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Clears extension storage in the given `storageArea`.
|
||||||
|
*/
|
||||||
|
export type clearStorageItemsParameters = {
|
||||||
|
/**
|
||||||
|
* ID of extension.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* StorageArea to remove data from.
|
||||||
|
*/
|
||||||
|
storageArea: StorageArea;
|
||||||
|
}
|
||||||
|
export type clearStorageItemsReturnValue = {
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets `values` in extension storage in the given `storageArea`. The provided `values`
|
||||||
|
will be merged with existing values in the storage area.
|
||||||
|
*/
|
||||||
|
export type setStorageItemsParameters = {
|
||||||
|
/**
|
||||||
|
* ID of extension.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* StorageArea to set data in.
|
||||||
|
*/
|
||||||
|
storageArea: StorageArea;
|
||||||
|
/**
|
||||||
|
* Values to set.
|
||||||
|
*/
|
||||||
|
values: { [key: string]: string };
|
||||||
|
}
|
||||||
|
export type setStorageItemsReturnValue = {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -2532,16 +2611,6 @@ stylesheet rules) this rule came from.
|
||||||
*/
|
*/
|
||||||
style: CSSStyle;
|
style: CSSStyle;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* CSS position-fallback rule representation.
|
|
||||||
*/
|
|
||||||
export interface CSSPositionFallbackRule {
|
|
||||||
name: Value;
|
|
||||||
/**
|
|
||||||
* List of keyframes.
|
|
||||||
*/
|
|
||||||
tryRules: CSSTryRule[];
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* CSS @position-try rule representation.
|
* CSS @position-try rule representation.
|
||||||
*/
|
*/
|
||||||
|
|
@ -2888,10 +2957,6 @@ attributes) for a DOM node identified by `nodeId`.
|
||||||
* A list of CSS keyframed animations matching this node.
|
* A list of CSS keyframed animations matching this node.
|
||||||
*/
|
*/
|
||||||
cssKeyframesRules?: CSSKeyframesRule[];
|
cssKeyframesRules?: CSSKeyframesRule[];
|
||||||
/**
|
|
||||||
* A list of CSS position fallbacks matching this node.
|
|
||||||
*/
|
|
||||||
cssPositionFallbackRules?: CSSPositionFallbackRule[];
|
|
||||||
/**
|
/**
|
||||||
* A list of CSS @position-try rules matching this node, based on the position-try-fallbacks property.
|
* A list of CSS @position-try rules matching this node, based on the position-try-fallbacks property.
|
||||||
*/
|
*/
|
||||||
|
|
@ -3496,7 +3561,7 @@ front-end.
|
||||||
/**
|
/**
|
||||||
* Pseudo element type.
|
* Pseudo element type.
|
||||||
*/
|
*/
|
||||||
export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new";
|
export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new";
|
||||||
/**
|
/**
|
||||||
* Shadow root type.
|
* Shadow root type.
|
||||||
*/
|
*/
|
||||||
|
|
@ -3646,6 +3711,13 @@ The property is always undefined now.
|
||||||
compatibilityMode?: CompatibilityMode;
|
compatibilityMode?: CompatibilityMode;
|
||||||
assignedSlot?: BackendNode;
|
assignedSlot?: BackendNode;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* A structure to hold the top-level node of a detached tree and an array of its retained descendants.
|
||||||
|
*/
|
||||||
|
export interface DetachedElementInfo {
|
||||||
|
treeNode: Node;
|
||||||
|
retainedNodeIds: NodeId[];
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* A structure holding an RGBA color.
|
* A structure holding an RGBA color.
|
||||||
*/
|
*/
|
||||||
|
|
@ -4693,6 +4765,17 @@ File wrapper.
|
||||||
export type getFileInfoReturnValue = {
|
export type getFileInfoReturnValue = {
|
||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Returns list of detached nodes
|
||||||
|
*/
|
||||||
|
export type getDetachedDomNodesParameters = {
|
||||||
|
}
|
||||||
|
export type getDetachedDomNodesReturnValue = {
|
||||||
|
/**
|
||||||
|
* The list of detached nodes
|
||||||
|
*/
|
||||||
|
detachedNodes: DetachedElementInfo[];
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Enables console to refer to the node with given id via $x (see Command Line API for more details
|
* Enables console to refer to the node with given id via $x (see Command Line API for more details
|
||||||
$x functions).
|
$x functions).
|
||||||
|
|
@ -11369,7 +11452,7 @@ as an ad.
|
||||||
* All Permissions Policy features. This enum should match the one defined
|
* All Permissions Policy features. This enum should match the one defined
|
||||||
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.
|
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.
|
||||||
*/
|
*/
|
||||||
export type PermissionsPolicyFeature = "accelerometer"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
|
export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
|
||||||
/**
|
/**
|
||||||
* Reason for a permissions policy feature to be disabled.
|
* Reason for a permissions policy feature to be disabled.
|
||||||
*/
|
*/
|
||||||
|
|
@ -11784,7 +11867,7 @@ Example URLs: http://www.google.com/file.html -> "google.com"
|
||||||
*/
|
*/
|
||||||
fixed?: number;
|
fixed?: number;
|
||||||
}
|
}
|
||||||
export type ClientNavigationReason = "formSubmissionGet"|"formSubmissionPost"|"httpHeaderRefresh"|"scriptInitiated"|"metaTagRefresh"|"pageBlockInterstitial"|"reload"|"anchorClick";
|
export type ClientNavigationReason = "anchorClick"|"formSubmissionGet"|"formSubmissionPost"|"httpHeaderRefresh"|"initialFrameNavigation"|"metaTagRefresh"|"other"|"pageBlockInterstitial"|"reload"|"scriptInitiated";
|
||||||
export type ClientNavigationDisposition = "currentTab"|"newTab"|"newWindow"|"download";
|
export type ClientNavigationDisposition = "currentTab"|"newTab"|"newWindow"|"download";
|
||||||
export interface InstallabilityErrorArgument {
|
export interface InstallabilityErrorArgument {
|
||||||
/**
|
/**
|
||||||
|
|
@ -12298,6 +12381,10 @@ when bfcache navigation fails.
|
||||||
* Frame's new url.
|
* Frame's new url.
|
||||||
*/
|
*/
|
||||||
url: string;
|
url: string;
|
||||||
|
/**
|
||||||
|
* Navigation type
|
||||||
|
*/
|
||||||
|
navigationType: "fragment"|"historyApi"|"other";
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Compressed image data requested by the `startScreencast`.
|
* Compressed image data requested by the `startScreencast`.
|
||||||
|
|
@ -16922,7 +17009,7 @@ possible for multiple rule sets and links to trigger a single attempt.
|
||||||
/**
|
/**
|
||||||
* List of FinalStatus reasons for Prerender2.
|
* List of FinalStatus reasons for Prerender2.
|
||||||
*/
|
*/
|
||||||
export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed";
|
export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"|"SlowNetwork"|"OtherPrerenderedPageActivated";
|
||||||
/**
|
/**
|
||||||
* Preloading status values, see also PreloadingTriggeringOutcome. This
|
* Preloading status values, see also PreloadingTriggeringOutcome. This
|
||||||
status is shared by prefetchStatusUpdated and prerenderStatusUpdated.
|
status is shared by prefetchStatusUpdated and prerenderStatusUpdated.
|
||||||
|
|
@ -17270,6 +17357,101 @@ supported yet.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This domain allows configuring virtual Bluetooth devices to test
|
||||||
|
the web-bluetooth API.
|
||||||
|
*/
|
||||||
|
export module BluetoothEmulation {
|
||||||
|
/**
|
||||||
|
* Indicates the various states of Central.
|
||||||
|
*/
|
||||||
|
export type CentralState = "absent"|"powered-off"|"powered-on";
|
||||||
|
/**
|
||||||
|
* Stores the manufacturer data
|
||||||
|
*/
|
||||||
|
export interface ManufacturerData {
|
||||||
|
/**
|
||||||
|
* Company identifier
|
||||||
|
https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml
|
||||||
|
https://usb.org/developers
|
||||||
|
*/
|
||||||
|
key: number;
|
||||||
|
/**
|
||||||
|
* Manufacturer-specific data
|
||||||
|
*/
|
||||||
|
data: binary;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Stores the byte data of the advertisement packet sent by a Bluetooth device.
|
||||||
|
*/
|
||||||
|
export interface ScanRecord {
|
||||||
|
name?: string;
|
||||||
|
uuids?: string[];
|
||||||
|
/**
|
||||||
|
* Stores the external appearance description of the device.
|
||||||
|
*/
|
||||||
|
appearance?: number;
|
||||||
|
/**
|
||||||
|
* Stores the transmission power of a broadcasting device.
|
||||||
|
*/
|
||||||
|
txPower?: number;
|
||||||
|
/**
|
||||||
|
* Key is the company identifier and the value is an array of bytes of
|
||||||
|
manufacturer specific data.
|
||||||
|
*/
|
||||||
|
manufacturerData?: ManufacturerData[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Stores the advertisement packet information that is sent by a Bluetooth device.
|
||||||
|
*/
|
||||||
|
export interface ScanEntry {
|
||||||
|
deviceAddress: string;
|
||||||
|
rssi: number;
|
||||||
|
scanRecord: ScanRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable the BluetoothEmulation domain.
|
||||||
|
*/
|
||||||
|
export type enableParameters = {
|
||||||
|
/**
|
||||||
|
* State of the simulated central.
|
||||||
|
*/
|
||||||
|
state: CentralState;
|
||||||
|
}
|
||||||
|
export type enableReturnValue = {
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Disable the BluetoothEmulation domain.
|
||||||
|
*/
|
||||||
|
export type disableParameters = {
|
||||||
|
}
|
||||||
|
export type disableReturnValue = {
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Simulates a peripheral with |address|, |name| and |knownServiceUuids|
|
||||||
|
that has already been connected to the system.
|
||||||
|
*/
|
||||||
|
export type simulatePreconnectedPeripheralParameters = {
|
||||||
|
address: string;
|
||||||
|
name: string;
|
||||||
|
manufacturerData: ManufacturerData[];
|
||||||
|
knownServiceUuids: string[];
|
||||||
|
}
|
||||||
|
export type simulatePreconnectedPeripheralReturnValue = {
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Simulates an advertisement packet described in |entry| being received by
|
||||||
|
the central.
|
||||||
|
*/
|
||||||
|
export type simulateAdvertisementParameters = {
|
||||||
|
entry: ScanEntry;
|
||||||
|
}
|
||||||
|
export type simulateAdvertisementReturnValue = {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This domain is deprecated - use Runtime or Log instead.
|
* This domain is deprecated - use Runtime or Log instead.
|
||||||
*/
|
*/
|
||||||
|
|
@ -20122,6 +20304,10 @@ Error was thrown.
|
||||||
"Audits.checkContrast": Audits.checkContrastParameters;
|
"Audits.checkContrast": Audits.checkContrastParameters;
|
||||||
"Audits.checkFormsIssues": Audits.checkFormsIssuesParameters;
|
"Audits.checkFormsIssues": Audits.checkFormsIssuesParameters;
|
||||||
"Extensions.loadUnpacked": Extensions.loadUnpackedParameters;
|
"Extensions.loadUnpacked": Extensions.loadUnpackedParameters;
|
||||||
|
"Extensions.getStorageItems": Extensions.getStorageItemsParameters;
|
||||||
|
"Extensions.removeStorageItems": Extensions.removeStorageItemsParameters;
|
||||||
|
"Extensions.clearStorageItems": Extensions.clearStorageItemsParameters;
|
||||||
|
"Extensions.setStorageItems": Extensions.setStorageItemsParameters;
|
||||||
"Autofill.trigger": Autofill.triggerParameters;
|
"Autofill.trigger": Autofill.triggerParameters;
|
||||||
"Autofill.setAddresses": Autofill.setAddressesParameters;
|
"Autofill.setAddresses": Autofill.setAddressesParameters;
|
||||||
"Autofill.disable": Autofill.disableParameters;
|
"Autofill.disable": Autofill.disableParameters;
|
||||||
|
|
@ -20232,6 +20418,7 @@ Error was thrown.
|
||||||
"DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledParameters;
|
"DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledParameters;
|
||||||
"DOM.getNodeStackTraces": DOM.getNodeStackTracesParameters;
|
"DOM.getNodeStackTraces": DOM.getNodeStackTracesParameters;
|
||||||
"DOM.getFileInfo": DOM.getFileInfoParameters;
|
"DOM.getFileInfo": DOM.getFileInfoParameters;
|
||||||
|
"DOM.getDetachedDomNodes": DOM.getDetachedDomNodesParameters;
|
||||||
"DOM.setInspectedNode": DOM.setInspectedNodeParameters;
|
"DOM.setInspectedNode": DOM.setInspectedNodeParameters;
|
||||||
"DOM.setNodeName": DOM.setNodeNameParameters;
|
"DOM.setNodeName": DOM.setNodeNameParameters;
|
||||||
"DOM.setNodeValue": DOM.setNodeValueParameters;
|
"DOM.setNodeValue": DOM.setNodeValueParameters;
|
||||||
|
|
@ -20616,6 +20803,10 @@ Error was thrown.
|
||||||
"PWA.launchFilesInApp": PWA.launchFilesInAppParameters;
|
"PWA.launchFilesInApp": PWA.launchFilesInAppParameters;
|
||||||
"PWA.openCurrentPageInApp": PWA.openCurrentPageInAppParameters;
|
"PWA.openCurrentPageInApp": PWA.openCurrentPageInAppParameters;
|
||||||
"PWA.changeAppUserSettings": PWA.changeAppUserSettingsParameters;
|
"PWA.changeAppUserSettings": PWA.changeAppUserSettingsParameters;
|
||||||
|
"BluetoothEmulation.enable": BluetoothEmulation.enableParameters;
|
||||||
|
"BluetoothEmulation.disable": BluetoothEmulation.disableParameters;
|
||||||
|
"BluetoothEmulation.simulatePreconnectedPeripheral": BluetoothEmulation.simulatePreconnectedPeripheralParameters;
|
||||||
|
"BluetoothEmulation.simulateAdvertisement": BluetoothEmulation.simulateAdvertisementParameters;
|
||||||
"Console.clearMessages": Console.clearMessagesParameters;
|
"Console.clearMessages": Console.clearMessagesParameters;
|
||||||
"Console.disable": Console.disableParameters;
|
"Console.disable": Console.disableParameters;
|
||||||
"Console.enable": Console.enableParameters;
|
"Console.enable": Console.enableParameters;
|
||||||
|
|
@ -20722,6 +20913,10 @@ Error was thrown.
|
||||||
"Audits.checkContrast": Audits.checkContrastReturnValue;
|
"Audits.checkContrast": Audits.checkContrastReturnValue;
|
||||||
"Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue;
|
"Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue;
|
||||||
"Extensions.loadUnpacked": Extensions.loadUnpackedReturnValue;
|
"Extensions.loadUnpacked": Extensions.loadUnpackedReturnValue;
|
||||||
|
"Extensions.getStorageItems": Extensions.getStorageItemsReturnValue;
|
||||||
|
"Extensions.removeStorageItems": Extensions.removeStorageItemsReturnValue;
|
||||||
|
"Extensions.clearStorageItems": Extensions.clearStorageItemsReturnValue;
|
||||||
|
"Extensions.setStorageItems": Extensions.setStorageItemsReturnValue;
|
||||||
"Autofill.trigger": Autofill.triggerReturnValue;
|
"Autofill.trigger": Autofill.triggerReturnValue;
|
||||||
"Autofill.setAddresses": Autofill.setAddressesReturnValue;
|
"Autofill.setAddresses": Autofill.setAddressesReturnValue;
|
||||||
"Autofill.disable": Autofill.disableReturnValue;
|
"Autofill.disable": Autofill.disableReturnValue;
|
||||||
|
|
@ -20832,6 +21027,7 @@ Error was thrown.
|
||||||
"DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledReturnValue;
|
"DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledReturnValue;
|
||||||
"DOM.getNodeStackTraces": DOM.getNodeStackTracesReturnValue;
|
"DOM.getNodeStackTraces": DOM.getNodeStackTracesReturnValue;
|
||||||
"DOM.getFileInfo": DOM.getFileInfoReturnValue;
|
"DOM.getFileInfo": DOM.getFileInfoReturnValue;
|
||||||
|
"DOM.getDetachedDomNodes": DOM.getDetachedDomNodesReturnValue;
|
||||||
"DOM.setInspectedNode": DOM.setInspectedNodeReturnValue;
|
"DOM.setInspectedNode": DOM.setInspectedNodeReturnValue;
|
||||||
"DOM.setNodeName": DOM.setNodeNameReturnValue;
|
"DOM.setNodeName": DOM.setNodeNameReturnValue;
|
||||||
"DOM.setNodeValue": DOM.setNodeValueReturnValue;
|
"DOM.setNodeValue": DOM.setNodeValueReturnValue;
|
||||||
|
|
@ -21216,6 +21412,10 @@ Error was thrown.
|
||||||
"PWA.launchFilesInApp": PWA.launchFilesInAppReturnValue;
|
"PWA.launchFilesInApp": PWA.launchFilesInAppReturnValue;
|
||||||
"PWA.openCurrentPageInApp": PWA.openCurrentPageInAppReturnValue;
|
"PWA.openCurrentPageInApp": PWA.openCurrentPageInAppReturnValue;
|
||||||
"PWA.changeAppUserSettings": PWA.changeAppUserSettingsReturnValue;
|
"PWA.changeAppUserSettings": PWA.changeAppUserSettingsReturnValue;
|
||||||
|
"BluetoothEmulation.enable": BluetoothEmulation.enableReturnValue;
|
||||||
|
"BluetoothEmulation.disable": BluetoothEmulation.disableReturnValue;
|
||||||
|
"BluetoothEmulation.simulatePreconnectedPeripheral": BluetoothEmulation.simulatePreconnectedPeripheralReturnValue;
|
||||||
|
"BluetoothEmulation.simulateAdvertisement": BluetoothEmulation.simulateAdvertisementReturnValue;
|
||||||
"Console.clearMessages": Console.clearMessagesReturnValue;
|
"Console.clearMessages": Console.clearMessagesReturnValue;
|
||||||
"Console.disable": Console.disableReturnValue;
|
"Console.disable": Console.disableReturnValue;
|
||||||
"Console.enable": Console.enableReturnValue;
|
"Console.enable": Console.enableReturnValue;
|
||||||
|
|
|
||||||
3
packages/playwright-core/src/server/codegen/DEPS.list
Normal file
3
packages/playwright-core/src/server/codegen/DEPS.list
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[*]
|
||||||
|
../../utils/
|
||||||
|
../deviceDescriptors.ts
|
||||||
|
|
@ -14,13 +14,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { BrowserContextOptions } from '../../..';
|
import type { BrowserContextOptions } from '../../../types/types';
|
||||||
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
|
import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
|
||||||
import { sanitizeDeviceOptions, toSignalMap } from './language';
|
import { sanitizeDeviceOptions, toClickOptions, toKeyboardModifiers, toSignalMap } from './language';
|
||||||
import type { ActionInContext } from './codeGenerator';
|
|
||||||
import type { Action } from './recorderActions';
|
|
||||||
import type { MouseClickOptions } from './utils';
|
|
||||||
import { toModifiers } from './utils';
|
|
||||||
import { escapeWithQuotes, asLocator } from '../../utils';
|
import { escapeWithQuotes, asLocator } from '../../utils';
|
||||||
import { deviceDescriptors } from '../deviceDescriptors';
|
import { deviceDescriptors } from '../deviceDescriptors';
|
||||||
|
|
||||||
|
|
@ -72,14 +68,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
||||||
return formatter.format();
|
return formatter.format();
|
||||||
}
|
}
|
||||||
|
|
||||||
let subject: string;
|
const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.ContentFrame()`);
|
||||||
if (actionInContext.frame.isMainFrame) {
|
const subject = `${pageAlias}${locators.join('')}`;
|
||||||
subject = pageAlias;
|
|
||||||
} else {
|
|
||||||
const locators = actionInContext.frame.selectorsChain.map(selector => `.FrameLocator(${quote(selector)})`);
|
|
||||||
subject = `${pageAlias}${locators.join('')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const signals = toSignalMap(action);
|
const signals = toSignalMap(action);
|
||||||
|
|
||||||
if (signals.dialog) {
|
if (signals.dialog) {
|
||||||
|
|
@ -93,7 +83,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
lines.push(this._generateActionCall(subject, action));
|
lines.push(this._generateActionCall(subject, actionInContext));
|
||||||
|
|
||||||
if (signals.download) {
|
if (signals.download) {
|
||||||
lines.unshift(`var download${signals.download.downloadAlias} = await ${pageAlias}.RunAndWaitForDownloadAsync(async () =>\n{`);
|
lines.unshift(`var download${signals.download.downloadAlias} = await ${pageAlias}.RunAndWaitForDownloadAsync(async () =>\n{`);
|
||||||
|
|
@ -111,7 +101,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
||||||
return formatter.format();
|
return formatter.format();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _generateActionCall(subject: string, action: Action): string {
|
private _generateActionCall(subject: string, actionInContext: ActionInContext): string {
|
||||||
|
const action = actionInContext.action;
|
||||||
switch (action.name) {
|
switch (action.name) {
|
||||||
case 'openPage':
|
case 'openPage':
|
||||||
throw Error('Not reached');
|
throw Error('Not reached');
|
||||||
|
|
@ -121,16 +112,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
||||||
let method = 'Click';
|
let method = 'Click';
|
||||||
if (action.clickCount === 2)
|
if (action.clickCount === 2)
|
||||||
method = 'DblClick';
|
method = 'DblClick';
|
||||||
const modifiers = toModifiers(action.modifiers);
|
const options = toClickOptions(action);
|
||||||
const options: MouseClickOptions = {};
|
|
||||||
if (action.button !== 'left')
|
|
||||||
options.button = action.button;
|
|
||||||
if (modifiers.length)
|
|
||||||
options.modifiers = modifiers;
|
|
||||||
if (action.clickCount > 2)
|
|
||||||
options.clickCount = action.clickCount;
|
|
||||||
if (action.position)
|
|
||||||
options.position = action.position;
|
|
||||||
if (!Object.entries(options).length)
|
if (!Object.entries(options).length)
|
||||||
return `await ${subject}.${this._asLocator(action.selector)}.${method}Async();`;
|
return `await ${subject}.${this._asLocator(action.selector)}.${method}Async();`;
|
||||||
const optionsString = formatObject(options, ' ', 'Locator' + method + 'Options');
|
const optionsString = formatObject(options, ' ', 'Locator' + method + 'Options');
|
||||||
|
|
@ -145,7 +127,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
||||||
case 'setInputFiles':
|
case 'setInputFiles':
|
||||||
return `await ${subject}.${this._asLocator(action.selector)}.SetInputFilesAsync(${formatObject(action.files)});`;
|
return `await ${subject}.${this._asLocator(action.selector)}.SetInputFilesAsync(${formatObject(action.files)});`;
|
||||||
case 'press': {
|
case 'press': {
|
||||||
const modifiers = toModifiers(action.modifiers);
|
const modifiers = toKeyboardModifiers(action.modifiers);
|
||||||
const shortcut = [...modifiers, action.key].join('+');
|
const shortcut = [...modifiers, action.key].join('+');
|
||||||
return `await ${subject}.${this._asLocator(action.selector)}.PressAsync(${quote(shortcut)});`;
|
return `await ${subject}.${this._asLocator(action.selector)}.PressAsync(${quote(shortcut)});`;
|
||||||
}
|
}
|
||||||
|
|
@ -14,13 +14,10 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { BrowserContextOptions } from '../../..';
|
import type { BrowserContextOptions } from '../../../types/types';
|
||||||
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
|
import type * as types from '../types';
|
||||||
import { toSignalMap } from './language';
|
import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
|
||||||
import type { ActionInContext } from './codeGenerator';
|
import { toClickOptions, toKeyboardModifiers, toSignalMap } from './language';
|
||||||
import type { Action } from './recorderActions';
|
|
||||||
import type { MouseClickOptions } from './utils';
|
|
||||||
import { toModifiers } from './utils';
|
|
||||||
import { deviceDescriptors } from '../deviceDescriptors';
|
import { deviceDescriptors } from '../deviceDescriptors';
|
||||||
import { JavaScriptFormatter } from './javascript';
|
import { JavaScriptFormatter } from './javascript';
|
||||||
import { escapeWithQuotes, asLocator } from '../../utils';
|
import { escapeWithQuotes, asLocator } from '../../utils';
|
||||||
|
|
@ -63,16 +60,8 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
||||||
return formatter.format();
|
return formatter.format();
|
||||||
}
|
}
|
||||||
|
|
||||||
let subject: string;
|
const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector, false)}.contentFrame()`);
|
||||||
let inFrameLocator = false;
|
const subject = `${pageAlias}${locators.join('')}`;
|
||||||
if (actionInContext.frame.isMainFrame) {
|
|
||||||
subject = pageAlias;
|
|
||||||
} else {
|
|
||||||
const locators = actionInContext.frame.selectorsChain.map(selector => `.frameLocator(${quote(selector)})`);
|
|
||||||
subject = `${pageAlias}${locators.join('')}`;
|
|
||||||
inFrameLocator = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const signals = toSignalMap(action);
|
const signals = toSignalMap(action);
|
||||||
|
|
||||||
if (signals.dialog) {
|
if (signals.dialog) {
|
||||||
|
|
@ -82,7 +71,7 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
||||||
});`);
|
});`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let code = this._generateActionCall(subject, action, inFrameLocator);
|
let code = this._generateActionCall(subject, actionInContext, !!actionInContext.frame.framePath.length);
|
||||||
|
|
||||||
if (signals.popup) {
|
if (signals.popup) {
|
||||||
code = `Page ${signals.popup.popupAlias} = ${pageAlias}.waitForPopup(() -> {
|
code = `Page ${signals.popup.popupAlias} = ${pageAlias}.waitForPopup(() -> {
|
||||||
|
|
@ -101,7 +90,8 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
||||||
return formatter.format();
|
return formatter.format();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _generateActionCall(subject: string, action: Action, inFrameLocator: boolean): string {
|
private _generateActionCall(subject: string, actionInContext: ActionInContext, inFrameLocator: boolean): string {
|
||||||
|
const action = actionInContext.action;
|
||||||
switch (action.name) {
|
switch (action.name) {
|
||||||
case 'openPage':
|
case 'openPage':
|
||||||
throw Error('Not reached');
|
throw Error('Not reached');
|
||||||
|
|
@ -111,16 +101,7 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
||||||
let method = 'click';
|
let method = 'click';
|
||||||
if (action.clickCount === 2)
|
if (action.clickCount === 2)
|
||||||
method = 'dblclick';
|
method = 'dblclick';
|
||||||
const modifiers = toModifiers(action.modifiers);
|
const options = toClickOptions(action);
|
||||||
const options: MouseClickOptions = {};
|
|
||||||
if (action.button !== 'left')
|
|
||||||
options.button = action.button;
|
|
||||||
if (modifiers.length)
|
|
||||||
options.modifiers = modifiers;
|
|
||||||
if (action.clickCount > 2)
|
|
||||||
options.clickCount = action.clickCount;
|
|
||||||
if (action.position)
|
|
||||||
options.position = action.position;
|
|
||||||
const optionsText = formatClickOptions(options);
|
const optionsText = formatClickOptions(options);
|
||||||
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.${method}(${optionsText});`;
|
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.${method}(${optionsText});`;
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +114,7 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
||||||
case 'setInputFiles':
|
case 'setInputFiles':
|
||||||
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.setInputFiles(${formatPath(action.files.length === 1 ? action.files[0] : action.files)});`;
|
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.setInputFiles(${formatPath(action.files.length === 1 ? action.files[0] : action.files)});`;
|
||||||
case 'press': {
|
case 'press': {
|
||||||
const modifiers = toModifiers(action.modifiers);
|
const modifiers = toKeyboardModifiers(action.modifiers);
|
||||||
const shortcut = [...modifiers, action.key].join('+');
|
const shortcut = [...modifiers, action.key].join('+');
|
||||||
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.press(${quote(shortcut)});`;
|
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.press(${quote(shortcut)});`;
|
||||||
}
|
}
|
||||||
|
|
@ -279,7 +260,7 @@ function formatContextOptions(contextOptions: BrowserContextOptions, deviceName:
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatClickOptions(options: MouseClickOptions) {
|
function formatClickOptions(options: types.MouseClickOptions) {
|
||||||
const lines = [];
|
const lines = [];
|
||||||
if (options.button)
|
if (options.button)
|
||||||
lines.push(` .setButton(MouseButton.${options.button.toUpperCase()})`);
|
lines.push(` .setButton(MouseButton.${options.button.toUpperCase()})`);
|
||||||
|
|
@ -14,13 +14,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { BrowserContextOptions } from '../../..';
|
import type { BrowserContextOptions } from '../../../types/types';
|
||||||
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
|
import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
|
||||||
import { sanitizeDeviceOptions, toSignalMap } from './language';
|
import { sanitizeDeviceOptions, toSignalMap, toKeyboardModifiers, toClickOptions } from './language';
|
||||||
import type { ActionInContext } from './codeGenerator';
|
|
||||||
import type { Action } from './recorderActions';
|
|
||||||
import type { MouseClickOptions } from './utils';
|
|
||||||
import { toModifiers } from './utils';
|
|
||||||
import { deviceDescriptors } from '../deviceDescriptors';
|
import { deviceDescriptors } from '../deviceDescriptors';
|
||||||
import { escapeWithQuotes, asLocator } from '../../utils';
|
import { escapeWithQuotes, asLocator } from '../../utils';
|
||||||
|
|
||||||
|
|
@ -52,14 +48,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
||||||
return formatter.format();
|
return formatter.format();
|
||||||
}
|
}
|
||||||
|
|
||||||
let subject: string;
|
const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.contentFrame()`);
|
||||||
if (actionInContext.frame.isMainFrame) {
|
const subject = `${pageAlias}${locators.join('')}`;
|
||||||
subject = pageAlias;
|
|
||||||
} else {
|
|
||||||
const locators = actionInContext.frame.selectorsChain.map(selector => `.frameLocator(${quote(selector)})`);
|
|
||||||
subject = `${pageAlias}${locators.join('')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const signals = toSignalMap(action);
|
const signals = toSignalMap(action);
|
||||||
|
|
||||||
if (signals.dialog) {
|
if (signals.dialog) {
|
||||||
|
|
@ -74,7 +64,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
||||||
if (signals.download)
|
if (signals.download)
|
||||||
formatter.add(`const download${signals.download.downloadAlias}Promise = ${pageAlias}.waitForEvent('download');`);
|
formatter.add(`const download${signals.download.downloadAlias}Promise = ${pageAlias}.waitForEvent('download');`);
|
||||||
|
|
||||||
formatter.add(this._generateActionCall(subject, action));
|
formatter.add(wrapWithStep(actionInContext.description, this._generateActionCall(subject, actionInContext)));
|
||||||
|
|
||||||
if (signals.popup)
|
if (signals.popup)
|
||||||
formatter.add(`const ${signals.popup.popupAlias} = await ${signals.popup.popupAlias}Promise;`);
|
formatter.add(`const ${signals.popup.popupAlias} = await ${signals.popup.popupAlias}Promise;`);
|
||||||
|
|
@ -84,7 +74,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
||||||
return formatter.format();
|
return formatter.format();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _generateActionCall(subject: string, action: Action): string {
|
private _generateActionCall(subject: string, actionInContext: ActionInContext): string {
|
||||||
|
const action = actionInContext.action;
|
||||||
switch (action.name) {
|
switch (action.name) {
|
||||||
case 'openPage':
|
case 'openPage':
|
||||||
throw Error('Not reached');
|
throw Error('Not reached');
|
||||||
|
|
@ -94,16 +85,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
||||||
let method = 'click';
|
let method = 'click';
|
||||||
if (action.clickCount === 2)
|
if (action.clickCount === 2)
|
||||||
method = 'dblclick';
|
method = 'dblclick';
|
||||||
const modifiers = toModifiers(action.modifiers);
|
const options = toClickOptions(action);
|
||||||
const options: MouseClickOptions = {};
|
|
||||||
if (action.button !== 'left')
|
|
||||||
options.button = action.button;
|
|
||||||
if (modifiers.length)
|
|
||||||
options.modifiers = modifiers;
|
|
||||||
if (action.clickCount > 2)
|
|
||||||
options.clickCount = action.clickCount;
|
|
||||||
if (action.position)
|
|
||||||
options.position = action.position;
|
|
||||||
const optionsString = formatOptions(options, false);
|
const optionsString = formatOptions(options, false);
|
||||||
return `await ${subject}.${this._asLocator(action.selector)}.${method}(${optionsString});`;
|
return `await ${subject}.${this._asLocator(action.selector)}.${method}(${optionsString});`;
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +98,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
||||||
case 'setInputFiles':
|
case 'setInputFiles':
|
||||||
return `await ${subject}.${this._asLocator(action.selector)}.setInputFiles(${formatObject(action.files.length === 1 ? action.files[0] : action.files)});`;
|
return `await ${subject}.${this._asLocator(action.selector)}.setInputFiles(${formatObject(action.files.length === 1 ? action.files[0] : action.files)});`;
|
||||||
case 'press': {
|
case 'press': {
|
||||||
const modifiers = toModifiers(action.modifiers);
|
const modifiers = toKeyboardModifiers(action.modifiers);
|
||||||
const shortcut = [...modifiers, action.key].join('+');
|
const shortcut = [...modifiers, action.key].join('+');
|
||||||
return `await ${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)});`;
|
return `await ${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)});`;
|
||||||
}
|
}
|
||||||
|
|
@ -276,3 +258,9 @@ export class JavaScriptFormatter {
|
||||||
function quote(text: string) {
|
function quote(text: string) {
|
||||||
return escapeWithQuotes(text, '\'');
|
return escapeWithQuotes(text, '\'');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function wrapWithStep(description: string | undefined, body: string) {
|
||||||
|
return description ? `await test.step(\`${description}\`, async () => {
|
||||||
|
${body}
|
||||||
|
});` : body;
|
||||||
|
}
|
||||||
|
|
@ -15,8 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { asLocator } from '../../utils';
|
import { asLocator } from '../../utils';
|
||||||
import type { ActionInContext } from './codeGenerator';
|
import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
|
||||||
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
|
|
||||||
|
|
||||||
export class JsonlLanguageGenerator implements LanguageGenerator {
|
export class JsonlLanguageGenerator implements LanguageGenerator {
|
||||||
id = 'jsonl';
|
id = 'jsonl';
|
||||||
84
packages/playwright-core/src/server/codegen/language.ts
Normal file
84
packages/playwright-core/src/server/codegen/language.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { BrowserContextOptions } from '../../..';
|
||||||
|
import type * as actions from '../recorder/recorderActions';
|
||||||
|
import type * as types from '../types';
|
||||||
|
import type { ActionInContext, LanguageGenerator, LanguageGeneratorOptions } from './types';
|
||||||
|
|
||||||
|
export function generateCode(actions: ActionInContext[], languageGenerator: LanguageGenerator, options: LanguageGeneratorOptions) {
|
||||||
|
const header = languageGenerator.generateHeader(options);
|
||||||
|
const footer = languageGenerator.generateFooter(options.saveStorage);
|
||||||
|
const actionTexts = actions.map(a => languageGenerator.generateAction(a)).filter(Boolean);
|
||||||
|
const text = [header, ...actionTexts, footer].join('\n');
|
||||||
|
return { header, footer, actionTexts, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeDeviceOptions(device: any, options: BrowserContextOptions): BrowserContextOptions {
|
||||||
|
// Filter out all the properties from the device descriptor.
|
||||||
|
const cleanedOptions: Record<string, any> = {};
|
||||||
|
for (const property in options) {
|
||||||
|
if (JSON.stringify(device[property]) !== JSON.stringify((options as any)[property]))
|
||||||
|
cleanedOptions[property] = (options as any)[property];
|
||||||
|
}
|
||||||
|
return cleanedOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toSignalMap(action: actions.Action) {
|
||||||
|
let popup: actions.PopupSignal | undefined;
|
||||||
|
let download: actions.DownloadSignal | undefined;
|
||||||
|
let dialog: actions.DialogSignal | undefined;
|
||||||
|
for (const signal of action.signals) {
|
||||||
|
if (signal.name === 'popup')
|
||||||
|
popup = signal;
|
||||||
|
else if (signal.name === 'download')
|
||||||
|
download = signal;
|
||||||
|
else if (signal.name === 'dialog')
|
||||||
|
dialog = signal;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
popup,
|
||||||
|
download,
|
||||||
|
dialog,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toKeyboardModifiers(modifiers: number): types.SmartKeyboardModifier[] {
|
||||||
|
const result: types.SmartKeyboardModifier[] = [];
|
||||||
|
if (modifiers & 1)
|
||||||
|
result.push('Alt');
|
||||||
|
if (modifiers & 2)
|
||||||
|
result.push('ControlOrMeta');
|
||||||
|
if (modifiers & 4)
|
||||||
|
result.push('ControlOrMeta');
|
||||||
|
if (modifiers & 8)
|
||||||
|
result.push('Shift');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toClickOptions(action: actions.ClickAction): types.MouseClickOptions {
|
||||||
|
const modifiers = toKeyboardModifiers(action.modifiers);
|
||||||
|
const options: types.MouseClickOptions = {};
|
||||||
|
if (action.button !== 'left')
|
||||||
|
options.button = action.button;
|
||||||
|
if (modifiers.length)
|
||||||
|
options.modifiers = modifiers;
|
||||||
|
if (action.clickCount > 2)
|
||||||
|
options.clickCount = action.clickCount;
|
||||||
|
if (action.position)
|
||||||
|
options.position = action.position;
|
||||||
|
return options;
|
||||||
|
}
|
||||||
37
packages/playwright-core/src/server/codegen/languages.ts
Normal file
37
packages/playwright-core/src/server/codegen/languages.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { JavaLanguageGenerator } from './java';
|
||||||
|
import { JavaScriptLanguageGenerator } from './javascript';
|
||||||
|
import { JsonlLanguageGenerator } from './jsonl';
|
||||||
|
import { CSharpLanguageGenerator } from './csharp';
|
||||||
|
import { PythonLanguageGenerator } from './python';
|
||||||
|
|
||||||
|
export function languageSet() {
|
||||||
|
return new Set([
|
||||||
|
new JavaLanguageGenerator('junit'),
|
||||||
|
new JavaLanguageGenerator('library'),
|
||||||
|
new JavaScriptLanguageGenerator(/* isPlaywrightTest */false),
|
||||||
|
new JavaScriptLanguageGenerator(/* isPlaywrightTest */true),
|
||||||
|
new PythonLanguageGenerator(/* isAsync */false, /* isPytest */true),
|
||||||
|
new PythonLanguageGenerator(/* isAsync */false, /* isPytest */false),
|
||||||
|
new PythonLanguageGenerator(/* isAsync */true, /* isPytest */false),
|
||||||
|
new CSharpLanguageGenerator('mstest'),
|
||||||
|
new CSharpLanguageGenerator('nunit'),
|
||||||
|
new CSharpLanguageGenerator('library'),
|
||||||
|
new JsonlLanguageGenerator(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
@ -14,13 +14,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { BrowserContextOptions } from '../../..';
|
import type { BrowserContextOptions } from '../../../types/types';
|
||||||
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
|
import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
|
||||||
import { sanitizeDeviceOptions, toSignalMap } from './language';
|
import { sanitizeDeviceOptions, toSignalMap, toKeyboardModifiers, toClickOptions } from './language';
|
||||||
import type { ActionInContext } from './codeGenerator';
|
|
||||||
import type { Action } from './recorderActions';
|
|
||||||
import type { MouseClickOptions } from './utils';
|
|
||||||
import { toModifiers } from './utils';
|
|
||||||
import { escapeWithQuotes, toSnakeCase, asLocator } from '../../utils';
|
import { escapeWithQuotes, toSnakeCase, asLocator } from '../../utils';
|
||||||
import { deviceDescriptors } from '../deviceDescriptors';
|
import { deviceDescriptors } from '../deviceDescriptors';
|
||||||
|
|
||||||
|
|
@ -59,20 +55,14 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
||||||
return formatter.format();
|
return formatter.format();
|
||||||
}
|
}
|
||||||
|
|
||||||
let subject: string;
|
const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.content_frame()`);
|
||||||
if (actionInContext.frame.isMainFrame) {
|
const subject = `${pageAlias}${locators.join('')}`;
|
||||||
subject = pageAlias;
|
|
||||||
} else {
|
|
||||||
const locators = actionInContext.frame.selectorsChain.map(selector => `.frame_locator(${quote(selector)})`);
|
|
||||||
subject = `${pageAlias}${locators.join('')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const signals = toSignalMap(action);
|
const signals = toSignalMap(action);
|
||||||
|
|
||||||
if (signals.dialog)
|
if (signals.dialog)
|
||||||
formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`);
|
formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`);
|
||||||
|
|
||||||
let code = `${this._awaitPrefix}${this._generateActionCall(subject, action)}`;
|
let code = `${this._awaitPrefix}${this._generateActionCall(subject, actionInContext)}`;
|
||||||
|
|
||||||
if (signals.popup) {
|
if (signals.popup) {
|
||||||
code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as ${signals.popup.popupAlias}_info {
|
code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as ${signals.popup.popupAlias}_info {
|
||||||
|
|
@ -93,7 +83,8 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
||||||
return formatter.format();
|
return formatter.format();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _generateActionCall(subject: string, action: Action): string {
|
private _generateActionCall(subject: string, actionInContext: ActionInContext): string {
|
||||||
|
const action = actionInContext.action;
|
||||||
switch (action.name) {
|
switch (action.name) {
|
||||||
case 'openPage':
|
case 'openPage':
|
||||||
throw Error('Not reached');
|
throw Error('Not reached');
|
||||||
|
|
@ -103,16 +94,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
||||||
let method = 'click';
|
let method = 'click';
|
||||||
if (action.clickCount === 2)
|
if (action.clickCount === 2)
|
||||||
method = 'dblclick';
|
method = 'dblclick';
|
||||||
const modifiers = toModifiers(action.modifiers);
|
const options = toClickOptions(action);
|
||||||
const options: MouseClickOptions = {};
|
|
||||||
if (action.button !== 'left')
|
|
||||||
options.button = action.button;
|
|
||||||
if (modifiers.length)
|
|
||||||
options.modifiers = modifiers;
|
|
||||||
if (action.clickCount > 2)
|
|
||||||
options.clickCount = action.clickCount;
|
|
||||||
if (action.position)
|
|
||||||
options.position = action.position;
|
|
||||||
const optionsString = formatOptions(options, false);
|
const optionsString = formatOptions(options, false);
|
||||||
return `${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`;
|
return `${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`;
|
||||||
}
|
}
|
||||||
|
|
@ -125,7 +107,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
||||||
case 'setInputFiles':
|
case 'setInputFiles':
|
||||||
return `${subject}.${this._asLocator(action.selector)}.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`;
|
return `${subject}.${this._asLocator(action.selector)}.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`;
|
||||||
case 'press': {
|
case 'press': {
|
||||||
const modifiers = toModifiers(action.modifiers);
|
const modifiers = toKeyboardModifiers(action.modifiers);
|
||||||
const shortcut = [...modifiers, action.key].join('+');
|
const shortcut = [...modifiers, action.key].join('+');
|
||||||
return `${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)})`;
|
return `${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)})`;
|
||||||
}
|
}
|
||||||
50
packages/playwright-core/src/server/codegen/types.ts
Normal file
50
packages/playwright-core/src/server/codegen/types.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { BrowserContextOptions, LaunchOptions } from '../../../types/types';
|
||||||
|
import type * as actions from '../recorder/recorderActions';
|
||||||
|
import type { Language } from '../../utils';
|
||||||
|
export type { Language } from '../../utils';
|
||||||
|
|
||||||
|
export type LanguageGeneratorOptions = {
|
||||||
|
browserName: string;
|
||||||
|
launchOptions: LaunchOptions;
|
||||||
|
contextOptions: BrowserContextOptions;
|
||||||
|
deviceName?: string;
|
||||||
|
saveStorage?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FrameDescription = {
|
||||||
|
pageAlias: string;
|
||||||
|
framePath: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ActionInContext = {
|
||||||
|
frame: FrameDescription;
|
||||||
|
description?: string;
|
||||||
|
action: actions.Action;
|
||||||
|
committed?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface LanguageGenerator {
|
||||||
|
id: string;
|
||||||
|
groupName: string;
|
||||||
|
name: string;
|
||||||
|
highlighter: Language;
|
||||||
|
generateHeader(options: LanguageGeneratorOptions): string;
|
||||||
|
generateAction(actionInContext: ActionInContext): string;
|
||||||
|
generateFooter(saveStorage: string | undefined): string;
|
||||||
|
}
|
||||||
|
|
@ -52,7 +52,6 @@ export class DebugController extends SdkObject {
|
||||||
initialize(codegenId: string, sdkLanguage: Language) {
|
initialize(codegenId: string, sdkLanguage: Language) {
|
||||||
this._codegenId = codegenId;
|
this._codegenId = codegenId;
|
||||||
this._sdkLanguage = sdkLanguage;
|
this._sdkLanguage = sdkLanguage;
|
||||||
Recorder.setAppFactory(async () => new InspectingRecorderApp(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setAutoCloseAllowed(allowed: boolean) {
|
setAutoCloseAllowed(allowed: boolean) {
|
||||||
|
|
@ -62,7 +61,6 @@ export class DebugController extends SdkObject {
|
||||||
dispose() {
|
dispose() {
|
||||||
this.setReportStateChanged(false);
|
this.setReportStateChanged(false);
|
||||||
this.setAutoCloseAllowed(false);
|
this.setAutoCloseAllowed(false);
|
||||||
Recorder.setAppFactory(undefined);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setReportStateChanged(enabled: boolean) {
|
setReportStateChanged(enabled: boolean) {
|
||||||
|
|
@ -199,7 +197,7 @@ export class DebugController extends SdkObject {
|
||||||
const contexts = new Set<BrowserContext>();
|
const contexts = new Set<BrowserContext>();
|
||||||
for (const page of this._playwright.allPages())
|
for (const page of this._playwright.allPages())
|
||||||
contexts.add(page.context());
|
contexts.add(page.context());
|
||||||
const result = await Promise.all([...contexts].map(c => Recorder.show(c, { omitCallTracking: true })));
|
const result = await Promise.all([...contexts].map(c => Recorder.show(c, () => Promise.resolve(new InspectingRecorderApp(this)), { omitCallTracking: true })));
|
||||||
return result.filter(Boolean) as Recorder[];
|
return result.filter(Boolean) as Recorder[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"Galaxy S5": {
|
"Galaxy S5": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -121,7 +121,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S5 landscape": {
|
"Galaxy S5 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -132,7 +132,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S8": {
|
"Galaxy S8": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 740
|
"height": 740
|
||||||
|
|
@ -143,7 +143,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S8 landscape": {
|
"Galaxy S8 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 740,
|
"width": 740,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -154,7 +154,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S9+": {
|
"Galaxy S9+": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 320,
|
"width": 320,
|
||||||
"height": 658
|
"height": 658
|
||||||
|
|
@ -165,7 +165,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S9+ landscape": {
|
"Galaxy S9+ landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 658,
|
"width": 658,
|
||||||
"height": 320
|
"height": 320
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy Tab S4": {
|
"Galaxy Tab S4": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 712,
|
"width": 712,
|
||||||
"height": 1138
|
"height": 1138
|
||||||
|
|
@ -187,7 +187,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy Tab S4 landscape": {
|
"Galaxy Tab S4 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 1138,
|
"width": 1138,
|
||||||
"height": 712
|
"height": 712
|
||||||
|
|
@ -1098,7 +1098,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"LG Optimus L70": {
|
"LG Optimus L70": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 384,
|
"width": 384,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1109,7 +1109,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"LG Optimus L70 landscape": {
|
"LG Optimus L70 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 384
|
"height": 384
|
||||||
|
|
@ -1120,7 +1120,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 550": {
|
"Microsoft Lumia 550": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263",
|
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36 Edge/14.14263",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1131,7 +1131,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 550 landscape": {
|
"Microsoft Lumia 550 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263",
|
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36 Edge/14.14263",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1142,7 +1142,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 950": {
|
"Microsoft Lumia 950": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263",
|
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36 Edge/14.14263",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1153,7 +1153,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 950 landscape": {
|
"Microsoft Lumia 950 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263",
|
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36 Edge/14.14263",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1164,7 +1164,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 10": {
|
"Nexus 10": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 800,
|
"width": 800,
|
||||||
"height": 1280
|
"height": 1280
|
||||||
|
|
@ -1175,7 +1175,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 10 landscape": {
|
"Nexus 10 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"height": 800
|
"height": 800
|
||||||
|
|
@ -1186,7 +1186,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 4": {
|
"Nexus 4": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 384,
|
"width": 384,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1197,7 +1197,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 4 landscape": {
|
"Nexus 4 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 384
|
"height": 384
|
||||||
|
|
@ -1208,7 +1208,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5": {
|
"Nexus 5": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1219,7 +1219,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5 landscape": {
|
"Nexus 5 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1230,7 +1230,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5X": {
|
"Nexus 5X": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 732
|
"height": 732
|
||||||
|
|
@ -1241,7 +1241,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5X landscape": {
|
"Nexus 5X landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 732,
|
"width": 732,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1252,7 +1252,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6": {
|
"Nexus 6": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 732
|
"height": 732
|
||||||
|
|
@ -1263,7 +1263,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6 landscape": {
|
"Nexus 6 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 732,
|
"width": 732,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1274,7 +1274,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6P": {
|
"Nexus 6P": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 732
|
"height": 732
|
||||||
|
|
@ -1285,7 +1285,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6P landscape": {
|
"Nexus 6P landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 732,
|
"width": 732,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1296,7 +1296,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 7": {
|
"Nexus 7": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 960
|
"height": 960
|
||||||
|
|
@ -1307,7 +1307,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 7 landscape": {
|
"Nexus 7 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 960,
|
"width": 960,
|
||||||
"height": 600
|
"height": 600
|
||||||
|
|
@ -1362,7 +1362,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"Pixel 2": {
|
"Pixel 2": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 411,
|
"width": 411,
|
||||||
"height": 731
|
"height": 731
|
||||||
|
|
@ -1373,7 +1373,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 2 landscape": {
|
"Pixel 2 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 731,
|
"width": 731,
|
||||||
"height": 411
|
"height": 411
|
||||||
|
|
@ -1384,7 +1384,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 2 XL": {
|
"Pixel 2 XL": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 411,
|
"width": 411,
|
||||||
"height": 823
|
"height": 823
|
||||||
|
|
@ -1395,7 +1395,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 2 XL landscape": {
|
"Pixel 2 XL landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 823,
|
"width": 823,
|
||||||
"height": 411
|
"height": 411
|
||||||
|
|
@ -1406,7 +1406,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 3": {
|
"Pixel 3": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 393,
|
"width": 393,
|
||||||
"height": 786
|
"height": 786
|
||||||
|
|
@ -1417,7 +1417,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 3 landscape": {
|
"Pixel 3 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 786,
|
"width": 786,
|
||||||
"height": 393
|
"height": 393
|
||||||
|
|
@ -1428,7 +1428,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4": {
|
"Pixel 4": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 353,
|
"width": 353,
|
||||||
"height": 745
|
"height": 745
|
||||||
|
|
@ -1439,7 +1439,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4 landscape": {
|
"Pixel 4 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 745,
|
"width": 745,
|
||||||
"height": 353
|
"height": 353
|
||||||
|
|
@ -1450,7 +1450,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4a (5G)": {
|
"Pixel 4a (5G)": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 892
|
"height": 892
|
||||||
|
|
@ -1465,7 +1465,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4a (5G) landscape": {
|
"Pixel 4a (5G) landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"height": 892,
|
"height": 892,
|
||||||
"width": 412
|
"width": 412
|
||||||
|
|
@ -1480,7 +1480,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 5": {
|
"Pixel 5": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 393,
|
"width": 393,
|
||||||
"height": 851
|
"height": 851
|
||||||
|
|
@ -1495,7 +1495,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 5 landscape": {
|
"Pixel 5 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 851,
|
"width": 851,
|
||||||
"height": 393
|
"height": 393
|
||||||
|
|
@ -1510,7 +1510,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 7": {
|
"Pixel 7": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 915
|
"height": 915
|
||||||
|
|
@ -1525,7 +1525,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 7 landscape": {
|
"Pixel 7 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 915,
|
"width": 915,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1540,7 +1540,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Moto G4": {
|
"Moto G4": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1551,7 +1551,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Moto G4 landscape": {
|
"Moto G4 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1562,7 +1562,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Desktop Chrome HiDPI": {
|
"Desktop Chrome HiDPI": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 1792,
|
"width": 1792,
|
||||||
"height": 1120
|
"height": 1120
|
||||||
|
|
@ -1577,7 +1577,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Desktop Edge HiDPI": {
|
"Desktop Edge HiDPI": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36 Edg/128.0.6613.36",
|
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Safari/537.36 Edg/129.0.6668.29",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 1792,
|
"width": 1792,
|
||||||
"height": 1120
|
"height": 1120
|
||||||
|
|
@ -1592,7 +1592,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Desktop Firefox HiDPI": {
|
"Desktop Firefox HiDPI": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0",
|
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 1792,
|
"width": 1792,
|
||||||
"height": 1120
|
"height": 1120
|
||||||
|
|
@ -1622,7 +1622,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"Desktop Chrome": {
|
"Desktop Chrome": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080
|
"height": 1080
|
||||||
|
|
@ -1637,7 +1637,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Desktop Edge": {
|
"Desktop Edge": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36 Edg/128.0.6613.36",
|
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.29 Safari/537.36 Edg/129.0.6668.29",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080
|
"height": 1080
|
||||||
|
|
@ -1652,7 +1652,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Desktop Firefox": {
|
"Desktop Firefox": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0",
|
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080
|
"height": 1080
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ import type { Dialog } from '../dialog';
|
||||||
import type { ConsoleMessage } from '../console';
|
import type { ConsoleMessage } from '../console';
|
||||||
import { serializeError } from '../errors';
|
import { serializeError } from '../errors';
|
||||||
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
|
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
|
||||||
|
import { RecorderApp } from '../recorder/recorderApp';
|
||||||
|
|
||||||
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel {
|
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel {
|
||||||
_type_EventTarget = true;
|
_type_EventTarget = true;
|
||||||
|
|
@ -291,7 +292,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
||||||
}
|
}
|
||||||
|
|
||||||
async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> {
|
async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> {
|
||||||
await Recorder.show(this._context, params);
|
await Recorder.show(this._context, RecorderApp.factory(this._context), params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {
|
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
|
||||||
chromium: new BrowserTypeDispatcher(scope, playwright.chromium),
|
chromium: new BrowserTypeDispatcher(scope, playwright.chromium),
|
||||||
firefox: new BrowserTypeDispatcher(scope, playwright.firefox),
|
firefox: new BrowserTypeDispatcher(scope, playwright.firefox),
|
||||||
webkit: new BrowserTypeDispatcher(scope, playwright.webkit),
|
webkit: new BrowserTypeDispatcher(scope, playwright.webkit),
|
||||||
|
bidiChromium: new BrowserTypeDispatcher(scope, playwright.bidiChromium),
|
||||||
|
bidiFirefox: new BrowserTypeDispatcher(scope, playwright.bidiFirefox),
|
||||||
android,
|
android,
|
||||||
electron: new ElectronDispatcher(scope, playwright.electron),
|
electron: new ElectronDispatcher(scope, playwright.electron),
|
||||||
utils: playwright.options.isServer ? undefined : new LocalUtilsDispatcher(scope, playwright),
|
utils: playwright.options.isServer ? undefined : new LocalUtilsDispatcher(scope, playwright),
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,9 @@ export function isNonRecoverableDOMError(error: Error) {
|
||||||
export class FrameExecutionContext extends js.ExecutionContext {
|
export class FrameExecutionContext extends js.ExecutionContext {
|
||||||
readonly frame: frames.Frame;
|
readonly frame: frames.Frame;
|
||||||
private _injectedScriptPromise?: Promise<js.JSHandle>;
|
private _injectedScriptPromise?: Promise<js.JSHandle>;
|
||||||
readonly world: types.World | null;
|
readonly world: types.World;
|
||||||
|
|
||||||
constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame, world: types.World|null) {
|
constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame, world: types.World) {
|
||||||
super(frame, delegate, world || 'content-script');
|
super(frame, delegate, world || 'content-script');
|
||||||
this.frame = frame;
|
this.frame = frame;
|
||||||
this.world = world;
|
this.world = world;
|
||||||
|
|
@ -233,7 +233,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
this._page._timeoutSettings.timeout(options));
|
this._page._timeoutSettings.timeout(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _clickablePoint(): Promise<types.Point | 'error:notvisible' | 'error:notinviewport'> {
|
private async _clickablePoint(): Promise<types.Point | 'error:notvisible' | 'error:notinviewport' | 'error:notconnected'> {
|
||||||
const intersectQuadWithViewport = (quad: types.Quad): types.Quad => {
|
const intersectQuadWithViewport = (quad: types.Quad): types.Quad => {
|
||||||
return quad.map(point => ({
|
return quad.map(point => ({
|
||||||
x: Math.min(Math.max(point.x, 0), metrics.width),
|
x: Math.min(Math.max(point.x, 0), metrics.width),
|
||||||
|
|
@ -257,6 +257,8 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
this._page._delegate.getContentQuads(this),
|
this._page._delegate.getContentQuads(this),
|
||||||
this._page.mainFrame()._utilityContext().then(utility => utility.evaluate(() => ({ width: innerWidth, height: innerHeight }))),
|
this._page.mainFrame()._utilityContext().then(utility => utility.evaluate(() => ({ width: innerWidth, height: innerHeight }))),
|
||||||
] as const);
|
] as const);
|
||||||
|
if (quads === 'error:notconnected')
|
||||||
|
return quads;
|
||||||
if (!quads || !quads.length)
|
if (!quads || !quads.length)
|
||||||
return 'error:notvisible';
|
return 'error:notvisible';
|
||||||
|
|
||||||
|
|
@ -536,7 +538,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
return this._retryPointerAction(progress, 'tap', true /* waitForEnabled */, point => this._page.touchscreen.tap(point.x, point.y), { ...options, waitAfter: 'disabled' });
|
return this._retryPointerAction(progress, 'tap', true /* waitForEnabled */, point => this._page.touchscreen.tap(point.x, point.y), { ...options, waitAfter: 'disabled' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: { noWaitAfter?: boolean } & types.CommonActionOptions): Promise<string[]> {
|
async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise<string[]> {
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
const result = await this._selectOption(progress, elements, values, options);
|
const result = await this._selectOption(progress, elements, values, options);
|
||||||
|
|
@ -544,7 +546,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
}, this._page._timeoutSettings.timeout(options));
|
}, this._page._timeoutSettings.timeout(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: { noWaitAfter?: boolean } & types.CommonActionOptions): Promise<string[] | 'error:notconnected'> {
|
async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise<string[] | 'error:notconnected'> {
|
||||||
let resultingOptions: string[] = [];
|
let resultingOptions: string[] = [];
|
||||||
await this._retryAction(progress, 'select option', async () => {
|
await this._retryAction(progress, 'select option', async () => {
|
||||||
await progress.beforeInputAction(this);
|
await progress.beforeInputAction(this);
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,7 @@ class FFRouteImpl implements network.RouteDelegate {
|
||||||
this._request = request;
|
this._request = request;
|
||||||
}
|
}
|
||||||
|
|
||||||
async continue(request: network.Request, overrides: types.NormalizedContinueOverrides) {
|
async continue(overrides: types.NormalizedContinueOverrides) {
|
||||||
await this._session.sendMayFail('Network.resumeInterceptedRequest', {
|
await this._session.sendMayFail('Network.resumeInterceptedRequest', {
|
||||||
requestId: this._request._id,
|
requestId: this._request._id,
|
||||||
url: overrides.url,
|
url: overrides.url,
|
||||||
|
|
|
||||||
|
|
@ -163,14 +163,15 @@ export class FFPage implements PageDelegate {
|
||||||
if (!frame)
|
if (!frame)
|
||||||
return;
|
return;
|
||||||
const delegate = new FFExecutionContext(this._session, executionContextId);
|
const delegate = new FFExecutionContext(this._session, executionContextId);
|
||||||
let worldName: types.World|null = null;
|
let worldName: types.World;
|
||||||
if (auxData.name === UTILITY_WORLD_NAME)
|
if (auxData.name === UTILITY_WORLD_NAME)
|
||||||
worldName = 'utility';
|
worldName = 'utility';
|
||||||
else if (!auxData.name)
|
else if (!auxData.name)
|
||||||
worldName = 'main';
|
worldName = 'main';
|
||||||
|
else
|
||||||
|
return;
|
||||||
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
|
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
|
||||||
(context as any)[contextDelegateSymbol] = delegate;
|
(context as any)[contextDelegateSymbol] = delegate;
|
||||||
if (worldName)
|
|
||||||
frame._contextCreated(worldName, context);
|
frame._contextCreated(worldName, context);
|
||||||
this._contextIdToContext.set(executionContextId, context);
|
this._contextIdToContext.set(executionContextId, context);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,12 @@ import path from 'path';
|
||||||
import { FFBrowser } from './ffBrowser';
|
import { FFBrowser } from './ffBrowser';
|
||||||
import { kBrowserCloseMessageId } from './ffConnection';
|
import { kBrowserCloseMessageId } from './ffConnection';
|
||||||
import { BrowserType, kNoXServerRunningError } from '../browserType';
|
import { BrowserType, kNoXServerRunningError } from '../browserType';
|
||||||
import type { BrowserReadyState } from '../browserType';
|
import { BrowserReadyState } from '../browserType';
|
||||||
import type { Env } from '../../utils/processLauncher';
|
import type { Env } from '../../utils/processLauncher';
|
||||||
import type { ConnectionTransport } from '../transport';
|
import type { ConnectionTransport } from '../transport';
|
||||||
import type { BrowserOptions } from '../browser';
|
import type { BrowserOptions } from '../browser';
|
||||||
import type * as types from '../types';
|
import type * as types from '../types';
|
||||||
import { ManualPromise, wrapInASCIIBox } from '../../utils';
|
import { wrapInASCIIBox } from '../../utils';
|
||||||
import type { SdkObject } from '../instrumentation';
|
import type { SdkObject } from '../instrumentation';
|
||||||
import type { ProtocolError } from '../protocolError';
|
import type { ProtocolError } from '../protocolError';
|
||||||
|
|
||||||
|
|
@ -95,20 +95,10 @@ export class Firefox extends BrowserType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class JugglerReadyState implements BrowserReadyState {
|
class JugglerReadyState extends BrowserReadyState {
|
||||||
private readonly _jugglerPromise = new ManualPromise<void>();
|
override onBrowserOutput(message: string): void {
|
||||||
|
|
||||||
onBrowserOutput(message: string): void {
|
|
||||||
if (message.includes('Juggler listening to the pipe'))
|
if (message.includes('Juggler listening to the pipe'))
|
||||||
this._jugglerPromise.resolve();
|
this._wsEndpoint.resolve(undefined);
|
||||||
}
|
|
||||||
onBrowserExit(): void {
|
|
||||||
// Unblock launch when browser prematurely exits.
|
|
||||||
this._jugglerPromise.resolve();
|
|
||||||
}
|
|
||||||
async waitUntilReady(): Promise<{ wsEndpoint?: string }> {
|
|
||||||
await this._jugglerPromise;
|
|
||||||
return { };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -291,8 +291,7 @@ export class FrameManager {
|
||||||
if (request._documentId)
|
if (request._documentId)
|
||||||
frame.setPendingDocument({ documentId: request._documentId, request });
|
frame.setPendingDocument({ documentId: request._documentId, request });
|
||||||
if (request._isFavicon) {
|
if (request._isFavicon) {
|
||||||
if (route)
|
route?.continue({ isFallback: true }).catch(() => {});
|
||||||
route.continue(request, { isFallback: true }).catch(() => {});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._page.emitOnContext(BrowserContext.Events.Request, request);
|
this._page.emitOnContext(BrowserContext.Events.Request, request);
|
||||||
|
|
@ -659,18 +658,24 @@ export class Frame extends SdkObject {
|
||||||
}
|
}
|
||||||
url = helper.completeUserURL(url);
|
url = helper.completeUserURL(url);
|
||||||
|
|
||||||
const sameDocument = helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, (e: NavigationEvent) => !e.newDocument);
|
const navigationEvents: NavigationEvent[] = [];
|
||||||
const navigateResult = await this._page._delegate.navigateFrame(this, url, referer);
|
const collectNavigations = (arg: NavigationEvent) => navigationEvents.push(arg);
|
||||||
|
this.on(Frame.Events.InternalNavigation, collectNavigations);
|
||||||
|
const navigateResult = await this._page._delegate.navigateFrame(this, url, referer).finally(
|
||||||
|
() => this.off(Frame.Events.InternalNavigation, collectNavigations));
|
||||||
|
|
||||||
let event: NavigationEvent;
|
let event: NavigationEvent;
|
||||||
if (navigateResult.newDocumentId) {
|
if (navigateResult.newDocumentId) {
|
||||||
sameDocument.dispose();
|
const predicate = (event: NavigationEvent) => {
|
||||||
event = await helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, (event: NavigationEvent) => {
|
|
||||||
// We are interested either in this specific document, or any other document that
|
// We are interested either in this specific document, or any other document that
|
||||||
// did commit and replaced the expected document.
|
// did commit and replaced the expected document.
|
||||||
return event.newDocument && (event.newDocument.documentId === navigateResult.newDocumentId || !event.error);
|
return event.newDocument && (event.newDocument.documentId === navigateResult.newDocumentId || !event.error);
|
||||||
}).promise;
|
};
|
||||||
|
const events = navigationEvents.filter(predicate);
|
||||||
|
if (events.length)
|
||||||
|
event = events[0];
|
||||||
|
else
|
||||||
|
event = await helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, predicate).promise;
|
||||||
if (event.newDocument!.documentId !== navigateResult.newDocumentId) {
|
if (event.newDocument!.documentId !== navigateResult.newDocumentId) {
|
||||||
// This is just a sanity check. In practice, new navigation should
|
// This is just a sanity check. In practice, new navigation should
|
||||||
// cancel the previous one and report "request cancelled"-like error.
|
// cancel the previous one and report "request cancelled"-like error.
|
||||||
|
|
@ -679,7 +684,13 @@ export class Frame extends SdkObject {
|
||||||
if (event.error)
|
if (event.error)
|
||||||
throw event.error;
|
throw event.error;
|
||||||
} else {
|
} else {
|
||||||
event = await sameDocument.promise;
|
// Wait for same document navigation.
|
||||||
|
const predicate = (e: NavigationEvent) => !e.newDocument;
|
||||||
|
const events = navigationEvents.filter(predicate);
|
||||||
|
if (events.length)
|
||||||
|
event = events[0];
|
||||||
|
else
|
||||||
|
event = await helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, predicate).promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._firedLifecycleEvents.has(waitUntil))
|
if (!this._firedLifecycleEvents.has(waitUntil))
|
||||||
|
|
@ -788,7 +799,7 @@ export class Frame extends SdkObject {
|
||||||
const result = await resolved.injected.evaluateHandle((injected, { info, root }) => {
|
const result = await resolved.injected.evaluateHandle((injected, { info, root }) => {
|
||||||
const elements = injected.querySelectorAll(info.parsed, root || document);
|
const elements = injected.querySelectorAll(info.parsed, root || document);
|
||||||
const element: Element | undefined = elements[0];
|
const element: Element | undefined = elements[0];
|
||||||
const visible = element ? injected.isVisible(element) : false;
|
const visible = element ? injected.utils.isElementVisible(element) : false;
|
||||||
let log = '';
|
let log = '';
|
||||||
if (elements.length > 1) {
|
if (elements.length > 1) {
|
||||||
if (info.strict)
|
if (info.strict)
|
||||||
|
|
@ -889,7 +900,7 @@ export class Frame extends SdkObject {
|
||||||
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
|
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
|
||||||
progress.log(`setting frame content, waiting until "${waitUntil}"`);
|
progress.log(`setting frame content, waiting until "${waitUntil}"`);
|
||||||
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
|
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
|
||||||
const context = await this._utilityContext();
|
const context = this._page._delegate.useMainWorldForSetContent?.() ? await this._mainContext() : await this._utilityContext();
|
||||||
const lifecyclePromise = new Promise((resolve, reject) => {
|
const lifecyclePromise = new Promise((resolve, reject) => {
|
||||||
this._page._frameManager._consoleMessageTags.set(tag, () => {
|
this._page._frameManager._consoleMessageTags.set(tag, () => {
|
||||||
// Clear lifecycle right after document.open() - see 'tag' below.
|
// Clear lifecycle right after document.open() - see 'tag' below.
|
||||||
|
|
@ -1332,7 +1343,7 @@ export class Frame extends SdkObject {
|
||||||
}, this._page._timeoutSettings.timeout(options));
|
}, this._page._timeoutSettings.timeout(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
async selectOption(metadata: CallMetadata, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: { noWaitAfter?: boolean } & types.CommonActionOptions = {}): Promise<string[]> {
|
async selectOption(metadata: CallMetadata, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions = {}): Promise<string[]> {
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
return await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performLocatorHandlersCheckpoint */, handle => handle._selectOption(progress, elements, values, options));
|
return await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performLocatorHandlersCheckpoint */, handle => handle._selectOption(progress, elements, values, options));
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,21 @@
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
plugins: ["@typescript-eslint", "notice"],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 9,
|
||||||
|
sourceType: "module",
|
||||||
|
project: path.join(__dirname, '../../../../../tsconfig.json'),
|
||||||
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"no-restricted-globals": [
|
"no-restricted-globals": [
|
||||||
"error",
|
"error",
|
||||||
{ "name": "window" },
|
{ "name": "window" },
|
||||||
{ "name": "document" },
|
{ "name": "document" },
|
||||||
{ "name": "globalThis" },
|
{ "name": "globalThis" },
|
||||||
]
|
],
|
||||||
}
|
'@typescript-eslint/no-floating-promises': 'error',
|
||||||
|
"@typescript-eslint/no-unnecessary-boolean-literal-compare": 2,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,7 @@ export class ClockController {
|
||||||
const sinceLastSync = now - this._realTime!.lastSyncTicks;
|
const sinceLastSync = now - this._realTime!.lastSyncTicks;
|
||||||
this._realTime!.lastSyncTicks = now;
|
this._realTime!.lastSyncTicks = now;
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
this._runTo(shiftTicks(this._now.ticks, sinceLastSync)).catch(e => console.error(e)).then(() => this._updateRealTimeTimer());
|
void this._runTo(shiftTicks(this._now.ticks, sinceLastSync)).catch(e => console.error(e)).then(() => this._updateRealTimeTimer());
|
||||||
}, callAt - this._now.ticks),
|
}, callAt - this._now.ticks),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -239,7 +239,12 @@ export class ClockController {
|
||||||
|
|
||||||
addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number {
|
addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number {
|
||||||
this._replayLogOnce();
|
this._replayLogOnce();
|
||||||
if (options.func === undefined)
|
|
||||||
|
if (options.type === TimerType.AnimationFrame && !options.func)
|
||||||
|
throw new Error('Callback must be provided to requestAnimationFrame calls');
|
||||||
|
if (options.type === TimerType.IdleCallback && !options.func)
|
||||||
|
throw new Error('Callback must be provided to requestIdleCallback calls');
|
||||||
|
if ([TimerType.Timeout, TimerType.Interval].includes(options.type) && !options.func && options.delay === undefined)
|
||||||
throw new Error('Callback must be provided to timer calls');
|
throw new Error('Callback must be provided to timer calls');
|
||||||
|
|
||||||
let delay = options.delay ? +options.delay : 0;
|
let delay = options.delay ? +options.delay : 0;
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue