Merge branch 'main' into hello-angular-ct

This commit is contained in:
Younes Jaaidi 2024-03-22 09:44:13 +01:00
commit 06867cc720
No known key found for this signature in database
GPG key ID: 3126C5717BDF3241
278 changed files with 6370 additions and 3584 deletions

View file

@ -19,4 +19,5 @@ tests/components/
tests/installation/fixture-scripts/
examples/
DEPS
.cache/
.cache/
utils/

View file

@ -0,0 +1,15 @@
module.exports = {
extends: "./.eslintrc.js",
parserOptions: {
ecmaVersion: 9,
sourceType: "module",
project: "./tsconfig.json",
},
rules: {
"@typescript-eslint/no-base-to-string": "error",
"@typescript-eslint/no-unnecessary-boolean-literal-compare": 2,
},
parserOptions: {
project: "./tsconfig.json"
},
};

View file

@ -48,6 +48,7 @@ module.exports = {
"arrow-parens": [2, "as-needed"],
"prefer-const": 2,
"quote-props": [2, "consistent"],
"nonblock-statement-body-position": [2, "below"],
// anti-patterns
"no-var": 2,

View file

@ -23,7 +23,7 @@ body:
value: |
## 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.
The simpler you can make it, the more likely we are to successfully verify and fix 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.
- type: markdown
attributes:
value: |

View file

@ -12,6 +12,9 @@ on:
description: Comma-separated list of commit hashes to cherry-pick
required: true
permissions:
contents: write
jobs:
roll:
runs-on: ubuntu-22.04

View file

@ -55,7 +55,7 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-*
- run: node tests/config/checkCoverage.js ${{ matrix.browser }}
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: always()
@ -87,7 +87,7 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps chromium-tip-of-tree
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=chromium
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=chromium-*
env:
PWTEST_CHANNEL: chromium-tip-of-tree
PWTEST_BOT_NAME: "${{ matrix.os }}-chromium-tip-of-tree"

View file

@ -42,7 +42,7 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-*
- run: node tests/config/checkCoverage.js ${{ matrix.browser }}
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: always()
@ -75,7 +75,7 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
- run: npm run test -- --project=${{ matrix.browser }}
- run: npm run test -- --project=${{ matrix.browser }}-*
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: always()
shell: bash
@ -106,10 +106,10 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
- run: npm run test -- --project=${{ matrix.browser }} --workers=1
- run: npm run test -- --project=${{ matrix.browser }}-* --workers=1
if: matrix.browser == 'firefox'
shell: bash
- run: npm run test -- --project=${{ matrix.browser }}
- run: npm run test -- --project=${{ matrix.browser }}-*
if: matrix.browser != 'firefox'
shell: bash
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
@ -175,9 +175,9 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }} --headed
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-* --headed
if: always() && startsWith(matrix.os, 'ubuntu-')
- run: npm run test -- --project=${{ matrix.browser }} --headed
- run: npm run test -- --project=${{ matrix.browser }}-* --headed
if: always() && !startsWith(matrix.os, 'ubuntu-')
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: always()
@ -247,7 +247,7 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium ${{ matrix.channel }}
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-*
env:
PWTEST_TRACE: 1
PWTEST_CHANNEL: ${{ matrix.channel }}
@ -868,7 +868,7 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps chromium
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=chromium
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=chromium-*
env:
PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW: 1
- run: node tests/config/checkCoverage.js chromium

View file

@ -23,7 +23,7 @@ jobs:
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }} --workers=10 --retries=0
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-* --workers=10 --retries=0
env:
PWTEST_MODE: service2
PWTEST_TRACE: 1

View file

@ -32,7 +32,7 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-*
env:
PWTEST_VIDEO: 1
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json

View file

@ -38,6 +38,12 @@ jobs:
- run: npm run build
- run: dotnet build
working-directory: tests/webview2/webview2-app/
- name: Update to Evergreen WebView2 Runtime
shell: pwsh
run: |
# See here: https://developer.microsoft.com/en-us/microsoft-edge/webview2/
Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'setup.exe'
Start-Process -FilePath setup.exe -Verb RunAs -Wait
- run: npm run webview2test
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: always()

View file

@ -1,30 +0,0 @@
name: "Trigger: Chromium with Symbols Builds"
on:
workflow_dispatch:
release:
types: [published]
jobs:
trigger:
name: "trigger"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 18
- name: Get Chromium revision
id: chromium-version
run: |
REVISION=$(node -e "console.log(require('./packages/playwright-core/browsers.json').browsers.find(b => b.name === 'chromium-with-symbols').revision)")
echo "REVISION=$REVISION" >> $GITHUB_OUTPUT
- run: |
curl -X POST \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token ${GH_TOKEN}" \
--data "{\"event_type\": \"build_chromium_with_symbols\", \"client_payload\": {\"revision\": \"${CHROMIUM_REVISION}\"}}" \
https://api.github.com/repos/microsoft/playwright-browsers/dispatches
env:
GH_TOKEN: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
CHROMIUM_REVISION: ${{ steps.chromium-version.outputs.REVISION }}

View file

@ -1,6 +1,6 @@
# 🎭 Playwright
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-123.0.6312.4-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-123.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-17.4-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop -->
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-124.0.6367.8-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-123.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-17.4-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop -->
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->123.0.6312.4<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->124.0.6367.8<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->17.4<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->123.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |

View file

@ -238,3 +238,5 @@ public class HomepageTests extends AxeTestFixtures {
}
}
```
See experimental [JUnit integration](./junit.md) to automatically initialize Playwright objects and more.

View file

@ -375,6 +375,8 @@ public class TestGitHubAPI {
}
```
See experimental [JUnit integration](./junit.md) to automatically initialize Playwright objects and more.
## Prepare server state via API calls
The following test creates a new issue via API and then navigates to the list of all issues in the

View file

@ -186,7 +186,10 @@ context.on("dialog", lambda dialog: dialog.accept())
```
```csharp
context.Dialog += (_, dialog) => dialog.AcceptAsync();
Context.Dialog += async (_, dialog) =>
{
await dialog.AcceptAsync();
};
```
:::note
@ -390,7 +393,7 @@ browser_context.add_init_script(path="preload.js")
```
```csharp
await context.AddInitScriptAsync(scriptPath: "preload.js");
await Context.AddInitScriptAsync(scriptPath: "preload.js");
```
:::note
@ -1010,6 +1013,27 @@ Creates a new page in the browser context.
Returns all open pages in the context.
## async method: BrowserContext.removeCookies
* since: v1.43
Removes cookies from context. At least one of the removal criteria should be provided.
**Usage**
```js
await browserContext.removeCookies({ name: 'session-id' });
await browserContext.removeCookies({ domain: 'my-origin.com' });
await browserContext.removeCookies({ path: '/api/v1' });
await browserContext.removeCookies({ name: 'session-id', domain: 'my-origin.com' });
```
### param: BrowserContext.removeCookies.filter
* since: v1.43
- `filter` <[Object]>
- `name` ?<[string]>
- `domain` ?<[string]>
- `path` ?<[string]>
## property: BrowserContext.request
* since: v1.16
* langs:
@ -1159,7 +1183,7 @@ context.route("/api/**", handle_route)
await page.RouteAsync("/api/**", async r =>
{
if (r.Request.PostData.Contains("my-string"))
await r.FulfillAsync(body: "mocked-data");
await r.FulfillAsync(new() { Body = "mocked-data" });
else
await r.ContinueAsync();
});

View file

@ -74,28 +74,59 @@ await page.FrameLocator(".result-frame").First.getByRole(AriaRole.Button).ClickA
**Converting Locator to FrameLocator**
If you have a [Locator] object pointing to an `iframe` it can be converted to [FrameLocator] using [`:scope`](https://developer.mozilla.org/en-US/docs/Web/CSS/:scope) CSS selector:
If you have a [Locator] object pointing to an `iframe` it can be converted to [FrameLocator] using [`method: Locator.enterFrame`].
**Converting FrameLocator to Locator**
If you have a [FrameLocator] object it can be converted to [Locator] pointing to the same `iframe` using [`method: FrameLocator.exitFrame`].
## method: FrameLocator.exitFrame
* since: v1.43
- returns: <[Locator]>
Returns a [Locator] object pointing to the same `iframe` as this frame locator.
Useful when you have a [FrameLocator] object obtained somewhere, and later on would like to interact with the `iframe` element.
**Usage**
```js
const frameLocator = locator.frameLocator(':scope');
const frameLocator = page.frameLocator('iframe[name="embedded"]');
// ...
const locator = frameLocator.exitFrame();
await expect(locator).toBeVisible();
```
```java
Locator frameLocator = locator.frameLocator(':scope');
FrameLocator frameLocator = page.frameLocator("iframe[name=\"embedded\"]");
// ...
Locator locator = frameLocator.exitFrame();
assertThat(locator).isVisible();
```
```python async
frameLocator = locator.frame_locator(":scope")
frame_locator = page.frame_locator("iframe[name=\"embedded\"]")
# ...
locator = frame_locator.exit_frame
await expect(locator).to_be_visible()
```
```python sync
frameLocator = locator.frame_locator(":scope")
frame_locator = page.frame_locator("iframe[name=\"embedded\"]")
# ...
locator = frame_locator.exit_frame
expect(locator).to_be_visible()
```
```csharp
var frameLocator = locator.FrameLocator(":scope");
var frameLocator = Page.FrameLocator("iframe[name=\"embedded\"]");
// ...
var locator = frameLocator.ExitFrame;
await Expect(locator).ToBeVisibleAsync();
```
## method: FrameLocator.first
* since: v1.17
- returns: <[FrameLocator]>

View file

@ -747,6 +747,51 @@ Resolves given locator to the first matching DOM element. If there are no matchi
Resolves given locator to all matching DOM elements. If there are no matching elements, returns an empty list.
## method: Locator.enterFrame
* since: v1.43
- returns: <[FrameLocator]>
Returns a [FrameLocator] object pointing to the same `iframe` as this locator.
Useful when you have a [Locator] object obtained somewhere, and later on would like to interact with the content inside the frame.
**Usage**
```js
const locator = page.locator('iframe[name="embedded"]');
// ...
const frameLocator = locator.enterFrame();
await frameLocator.getByRole('button').click();
```
```java
Locator locator = page.locator("iframe[name=\"embedded\"]");
// ...
FrameLocator frameLocator = locator.enterFrame();
frameLocator.getByRole(AriaRole.BUTTON).click();
```
```python async
locator = page.locator("iframe[name=\"embedded\"]")
# ...
frame_locator = locator.enter_frame
await frame_locator.get_by_role("button").click()
```
```python sync
locator = page.locator("iframe[name=\"embedded\"]")
# ...
frame_locator = locator.enter_frame
frame_locator.get_by_role("button").click()
```
```csharp
var locator = Page.Locator("iframe[name=\"embedded\"]");
// ...
var frameLocator = locator.EnterFrame;
await frameLocator.GetByRole(AriaRole.Button).ClickAsync();
```
## async method: Locator.evaluate
* since: v1.14
- returns: <[Serializable]>

View file

@ -10,7 +10,7 @@ const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
(async () => {
const browser = await chromium.launch({
logger: {
isEnabled: (name, severity) => name === 'browser',
isEnabled: (name, severity) => name === 'api',
log: (name, severity, message, args) => console.log(`${name} ${message}`)
}
});

View file

@ -608,7 +608,7 @@ page.add_init_script(path="./preload.js")
```
```csharp
await page.AddInitScriptAsync(scriptPath: "./preload.js");
await Page.AddInitScriptAsync(scriptPath: "./preload.js");
```
:::note
@ -3146,32 +3146,38 @@ return value resolves to `[]`.
## async method: Page.addLocatorHandler
* since: v1.42
Sometimes, the web page can show an overlay that obstructs elements behind it and prevents certain actions, like click, from completing. When such an overlay is shown predictably, we recommend dismissing it as a part of your test flow. However, sometimes such an overlay may appear non-deterministically, for example certain cookies consent dialogs behave this way. In this case, [`method: Page.addLocatorHandler`] allows handling an overlay during an action that it would block.
:::warning Experimental
This method is experimental and its behavior may change in the upcoming releases.
:::
This method registers a handler for an overlay that is executed once the locator is visible on the page. The handler should get rid of the overlay so that actions blocked by it can proceed. This is useful for nondeterministic interstitial pages or dialogs, like a cookie consent dialog.
When testing a web page, sometimes unexpected overlays like a "Sign up" dialog appear and block actions you want to automate, e.g. clicking a button. These overlays don't always show up in the same way or at the same time, making them tricky to handle in automated tests.
Note that execution time of the handler counts towards the timeout of the action/assertion that executed the handler.
This method lets you set up a special function, called a handler, that activates when it detects that overlay is visible. The handler's job is to remove the overlay, allowing your test to continue as if the overlay wasn't there.
You can register multiple handlers. However, only a single handler will be running at a time. Any actions inside a handler must not require another handler to run.
Things to keep in mind:
* When an overlay is shown predictably, we recommend explicitly waiting for it in your test and dismissing it as a part of your normal test flow, instead of using [`method: Page.addLocatorHandler`].
* Playwright checks for the overlay every time before executing or retrying an action that requires an [actionability check](../actionability.md), or before performing an auto-waiting assertion check. When overlay is visible, Playwright calls the handler first, and then proceeds with the action/assertion. Note that the handler is only called when you perform an action/assertion - if the overlay becomes visible but you don't perform any actions, the handler will not be triggered.
* The execution time of the handler counts towards the timeout of the action/assertion that executed the handler. If your handler takes too long, it might cause timeouts.
* You can register multiple handlers. However, only a single handler will be running at a time. Make sure the actions within a handler don't depend on another handler.
:::warning
Running the interceptor will alter your page state mid-test. For example it will change the currently focused element and move the mouse. Make sure that the actions that run after the interceptor are self-contained and do not rely on the focus and mouse state.
Running the handler will alter your page state mid-test. For example it will change the currently focused element and move the mouse. Make sure that actions that run after the handler are self-contained and do not rely on the focus and mouse state being unchanged.
<br />
<br />
For example, consider a test that calls [`method: Locator.focus`] followed by [`method: Keyboard.press`]. If your handler clicks a button between these two actions, the focused element most likely will be wrong, and key press will happen on the unexpected element. Use [`method: Locator.press`] instead to avoid this problem.
<br />
<br />
Another example is a series of mouse actions, where [`method: Mouse.move`] is followed by [`method: Mouse.down`]. Again, when the handler runs between these two actions, the mouse position will be wrong during the mouse down. Prefer methods like [`method: Locator.click`] that are self-contained.
Another example is a series of mouse actions, where [`method: Mouse.move`] is followed by [`method: Mouse.down`]. Again, when the handler runs between these two actions, the mouse position will be wrong during the mouse down. Prefer self-contained actions like [`method: Locator.click`] that do not rely on the state being unchanged by a handler.
:::
**Usage**
An example that closes a cookie dialog when it appears:
An example that closes a "Sign up to the newsletter" dialog when it appears:
```js
// Setup the handler.
await page.addLocatorHandler(page.getByRole('button', { name: 'Accept all cookies' }), async () => {
await page.getByRole('button', { name: 'Reject all cookies' }).click();
await page.addLocatorHandler(page.getByText('Sign up to the newsletter'), async () => {
await page.getByRole('button', { name: 'No thanks' }).click();
});
// Write the test as usual.
@ -3181,8 +3187,8 @@ await page.getByRole('button', { name: 'Start here' }).click();
```java
// Setup the handler.
page.addLocatorHandler(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Accept all cookies")), () => {
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Reject all cookies")).click();
page.addLocatorHandler(page.getByText("Sign up to the newsletter"), () => {
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("No thanks")).click();
});
// Write the test as usual.
@ -3193,8 +3199,8 @@ page.getByRole("button", Page.GetByRoleOptions().setName("Start here")).click();
```python sync
# Setup the handler.
def handler():
page.get_by_role("button", name="Reject all cookies").click()
page.add_locator_handler(page.get_by_role("button", name="Accept all cookies"), handler)
page.get_by_role("button", name="No thanks").click()
page.add_locator_handler(page.get_by_text("Sign up to the newsletter"), handler)
# Write the test as usual.
page.goto("https://example.com")
@ -3204,8 +3210,8 @@ page.get_by_role("button", name="Start here").click()
```python async
# Setup the handler.
def handler():
await page.get_by_role("button", name="Reject all cookies").click()
await page.add_locator_handler(page.get_by_role("button", name="Accept all cookies"), handler)
await page.get_by_role("button", name="No thanks").click()
await page.add_locator_handler(page.get_by_text("Sign up to the newsletter"), handler)
# Write the test as usual.
await page.goto("https://example.com")
@ -3214,8 +3220,8 @@ await page.get_by_role("button", name="Start here").click()
```csharp
// Setup the handler.
await page.AddLocatorHandlerAsync(page.GetByRole(AriaRole.Button, new() { Name = "Accept all cookies" }), async () => {
await page.GetByRole(AriaRole.Button, new() { Name = "Reject all cookies" }).ClickAsync();
await page.AddLocatorHandlerAsync(page.GetByText("Sign up to the newsletter"), async () => {
await page.GetByRole(AriaRole.Button, new() { Name = "No thanks" }).ClickAsync();
});
// Write the test as usual.
@ -3228,7 +3234,7 @@ An example that skips the "Confirm your security details" page when it is shown:
```js
// Setup the handler.
await page.addLocatorHandler(page.getByText('Confirm your security details'), async () => {
await page.getByRole('button', 'Remind me later').click();
await page.getByRole('button', { name: 'Remind me later' }).click();
});
// Write the test as usual.

View file

@ -999,6 +999,7 @@ disable timeout.
If specified, traces are saved into this directory.
## browser-option-devtools
* deprecated: Use [debugging tools](../debug.md) instead.
- `devtools` <[boolean]>
**Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the

View file

@ -113,6 +113,13 @@ test('test', async ({ page }) => {
});
```
### Authenticating in UI mode
* langs: js
UI mode will not run the `setup` project by default to improve testing speed. We recommend to authenticate by manually running the `auth.setup.ts` from time to time, whenever existing authentication expires.
First [enable the `setup` project in the filters](./test-ui-mode#filtering-tests), then click the triangle button next to `auth.setup.ts` file, and then disable the `setup` project in the filters again.
## Moderate: one account per parallel worker
* langs: js
@ -256,7 +263,7 @@ existing authentication state instead.
Playwright provides a way to reuse the signed-in state in the tests. That way you can log
in only once and then skip the log in step for all of the tests.
Web apps use cookie-based or token-based authentication, where authenticated state is stored as [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) or in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage). Playwright provides [browserContext.storageState([options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state) method that can be used to retrieve storage state from authenticated contexts and then create new contexts with pre-populated state.
Web apps use cookie-based or token-based authentication, where authenticated state is stored as [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) or in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage). Playwright provides [`method: BrowserContext.storageState`] method that can be used to retrieve storage state from authenticated contexts and then create new contexts with pre-populated state.
Cookies and local storage state can be used across different browsers. They depend on your application's authentication model: some apps might require both cookies and local storage.
@ -457,6 +464,8 @@ test.describe(() => {
});
```
See also about [authenticating in the UI mode](#authenticating-in-ui-mode).
### Testing multiple roles together
* langs: js

View file

@ -8,7 +8,7 @@ title: "CI GitHub Actions"
Playwright tests can be run on any CI provider. In this section we will cover running tests on GitHub using GitHub actions. If you would like to see how to configure other CI providers check out our detailed [doc on Continuous Integration](./ci.md).
When [installing Playwright](./intro.md) using the [VS Code extension](./getting-started-vscode.md) or with `npm init playwright@latest` you are given the option to add a [GitHub Actions](https://docs.github.com/en/actions). This creates a `playwright.yml` file inside a `.github/workflows` folder containing everything you need so that your tests run on each push and pull request into the main/master branch.
When [installing Playwright](./intro.md) using the [VS Code extension](./getting-started-vscode.md) or with `npm init playwright@latest` you are given the option to add a [GitHub Actions](https://docs.github.com/en/actions) workflow. This creates a `playwright.yml` file inside a `.github/workflows` folder containing everything you need so that your tests run on each push and pull request into the main/master branch.
#### You will learn
* langs: js

View file

@ -32,8 +32,11 @@ page.get_by_role("button").click()
```
```csharp
page.Dialog += (_, dialog) => dialog.AcceptAsync();
await page.GetByRole(AriaRole.Button).ClickAsync();
Page.Dialog += async (_, dialog) =>
{
await dialog.AcceptAsync();
};
await Page.GetByRole(AriaRole.Button).ClickAsync();
```
:::note
@ -116,10 +119,10 @@ page.close(run_before_unload=True)
```
```csharp
page.Dialog += (_, dialog) =>
Page.Dialog += async (_, dialog) =>
{
Assert.AreEqual("beforeunload", dialog.Type);
dialog.DismissAsync();
await dialog.DismissAsync();
};
await page.CloseAsync(runBeforeUnload: true);
await Page.CloseAsync(new() { RunBeforeUnload = true });
```

180
docs/src/junit-java.md Normal file
View file

@ -0,0 +1,180 @@
---
id: junit
title: "JUnit (experimental)"
---
## Introduction
With a few lines of code, you can hook up Playwright to your favorite Java test runner.
In [JUnit](https://junit.org/junit5/), you can use Playwright [fixtures](./junit.md#fixtures) to automatically initialize [Playwright], [Browser], [BrowserContext] or [Page]. In the example below, all three test methods use the same
[Browser]. Each test uses its own [BrowserContext] and [Page].
<!-- TOC -->
```java
package org.example;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.junit.UsePlaywright;
import org.junit.jupiter.api.Test;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
@UsePlaywright
public class TestExample {
@Test
void shouldClickButton(Page page) {
page.navigate("data:text/html,<script>var result;</script><button onclick='result=\"Clicked\"'>Go</button>");
page.locator("button").click();
assertEquals("Clicked", page.evaluate("result"));
}
@Test
void shouldCheckTheBox(Page page) {
page.setContent("<input id='checkbox' type='checkbox'></input>");
page.locator("input").check();
assertEquals(true, page.evaluate("window['checkbox'].checked"));
}
@Test
void shouldSearchWiki(Page page) {
page.navigate("https://www.wikipedia.org/");
page.locator("input[name=\"search\"]").click();
page.locator("input[name=\"search\"]").fill("playwright");
page.locator("input[name=\"search\"]").press("Enter");
assertThat(page).hasURL("https://en.wikipedia.org/wiki/Playwright");
}
}
```
## Fixtures
Simply add JUnit annotation `@UsePlaywright` to your test classes to enable Playwright fixtures. Test fixtures are used to establish environment for each test, giving the test everything it needs and nothing else.
```java
@UsePlaywright
public class TestExample {
@Test
void basicTest(Page page) {
page.navigate("https://playwright.dev/");
assertThat(page).hasTitle(Pattern.compile("Playwright"));
}
}
```
The `Page page` argument tells JUnit to setup the `page` fixture and provide it to your test method.
Here is a list of the pre-defined fixtures:
|Fixture |Type |Description |
|:-------------|:------------------|:--------------------------------|
|page |[Page] |Isolated page for this test run.|
|browserContext|[BrowserContext] |Isolated context for this test run. The `page` fixture belongs to this context as well.|
|browser |[Browser] |Browsers are shared across tests to optimize resources.|
|playwright |[Playwright] |Playwright instance is shared between tests running on the same thread.|
|request |[APIRequestContext]|Isolated APIRequestContext for this test run. Learn how to do [API testing](./api-testing).|
## Customizing options
To customize fixture options, you should implement an `OptionsFactory` and specify the class in the `@UsePlaywright()` annotation.
You can easily override launch options for [`method: BrowserType.launch`], or context options for [`method: Browser.newContext`] and [`method: APIRequest.newContext`]. See the following example:
```java
import com.microsoft.playwright.junit.Options;
import com.microsoft.playwright.junit.OptionsFactory;
import com.microsoft.playwright.junit.UsePlaywright;
@UsePlaywright(MyTest.CustomOptions.class)
public class MyTest {
public static class CustomOptions implements OptionsFactory {
@Override
public Options getOptions() {
return new Options()
.setHeadless(false)
.setContextOption(new Browser.NewContextOptions()
.setBaseURL("https://github.com"))
.setApiRequestOptions(new APIRequest.NewContextOptions()
.setBaseURL("https://playwright.dev"));
}
}
@Test
public void testWithCustomOptions(Page page, APIRequestContext request) {
page.navigate("/");
assertThat(page).hasURL(Pattern.compile("github"));
APIResponse response = request.get("/");
assertTrue(response.text().contains("Playwright"));
}
}
```
## Running Tests in Parallel
By default JUnit will run all tests sequentially on a single thread. Since JUnit 5.3 you can change this behavior to run tests in parallel
to speed up execution (see [this page](https://junit.org/junit5/docs/snapshot/user-guide/index.html#writing-tests-parallel-execution)).
Since it is not safe to use same Playwright objects from multiple threads without extra synchronization we recommend you create Playwright
instance per thread and use it on that thread exclusively. Here is an example how to run multiple test classes in parallel.
```java
@UsePlaywright
class Test1 {
@Test
void shouldClickButton(Page page) {
page.navigate("data:text/html,<script>var result;</script><button onclick='result=\"Clicked\"'>Go</button>");
page.locator("button").click();
assertEquals("Clicked", page.evaluate("result"));
}
@Test
void shouldCheckTheBox(Page page) {
page.setContent("<input id='checkbox' type='checkbox'></input>");
page.locator("input").check();
assertEquals(true, page.evaluate("window['checkbox'].checked"));
}
@Test
void shouldSearchWiki(Page page) {
page.navigate("https://www.wikipedia.org/");
page.locator("input[name=\"search\"]").click();
page.locator("input[name=\"search\"]").fill("playwright");
page.locator("input[name=\"search\"]").press("Enter");
assertThat(page).hasURL("https://en.wikipedia.org/wiki/Playwright");
}
}
@UsePlaywright
class Test2 {
@Test
void shouldReturnInnerHTML(Page page) {
page.setContent("<div>hello</div>");
assertEquals("hello", page.innerHTML("css=div"));
}
@Test
void shouldClickButton(Page page) {
Page popup = page.waitForPopup(() -> {
page.evaluate("window.open('about:blank');");
});
assertEquals("about:blank", popup.url());
}
}
```
Configure JUnit to run tests in each class sequentially and run multiple classes on parallel threads (with max
number of thread equal to 1/2 of the number of CPU cores):
```bash
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = concurrent
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=0.5
```

View file

@ -550,7 +550,7 @@ await page.RouteAsync("**/*", async route => {
});
// Continue requests as POST.
await page.RouteAsync("**/*", async route => await route.ContinueAsync(method: "POST"));
await Page.RouteAsync("**/*", async route => await route.ContinueAsync(new() { Method = "POST" }));
```
You can continue requests with modifications. Example above removes an HTTP header from the outgoing requests.

View file

@ -4,6 +4,45 @@ title: "Release notes"
toc_max_heading_level: 2
---
## Version 1.42
### New Locator Handler
New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears.
```csharp
// Setup the handler.
await Page.AddLocatorHandlerAsync(
Page.GetByRole(AriaRole.Heading, new() { Name = "Hej! You are in control of your cookies." }),
async () =>
{
await Page.GetByRole(AriaRole.Button, new() { Name = "Accept all" }).ClickAsync();
});
// Write the test as usual.
await Page.GotoAsync("https://www.ikea.com/");
await Page.GetByRole(AriaRole.Link, new() { Name = "Collection of blue and white" }).ClickAsync();
await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Light and easy" })).ToBeVisibleAsync();
```
### New APIs
- [`method: Page.pdf`] accepts two new options [`option: tagged`] and [`option: outline`].
### Announcements
* ⚠️ Ubuntu 18 is not supported anymore.
### Browser Versions
* Chromium 123.0.6312.4
* Mozilla Firefox 123.0
* WebKit 17.4
This version was also tested against the following stable channels:
* Google Chrome 122
* Microsoft Edge 123
## Version 1.41
### New APIs
@ -511,7 +550,7 @@ This version was also tested against the following stable channels:
### New .runsettings file support
`Microsoft.Playwright.NUnit` and `Microsoft.Playwright.MSTest` will now consider the `.runsettings` file and passed settings via the CLI when running end-to-end tests. See in the [documentation](https://playwright.dev/dotnet/docs/test-runners) for a full list of supported settings.
`Microsoft.Playwright.NUnit` and `Microsoft.Playwright.MSTest` will now consider the `.runsettings` file and passed settings via the CLI when running end-to-end tests. See in the [documentation](./test-runners) for a full list of supported settings.
The following does now work:
@ -574,7 +613,7 @@ Linux support looks like this:
### New introduction docs
We rewrote our Getting Started docs to be more end-to-end testing focused. Check them out on [playwright.dev](https://playwright.dev/dotnet/docs/intro).
We rewrote our Getting Started docs to be more end-to-end testing focused. Check them out on [playwright.dev](./intro).
## Version 1.23
@ -952,31 +991,31 @@ This version of Playwright was also tested against the following stable channels
### 🖱️ Mouse Wheel
By using [`Page.Mouse.WheelAsync`](https://playwright.dev/dotnet/docs/next/api/class-mouse#mouse-wheel) you are now able to scroll vertically or horizontally.
By using [`method: Mouse.wheel`] you are now able to scroll vertically or horizontally.
### 📜 New Headers API
Previously it was not possible to get multiple header values of a response. This is now possible and additional helper functions are available:
- [Request.AllHeadersAsync()](https://playwright.dev/dotnet/docs/next/api/class-request#request-all-headers)
- [Request.HeadersArrayAsync()](https://playwright.dev/dotnet/docs/next/api/class-request#request-headers-array)
- [Request.HeaderValueAsync(name: string)](https://playwright.dev/dotnet/docs/next/api/class-request#request-header-value)
- [Response.AllHeadersAsync()](https://playwright.dev/dotnet/docs/next/api/class-response#response-all-headers)
- [Response.HeadersArrayAsync()](https://playwright.dev/dotnet/docs/next/api/class-response#response-headers-array)
- [Response.HeaderValueAsync(name: string)](https://playwright.dev/dotnet/docs/next/api/class-response#response-header-value)
- [Response.HeaderValuesAsync(name: string)](https://playwright.dev/dotnet/docs/next/api/class-response#response-header-values)
- [`method: Request.allHeaders`]
- [`method: Request.headersArray`]
- [`method: Request.headerValue`]
- [`method: Response.allHeaders`]
- [`method: Response.headersArray`]
- [`method: Response.headerValue`]
- [`method: Response.headerValues`]
### 🌈 Forced-Colors emulation
Its now possible to emulate the `forced-colors` CSS media feature by passing it in the [context options](https://playwright.dev/dotnet/docs/next/api/class-browser#browser-new-context-option-forced-colors) or calling [Page.EmulateMediaAsync()](https://playwright.dev/dotnet/docs/next/api/class-page#page-emulate-media).
Its now possible to emulate the `forced-colors` CSS media feature by passing it in the [`method: Browser.newContext`] or calling [`method: Page.emulateMedia`].
### New APIs
- [Page.RouteAsync()](https://playwright.dev/dotnet/docs/next/api/class-page#page-route) accepts new `times` option to specify how many times this route should be matched.
- [Page.SetCheckedAsync(selector: string, checked: Boolean)](https://playwright.dev/dotnet/docs/next/api/class-page#page-set-checked) and [Locator.SetCheckedAsync(selector: string, checked: Boolean)](https://playwright.dev/dotnet/docs/next/api/class-locator#locator-set-checked) was introduced to set the checked state of a checkbox.
- [Request.SizesAsync()](https://playwright.dev/dotnet/docs/next/api/class-request#request-sizes) Returns resource size information for given http request.
- [Tracing.StartChunkAsync()](https://playwright.dev/dotnet/docs/next/api/class-tracing#tracing-start-chunk) - Start a new trace chunk.
- [Tracing.StopChunkAsync()](https://playwright.dev/dotnet/docs/next/api/class-tracing#tracing-stop-chunk) - Stops a new trace chunk.
- [`method: Page.route`] accepts new `times` option to specify how many times this route should be matched.
- [`method: Page.setChecked`] and [`method: Locator.setChecked`] were introduced to set the checked state of a checkbox.
- [`method: Request.sizes`] Returns resource size information for given http request.
- [`method: Tracing.startChunk`] - Start a new trace chunk.
- [`method: Tracing.stopChunk`] - Stops a new trace chunk.
### Important ⚠
* ⬆ .NET Core Apps 2.1 are **no longer** supported for our CLI tooling. As of August 31st, 2021, .NET Core 2.1 is no [longer supported](https://devblogs.microsoft.com/dotnet/net-core-2-1-will-reach-end-of-support-on-august-21-2021/) and will not receive any security updates. We've decided to move the CLI forward and require .NET Core 3.1 as a minimum.

View file

@ -4,6 +4,126 @@ title: "Release notes"
toc_max_heading_level: 2
---
## Version 1.42
### Experimental JUnit integration
Add new [`@UsePlaywright`](./junit.md) annotation to your test classes to start using Playwright
fixtures for [Page], [BrowserContext], [Browser], [APIRequestContext] and [Playwright] in the
test methods.
```java
package org.example;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.junit.UsePlaywright;
import org.junit.jupiter.api.Test;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
@UsePlaywright
public class TestExample {
void shouldNavigateToInstallationGuide(Page page) {
page.navigate("https://playwright.dev/java/");
page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("Docs")).click();
assertThat(page.getByRole(AriaRole.HEADING, new Page.GetByRoleOptions().setName("Installation"))).isVisible();
}
@Test
void shouldCheckTheBox(Page page) {
page.setContent("<input id='checkbox' type='checkbox'></input>");
page.locator("input").check();
assertEquals(true, page.evaluate("window['checkbox'].checked"));
}
@Test
void shouldSearchWiki(Page page) {
page.navigate("https://www.wikipedia.org/");
page.locator("input[name=\"search\"]").click();
page.locator("input[name=\"search\"]").fill("playwright");
page.locator("input[name=\"search\"]").press("Enter");
assertThat(page).hasURL("https://en.wikipedia.org/wiki/Playwright");
}
}
```
In the example above, all three test methods use the same [Browser]. Each test
uses its own [BrowserContext] and [Page].
**Custom options**
Implement your own `OptionsFactory` to initialize the fixtures with custom configuration.
```java
import com.microsoft.playwright.junit.Options;
import com.microsoft.playwright.junit.OptionsFactory;
import com.microsoft.playwright.junit.UsePlaywright;
@UsePlaywright(MyTest.CustomOptions.class)
public class MyTest {
public static class CustomOptions implements OptionsFactory {
@Override
public Options getOptions() {
return new Options()
.setHeadless(false)
.setContextOption(new Browser.NewContextOptions()
.setBaseURL("https://github.com"))
.setApiRequestOptions(new APIRequest.NewContextOptions()
.setBaseURL("https://playwright.dev"));
}
}
@Test
public void testWithCustomOptions(Page page, APIRequestContext request) {
page.navigate("/");
assertThat(page).hasURL(Pattern.compile("github"));
APIResponse response = request.get("/");
assertTrue(response.text().contains("Playwright"));
}
}
```
Learn more about the fixtures in our [JUnit guide](./junit.md).
### New Locator Handler
New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears.
```java
// Setup the handler.
page.addLocatorHandler(
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Hej! You are in control of your cookies.")),
() - > {
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Accept all")).click();
});
// Write the test as usual.
page.navigate("https://www.ikea.com/");
page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("Collection of blue and white")).click();
assertThat(page.getByRole(AriaRole.HEADING, new Page.GetByRoleOptions().setName("Light and easy"))).isVisible();
```
### New APIs
- [`method: Page.pdf`] accepts two new options [`option: tagged`] and [`option: outline`].
### Announcements
* ⚠️ Ubuntu 18 is not supported anymore.
### Browser Versions
* Chromium 123.0.6312.4
* Mozilla Firefox 123.0
* WebKit 17.4
This version was also tested against the following stable channels:
* Google Chrome 122
* Microsoft Edge 123
## Version 1.41
### New APIs
@ -931,31 +1051,31 @@ This version of Playwright was also tested against the following stable channels
### 🖱️ Mouse Wheel
By using [`Mouse.wheel`](https://playwright.dev/java/docs/api/class-mouse#mouse-wheel) you are now able to scroll vertically or horizontally.
By using [`method: Mouse.wheel`] you are now able to scroll vertically or horizontally.
### 📜 New Headers API
Previously it was not possible to get multiple header values of a response. This is now possible and additional helper functions are available:
- [Request.allHeaders()](https://playwright.dev/java/docs/api/class-request#request-all-headers)
- [Request.headersArray()](https://playwright.dev/java/docs/api/class-request#request-headers-array)
- [Request.headerValue(name: string)](https://playwright.dev/java/docs/api/class-request#request-header-value)
- [Response.allHeaders()](https://playwright.dev/java/docs/api/class-response#response-all-headers)
- [Response.headersArray()](https://playwright.dev/java/docs/api/class-response#response-headers-array)
- [Response.headerValue(name: string)](https://playwright.dev/java/docs/api/class-response#response-header-value)
- [Response.headerValues(name: string)](https://playwright.dev/java/docs/api/class-response#response-header-values)
- [`method: Request.allHeaders`]
- [`method: Request.headersArray`]
- [`method: Request.headerValue`]
- [`method: Response.allHeaders`]
- [`method: Response.headersArray`]
- [`method: Response.headerValue`]
- [`method: Response.headerValues`]
### 🌈 Forced-Colors emulation
Its now possible to emulate the `forced-colors` CSS media feature by passing it in the [context options](https://playwright.dev/java/docs/api/class-browser#browser-new-context-option-color-scheme) or calling [Page.emulateMedia()](https://playwright.dev/java/docs/api/class-page#page-emulate-media).
Its now possible to emulate the `forced-colors` CSS media feature by passing it in the [`method: Browser.newContext`] or calling [`method: Page.emulateMedia`].
### New APIs
- [Page.route()](https://playwright.dev/java/docs/api/class-page#page-route) accepts new `times` option to specify how many times this route should be matched.
- [Page.setChecked(selector: string, checked: boolean)](https://playwright.dev/java/docs/api/class-page#page-set-checked) and [Locator.setChecked(selector: string, checked: boolean)](https://playwright.dev/java/docs/api/class-locator#locator-set-checked) was introduced to set the checked state of a checkbox.
- [Request.sizes()](https://playwright.dev/java/docs/api/class-request#request-sizes) Returns resource size information for given http request.
- [Tracing.startChunk()](https://playwright.dev/java/docs/api/class-tracing#tracing-start-chunk) - Start a new trace chunk.
- [Tracing.stopChunk()](https://playwright.dev/java/docs/api/class-tracing#tracing-stop-chunk) - Stops a new trace chunk.
- [`method: Page.route`] accepts new `times` option to specify how many times this route should be matched.
- [`method: Page.setChecked`] and [`method: Locator.setChecked`] were introduced to set the checked state of a checkbox.
- [`method: Request.sizes`] Returns resource size information for given http request.
- [`method: Tracing.startChunk`] - Start a new trace chunk.
- [`method: Tracing.stopChunk`] - Stops a new trace chunk.
### Browser Versions

View file

@ -6,6 +6,89 @@ toc_max_heading_level: 2
import LiteYouTube from '@site/src/components/LiteYouTube';
## Version 1.42
<LiteYouTube
id="KjSaIQLlgns"
title="Playwright 1.41 & 1.42"
/>
### New APIs
- New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears:
```js
// Setup the handler.
await page.addLocatorHandler(
page.getByRole('heading', { name: 'Hej! You are in control of your cookies.' }),
async () => {
await page.getByRole('button', { name: 'Accept all' }).click();
});
// Write the test as usual.
await page.goto('https://www.ikea.com/');
await page.getByRole('link', { name: 'Collection of blue and white' }).click();
await expect(page.getByRole('heading', { name: 'Light and easy' })).toBeVisible();
```
- `expect(callback).toPass()` timeout can now be configured by `expect.toPass.timeout` option [globally](./api/class-testconfig#test-config-expect) or in [project config](./api/class-testproject#test-project-expect)
- [`event: ElectronApplication.console`] event is emitted when Electron main process calls console API methods.
```js
electronApp.on('console', async msg => {
const values = [];
for (const arg of msg.args())
values.push(await arg.jsonValue());
console.log(...values);
});
await electronApp.evaluate(() => console.log('hello', 5, { foo: 'bar' }));
```
- [New syntax](./test-annotations#tag-tests) for adding tags to the tests (@-tokens in the test title are still supported):
```js
test('test customer login', {
tag: ['@fast', '@login'],
}, async ({ page }) => {
// ...
});
```
Use `--grep` command line option to run only tests with certain tags.
```sh
npx playwright test --grep @fast
```
- `--project` command line [flag](./test-cli#reference) now supports '*' wildcard:
```sh
npx playwright test --project='*mobile*'
```
- [New syntax](./test-annotations#annotate-tests) for test annotations:
```js
test('test full report', {
annotation: [
{ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/23180' },
{ type: 'docs', description: 'https://playwright.dev/docs/test-annotations#tag-tests' },
],
}, async ({ page }) => {
// ...
});
```
- [`method: Page.pdf`] accepts two new options [`tagged`](./api/class-page#page-pdf-option-tagged) and [`outline`](./api/class-page#page-pdf-option-outline).
### Announcements
* ⚠️ Ubuntu 18 is not supported anymore.
### Browser Versions
* Chromium 123.0.6312.4
* Mozilla Firefox 123.0
* WebKit 17.4
This version was also tested against the following stable channels:
* Google Chrome 122
* Microsoft Edge 123
## Version 1.41
### New APIs
@ -1900,31 +1983,31 @@ This version of Playwright was also tested against the following stable channels
#### 🖱️ Mouse Wheel
By using [`Page.mouse.wheel`](https://playwright.dev/docs/api/class-mouse#mouse-wheel) you are now able to scroll vertically or horizontally.
By using [`method: Mouse.wheel`] you are now able to scroll vertically or horizontally.
#### 📜 New Headers API
Previously it was not possible to get multiple header values of a response. This is now possible and additional helper functions are available:
- [Request.allHeaders()](https://playwright.dev/docs/api/class-request#request-all-headers)
- [Request.headersArray()](https://playwright.dev/docs/api/class-request#request-headers-array)
- [Request.headerValue(name: string)](https://playwright.dev/docs/api/class-request#request-header-value)
- [Response.allHeaders()](https://playwright.dev/docs/api/class-response#response-all-headers)
- [Response.headersArray()](https://playwright.dev/docs/api/class-response#response-headers-array)
- [Response.headerValue(name: string)](https://playwright.dev/docs/api/class-response#response-header-value)
- [Response.headerValues(name: string)](https://playwright.dev/docs/api/class-response#response-header-values)
- [`method: Request.allHeaders`]
- [`method: Request.headersArray`]
- [`method: Request.headerValue`]
- [`method: Response.allHeaders`]
- [`method: Response.headersArray`]
- [`method: Response.headerValue`]
- [`method: Response.headerValues`]
#### 🌈 Forced-Colors emulation
Its now possible to emulate the `forced-colors` CSS media feature by passing it in the [context options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-forced-colors) or calling [Page.emulateMedia()](https://playwright.dev/docs/api/class-page#page-emulate-media).
Its now possible to emulate the `forced-colors` CSS media feature by passing it in the [`method: Browser.newContext`] or calling [`method: Page.emulateMedia`].
#### New APIs
- [Page.route()](https://playwright.dev/docs/api/class-page#page-route) accepts new `times` option to specify how many times this route should be matched.
- [Page.setChecked(selector: string, checked: boolean)](https://playwright.dev/docs/api/class-page#page-set-checked) and [Locator.setChecked(selector: string, checked: boolean)](https://playwright.dev/docs/api/class-locator#locator-set-checked) was introduced to set the checked state of a checkbox.
- [Request.sizes()](https://playwright.dev/docs/api/class-request#request-sizes) Returns resource size information for given http request.
- [BrowserContext.tracing.startChunk()](https://playwright.dev/docs/api/class-tracing#tracing-start-chunk) - Start a new trace chunk.
- [BrowserContext.tracing.stopChunk()](https://playwright.dev/docs/api/class-tracing#tracing-stop-chunk) - Stops a new trace chunk.
- [`method: Page.route`] accepts new `times` option to specify how many times this route should be matched.
- [`method: Page.setChecked`] and [`method: Locator.setChecked`] were introduced to set the checked state of a checkbox.
- [`method: Request.sizes`] Returns resource size information for given http request.
- [`method: Tracing.startChunk`] - Start a new trace chunk.
- [`method: Tracing.stopChunk`] - Stops a new trace chunk.
### 🎭 Playwright Test
@ -1939,11 +2022,11 @@ test.describe.parallel('group', () => {
});
```
By default, tests in a single file are run in order. If you have many independent tests in a single file, you can now run them in parallel with [test.describe.parallel(title, callback)](https://playwright.dev/docs/api/class-test#test-describe-parallel).
By default, tests in a single file are run in order. If you have many independent tests in a single file, you can now run them in parallel with [test.describe.parallel(title, callback)](./api/class-test#test-describe-parallel).
#### 🛠 Add `--debug` CLI flag
By using `npx playwright test --debug` it will enable the [Playwright Inspector](https://playwright.dev/docs/debug#playwright-inspector) for you to debug your tests.
By using `npx playwright test --debug` it will enable the [Playwright Inspector](./debug#playwright-inspector) for you to debug your tests.
### Browser Versions

View file

@ -4,6 +4,43 @@ title: "Release notes"
toc_max_heading_level: 2
---
## Version 1.42
### New Locator Handler
New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears.
```python
# Setup the handler.
page.add_locator_handler(
page.get_by_role("heading", name="Hej! You are in control of your cookies."),
lambda: page.get_by_role("button", name="Accept all").click(),
)
# Write the test as usual.
page.goto("https://www.ikea.com/")
page.get_by_role("link", name="Collection of blue and white").click()
expect(page.get_by_role("heading", name="Light and easy")).to_be_visible()
```
### New APIs
- [`method: Page.pdf`] accepts two new options [`option: tagged`] and [`option: outline`].
### Announcements
* ⚠️ Ubuntu 18 is not supported anymore.
### Browser Versions
* Chromium 123.0.6312.4
* Mozilla Firefox 123.0
* WebKit 17.4
This version was also tested against the following stable channels:
* Google Chrome 122
* Microsoft Edge 123
## Version 1.41
### New APIs
@ -1003,31 +1040,31 @@ This version of Playwright was also tested against the following stable channels
### 🖱️ Mouse Wheel
By using [`Page.mouse.wheel`](https://playwright.dev/python/docs/api/class-mouse#mouse-wheel) you are now able to scroll vertically or horizontally.
By using [`method: Mouse.wheel`] you are now able to scroll vertically or horizontally.
### 📜 New Headers API
Previously it was not possible to get multiple header values of a response. This is now possible and additional helper functions are available:
- [Request.all_headers()](https://playwright.dev/python/docs/api/class-request#request-all-headers)
- [Request.headers_array()](https://playwright.dev/python/docs/api/class-request#request-headers-array)
- [Request.header_value(name: str)](https://playwright.dev/python/docs/api/class-request#request-header-value)
- [Response.all_headers()](https://playwright.dev/python/docs/api/class-response#response-all-headers)
- [Response.headers_array()](https://playwright.dev/python/docs/api/class-response#response-headers-array)
- [Response.header_value(name: str)](https://playwright.dev/python/docs/api/class-response#response-header-value)
- [Response.header_values(name: str)](https://playwright.dev/python/docs/api/class-response#response-header-values)
- [`method: Request.allHeaders`]
- [`method: Request.headersArray`]
- [`method: Request.headerValue`]
- [`method: Response.allHeaders`]
- [`method: Response.headersArray`]
- [`method: Response.headerValue`]
- [`method: Response.headerValues`]
### 🌈 Forced-Colors emulation
Its now possible to emulate the `forced-colors` CSS media feature by passing it in the [context options](https://playwright.dev/python/docs/api/class-browser#browser-new-context-option-forced-colors) or calling [Page.emulate_media()](https://playwright.dev/python/docs/api/class-page#page-emulate-media).
Its now possible to emulate the `forced-colors` CSS media feature by passing it in the [`method: Browser.newContext`] or calling [`method: Page.emulateMedia`].
### New APIs
- [Page.route()](https://playwright.dev/python/docs/api/class-page#page-route) accepts new `times` option to specify how many times this route should be matched.
- [Page.set_checked(selector: str, checked: bool)](https://playwright.dev/python/docs/api/class-page#page-set-checked) and [Locator.set_checked(selector: str, checked: bool)](https://playwright.dev/python/docs/api/class-locator#locator-set-checked) was introduced to set the checked state of a checkbox.
- [Request.sizes()](https://playwright.dev/python/docs/api/class-request#request-sizes) Returns resource size information for given http request.
- [BrowserContext.tracing.start_chunk()](https://playwright.dev/python/docs/api/class-tracing#tracing-start-chunk) - Start a new trace chunk.
- [BrowserContext.tracing.stop_chunk()](https://playwright.dev/python/docs/api/class-tracing#tracing-stop-chunk) - Stops a new trace chunk.
- [`method: Page.route`] accepts new `times` option to specify how many times this route should be matched.
- [`method: Page.setChecked`] and [`method: Locator.setChecked`] were introduced to set the checked state of a checkbox.
- [`method: Request.sizes`] Returns resource size information for given http request.
- [`method: Tracing.startChunk`] - Start a new trace chunk.
- [`method: Tracing.stopChunk`] - Stops a new trace chunk.
### Browser Versions

View file

@ -83,6 +83,8 @@ public class TestExample {
See [here](./test-runners.md) for further details on how to run tests in parallel, etc.
See experimental [JUnit integration](./junit.md) to automatically initialize Playwright objects and more.
## What's Next
- [Debugging tests](./debug.md)

View file

@ -137,10 +137,12 @@ self.addEventListener('fetch', event => {
(async () => {
// 1. Try to first serve directly from caches
const response = await caches.match(event.request);
if (response) return response;
if (response)
return response;
// 2. Re-write request for /foo to /bar
if (event.request.url.endsWith('foo')) return fetch('./bar');
if (event.request.url.endsWith('foo'))
return fetch('./bar');
// 3. Prevent tracker.js from being retrieved, and returns a placeholder response
if (event.request.url.endsWith('tracker.js')) {

View file

@ -210,6 +210,14 @@ Optional description that will be reflected in a test report.
Test function as passed to `test(title, testFunction)`.
## property: TestInfo.tags
* since: v1.43
- type: <[Array]<[string]>>
Tags that apply to the test. Learn more about [tags](../test-annotations.md#tag-tests).
Note that any changes made to this list while the test is running will not be visible to test reporters.
## property: TestInfo.testId
* since: v1.32
- type: <[string]>

View file

@ -546,8 +546,8 @@ export default defineConfig({
## property: TestOptions.trace
* since: v1.10
- type: <[Object]|[TraceMode]<"off"|"on"|"retain-on-failure"|"on-first-retry">>
- `mode` <[TraceMode]<"off"|"on"|"retain-on-failure"|"on-first-retry"|"on-all-retries">> Trace recording mode.
- type: <[Object]|[TraceMode]<"off"|"on"|"retain-on-failure"|"on-first-retry"|"retain-on-first-failure">>
- `mode` <[TraceMode]<"off"|"on"|"retain-on-failure"|"on-first-retry"|"on-all-retries"|"retain-on-first-failure">> Trace recording mode.
- `attachments` ?<[boolean]> Whether to include test attachments. Defaults to true. Optional.
- `screenshots` ?<[boolean]> Whether to capture screenshots during tracing. Screenshots are used to build a timeline preview. Defaults to true. Optional.
- `snapshots` ?<[boolean]> Whether to capture DOM snapshot on every action. Defaults to true. Optional.
@ -559,6 +559,7 @@ Whether to record trace for each test. Defaults to `'off'`.
* `'retain-on-failure'`: Record trace for each test, but remove all traces from successful test runs.
* `'on-first-retry'`: Record trace only when retrying a test for the first time.
* `'on-all-retries'`: Record traces only when retrying for all retries.
* `'retain-on-first-failure'`: Record traces only when the test fails for the first time.
For more control, pass an object that specifies `mode` and trace features to enable.

View file

@ -147,6 +147,6 @@ export default defineConfig({
| Option | Description |
| :- | :- |
| [`property: TestConfig.expect`] | [Web first assertions](./test-assertions.md) like `expect(locator).toHaveText()` have a separate timeout of 5 seconds by default. This is the maximum time the `expect()` should wait for the condition to be met. Learn more about [test and expect timeouts](./test-timeouts.md) and how to set them for a single test. |
| [`method: PageAssertions.toHaveScreenshot#1`] | Configuration for the `expect(locator).toHaveScreeshot()` method. |
| [`method: PageAssertions.toHaveScreenshot#1`] | Configuration for the `expect(locator).toHaveScreenshot()` method. |
| [`method: SnapshotAssertions.toMatchSnapshot#1`]| Configuration for the `expect(locator).toMatchSnapshot()` method.|

View file

@ -354,6 +354,7 @@ npx playwright test --reporter="./myreporter/my-awesome-reporter.ts"
* [Allure](https://www.npmjs.com/package/allure-playwright)
* [Argos Visual Testing](https://argos-ci.com/docs/playwright)
* [Currents](https://www.npmjs.com/package/@currents/playwright)
* [GitHub Actions Reporter](https://www.npmjs.com/package/@estruyf/github-actions-reporter)
* [Monocart](https://github.com/cenfun/monocart-reporter)
* [ReportPortal](https://github.com/reportportal/agent-js-playwright)
* [Serenity/JS](https://serenity-js.org/handbook/test-runners/playwright-test)

View file

@ -87,6 +87,8 @@ public class TestExample {
}
```
See experimental [JUnit integration](./junit.md) to automatically initialize Playwright objects and more.
### Running Tests in Parallel
By default JUnit will run all tests sequentially on a single thread. Since JUnit 5.3 you can change this behavior to run tests in parallel

View file

@ -53,14 +53,14 @@ def test_my_app_is_working(fixture_name):
**Function scope**: These fixtures are created when requested in a test function and destroyed when the test ends.
- `context`: New [browser context](https://playwright.dev/python/docs/browser-contexts) for a test.
- `page`: New [browser page](https://playwright.dev/python/docs/pages) for a test.
- `context`: New [browser context](./browser-contexts) for a test.
- `page`: New [browser page](./pages) for a test.
**Session scope**: These fixtures are created when requested in a test function and destroyed when all tests end.
- `playwright`: [Playwright](https://playwright.dev/python/docs/api/class-playwright) instance.
- `browser_type`: [BrowserType](https://playwright.dev/python/docs/api/class-browsertype) instance of the current browser.
- `browser`: [Browser](https://playwright.dev/python/docs/api/class-browser) instance launched by Playwright.
- `playwright`: [Playwright](./api/class-playwright) instance.
- `browser_type`: [BrowserType](./api/class-browsertype) instance of the current browser.
- `browser`: [Browser](./api/class-browser) instance launched by Playwright.
- `browser_name`: Browser name as string.
- `browser_channel`: Browser channel as string.
- `is_chromium`, `is_webkit`, `is_firefox`: Booleans for the respective browser types.

View file

@ -44,6 +44,8 @@ The snapshot name `example-test-1-chromium-darwin.png` consists of a few parts:
- `chromium-darwin` - the browser name and the platform. Screenshots differ between browsers and platforms due to different rendering, fonts and more, so you will need different snapshots for them. If you use multiple projects in your [configuration file](./test-configuration.md), project name will be used instead of `chromium`.
The snapshot name and path can be configured with [`snapshotPathTemplate`](./api/class-testproject#test-project-snapshot-path-template) in the playwright config.
## Updating screenshots
Sometimes you need to update the reference screenshot, for example when the page has changed. Do this with the `--update-snapshots` flag.

View file

@ -5,7 +5,26 @@ title: "TypeScript"
## Introduction
Playwright supports TypeScript out of the box. You just write tests in TypeScript, and Playwright will read them, transform to JavaScript and run.
Playwright supports TypeScript out of the box. You just write tests in TypeScript, and Playwright will read them, transform to JavaScript and run. Note that Playwright does not check the types and will run tests even if there are non-critical TypeScript compilation errors.
We recommend you run TypeScript compiler alongside Playwright. For example on GitHub actions:
```yaml
jobs:
test:
runs-on: ubuntu-latest
steps:
...
- name: Run type checks
run: npx tsc -p tsconfig.json --noEmit
- name: Run Playwright tests
run: npx playwright test
```
For local development, you can run `tsc` in [watch](https://www.typescriptlang.org/docs/handbook/configuring-watch.html) mode like this:
```sh
npx tsc -p tsconfig.json --noEmit -w
```
## tsconfig.json

86
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "playwright-internal",
"version": "1.42.0-next",
"version": "1.43.0-next",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "playwright-internal",
"version": "1.42.0-next",
"version": "1.43.0-next",
"license": "Apache-2.0",
"workspaces": [
"packages/*"
@ -8360,10 +8360,10 @@
}
},
"packages/playwright": {
"version": "1.42.0-next",
"version": "1.43.0-next",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.42.0-next"
"playwright-core": "1.43.0-next"
},
"bin": {
"playwright": "cli.js"
@ -8377,11 +8377,11 @@
},
"packages/playwright-browser-chromium": {
"name": "@playwright/browser-chromium",
"version": "1.42.0-next",
"version": "1.43.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.42.0-next"
"playwright-core": "1.43.0-next"
},
"engines": {
"node": ">=16"
@ -8389,11 +8389,11 @@
},
"packages/playwright-browser-firefox": {
"name": "@playwright/browser-firefox",
"version": "1.42.0-next",
"version": "1.43.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.42.0-next"
"playwright-core": "1.43.0-next"
},
"engines": {
"node": ">=16"
@ -8401,22 +8401,22 @@
},
"packages/playwright-browser-webkit": {
"name": "@playwright/browser-webkit",
"version": "1.42.0-next",
"version": "1.43.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.42.0-next"
"playwright-core": "1.43.0-next"
},
"engines": {
"node": ">=16"
}
},
"packages/playwright-chromium": {
"version": "1.42.0-next",
"version": "1.43.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.42.0-next"
"playwright-core": "1.43.0-next"
},
"bin": {
"playwright": "cli.js"
@ -8426,7 +8426,7 @@
}
},
"packages/playwright-core": {
"version": "1.42.0-next",
"version": "1.43.0-next",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
@ -8614,11 +8614,11 @@
},
"packages/playwright-ct-core": {
"name": "@playwright/experimental-ct-core",
"version": "1.42.0-next",
"version": "1.43.0-next",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.42.0-next",
"playwright-core": "1.42.0-next",
"playwright": "1.43.0-next",
"playwright-core": "1.43.0-next",
"vite": "^5.0.12"
},
"bin": {
@ -8630,15 +8630,14 @@
},
"packages/playwright-ct-react": {
"name": "@playwright/experimental-ct-react",
"version": "1.42.0-next",
"version": "1.43.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.42.0-next",
"@playwright/experimental-ct-core": "1.43.0-next",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {
"playwright": "cli.js",
"pw-react": "cli.js"
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
@ -8646,15 +8645,14 @@
},
"packages/playwright-ct-react17": {
"name": "@playwright/experimental-ct-react17",
"version": "1.42.0-next",
"version": "1.43.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.42.0-next",
"@playwright/experimental-ct-core": "1.43.0-next",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {
"playwright": "cli.js",
"pw-react17": "cli.js"
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
@ -8662,15 +8660,14 @@
},
"packages/playwright-ct-solid": {
"name": "@playwright/experimental-ct-solid",
"version": "1.42.0-next",
"version": "1.43.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.42.0-next",
"@playwright/experimental-ct-core": "1.43.0-next",
"vite-plugin-solid": "^2.7.0"
},
"bin": {
"playwright": "cli.js",
"pw-solid": "cli.js"
"playwright": "cli.js"
},
"devDependencies": {
"solid-js": "^1.7.0"
@ -8681,15 +8678,14 @@
},
"packages/playwright-ct-svelte": {
"name": "@playwright/experimental-ct-svelte",
"version": "1.42.0-next",
"version": "1.43.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.42.0-next",
"@playwright/experimental-ct-core": "1.43.0-next",
"@sveltejs/vite-plugin-svelte": "^3.0.1"
},
"bin": {
"playwright": "cli.js",
"pw-svelte": "cli.js"
"playwright": "cli.js"
},
"devDependencies": {
"svelte": "^4.2.8"
@ -8700,15 +8696,14 @@
},
"packages/playwright-ct-vue": {
"name": "@playwright/experimental-ct-vue",
"version": "1.42.0-next",
"version": "1.43.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.42.0-next",
"@playwright/experimental-ct-core": "1.43.0-next",
"@vitejs/plugin-vue": "^4.2.1"
},
"bin": {
"playwright": "cli.js",
"pw-vue": "cli.js"
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
@ -8716,15 +8711,14 @@
},
"packages/playwright-ct-vue2": {
"name": "@playwright/experimental-ct-vue2",
"version": "1.42.0-next",
"version": "1.43.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.42.0-next",
"@playwright/experimental-ct-core": "1.43.0-next",
"@vitejs/plugin-vue2": "^2.2.0"
},
"bin": {
"playwright": "cli.js",
"pw-vue2": "cli.js"
"playwright": "cli.js"
},
"devDependencies": {
"vue": "^2.7.14"
@ -8769,11 +8763,11 @@
}
},
"packages/playwright-firefox": {
"version": "1.42.0-next",
"version": "1.43.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.42.0-next"
"playwright-core": "1.43.0-next"
},
"bin": {
"playwright": "cli.js"
@ -8784,10 +8778,10 @@
},
"packages/playwright-test": {
"name": "@playwright/test",
"version": "1.42.0-next",
"version": "1.43.0-next",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.42.0-next"
"playwright": "1.43.0-next"
},
"bin": {
"playwright": "cli.js"
@ -8797,11 +8791,11 @@
}
},
"packages/playwright-webkit": {
"version": "1.42.0-next",
"version": "1.43.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.42.0-next"
"playwright-core": "1.43.0-next"
},
"bin": {
"playwright": "cli.js"

View file

@ -1,7 +1,7 @@
{
"name": "playwright-internal",
"private": true,
"version": "1.42.0-next",
"version": "1.43.0-next",
"description": "A high-level API to automate web browsers",
"repository": {
"type": "git",
@ -16,9 +16,9 @@
},
"license": "Apache-2.0",
"scripts": {
"ctest": "playwright test --config=tests/library/playwright.config.ts --project=chromium",
"ftest": "playwright test --config=tests/library/playwright.config.ts --project=firefox",
"wtest": "playwright test --config=tests/library/playwright.config.ts --project=webkit",
"ctest": "playwright test --config=tests/library/playwright.config.ts --project=chromium-*",
"ftest": "playwright test --config=tests/library/playwright.config.ts --project=firefox-*",
"wtest": "playwright test --config=tests/library/playwright.config.ts --project=webkit-*",
"atest": "playwright test --config=tests/android/playwright.config.ts",
"etest": "playwright test --config=tests/electron/playwright.config.ts",
"webview2test": "playwright test --config=tests/webview2/playwright.config.ts",
@ -29,7 +29,7 @@
"ttest": "node ./tests/playwright-test/stable-test-runner/node_modules/@playwright/test/cli test --config=tests/playwright-test/playwright.config.ts",
"ct": "playwright test tests/components/test-all.spec.js --reporter=list",
"test": "playwright test --config=tests/library/playwright.config.ts",
"eslint": "eslint --cache --report-unused-disable-directives --ext ts,tsx .",
"eslint": "eslint --cache --report-unused-disable-directives --ext ts,tsx,js,jsx,mjs .",
"tsc": "tsc -p .",
"build-installer": "babel -s --extensions \".ts\" --out-dir packages/playwright-core/lib/utils/ packages/playwright-core/src/utils",
"doc": "node utils/doclint/cli.js",

View file

@ -1,9 +0,0 @@
module.exports = {
extends: ".eslintrc.js",
rules: {
"@typescript-eslint/no-base-to-string": "error",
},
parserOptions: {
project: "./tsconfig.json"
},
};

View file

@ -14,7 +14,6 @@
limitations under the License.
*/
import { testCaseLabels } from './labelUtils';
import type { TestCaseSummary } from './types';
export class Filter {
@ -108,13 +107,13 @@ export class Filter {
if (test.outcome === 'skipped')
status = 'skipped';
const searchValues: SearchValues = {
text: (status + ' ' + test.projectName + ' ' + (test.botName || '') + ' ' + test.location.file + ' ' + test.path.join(' ') + ' ' + test.title).toLowerCase(),
text: (status + ' ' + test.projectName + ' ' + test.tags.join(' ') + ' ' + test.location.file + ' ' + test.path.join(' ') + ' ' + test.title).toLowerCase(),
project: test.projectName.toLowerCase(),
status: status as any,
file: test.location.file,
line: String(test.location.line),
column: String(test.location.column),
labels: testCaseLabels(test).map(label => label.toLowerCase()),
labels: test.tags.map(tag => tag.toLowerCase()),
};
(test as any).searchValues = searchValues;
}

View file

@ -14,22 +14,6 @@
* limitations under the License.
*/
import type { TestCaseSummary } from './types';
const labelsSymbol = Symbol('labels');
// Note: all labels start with "@"
export function testCaseLabels(test: TestCaseSummary): string[] {
if (!(test as any)[labelsSymbol]) {
const labels: string[] = [];
if (test.botName)
labels.push('@' + test.botName);
labels.push(...test.tags);
(test as any)[labelsSymbol] = labels;
}
return (test as any)[labelsSymbol];
}
// hash string to integer in range [0, 6] for color index, to get same color for same tag
export function hashStringToInt(str: string) {
let hash = 0;

View file

@ -23,7 +23,7 @@ import { ProjectLink } from './links';
import { statusIcon } from './statusIcon';
import './testCaseView.css';
import { TestResultView } from './testResultView';
import { hashStringToInt, testCaseLabels } from './labelUtils';
import { hashStringToInt } from './labelUtils';
import { msToString } from './uiUtils';
export const TestCaseView: React.FC<{
@ -37,7 +37,7 @@ export const TestCaseView: React.FC<{
const labels = React.useMemo(() => {
if (!test)
return undefined;
return testCaseLabels(test);
return test.tags;
}, [test]);
return <div className='test-case-column vbox'>

View file

@ -23,7 +23,7 @@ import { generateTraceUrl, Link, navigate, ProjectLink } from './links';
import { statusIcon } from './statusIcon';
import './testFileView.css';
import { video, image, trace } from './icons';
import { hashStringToInt, testCaseLabels } from './labelUtils';
import { hashStringToInt } from './labelUtils';
export const TestFileView: React.FC<React.PropsWithChildren<{
report: HTMLReport;
@ -52,7 +52,7 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
</Link>
{report.projectNames.length > 1 && !!test.projectName &&
<ProjectLink projectNames={report.projectNames} projectName={test.projectName} />}
<LabelsClickView labels={testCaseLabels(test)} />
<LabelsClickView labels={test.tags} />
</span>
</div>
<span data-testid='test-duration' style={{ minWidth: '50px', textAlign: 'right' }}>{msToString(test.duration)}</span>

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/browser-chromium",
"version": "1.42.0-next",
"version": "1.43.0-next",
"description": "Playwright package that automatically installs Chromium",
"repository": {
"type": "git",
@ -27,6 +27,6 @@
"install": "node install.js"
},
"dependencies": {
"playwright-core": "1.42.0-next"
"playwright-core": "1.43.0-next"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/browser-firefox",
"version": "1.42.0-next",
"version": "1.43.0-next",
"description": "Playwright package that automatically installs Firefox",
"repository": {
"type": "git",
@ -27,6 +27,6 @@
"install": "node install.js"
},
"dependencies": {
"playwright-core": "1.42.0-next"
"playwright-core": "1.43.0-next"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/browser-webkit",
"version": "1.42.0-next",
"version": "1.43.0-next",
"description": "Playwright package that automatically installs WebKit",
"repository": {
"type": "git",
@ -27,6 +27,6 @@
"install": "node install.js"
},
"dependencies": {
"playwright-core": "1.42.0-next"
"playwright-core": "1.43.0-next"
}
}

View file

@ -15,5 +15,5 @@
* limitations under the License.
*/
const { program } = require('playwright-core/lib/program');
const { program } = require('playwright-core/lib/cli/program');
program.parse(process.argv);

View file

@ -1,6 +1,6 @@
{
"name": "playwright-chromium",
"version": "1.42.0-next",
"version": "1.43.0-next",
"description": "A high-level API to automate Chromium",
"repository": {
"type": "git",
@ -30,6 +30,6 @@
"install": "node install.js"
},
"dependencies": {
"playwright-core": "1.42.0-next"
"playwright-core": "1.43.0-next"
}
}

View file

@ -1,3 +1,3 @@
module.exports = {
extends: "../.eslintrc-with-ts-config.js",
extends: "../../.eslintrc-with-ts-config.js",
};

View file

@ -3,43 +3,37 @@
"browsers": [
{
"name": "chromium",
"revision": "1105",
"revision": "1110",
"installByDefault": true,
"browserVersion": "123.0.6312.4"
},
{
"name": "chromium-with-symbols",
"revision": "1105",
"installByDefault": false,
"browserVersion": "123.0.6312.4"
"browserVersion": "124.0.6367.8"
},
{
"name": "chromium-tip-of-tree",
"revision": "1195",
"revision": "1204",
"installByDefault": false,
"browserVersion": "123.0.6312.0"
"browserVersion": "125.0.6370.0"
},
{
"name": "firefox",
"revision": "1440",
"revision": "1445",
"installByDefault": true,
"browserVersion": "123.0"
},
{
"name": "firefox-asan",
"revision": "1440",
"revision": "1445",
"installByDefault": false,
"browserVersion": "123.0"
},
{
"name": "firefox-beta",
"revision": "1440",
"revision": "1444",
"installByDefault": false,
"browserVersion": "124.0b3"
},
{
"name": "webkit",
"revision": "1983",
"revision": "1990",
"installByDefault": true,
"revisionOverrides": {
"mac10.14": "1446",

View file

@ -1,6 +1,6 @@
{
"name": "playwright-core",
"version": "1.42.0-next",
"version": "1.43.0-next",
"description": "A high-level API to automate web browsers",
"repository": {
"type": "git",

View file

@ -23,8 +23,8 @@ import type { Command } from '../utilsBundle';
import { program } from '../utilsBundle';
export { program } from '../utilsBundle';
import { runDriver, runServer, printApiJson, launchBrowserServer } from './driver';
import type { OpenTraceViewerOptions } from '../server/trace/viewer/traceViewer';
import { openTraceInBrowser, openTraceViewerApp } from '../server/trace/viewer/traceViewer';
import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer';
import type { TraceViewerServerOptions } from '../server/trace/viewer/traceViewer';
import * as playwright from '../..';
import type { BrowserContext } from '../client/browserContext';
import type { Browser } from '../client/browser';
@ -305,19 +305,16 @@ program
if (options.browser === 'wk')
options.browser = 'webkit';
const openOptions: OpenTraceViewerOptions = {
headless: false,
const openOptions: TraceViewerServerOptions = {
host: options.host,
port: +options.port,
isServer: !!options.stdin,
};
if (options.port !== undefined || options.host !== undefined) {
openTraceInBrowser(traces, openOptions).catch(logErrorAndExit);
} else {
openTraceViewerApp(traces, options.browser, openOptions).then(page => {
page.on('close', () => gracefullyProcessExitDoNotHang(0));
}).catch(logErrorAndExit);
}
if (options.port !== undefined || options.host !== undefined)
runTraceInBrowser(traces, openOptions).catch(logErrorAndExit);
else
runTraceViewerApp(traces, options.browser, openOptions, true).catch(logErrorAndExit);
}).addHelpText('afterAll', `
Examples:

View file

@ -269,6 +269,10 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
await this._channel.clearCookies();
}
async removeCookies(filter: network.RemoveNetworkCookieParam): Promise<void> {
await this._channel.removeCookies({ filter });
}
async grantPermissions(permissions: string[], options?: { origin?: string }): Promise<void> {
await this._channel.grantPermissions({ permissions, ...options });
}
@ -526,7 +530,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
function toAcceptDownloadsProtocol(acceptDownloads?: boolean) {
if (acceptDownloads === undefined)
return undefined;
if (acceptDownloads === true)
if (acceptDownloads)
return 'accept';
return 'deny';
}

View file

@ -19,6 +19,7 @@ import { ChannelOwner } from './channelOwner';
import { parseSerializedValue, serializeValue } from '../protocol/serializers';
import type * as api from '../../types/types';
import type * as structs from '../../types/structs';
import { isTargetClosedError } from './errors';
export class JSHandle<T = any> extends ChannelOwner<channels.JSHandleChannel> implements api.JSHandle {
private _preview: string;
@ -68,7 +69,13 @@ export class JSHandle<T = any> extends ChannelOwner<channels.JSHandleChannel> im
}
async dispose() {
return await this._channel.dispose();
try {
await this._channel.dispose();
} catch (e) {
if (isTargetClosedError(e))
return;
throw e;
}
}
async _objectCount() {

View file

@ -192,6 +192,10 @@ export class Locator implements api.Locator {
return await this._frame.$$(this._selector);
}
enterFrame() {
return new FrameLocator(this._frame, this._selector);
}
first(): Locator {
return new Locator(this._frame, this._selector + ' >> nth=0');
}
@ -404,6 +408,10 @@ export class FrameLocator implements api.FrameLocator {
return this.locator(getByRoleSelector(role, options));
}
exitFrame() {
return new Locator(this._frame, this._frameSelector);
}
frameLocator(selector: string): FrameLocator {
return new FrameLocator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selector);
}

View file

@ -58,6 +58,12 @@ export type SetNetworkCookieParam = {
sameSite?: 'Strict' | 'Lax' | 'None'
};
export type RemoveNetworkCookieParam = {
name?: string,
domain?: string,
path?: string,
};
type SerializedFallbackOverrides = {
url?: string;
method?: string;
@ -135,7 +141,7 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
return null;
const contentType = this.headers()['content-type'];
if (contentType === 'application/x-www-form-urlencoded') {
if (contentType?.includes('application/x-www-form-urlencoded')) {
const entries: Record<string, string> = {};
const parsed = new URLSearchParams(postData);
for (const [k, v] of parsed.entries())

View file

@ -828,6 +828,14 @@ scheme.BrowserContextAddInitScriptParams = tObject({
scheme.BrowserContextAddInitScriptResult = tOptional(tObject({}));
scheme.BrowserContextClearCookiesParams = tOptional(tObject({}));
scheme.BrowserContextClearCookiesResult = tOptional(tObject({}));
scheme.BrowserContextRemoveCookiesParams = tObject({
filter: tObject({
name: tOptional(tString),
domain: tOptional(tString),
path: tOptional(tString),
}),
});
scheme.BrowserContextRemoveCookiesResult = tOptional(tObject({}));
scheme.BrowserContextClearPermissionsParams = tOptional(tObject({}));
scheme.BrowserContextClearPermissionsResult = tOptional(tObject({}));
scheme.BrowserContextCloseParams = tObject({

View file

@ -276,6 +276,22 @@ export abstract class BrowserContext extends SdkObject {
return await this.doGetCookies(urls as string[]);
}
async removeCookies(filter: {name?: string, domain?: string, path?: string}): Promise<void> {
if (!filter.name && !filter.domain && !filter.path)
throw new Error(`Either name, domain or path are required`);
const currentCookies = await this.cookies();
const cookiesToKeep = currentCookies.filter(cookie => {
return !((!filter.name || filter.name === cookie.name) &&
(!filter.domain || filter.domain === cookie.domain) &&
(!filter.path || filter.path === cookie.path));
});
await this.clearCookies();
await this.addCookies(cookiesToKeep);
}
setHTTPCredentials(httpCredentials?: types.Credentials): Promise<void> {
return this.doSetHTTPCredentials(httpCredentials);
}
@ -468,14 +484,34 @@ export abstract class BrowserContext extends SdkObject {
cookies: await this.cookies(),
origins: []
};
if (this._origins.size) {
const originsToSave = new Set(this._origins);
// First try collecting storage stage from existing pages.
for (const page of this.pages()) {
const origin = page.mainFrame().origin();
if (!origin || !originsToSave.has(origin))
continue;
try {
const storage = await page.mainFrame().nonStallingEvaluateInExistingContext(`({
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })),
})`, false, 'utility');
if (storage.localStorage.length)
result.origins.push({ origin, localStorage: storage.localStorage } as channels.OriginStorage);
originsToSave.delete(origin);
} catch {
// When failed on the live page, we'll retry on the blank page below.
}
}
// If there are still origins to save, create a blank page to iterate over origins.
if (originsToSave.size) {
const internalMetadata = serverSideCallMetadata();
const page = await this.newPage(internalMetadata);
await page._setServerRequestInterceptor(handler => {
handler.fulfill({ body: '<html></html>', requestUrl: handler.request().url() }).catch(() => {});
return true;
});
for (const origin of this._origins) {
for (const origin of originsToSave) {
const originStorage: channels.OriginStorage = { origin, localStorage: [] };
const frame = page.mainFrame();
await frame.goto(internalMetadata, origin);

View file

@ -562,7 +562,7 @@ export class CRBrowserContext extends BrowserContext {
override async clearCache(): Promise<void> {
for (const page of this._crPages())
await page._mainFrameSession._networkManager.clearCache();
await page._networkManager.clearCache();
}
async cancelDownload(guid: string) {
@ -594,7 +594,8 @@ export class CRBrowserContext extends BrowserContext {
targetId = (page._delegate as CRPage)._targetId;
} else if (page instanceof Frame) {
const session = (page._page._delegate as CRPage)._sessions.get(page._id);
if (!session) throw new Error(`This frame does not have a separate CDP session, it is a part of the parent frame's session`);
if (!session)
throw new Error(`This frame does not have a separate CDP session, it is a part of the parent frame's session`);
targetId = session._targetId;
} else {
throw new Error('page: expected Page or Frame');

View file

@ -26,70 +26,88 @@ import type * as contexts from '../browserContext';
import type * as frames from '../frames';
import type * as types from '../types';
import type { CRPage } from './crPage';
import { assert, headersObjectToArray } from '../../utils';
import { assert, headersArrayToObject, headersObjectToArray } from '../../utils';
import type { CRServiceWorker } from './crServiceWorker';
import { isProtocolError } from '../protocolError';
import { isProtocolError, isSessionClosedError } from '../protocolError';
type SessionInfo = {
session: CRSession;
isMain?: boolean;
workerFrame?: frames.Frame;
eventListeners: RegisteredListener[];
};
export class CRNetworkManager {
private _session: CRSession;
private _page: Page | null;
private _serviceWorker: CRServiceWorker | null;
private _parentManager: CRNetworkManager | null;
private _requestIdToRequest = new Map<string, InterceptableRequest>();
private _requestIdToRequestWillBeSentEvent = new Map<string, Protocol.Network.requestWillBeSentPayload>();
private _credentials: {origin?: string, username: string, password: string} | null = null;
private _attemptedAuthentications = new Set<string>();
private _userRequestInterceptionEnabled = false;
private _protocolRequestInterceptionEnabled = false;
private _offline = false;
private _extraHTTPHeaders: types.HeadersArray = [];
private _requestIdToRequestPausedEvent = new Map<string, Protocol.Fetch.requestPausedPayload>();
private _eventListeners: RegisteredListener[];
private _responseExtraInfoTracker = new ResponseExtraInfoTracker();
private _sessions = new Map<CRSession, SessionInfo>();
constructor(session: CRSession, page: Page | null, serviceWorker: CRServiceWorker | null, parentManager: CRNetworkManager | null) {
this._session = session;
constructor(page: Page | null, serviceWorker: CRServiceWorker | null) {
this._page = page;
this._serviceWorker = serviceWorker;
this._parentManager = parentManager;
this._eventListeners = this.instrumentNetworkEvents({ session });
}
instrumentNetworkEvents(sessionInfo: SessionInfo): RegisteredListener[] {
const listeners = [
eventsHelper.addEventListener(sessionInfo.session, 'Fetch.requestPaused', this._onRequestPaused.bind(this, sessionInfo)),
eventsHelper.addEventListener(sessionInfo.session, 'Fetch.authRequired', this._onAuthRequired.bind(this)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this, sessionInfo)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.requestWillBeSentExtraInfo', this._onRequestWillBeSentExtraInfo.bind(this)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.requestServedFromCache', this._onRequestServedFromCache.bind(this)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.responseReceived', this._onResponseReceived.bind(this, sessionInfo)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.responseReceivedExtraInfo', this._onResponseReceivedExtraInfo.bind(this)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.loadingFinished', this._onLoadingFinished.bind(this)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.loadingFailed', this._onLoadingFailed.bind(this, sessionInfo)),
async addSession(session: CRSession, workerFrame?: frames.Frame, isMain?: boolean) {
const sessionInfo: SessionInfo = { session, isMain, workerFrame, eventListeners: [] };
sessionInfo.eventListeners = [
eventsHelper.addEventListener(session, 'Fetch.requestPaused', this._onRequestPaused.bind(this, sessionInfo)),
eventsHelper.addEventListener(session, 'Fetch.authRequired', this._onAuthRequired.bind(this, sessionInfo)),
eventsHelper.addEventListener(session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this, sessionInfo)),
eventsHelper.addEventListener(session, 'Network.requestWillBeSentExtraInfo', this._onRequestWillBeSentExtraInfo.bind(this)),
eventsHelper.addEventListener(session, 'Network.requestServedFromCache', this._onRequestServedFromCache.bind(this)),
eventsHelper.addEventListener(session, 'Network.responseReceived', this._onResponseReceived.bind(this, sessionInfo)),
eventsHelper.addEventListener(session, 'Network.responseReceivedExtraInfo', this._onResponseReceivedExtraInfo.bind(this)),
eventsHelper.addEventListener(session, 'Network.loadingFinished', this._onLoadingFinished.bind(this, sessionInfo)),
eventsHelper.addEventListener(session, 'Network.loadingFailed', this._onLoadingFailed.bind(this, sessionInfo)),
];
if (this._page) {
listeners.push(...[
eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketCreated', e => this._page!._frameManager.onWebSocketCreated(e.requestId, e.url)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketWillSendHandshakeRequest', e => this._page!._frameManager.onWebSocketRequest(e.requestId)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketHandshakeResponseReceived', e => this._page!._frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page!._frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page!._frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketClosed', e => this._page!._frameManager.webSocketClosed(e.requestId)),
eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketFrameError', e => this._page!._frameManager.webSocketError(e.requestId, e.errorMessage)),
sessionInfo.eventListeners.push(...[
eventsHelper.addEventListener(session, 'Network.webSocketCreated', e => this._page!._frameManager.onWebSocketCreated(e.requestId, e.url)),
eventsHelper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', e => this._page!._frameManager.onWebSocketRequest(e.requestId)),
eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page!._frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)),
eventsHelper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page!._frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)),
eventsHelper.addEventListener(session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page!._frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)),
eventsHelper.addEventListener(session, 'Network.webSocketClosed', e => this._page!._frameManager.webSocketClosed(e.requestId)),
eventsHelper.addEventListener(session, 'Network.webSocketFrameError', e => this._page!._frameManager.webSocketError(e.requestId, e.errorMessage)),
]);
}
return listeners;
this._sessions.set(session, sessionInfo);
await Promise.all([
session.send('Network.enable'),
this._updateProtocolRequestInterceptionForSession(sessionInfo, true /* initial */),
this._setOfflineForSession(sessionInfo, true /* initial */),
this._setExtraHTTPHeadersForSession(sessionInfo, true /* initial */),
]);
}
async initialize() {
await this._session.send('Network.enable');
removeSession(session: CRSession) {
const info = this._sessions.get(session);
if (info)
eventsHelper.removeEventListeners(info.eventListeners);
this._sessions.delete(session);
}
dispose() {
eventsHelper.removeEventListeners(this._eventListeners);
private async _forEachSession(cb: (sessionInfo: SessionInfo) => Promise<any>) {
await Promise.all([...this._sessions.values()].map(info => {
if (info.isMain)
return cb(info);
return cb(info).catch(e => {
// Broadcasting a message to the closed target should be a noop.
if (isSessionClosedError(e))
return;
throw e;
});
}));
}
async authenticate(credentials: types.Credentials | null) {
@ -98,8 +116,20 @@ export class CRNetworkManager {
}
async setOffline(offline: boolean) {
await this._session.send('Network.emulateNetworkConditions', {
offline,
if (offline === this._offline)
return;
this._offline = offline;
await this._forEachSession(info => this._setOfflineForSession(info));
}
private async _setOfflineForSession(info: SessionInfo, initial?: boolean) {
if (initial && !this._offline)
return;
// Workers are affected by the owner frame's Network.emulateNetworkConditions.
if (info.workerFrame)
return;
await info.session.send('Network.emulateNetworkConditions', {
offline: this._offline,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
@ -117,28 +147,46 @@ export class CRNetworkManager {
if (enabled === this._protocolRequestInterceptionEnabled)
return;
this._protocolRequestInterceptionEnabled = enabled;
if (enabled) {
await Promise.all([
this._session.send('Network.setCacheDisabled', { cacheDisabled: true }),
this._session.send('Fetch.enable', {
handleAuthRequests: true,
patterns: [{ urlPattern: '*', requestStage: 'Request' }],
}),
]);
} else {
await Promise.all([
this._session.send('Network.setCacheDisabled', { cacheDisabled: false }),
this._session.send('Fetch.disable')
]);
await this._forEachSession(info => this._updateProtocolRequestInterceptionForSession(info));
}
private async _updateProtocolRequestInterceptionForSession(info: SessionInfo, initial?: boolean) {
const enabled = this._protocolRequestInterceptionEnabled;
if (initial && !enabled)
return;
const cachePromise = info.session.send('Network.setCacheDisabled', { cacheDisabled: enabled });
let fetchPromise = Promise.resolve<any>(undefined);
if (!info.workerFrame) {
if (enabled)
fetchPromise = info.session.send('Fetch.enable', { handleAuthRequests: true, patterns: [{ urlPattern: '*', requestStage: 'Request' }] });
else
fetchPromise = info.session.send('Fetch.disable');
}
await Promise.all([cachePromise, fetchPromise]);
}
async setExtraHTTPHeaders(extraHTTPHeaders: types.HeadersArray) {
if (!this._extraHTTPHeaders.length && !extraHTTPHeaders.length)
return;
this._extraHTTPHeaders = extraHTTPHeaders;
await this._forEachSession(info => this._setExtraHTTPHeadersForSession(info));
}
private async _setExtraHTTPHeadersForSession(info: SessionInfo, initial?: boolean) {
if (initial && !this._extraHTTPHeaders.length)
return;
await info.session.send('Network.setExtraHTTPHeaders', { headers: headersArrayToObject(this._extraHTTPHeaders, false /* lowerCase */) });
}
async clearCache() {
// Sending 'Network.setCacheDisabled' with 'cacheDisabled = true' will clear the MemoryCache.
await this._session.send('Network.setCacheDisabled', { cacheDisabled: true });
if (!this._protocolRequestInterceptionEnabled)
await this._session.send('Network.setCacheDisabled', { cacheDisabled: false });
await this._session.send('Network.clearBrowserCache');
await this._forEachSession(async info => {
// Sending 'Network.setCacheDisabled' with 'cacheDisabled = true' will clear the MemoryCache.
await info.session.send('Network.setCacheDisabled', { cacheDisabled: true });
if (!this._protocolRequestInterceptionEnabled)
await info.session.send('Network.setCacheDisabled', { cacheDisabled: false });
if (!info.workerFrame)
await info.session.send('Network.clearBrowserCache');
});
}
_onRequestWillBeSent(sessionInfo: SessionInfo, event: Protocol.Network.requestWillBeSentPayload) {
@ -165,7 +213,7 @@ export class CRNetworkManager {
this._responseExtraInfoTracker.requestWillBeSentExtraInfo(event);
}
_onAuthRequired(event: Protocol.Fetch.authRequiredPayload) {
_onAuthRequired(sessionInfo: SessionInfo, event: Protocol.Fetch.authRequiredPayload) {
let response: 'Default' | 'CancelAuth' | 'ProvideCredentials' = 'Default';
const shouldProvideCredentials = this._shouldProvideCredentials(event.request.url);
if (this._attemptedAuthentications.has(event.requestId)) {
@ -175,7 +223,7 @@ export class CRNetworkManager {
this._attemptedAuthentications.add(event.requestId);
}
const { username, password } = shouldProvideCredentials && this._credentials ? this._credentials : { username: undefined, password: undefined };
this._session._sendMayFail('Fetch.continueWithAuth', {
sessionInfo.session._sendMayFail('Fetch.continueWithAuth', {
requestId: event.requestId,
authChallengeResponse: { response, username, password },
});
@ -191,7 +239,7 @@ export class CRNetworkManager {
if (!event.networkId) {
// Fetch without networkId means that request was not recognized by inspector, and
// it will never receive Network.requestWillBeSent. Continue the request to not affect it.
this._session._sendMayFail('Fetch.continueRequest', { requestId: event.requestId });
sessionInfo.session._sendMayFail('Fetch.continueRequest', { requestId: event.requestId });
return;
}
if (event.request.url.startsWith('data:'))
@ -215,7 +263,7 @@ export class CRNetworkManager {
//
// Note: make sure not to prematurely continue the redirect, which shares the
// `networkId` between the original request and the redirect.
this._session._sendMayFail('Fetch.continueRequest', {
sessionInfo.session._sendMayFail('Fetch.continueRequest', {
...alreadyContinuedParams,
requestId: event.requestId,
});
@ -266,7 +314,7 @@ export class CRNetworkManager {
];
if (requestHeaders['Access-Control-Request-Headers'])
responseHeaders.push({ name: 'Access-Control-Allow-Headers', value: requestHeaders['Access-Control-Request-Headers'] });
this._session._sendMayFail('Fetch.fulfillRequest', {
sessionInfo.session._sendMayFail('Fetch.fulfillRequest', {
requestId: requestPausedEvent.requestId,
responseCode: 204,
responsePhrase: network.STATUS_TEXTS['204'],
@ -279,7 +327,7 @@ export class CRNetworkManager {
// Non-service-worker requests MUST have a frame—if they don't, we pretend there was no request
if (!frame && !this._serviceWorker) {
if (requestPausedEvent)
this._session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId });
sessionInfo.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId });
return;
}
@ -289,9 +337,9 @@ export class CRNetworkManager {
if (redirectedFrom || (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled)) {
// Chromium does not preserve header overrides between redirects, so we have to do it ourselves.
const headers = redirectedFrom?._originalRequestRoute?._alreadyContinuedParams?.headers;
this._session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId, headers });
sessionInfo.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId, headers });
} else {
route = new RouteImpl(this._session, requestPausedEvent.requestId);
route = new RouteImpl(sessionInfo.session, requestPausedEvent.requestId);
}
}
const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document';
@ -426,16 +474,15 @@ export class CRNetworkManager {
(this._page?._frameManager || this._serviceWorker)!.requestReceivedResponse(response);
}
_onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) {
_onLoadingFinished(sessionInfo: SessionInfo, event: Protocol.Network.loadingFinishedPayload) {
this._responseExtraInfoTracker.loadingFinished(event);
let request = this._requestIdToRequest.get(event.requestId);
if (!request)
request = this._maybeAdoptMainRequest(event.requestId);
const request = this._requestIdToRequest.get(event.requestId);
// For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469
if (!request)
return;
this._maybeUpdateOOPIFMainRequest(sessionInfo, request);
// Under certain conditions we never get the Network.responseReceived
// event from protocol. @see https://crbug.com/883475
@ -453,8 +500,6 @@ export class CRNetworkManager {
this._responseExtraInfoTracker.loadingFailed(event);
let request = this._requestIdToRequest.get(event.requestId);
if (!request)
request = this._maybeAdoptMainRequest(event.requestId);
if (!request) {
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(event.requestId);
@ -472,33 +517,27 @@ export class CRNetworkManager {
// @see https://crbug.com/750469
if (!request)
return;
this._maybeUpdateOOPIFMainRequest(sessionInfo, request);
const response = request.request._existingResponse();
if (response) {
response.setTransferSize(null);
response.setEncodedBodySize(null);
response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp));
} else {
// Loading failed before response has arrived - there will be no extra info events.
request.request.setRawRequestHeaders(null);
}
this._deleteRequest(request);
request.request._setFailureText(event.errorText);
request.request._setFailureText(event.errorText || event.blockedReason || '');
(this._page?._frameManager || this._serviceWorker)!.requestFailed(request.request, !!event.canceled);
}
private _maybeAdoptMainRequest(requestId: Protocol.Network.RequestId): InterceptableRequest | undefined {
private _maybeUpdateOOPIFMainRequest(sessionInfo: SessionInfo, request: InterceptableRequest) {
// OOPIF has a main request that starts in the parent session but finishes in the child session.
if (!this._parentManager)
return;
const request = this._parentManager._requestIdToRequest.get(requestId);
// Main requests have matching loaderId and requestId.
if (!request || request._documentId !== requestId)
return;
this._requestIdToRequest.set(requestId, request);
request.session = this._session;
this._parentManager._requestIdToRequest.delete(requestId);
if (request._interceptionId && this._parentManager._attemptedAuthentications.has(request._interceptionId)) {
this._parentManager._attemptedAuthentications.delete(request._interceptionId);
this._attemptedAuthentications.add(request._interceptionId);
}
return request;
// We check for the main request by matching loaderId and requestId, and if it now belongs to
// a child session, migrate it there.
if (request.session !== sessionInfo.session && !sessionInfo.isMain && request._documentId === request._requestId)
request.session = sessionInfo.session;
}
}

View file

@ -20,7 +20,7 @@ import type { RegisteredListener } from '../../utils/eventsHelper';
import { eventsHelper } from '../../utils/eventsHelper';
import { registry } from '../registry';
import { rewriteErrorMessage } from '../../utils/stackTrace';
import { assert, createGuid, headersArrayToObject } from '../../utils';
import { assert, createGuid } from '../../utils';
import * as dialog from '../dialog';
import * as dom from '../dom';
import * as frames from '../frames';
@ -61,6 +61,7 @@ export class CRPage implements PageDelegate {
readonly rawTouchscreen: RawTouchscreenImpl;
readonly _targetId: string;
readonly _opener: CRPage | null;
readonly _networkManager: CRNetworkManager;
private readonly _pdf: CRPDF;
private readonly _coverage: CRCoverage;
readonly _browserContext: CRBrowserContext;
@ -92,6 +93,13 @@ export class CRPage implements PageDelegate {
this._coverage = new CRCoverage(client);
this._browserContext = browserContext;
this._page = new Page(this, browserContext);
this._networkManager = new CRNetworkManager(this._page, null);
// Sync any browser context state to the network manager. This does not talk over CDP because
// we have not connected any sessions to the network manager yet.
this.updateOffline();
this.updateExtraHTTPHeaders();
this.updateHttpCredentials();
this.updateRequestInterception();
this._mainFrameSession = new FrameSession(this, client, targetId, null);
this._sessions.set(targetId, this._mainFrameSession);
if (opener && !browserContext._options.noDefaultViewport) {
@ -184,7 +192,11 @@ export class CRPage implements PageDelegate {
}
async updateExtraHTTPHeaders(): Promise<void> {
await this._forAllFrameSessions(frame => frame._updateExtraHTTPHeaders(false));
const headers = network.mergeHeaders([
this._browserContext._options.extraHTTPHeaders,
this._page.extraHTTPHeaders()
]);
await this._networkManager.setExtraHTTPHeaders(headers);
}
async updateGeolocation(): Promise<void> {
@ -192,11 +204,11 @@ export class CRPage implements PageDelegate {
}
async updateOffline(): Promise<void> {
await this._forAllFrameSessions(frame => frame._updateOffline(false));
await this._networkManager.setOffline(!!this._browserContext._options.offline);
}
async updateHttpCredentials(): Promise<void> {
await this._forAllFrameSessions(frame => frame._updateHttpCredentials(false));
await this._networkManager.authenticate(this._browserContext._options.httpCredentials || null);
}
async updateEmulatedViewportSize(preserveWindowBoundaries?: boolean): Promise<void> {
@ -216,7 +228,7 @@ export class CRPage implements PageDelegate {
}
async updateRequestInterception(): Promise<void> {
await this._forAllFrameSessions(frame => frame._updateRequestInterception());
await this._networkManager.setRequestInterception(this._page.needsRequestInterception());
}
async updateFileChooserInterception() {
@ -333,7 +345,7 @@ export class CRPage implements PageDelegate {
injected.setInputFiles(node, files), files);
}
async setInputFilePaths(progress: Progress, handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void> {
async setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void> {
const frame = await handle.ownerFrame();
if (!frame)
throw new Error('Cannot set input files to detached input element');
@ -392,7 +404,6 @@ class FrameSession {
readonly _client: CRSession;
readonly _crPage: CRPage;
readonly _page: Page;
readonly _networkManager: CRNetworkManager;
private readonly _parentSession: FrameSession | null;
private readonly _childSessions = new Set<FrameSession>();
private readonly _contextIdToContext = new Map<number, dom.FrameExecutionContext>();
@ -418,7 +429,6 @@ class FrameSession {
this._crPage = crPage;
this._page = crPage._page;
this._targetId = targetId;
this._networkManager = new CRNetworkManager(client, this._page, null, parentSession ? parentSession._networkManager : null);
this._parentSession = parentSession;
if (parentSession)
parentSession._childSessions.add(this);
@ -533,7 +543,7 @@ class FrameSession {
source: '',
worldName: UTILITY_WORLD_NAME,
}),
this._networkManager.initialize(),
this._crPage._networkManager.addSession(this._client, undefined, this._isMainFrame()),
this._client.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }),
];
if (!isSettingStorageState) {
@ -559,10 +569,6 @@ class FrameSession {
if (!this._crPage._browserContext._browser.options.headful)
promises.push(this._setDefaultFontFamilies(this._client));
promises.push(this._updateGeolocation(true));
promises.push(this._updateExtraHTTPHeaders(true));
promises.push(this._updateRequestInterception());
promises.push(this._updateOffline(true));
promises.push(this._updateHttpCredentials(true));
promises.push(this._updateEmulateMedia());
promises.push(this._updateFileChooserInterception(true));
for (const binding of this._crPage._page.allBindings())
@ -586,7 +592,7 @@ class FrameSession {
if (this._parentSession)
this._parentSession._childSessions.delete(this);
eventsHelper.removeEventListeners(this._eventListeners);
this._networkManager.dispose();
this._crPage._networkManager.removeSession(this._client);
this._crPage._sessions.delete(this._targetId);
this._client.dispose();
}
@ -752,7 +758,8 @@ class FrameSession {
});
// This might fail if the target is closed before we initialize.
session._sendMayFail('Runtime.enable');
session._sendMayFail('Network.enable');
// TODO: attribute workers to the right frame.
this._crPage._networkManager.addSession(session, this._page._frameManager.frame(this._targetId) ?? undefined).catch(() => {});
session._sendMayFail('Runtime.runIfWaitingForDebugger');
session._sendMayFail('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true });
session.on('Target.attachedToTarget', event => this._onAttachedToTarget(event));
@ -762,8 +769,6 @@ class FrameSession {
this._page._addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace));
});
session.on('Runtime.exceptionThrown', exception => this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, exceptionToError(exception.exceptionDetails), this._page));
// TODO: attribute workers to the right frame.
this._networkManager.instrumentNetworkEvents({ session, workerFrame: this._page._frameManager.frame(this._targetId) ?? undefined });
}
_onDetachedFromTarget(event: Protocol.Target.detachedFromTargetPayload) {
@ -981,33 +986,12 @@ class FrameSession {
await this._client._sendMayFail('Page.stopScreencast');
}
async _updateExtraHTTPHeaders(initial: boolean): Promise<void> {
const headers = network.mergeHeaders([
this._crPage._browserContext._options.extraHTTPHeaders,
this._page.extraHTTPHeaders()
]);
if (!initial || headers.length)
await this._client.send('Network.setExtraHTTPHeaders', { headers: headersArrayToObject(headers, false /* lowerCase */) });
}
async _updateGeolocation(initial: boolean): Promise<void> {
const geolocation = this._crPage._browserContext._options.geolocation;
if (!initial || geolocation)
await this._client.send('Emulation.setGeolocationOverride', geolocation || {});
}
async _updateOffline(initial: boolean): Promise<void> {
const offline = !!this._crPage._browserContext._options.offline;
if (!initial || offline)
await this._networkManager.setOffline(offline);
}
async _updateHttpCredentials(initial: boolean): Promise<void> {
const credentials = this._crPage._browserContext._options.httpCredentials || null;
if (!initial || credentials)
await this._networkManager.authenticate(credentials);
}
async _updateViewport(preserveWindowBoundaries?: boolean): Promise<void> {
if (this._crPage._browserContext._browser.isClank())
return;
@ -1106,10 +1090,6 @@ class FrameSession {
await session.send('Page.setFontFamilies', fontFamilies);
}
async _updateRequestInterception(): Promise<void> {
await this._networkManager.setRequestInterception(this._page.needsRequestInterception());
}
async _updateFileChooserInterception(initial: boolean) {
const enabled = this._page.fileChooserIntercepted();
if (initial && !enabled)

View file

@ -34,13 +34,13 @@ export class CRServiceWorker extends Worker {
this._session = session;
this._browserContext = browserContext;
if (!!process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS)
this._networkManager = new CRNetworkManager(session, null, this, null);
this._networkManager = new CRNetworkManager(null, this);
session.once('Runtime.executionContextCreated', event => {
this._createExecutionContext(new CRExecutionContext(session, event.context));
});
if (this._networkManager && this._isNetworkInspectionEnabled()) {
this._networkManager.initialize().catch(() => {});
this._networkManager.addSession(session, undefined, true /* isMain */).catch(() => {});
this.updateRequestInterception();
this.updateExtraHTTPHeaders(true);
this.updateHttpCredentials(true);
@ -56,6 +56,7 @@ export class CRServiceWorker extends Worker {
}
override didClose() {
this._networkManager?.removeSession(this._session);
this._session.dispose();
super.didClose();
}

View file

@ -831,7 +831,7 @@ CORS RFC1918 enforcement.
resourceIPAddressSpace?: Network.IPAddressSpace;
clientSecurityState?: Network.ClientSecurityState;
}
export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation";
export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation"|"InvalidInfoHeader"|"NoRegisterSourceHeader"|"NoRegisterTriggerHeader"|"NoRegisterOsSourceHeader"|"NoRegisterOsTriggerHeader";
/**
* Details for issues around "Attribution Reporting API" usage.
Explainer: https://github.com/WICG/attribution-reporting-api
@ -2493,6 +2493,28 @@ stylesheet rules) this rule came from.
*/
tryRules: CSSTryRule[];
}
/**
* CSS @position-try rule representation.
*/
export interface CSSPositionTryRule {
/**
* The prelude dashed-ident name
*/
name: Value;
/**
* The css style sheet identifier (absent for user agent stylesheet and user-specified
stylesheet rules) this rule came from.
*/
styleSheetId?: StyleSheetId;
/**
* Parent stylesheet's origin.
*/
origin: StyleSheetOrigin;
/**
* Associated style declaration.
*/
style: CSSStyle;
}
/**
* CSS keyframes rule representation.
*/
@ -2820,6 +2842,10 @@ attributes) for a DOM node identified by `nodeId`.
* 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-options property.
*/
cssPositionTryRules?: CSSPositionTryRule[];
/**
* A list of CSS at-property rules matching this node.
*/
@ -2882,6 +2908,17 @@ the full layer tree for the tree scope and their ordering.
export type getLayersForNodeReturnValue = {
rootLayer: CSSLayerData;
}
/**
* Given a CSS selector text and a style sheet ID, getLocationForSelector
returns an array of locations of the CSS selector in the style sheet.
*/
export type getLocationForSelectorParameters = {
styleSheetId: StyleSheetId;
selectorText: string;
}
export type getLocationForSelectorReturnValue = {
ranges: SourceRange[];
}
/**
* Starts tracking the given computed styles for updates. The specified array of properties
replaces the one previously specified. Pass empty array to disable tracking.
@ -8052,6 +8089,7 @@ milliseconds relatively to this requestTime.
headers: Headers;
/**
* HTTP POST request data.
Use postDataEntries instead.
*/
postData?: string;
/**
@ -8059,7 +8097,7 @@ milliseconds relatively to this requestTime.
*/
hasPostData?: boolean;
/**
* Request body elements. This will be converted from base64 to binary
* Request body elements (post data broken into individual entries).
*/
postDataEntries?: PostDataEntry[];
/**
@ -9787,6 +9825,18 @@ matches provided URL.
* Connection type if known.
*/
connectionType?: ConnectionType;
/**
* WebRTC packet loss (percent, 0-100). 0 disables packet loss emulation, 100 drops all the packets.
*/
packetLoss?: number;
/**
* WebRTC packet queue length (packet). 0 removes any queue length limitations.
*/
packetQueueLength?: number;
/**
* WebRTC packetReordering feature.
*/
packetReordering?: boolean;
}
export type emulateNetworkConditionsReturnValue = {
}
@ -11065,7 +11115,7 @@ as an ad.
* All Permissions Policy features. This enum should match the one defined
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-factor"|"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"|"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"|"window-placement"|"xr-spatial-tracking";
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"|"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"|"window-placement"|"xr-spatial-tracking";
/**
* Reason for a permissions policy feature to be disabled.
*/
@ -11534,7 +11584,7 @@ Example URLs: http://www.google.com/file.html -> "google.com"
/**
* List of not restored reasons for back-forward cache.
*/
export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"Portal"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame";
export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"Portal"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame";
/**
* Types of not restored reasons for back-forward cache.
*/
@ -11811,7 +11861,7 @@ open.
*/
type: DialogType;
/**
* True if browser is capable showing or acting on the given dialog. When browser has no
* True iff browser is capable showing or acting on the given dialog. When browser has no
dialog handler for given target, calling alert while Page domain is engaged will stall
the page execution. Execution can be resumed via calling Page.handleJavaScriptDialog.
*/
@ -13533,34 +13583,10 @@ Tokens from that issuer.
* Enum of network fetches auctions can do.
*/
export type InterestGroupAuctionFetchType = "bidderJs"|"bidderWasm"|"sellerJs"|"bidderTrustedSignals"|"sellerTrustedSignals";
/**
* Ad advertising element inside an interest group.
*/
export interface InterestGroupAd {
renderURL: string;
metadata?: string;
}
/**
* The full details of an interest group.
*/
export interface InterestGroupDetails {
ownerOrigin: string;
name: string;
expirationTime: Network.TimeSinceEpoch;
joiningOrigin: string;
biddingLogicURL?: string;
biddingWasmHelperURL?: string;
updateURL?: string;
trustedBiddingSignalsURL?: string;
trustedBiddingSignalsKeys: string[];
userBiddingSignals?: string;
ads: InterestGroupAd[];
adComponents: InterestGroupAd[];
}
/**
* Enum of shared storage access types.
*/
export type SharedStorageAccessType = "documentAddModule"|"documentSelectURL"|"documentRun"|"documentSet"|"documentAppend"|"documentDelete"|"documentClear"|"workletSet"|"workletAppend"|"workletDelete"|"workletClear"|"workletGet"|"workletKeys"|"workletEntries"|"workletLength"|"workletRemainingBudget";
export type SharedStorageAccessType = "documentAddModule"|"documentSelectURL"|"documentRun"|"documentSet"|"documentAppend"|"documentDelete"|"documentClear"|"documentGet"|"workletSet"|"workletAppend"|"workletDelete"|"workletClear"|"workletGet"|"workletKeys"|"workletEntries"|"workletLength"|"workletRemainingBudget"|"headerSet"|"headerAppend"|"headerDelete"|"headerClear";
/**
* Struct for a single key-value pair in an origin's shared storage.
*/
@ -13644,22 +13670,28 @@ SharedStorageAccessType.documentAppend,
SharedStorageAccessType.documentDelete,
SharedStorageAccessType.workletSet,
SharedStorageAccessType.workletAppend,
SharedStorageAccessType.workletDelete, and
SharedStorageAccessType.workletGet.
SharedStorageAccessType.workletDelete,
SharedStorageAccessType.workletGet,
SharedStorageAccessType.headerSet,
SharedStorageAccessType.headerAppend, and
SharedStorageAccessType.headerDelete.
*/
key?: string;
/**
* Value for a specific entry in an origin's shared storage.
Present only for SharedStorageAccessType.documentSet,
SharedStorageAccessType.documentAppend,
SharedStorageAccessType.workletSet, and
SharedStorageAccessType.workletAppend.
SharedStorageAccessType.workletSet,
SharedStorageAccessType.workletAppend,
SharedStorageAccessType.headerSet, and
SharedStorageAccessType.headerAppend.
*/
value?: string;
/**
* Whether or not to set an entry for a key if that key is already present.
Present only for SharedStorageAccessType.documentSet and
SharedStorageAccessType.workletSet.
Present only for SharedStorageAccessType.documentSet,
SharedStorageAccessType.workletSet, and
SharedStorageAccessType.headerSet.
*/
ignoreIfPresent?: boolean;
}
@ -13789,6 +13821,23 @@ int
}
export type AttributionReportingEventLevelResult = "success"|"successDroppedLowerPriority"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"deduplicated"|"excessiveAttributions"|"priorityTooLow"|"neverAttributedSource"|"excessiveReportingOrigins"|"noMatchingSourceFilterData"|"prohibitedByBrowserPolicy"|"noMatchingConfigurations"|"excessiveReports"|"falselyAttributedSource"|"reportWindowPassed"|"notRegistered"|"reportWindowNotStarted"|"noMatchingTriggerData";
export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports";
/**
* A single Related Website Set object.
*/
export interface RelatedWebsiteSet {
/**
* The primary site of this set, along with the ccTLDs if there is any.
*/
primarySites: string[];
/**
* The associated sites of this set, along with the ccTLDs if there is any.
*/
associatedSites: string[];
/**
* The service sites of this set, along with the ccTLDs if there is any.
*/
serviceSites: string[];
}
/**
* A cache's contents have been modified.
@ -14216,7 +14265,13 @@ Leaves other stored data, including the issuer's Redemption Records, intact.
name: string;
}
export type getInterestGroupDetailsReturnValue = {
details: InterestGroupDetails;
/**
* This largely corresponds to:
https://wicg.github.io/turtledove/#dictdef-generatebidinterestgroup
but has absolute expirationTime instead of relative lifetimeMs and
also adds joiningOrigin.
*/
details: { [key: string]: string };
}
/**
* Enables/Disables issuing of interestGroupAccessed events.
@ -14345,6 +14400,15 @@ interestGroupAuctionNetworkRequestCreated.
}
export type setAttributionReportingTrackingReturnValue = {
}
/**
* Returns the effective Related Website Sets in use by this profile for the browser
session. The effective Related Website Sets will not change during a browser session.
*/
export type getRelatedWebsiteSetsParameters = {
}
export type getRelatedWebsiteSetsReturnValue = {
sets: RelatedWebsiteSet[];
}
}
/**
@ -14582,10 +14646,10 @@ supported.
export type SessionID = string;
export interface TargetInfo {
targetId: TargetID;
type: string;
/**
* List of types: https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/devtools_agent_host_impl.cc?ss=chromium&q=f:devtools%20-f:out%20%22::kTypeTab%5B%5D%22
*/
type: string;
title: string;
url: string;
/**
@ -16385,7 +16449,7 @@ See also:
requestId?: Network.RequestId;
/**
* Error information
`errorMessage` is null if `errorType` is null.
`errorMessage` is null iff `errorType` is null.
*/
errorType?: RuleSetErrorType;
/**
@ -19515,6 +19579,7 @@ Error was thrown.
"CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeParameters;
"CSS.getStyleSheetText": CSS.getStyleSheetTextParameters;
"CSS.getLayersForNode": CSS.getLayersForNodeParameters;
"CSS.getLocationForSelector": CSS.getLocationForSelectorParameters;
"CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesParameters;
"CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesParameters;
"CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeParameters;
@ -19885,6 +19950,7 @@ Error was thrown.
"Storage.runBounceTrackingMitigations": Storage.runBounceTrackingMitigationsParameters;
"Storage.setAttributionReportingLocalTestingMode": Storage.setAttributionReportingLocalTestingModeParameters;
"Storage.setAttributionReportingTracking": Storage.setAttributionReportingTrackingParameters;
"Storage.getRelatedWebsiteSets": Storage.getRelatedWebsiteSetsParameters;
"SystemInfo.getInfo": SystemInfo.getInfoParameters;
"SystemInfo.getFeatureState": SystemInfo.getFeatureStateParameters;
"SystemInfo.getProcessInfo": SystemInfo.getProcessInfoParameters;
@ -20097,6 +20163,7 @@ Error was thrown.
"CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeReturnValue;
"CSS.getStyleSheetText": CSS.getStyleSheetTextReturnValue;
"CSS.getLayersForNode": CSS.getLayersForNodeReturnValue;
"CSS.getLocationForSelector": CSS.getLocationForSelectorReturnValue;
"CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesReturnValue;
"CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesReturnValue;
"CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeReturnValue;
@ -20467,6 +20534,7 @@ Error was thrown.
"Storage.runBounceTrackingMitigations": Storage.runBounceTrackingMitigationsReturnValue;
"Storage.setAttributionReportingLocalTestingMode": Storage.setAttributionReportingLocalTestingModeReturnValue;
"Storage.setAttributionReportingTracking": Storage.setAttributionReportingTrackingReturnValue;
"Storage.getRelatedWebsiteSets": Storage.getRelatedWebsiteSetsReturnValue;
"SystemInfo.getInfo": SystemInfo.getInfoReturnValue;
"SystemInfo.getFeatureState": SystemInfo.getFeatureStateReturnValue;
"SystemInfo.getProcessInfo": SystemInfo.getProcessInfoReturnValue;

View file

@ -110,7 +110,7 @@
"defaultBrowserType": "webkit"
},
"Galaxy S5": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@ -121,7 +121,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@ -132,7 +132,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 740
@ -143,7 +143,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 740,
"height": 360
@ -154,7 +154,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 320,
"height": 658
@ -165,7 +165,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+ landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 658,
"height": 320
@ -176,7 +176,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36",
"viewport": {
"width": 712,
"height": 1138
@ -187,7 +187,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36",
"viewport": {
"width": 1138,
"height": 712
@ -978,7 +978,7 @@
"defaultBrowserType": "webkit"
},
"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/123.0.6312.4 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/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 384,
"height": 640
@ -989,7 +989,7 @@
"defaultBrowserType": "chromium"
},
"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/123.0.6312.4 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/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 384
@ -1000,7 +1000,7 @@
"defaultBrowserType": "chromium"
},
"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/123.0.6312.4 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/124.0.6367.8 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 640,
"height": 360
@ -1011,7 +1011,7 @@
"defaultBrowserType": "chromium"
},
"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/123.0.6312.4 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/124.0.6367.8 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 360,
"height": 640
@ -1022,7 +1022,7 @@
"defaultBrowserType": "chromium"
},
"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/123.0.6312.4 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/124.0.6367.8 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 360,
"height": 640
@ -1033,7 +1033,7 @@
"defaultBrowserType": "chromium"
},
"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/123.0.6312.4 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/124.0.6367.8 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 640,
"height": 360
@ -1044,7 +1044,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36",
"viewport": {
"width": 800,
"height": 1280
@ -1055,7 +1055,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36",
"viewport": {
"width": 1280,
"height": 800
@ -1066,7 +1066,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 384,
"height": 640
@ -1077,7 +1077,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 384
@ -1088,7 +1088,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@ -1099,7 +1099,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@ -1110,7 +1110,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 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/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@ -1121,7 +1121,7 @@
"defaultBrowserType": "chromium"
},
"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/123.0.6312.4 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/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@ -1132,7 +1132,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@ -1143,7 +1143,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@ -1154,7 +1154,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 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/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@ -1165,7 +1165,7 @@
"defaultBrowserType": "chromium"
},
"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/123.0.6312.4 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/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@ -1176,7 +1176,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36",
"viewport": {
"width": 600,
"height": 960
@ -1187,7 +1187,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36",
"viewport": {
"width": 960,
"height": 600
@ -1242,7 +1242,7 @@
"defaultBrowserType": "webkit"
},
"Pixel 2": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 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/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 411,
"height": 731
@ -1253,7 +1253,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 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/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 731,
"height": 411
@ -1264,7 +1264,7 @@
"defaultBrowserType": "chromium"
},
"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/123.0.6312.4 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/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 411,
"height": 823
@ -1275,7 +1275,7 @@
"defaultBrowserType": "chromium"
},
"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/123.0.6312.4 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/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 823,
"height": 411
@ -1286,7 +1286,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 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/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 393,
"height": 786
@ -1297,7 +1297,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 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/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 786,
"height": 393
@ -1308,7 +1308,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 353,
"height": 745
@ -1319,7 +1319,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 745,
"height": 353
@ -1330,7 +1330,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G)": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"screen": {
"width": 412,
"height": 892
@ -1345,7 +1345,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G) landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"screen": {
"height": 892,
"width": 412
@ -1360,7 +1360,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"screen": {
"width": 393,
"height": 851
@ -1375,7 +1375,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"screen": {
"width": 851,
"height": 393
@ -1390,7 +1390,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"screen": {
"width": 412,
"height": 915
@ -1405,7 +1405,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"screen": {
"width": 915,
"height": 412
@ -1420,7 +1420,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@ -1431,7 +1431,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@ -1442,7 +1442,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Chrome HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36",
"screen": {
"width": 1792,
"height": 1120
@ -1457,7 +1457,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36 Edg/123.0.6312.4",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36 Edg/124.0.6367.8",
"screen": {
"width": 1792,
"height": 1120
@ -1502,7 +1502,7 @@
"defaultBrowserType": "webkit"
},
"Desktop Chrome": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36",
"screen": {
"width": 1920,
"height": 1080
@ -1517,7 +1517,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36 Edg/123.0.6312.4",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36 Edg/124.0.6367.8",
"screen": {
"width": 1920,
"height": 1080

View file

@ -224,6 +224,10 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
await this._context.clearCookies();
}
async removeCookies(params: channels.BrowserContextRemoveCookiesParams): Promise<void> {
await this._context.removeCookies(params.filter);
}
async grantPermissions(params: channels.BrowserContextGrantPermissionsParams): Promise<void> {
await this._context.grantPermissions(params.permissions, params.origin);
}

View file

@ -24,7 +24,6 @@ import { SdkObject } from '../instrumentation';
import type { PlaywrightDispatcher } from './playwrightDispatcher';
import { eventsHelper } from '../..//utils/eventsHelper';
import type { RegisteredListener } from '../..//utils/eventsHelper';
import type * as trace from '@trace/trace';
import { isProtocolError } from '../protocolError';
export const dispatcherSymbol = Symbol('dispatcher');
@ -81,7 +80,7 @@ export class Dispatcher<Type extends { guid: string }, ChannelType, ParentScopeT
}
if (this._parent)
this._connection.sendCreate(this._parent, type, guid, initializer, this._parent._object);
this._connection.sendCreate(this._parent, type, guid, initializer);
this._connection.maybeDisposeStaleDispatchers(this._gcBucket);
}
@ -121,8 +120,7 @@ export class Dispatcher<Type extends { guid: string }, ChannelType, ParentScopeT
// Just ignore this event outside of tests.
return;
}
const sdkObject = this._object instanceof SdkObject ? this._object : undefined;
this._connection.sendEvent(this, method as string, params, sdkObject);
this._connection.sendEvent(this, method as string, params);
}
_dispose(reason?: 'gc') {
@ -195,39 +193,24 @@ export class DispatcherConnection {
this._isLocal = !!isLocal;
}
sendEvent(dispatcher: DispatcherScope, event: string, params: any, sdkObject?: SdkObject) {
sendEvent(dispatcher: DispatcherScope, event: string, params: any) {
const validator = findValidator(dispatcher._type, event, 'Event');
params = validator(params, '', { tChannelImpl: this._tChannelImplToWire.bind(this), binary: this._isLocal ? 'buffer' : 'toBase64' });
this._sendMessageToClient(dispatcher._guid, dispatcher._type, event, params, sdkObject);
this.onmessage({ guid: dispatcher._guid, method: event, params });
}
sendCreate(parent: DispatcherScope, type: string, guid: string, initializer: any, sdkObject?: SdkObject) {
sendCreate(parent: DispatcherScope, type: string, guid: string, initializer: any) {
const validator = findValidator(type, '', 'Initializer');
initializer = validator(initializer, '', { tChannelImpl: this._tChannelImplToWire.bind(this), binary: this._isLocal ? 'buffer' : 'toBase64' });
this._sendMessageToClient(parent._guid, type, '__create__', { type, initializer, guid }, sdkObject);
this.onmessage({ guid: parent._guid, method: '__create__', params: { type, initializer, guid } });
}
sendAdopt(parent: DispatcherScope, dispatcher: DispatcherScope) {
this._sendMessageToClient(parent._guid, dispatcher._type, '__adopt__', { guid: dispatcher._guid });
this.onmessage({ guid: parent._guid, method: '__adopt__', params: { guid: dispatcher._guid } });
}
sendDispose(dispatcher: DispatcherScope, reason?: 'gc') {
this._sendMessageToClient(dispatcher._guid, dispatcher._type, '__dispose__', { reason });
}
private _sendMessageToClient(guid: string, type: string, method: string, params: any, sdkObject?: SdkObject) {
if (sdkObject) {
const event: trace.EventTraceEvent = {
type: 'event',
class: type,
method,
params: params || {},
time: monotonicTime(),
pageId: sdkObject?.attribution?.page?.guid,
};
sdkObject.instrumentation?.onEvent(sdkObject, event);
}
this.onmessage({ guid, method, params });
this.onmessage({ guid: dispatcher._guid, method: '__dispose__', params: { reason } });
}
private _tChannelImplFromWire(names: '*' | string[], arg: any, path: string, context: ValidatorContext): any {

View file

@ -14,6 +14,7 @@
* limitations under the License.
*/
import fs from 'fs';
import type * as channels from '@protocol/channels';
import * as injectedScriptSource from '../generated/injectedScriptSource';
import { isSessionClosedError } from './protocolError';
@ -642,10 +643,14 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
await progress.beforeInputAction(this);
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
progress.throwIfAborted(); // Avoid action that has side-effects.
if (localPaths)
await this._page._delegate.setInputFilePaths(progress, retargeted, localPaths);
else
if (localPaths) {
await Promise.all(localPaths.map(localPath => (
fs.promises.access(localPath, fs.constants.F_OK)
)));
await this._page._delegate.setInputFilePaths(retargeted, localPaths);
} else {
await this._page._delegate.setInputFiles(retargeted, filePayloads!);
}
});
return 'done';
}

View file

@ -40,7 +40,7 @@ import { TargetClosedError } from '../errors';
export const UTILITY_WORLD_NAME = '__playwright_utility_world__';
export class FFPage implements PageDelegate {
readonly cspErrorsAsynchronousForInlineScipts = true;
readonly cspErrorsAsynchronousForInlineScripts = true;
readonly rawMouse: RawMouseImpl;
readonly rawKeyboard: RawKeyboardImpl;
readonly rawTouchscreen: RawTouchscreenImpl;
@ -543,16 +543,12 @@ export class FFPage implements PageDelegate {
injected.setInputFiles(node, files), files);
}
async setInputFilePaths(progress: Progress, handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void> {
await Promise.all([
this._session.send('Page.setFileInputFiles', {
frameId: handle._context.frame._id,
objectId: handle._objectId,
files
}),
handle.dispatchEvent(progress.metadata, 'input'),
handle.dispatchEvent(progress.metadata, 'change')
]);
async setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void> {
await this._session.send('Page.setFileInputFiles', {
frameId: handle._context.frame._id,
objectId: handle._objectId,
files
});
}
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {

View file

@ -914,6 +914,12 @@ export class Frame extends SdkObject {
return this._url;
}
origin(): string | undefined {
if (!this._url.startsWith('http'))
return;
return network.parsedURL(this._url)?.origin;
}
parentFrame(): Frame | null {
return this._parentFrame;
}
@ -942,7 +948,7 @@ export class Frame extends SdkObject {
const result = (await context.evaluateHandle(addScriptContent, { content: content!, type })).asElement()!;
// Another round trip to the browser to ensure that we receive CSP error messages
// (if any) logged asynchronously in a separate task on the content main thread.
if (this._page._delegate.cspErrorsAsynchronousForInlineScipts)
if (this._page._delegate.cspErrorsAsynchronousForInlineScripts)
await context.evaluate(() => true);
return result;
});
@ -1686,6 +1692,17 @@ export class Frame extends SdkObject {
if (db.name)
indexedDB.deleteDatabase(db.name!);
}
// Clean StorageManager
const root = await navigator.storage.getDirectory();
const entries = await (root as any).entries();
// Manual loop instead of for await because in Firefox's utility context instanceof AsyncIterable is not working.
let entry = await entries.next();
while (!entry.done) {
const [name] = entry.value;
await root.removeEntry(name, { recursive: true });
entry = await entries.next();
}
}, { ls: newStorage?.localStorage }).catch(() => {});
}

View file

@ -29,6 +29,6 @@ export { createPlaywright } from './playwright';
export type { DispatcherScope } from './dispatchers/dispatcher';
export type { Playwright } from './playwright';
export { openTraceInBrowser, openTraceViewerApp } from './trace/viewer/traceViewer';
export { openTraceInBrowser, openTraceViewerApp, runTraceViewerApp, startTraceViewerServer, installRootRedirect } from './trace/viewer/traceViewer';
export { serverSideCallMetadata } from './instrumentation';
export { SocksProxy } from '../common/socksProxy';

View file

@ -231,6 +231,12 @@ x-pw-tool-item.value > x-div {
mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path fill-rule='evenodd' clip-rule='evenodd' d='M4 6h8v1H4V6zm8 3H4v1h8V9z'/><path fill-rule='evenodd' clip-rule='evenodd' d='M1 4l1-1h12l1 1v8l-1 1H2l-1-1V4zm1 0v8h12V4H2z'/></svg>");
}
x-pw-tool-item.screenshot > x-div {
/* codicon: device-camera */
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path fill-rule='evenodd' clip-rule='evenodd' d='M10.707 3H14.5l.5.5v9l-.5.5h-13l-.5-.5v-9l.5-.5h3.793l.853-.854L6.5 2h3l.354.146.853.854zM2 12h12V4h-3.5l-.354-.146L9.293 3H6.707l-.853.854L5.5 4H2v8zm1.5-7a.5.5 0 1 0 0 1 .5.5 0 0 0 0-1zM8 6a2 2 0 1 1 0 4 2 2 0 0 1 0-4zm0-1a3 3 0 1 0 0 6 3 3 0 0 0 0-6z'/></svg>");
mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path fill-rule='evenodd' clip-rule='evenodd' d='M10.707 3H14.5l.5.5v9l-.5.5h-13l-.5-.5v-9l.5-.5h3.793l.853-.854L6.5 2h3l.354.146.853.854zM2 12h12V4h-3.5l-.354-.146L9.293 3H6.707l-.853.854L5.5 4H2v8zm1.5-7a.5.5 0 1 0 0 1 .5.5 0 0 0 0-1zM8 6a2 2 0 1 1 0 4 2 2 0 0 1 0-4zm0-1a3 3 0 1 0 0 6 3 3 0 0 0 0-6z'/></svg>");
}
x-pw-tool-item.accept > x-div {
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path d='M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/></svg>");
mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path d='M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/></svg>");

View file

@ -761,6 +761,7 @@ class Overlay {
private _assertVisibilityToggle: HTMLElement;
private _assertTextToggle: HTMLElement;
private _assertValuesToggle: HTMLElement;
private _assertScreenshotButton: HTMLElement;
private _offsetX = 0;
private _dragState: { offsetX: number, dragStart: { x: number, y: number } } | undefined;
private _measure: { width: number, height: number } = { width: 0, height: 0 };
@ -807,6 +808,12 @@ class Overlay {
this._assertValuesToggle.appendChild(this._recorder.injectedScript.document.createElement('x-div'));
toolsListElement.appendChild(this._assertValuesToggle);
this._assertScreenshotButton = this._recorder.injectedScript.document.createElement('x-pw-tool-item');
this._assertScreenshotButton.title = 'Assert screenshot';
this._assertScreenshotButton.classList.add('screenshot');
this._assertScreenshotButton.appendChild(this._recorder.injectedScript.document.createElement('x-div'));
toolsListElement.appendChild(this._assertScreenshotButton);
this._updateVisualPosition();
this._refreshListeners();
}
@ -845,6 +852,15 @@ class Overlay {
if (!this._assertValuesToggle.classList.contains('disabled'))
this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingValue' ? 'recording' : 'assertingValue');
}),
addEventListener(this._assertScreenshotButton, 'click', () => {
if (!this._assertScreenshotButton.classList.contains('disabled')) {
this._recorder.delegate.recordAction?.({
name: 'assertScreenshot',
signals: [],
});
this.flashToolSucceeded('assertScreenshot');
}
}),
];
}
@ -867,6 +883,7 @@ class Overlay {
this._assertTextToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting');
this._assertValuesToggle.classList.toggle('active', state.mode === 'assertingValue');
this._assertValuesToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting');
this._assertScreenshotButton.classList.toggle('disabled', state.mode !== 'recording');
if (this._offsetX !== state.overlay.offsetX) {
this._offsetX = state.overlay.offsetX;
this._updateVisualPosition();
@ -877,8 +894,12 @@ class Overlay {
this._showOverlay();
}
flashToolSucceeded(tool: 'assertingVisibility' | 'assertingValue') {
const element = tool === 'assertingVisibility' ? this._assertVisibilityToggle : this._assertValuesToggle;
flashToolSucceeded(tool: 'assertingVisibility' | 'assertingValue' | 'assertScreenshot') {
const element = {
'assertingVisibility': this._assertVisibilityToggle,
'assertingValue': this._assertValuesToggle,
'assertScreenshot': this._assertScreenshotButton,
}[tool];
element.classList.add('succeeded');
setTimeout(() => element.classList.remove('succeeded'), 2000);
}

View file

@ -16,10 +16,9 @@
import type { SelectorEngine, SelectorRoot } from './selectorEngine';
import { matchesAttributePart } from './selectorUtils';
import { beginAriaCaches, endAriaCaches, getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaLevel, getAriaPressed, getAriaSelected, getElementAccessibleName, getElementsByRole, isElementHiddenForAria, kAriaCheckedRoles, kAriaExpandedRoles, kAriaLevelRoles, kAriaPressedRoles, kAriaSelectedRoles } from './roleUtils';
import { beginAriaCaches, endAriaCaches, getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaLevel, getAriaPressed, getAriaRole, getAriaSelected, getElementAccessibleName, isElementHiddenForAria, kAriaCheckedRoles, kAriaExpandedRoles, kAriaLevelRoles, kAriaPressedRoles, kAriaSelectedRoles } from './roleUtils';
import { parseAttributeSelector, type AttributeSelectorPart, type AttributeSelectorOperator } from '../../utils/isomorphic/selectorParser';
import { normalizeWhiteSpace } from '../../utils/isomorphic/stringUtils';
import { isInsideScope } from './domUtils';
type RoleEngineOptions = {
role: string;
@ -126,27 +125,26 @@ function validateAttributes(attrs: AttributeSelectorPart[], role: string): RoleE
}
function queryRole(scope: SelectorRoot, options: RoleEngineOptions, internal: boolean): Element[] {
const doc = scope.nodeType === 9 /* Node.DOCUMENT_NODE */ ? scope as Document : scope.ownerDocument;
const elements = doc ? getElementsByRole(doc, options.role) : [];
return elements.filter(element => {
if (!isInsideScope(scope, element))
return false;
const result: Element[] = [];
const match = (element: Element) => {
if (getAriaRole(element) !== options.role)
return;
if (options.selected !== undefined && getAriaSelected(element) !== options.selected)
return false;
return;
if (options.checked !== undefined && getAriaChecked(element) !== options.checked)
return false;
return;
if (options.pressed !== undefined && getAriaPressed(element) !== options.pressed)
return false;
return;
if (options.expanded !== undefined && getAriaExpanded(element) !== options.expanded)
return false;
return;
if (options.level !== undefined && getAriaLevel(element) !== options.level)
return false;
return;
if (options.disabled !== undefined && getAriaDisabled(element) !== options.disabled)
return false;
return;
if (!options.includeHidden) {
const isHidden = isElementHiddenForAria(element);
if (isHidden)
return false;
return;
}
if (options.name !== undefined) {
// Always normalize whitespace in the accessible name.
@ -157,10 +155,25 @@ function queryRole(scope: SelectorRoot, options: RoleEngineOptions, internal: bo
if (internal && !options.exact && options.nameOp === '=')
options.nameOp = '*=';
if (!matchesAttributePart(accessibleName, { name: '', jsonPath: [], op: options.nameOp || '=', value: options.name, caseSensitive: !!options.exact }))
return false;
return;
}
return true;
});
result.push(element);
};
const query = (root: Element | ShadowRoot | Document) => {
const shadows: ShadowRoot[] = [];
if ((root as Element).shadowRoot)
shadows.push((root as Element).shadowRoot!);
for (const element of root.querySelectorAll('*')) {
match(element);
if (element.shadowRoot)
shadows.push(element.shadowRoot);
}
shadows.forEach(query);
};
query(scope);
return result;
}
export function createRoleEngine(internal: boolean): SelectorEngine {

View file

@ -845,51 +845,11 @@ function getAccessibleNameFromAssociatedLabels(labels: Iterable<HTMLLabelElement
})).filter(accessibleName => !!accessibleName).join(' ');
}
export function getElementsByRole(document: Document, role: string): Element[] {
if (document === cacheElementsByRoleDocument)
return cacheElementsByRole!.get(role) || [];
const map = calculateElementsByRoleMap(document);
if (cachesCounter) {
cacheElementsByRoleDocument = document;
cacheElementsByRole = map;
}
return map.get(role) || [];
}
function calculateElementsByRoleMap(document: Document) {
const result = new Map<string, Element[]>();
const visit = (root: Element | ShadowRoot | Document) => {
const shadows: ShadowRoot[] = [];
if ((root as Element).shadowRoot)
shadows.push((root as Element).shadowRoot!);
for (const element of root.querySelectorAll('*')) {
const role = getAriaRole(element);
if (role) {
let list = result.get(role);
if (!list) {
list = [];
result.set(role, list);
}
list.push(element);
}
if (element.shadowRoot)
shadows.push(element.shadowRoot);
}
shadows.forEach(visit);
};
visit(document);
return result;
}
let cacheAccessibleName: Map<Element, string> | undefined;
let cacheAccessibleNameHidden: Map<Element, string> | undefined;
let cacheIsHidden: Map<Element, boolean> | undefined;
let cachePseudoContentBefore: Map<Element, string> | undefined;
let cachePseudoContentAfter: Map<Element, string> | undefined;
let cacheElementsByRole: Map<string, Element[]> | undefined;
let cacheElementsByRoleDocument: Document | undefined;
let cachesCounter = 0;
export function beginAriaCaches() {
@ -908,7 +868,5 @@ export function endAriaCaches() {
cacheIsHidden = undefined;
cachePseudoContentBefore = undefined;
cachePseudoContentAfter = undefined;
cacheElementsByRole = undefined;
cacheElementsByRoleDocument = undefined;
}
}

View file

@ -86,12 +86,18 @@ function buildComponentsTreeVue3(instance: VueVNode): ComponentNode {
// @see https://github.com/vuejs/devtools/blob/e7132f3392b975e39e1d9a23cf30456c270099c2/packages/app-backend-vue3/src/components/util.ts#L29
function getInstanceName(instance: VueVNode): string {
const name = getComponentTypeName(instance.type || {});
if (name) return name;
if (instance.root === instance) return 'Root';
for (const key in instance.parent?.type?.components)
if (instance.parent?.type.components[key] === instance.type) return saveComponentName(instance, key);
for (const key in instance.appContext?.components)
if (instance.appContext.components[key] === instance.type) return saveComponentName(instance, key);
if (name)
return name;
if (instance.root === instance)
return 'Root';
for (const key in instance.parent?.type?.components) {
if (instance.parent?.type.components[key] === instance.type)
return saveComponentName(instance, key);
}
for (const key in instance.appContext?.components) {
if (instance.appContext.components[key] === instance.type)
return saveComponentName(instance, key);
}
return 'Anonymous Component';
}
@ -132,7 +138,8 @@ function buildComponentsTreeVue3(instance: VueVNode): ComponentNode {
// @see https://github.com/vuejs/devtools/blob/e7132f3392b975e39e1d9a23cf30456c270099c2/packages/app-backend-vue3/src/components/el.ts#L15
function getFragmentRootElements(vnode: any): Element[] {
if (!vnode.children) return [];
if (!vnode.children)
return [];
const list = [];

View file

@ -36,7 +36,6 @@ export type Attribution = {
import type { CallMetadata } from '@protocol/callMetadata';
export type { CallMetadata } from '@protocol/callMetadata';
import type * as trace from '@trace/trace';
export const kTestSdkObjects = new WeakSet<SdkObject>();
@ -63,7 +62,6 @@ export interface Instrumentation {
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void;
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onEvent(sdkObject: SdkObject, event: trace.EventTraceEvent): void;
onPageOpen(page: Page): void;
onPageClose(page: Page): void;
onBrowserOpen(browser: Browser): void;
@ -75,7 +73,6 @@ export interface InstrumentationListener {
onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
onCallLog?(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void;
onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onEvent?(sdkObject: SdkObject, event: trace.EventTraceEvent): void;
onPageOpen?(page: Page): void;
onPageClose?(page: Page): void;
onBrowserOpen?(browser: Browser): void;

View file

@ -19,7 +19,7 @@ import type * as dom from './dom';
import * as frames from './frames';
import * as input from './input';
import * as js from './javascript';
import * as network from './network';
import type * as network from './network';
import type * as channels from '@protocol/channels';
import type { ScreenshotOptions } from './screenshotter';
import { Screenshotter, validateScreenshotOptions } from './screenshotter';
@ -80,7 +80,7 @@ export interface PageDelegate {
getOwnerFrame(handle: dom.ElementHandle): Promise<string | null>; // Returns frameId.
getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null>;
setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void>;
setInputFilePaths(progress: Progress, handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void>;
setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void>;
getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null>;
getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle>;
scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'error:notvisible' | 'error:notconnected' | 'done'>;
@ -95,7 +95,7 @@ export interface PageDelegate {
// Work around Chrome's non-associated input and protocol.
inputActionEpilogue(): Promise<void>;
// Work around for asynchronously dispatched CSP errors in Firefox.
readonly cspErrorsAsynchronousForInlineScipts?: boolean;
readonly cspErrorsAsynchronousForInlineScripts?: boolean;
// Work around for mouse position in Firefox.
resetForReuse(): Promise<void>;
// WebKit hack.
@ -706,12 +706,9 @@ export class Page extends SdkObject {
frameNavigatedToNewDocument(frame: frames.Frame) {
this.emit(Page.Events.InternalFrameNavigatedToNewDocument, frame);
const url = frame.url();
if (!url.startsWith('http'))
return;
const purl = network.parsedURL(url);
if (purl)
this._browserContext.addVisitedOrigin(purl.origin);
const origin = frame.origin();
if (origin)
this._browserContext.addVisitedOrigin(origin);
}
allBindings() {

View file

@ -626,13 +626,8 @@ class ContextRecorder extends EventEmitter {
callMetadata.endTime = monotonicTime();
await frame.instrumentation.onAfterCall(frame, callMetadata);
const timer = setTimeout(() => {
// Commit the action after 5 seconds so that no further signals are added to it.
actionInContext.committed = true;
this._timers.delete(timer);
}, 5000);
this._setCommittedAfterTimeout(actionInContext);
this._generator.didPerformAction(actionInContext);
this._timers.add(timer);
};
const kActionTimeout = 5000;
@ -664,9 +659,19 @@ class ContextRecorder extends EventEmitter {
frame: frameDescription,
action
};
this._setCommittedAfterTimeout(actionInContext);
this._generator.addAction(actionInContext);
}
private _setCommittedAfterTimeout(actionInContext: ActionInContext) {
const timer = setTimeout(() => {
// Commit the action after 5 seconds so that no further signals are added to it.
actionInContext.committed = true;
this._timers.delete(timer);
}, isUnderTest() ? 500 : 5000);
this._timers.add(timer);
}
private _onFrameNavigated(frame: Frame, page: Page) {
const pageAlias = this._pageAliases.get(page);
this._generator.signal(pageAlias!, frame, { name: 'navigation', url: frame.url() });

View file

@ -164,6 +164,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
const assertion = action.value ? `ToHaveValueAsync(${quote(action.value)})` : `ToBeEmptyAsync()`;
return `await Expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
}
case 'assertScreenshot':
return `// AssertScreenshot(await ${subject}.ScreenshotAsync());`;
}
}

View file

@ -152,6 +152,8 @@ export class JavaLanguageGenerator implements LanguageGenerator {
const assertion = action.value ? `hasValue(${quote(action.value)})` : `isEmpty()`;
return `assertThat(${subject}.${this._asLocator(action.selector, inFrameLocator)}).${assertion};`;
}
case 'assertScreenshot':
return `// assertScreenshot(${subject}.screenshot());`;
}
}

View file

@ -135,6 +135,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
const assertion = action.value ? `toHaveValue(${quote(action.value)})` : `toBeEmpty()`;
return `${this._isTest ? '' : '// '}await expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
}
case 'assertScreenshot':
return `${this._isTest ? '' : '// '}await expect(${subject}).toHaveScreenshot();`;
}
}

View file

@ -73,7 +73,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
if (signals.dialog)
formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`);
let code = `${this._awaitPrefix}${this._generateActionCall(subject, action)}`;
let code = this._generateActionCall(subject, action);
if (signals.popup) {
code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as ${signals.popup.popupAlias}_info {
@ -99,7 +99,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
case 'openPage':
throw Error('Not reached');
case 'closePage':
return `${subject}.close()`;
return `${this._awaitPrefix}${subject}.close()`;
case 'click': {
let method = 'click';
if (action.clickCount === 2)
@ -115,35 +115,37 @@ export class PythonLanguageGenerator implements LanguageGenerator {
if (action.position)
options.position = action.position;
const optionsString = formatOptions(options, false);
return `${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`;
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`;
}
case 'check':
return `${subject}.${this._asLocator(action.selector)}.check()`;
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.check()`;
case 'uncheck':
return `${subject}.${this._asLocator(action.selector)}.uncheck()`;
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.uncheck()`;
case 'fill':
return `${subject}.${this._asLocator(action.selector)}.fill(${quote(action.text)})`;
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.fill(${quote(action.text)})`;
case 'setInputFiles':
return `${subject}.${this._asLocator(action.selector)}.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`;
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`;
case 'press': {
const modifiers = toModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+');
return `${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)})`;
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)})`;
}
case 'navigate':
return `${subject}.goto(${quote(action.url)})`;
return `${this._awaitPrefix}${subject}.goto(${quote(action.url)})`;
case 'select':
return `${subject}.${this._asLocator(action.selector)}.select_option(${formatValue(action.options.length === 1 ? action.options[0] : action.options)})`;
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.select_option(${formatValue(action.options.length === 1 ? action.options[0] : action.options)})`;
case 'assertText':
return `expect(${subject}.${this._asLocator(action.selector)}).${action.substring ? 'to_contain_text' : 'to_have_text'}(${quote(action.text)})`;
return `${this._awaitPrefix}expect(${subject}.${this._asLocator(action.selector)}).${action.substring ? 'to_contain_text' : 'to_have_text'}(${quote(action.text)})`;
case 'assertChecked':
return `expect(${subject}.${this._asLocator(action.selector)}).${action.checked ? 'to_be_checked()' : 'not_to_be_checked()'}`;
return `${this._awaitPrefix}expect(${subject}.${this._asLocator(action.selector)}).${action.checked ? 'to_be_checked()' : 'not_to_be_checked()'}`;
case 'assertVisible':
return `expect(${subject}.${this._asLocator(action.selector)}).to_be_visible()`;
return `${this._awaitPrefix}expect(${subject}.${this._asLocator(action.selector)}).to_be_visible()`;
case 'assertValue': {
const assertion = action.value ? `to_have_value(${quote(action.value)})` : `to_be_empty()`;
return `expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
return `${this._awaitPrefix}expect(${subject}.${this._asLocator(action.selector)}).${assertion}`;
}
case 'assertScreenshot':
return `# assert_screenshot(${this._awaitPrefix}${subject}.screenshot())`;
}
}
@ -162,7 +164,7 @@ def browser_context_args(browser_context_args, playwright) {
return {${contextOptions}}
}
` : '';
formatter.add(`${options.deviceName ? 'import pytest\n' : ''}
formatter.add(`${options.deviceName ? 'import pytest\n' : ''}import re
from playwright.sync_api import Page, expect
${fixture}
@ -170,7 +172,7 @@ def test_example(page: Page) -> None {`);
} else if (this._isAsync) {
formatter.add(`
import asyncio
import re
from playwright.async_api import Playwright, async_playwright, expect
@ -179,6 +181,7 @@ async def run(playwright: Playwright) -> None {
context = await browser.new_context(${formatContextOptions(options.contextOptions, options.deviceName)})`);
} else {
formatter.add(`
import re
from playwright.sync_api import Playwright, sync_playwright, expect

View file

@ -30,6 +30,7 @@ export type ActionName =
'assertText' |
'assertValue' |
'assertChecked' |
'assertScreenshot' |
'assertVisible';
export type ActionBase = {
@ -119,7 +120,11 @@ export type AssertVisibleAction = ActionBase & {
selector: string,
};
export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction;
export type AssertScreenshotAction = ActionBase & {
name: 'assertScreenshot',
};
export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction | AssertScreenshotAction;
export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction | AssertVisibleAction;
// Signals.

View file

@ -121,29 +121,6 @@ const DOWNLOAD_PATHS: Record<BrowserName | InternalTool, DownloadPaths> = {
'mac13-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac-arm64.zip',
'win64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-win64.zip',
},
'chromium-with-symbols': {
'<unknown>': undefined,
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': 'builds/chromium/%s/chromium-with-symbols-linux.zip',
'ubuntu22.04-x64': 'builds/chromium/%s/chromium-with-symbols-linux.zip',
'ubuntu18.04-arm64': undefined,
'ubuntu20.04-arm64': 'builds/chromium/%s/chromium-with-symbols-linux-arm64.zip',
'ubuntu22.04-arm64': 'builds/chromium/%s/chromium-with-symbols-linux-arm64.zip',
'debian11-x64': 'builds/chromium/%s/chromium-with-symbols-linux.zip',
'debian11-arm64': 'builds/chromium/%s/chromium-with-symbols-linux-arm64.zip',
'debian12-x64': 'builds/chromium/%s/chromium-with-symbols-linux.zip',
'debian12-arm64': 'builds/chromium/%s/chromium-with-symbols-linux-arm64.zip',
'mac10.13': 'builds/chromium/%s/chromium-with-symbols-mac.zip',
'mac10.14': 'builds/chromium/%s/chromium-with-symbols-mac.zip',
'mac10.15': 'builds/chromium/%s/chromium-with-symbols-mac.zip',
'mac11': 'builds/chromium/%s/chromium-with-symbols-mac.zip',
'mac11-arm64': 'builds/chromium/%s/chromium-with-symbols-mac-arm64.zip',
'mac12': 'builds/chromium/%s/chromium-with-symbols-mac.zip',
'mac12-arm64': 'builds/chromium/%s/chromium-with-symbols-mac-arm64.zip',
'mac13': 'builds/chromium/%s/chromium-with-symbols-mac.zip',
'mac13-arm64': 'builds/chromium/%s/chromium-with-symbols-mac-arm64.zip',
'win64': 'builds/chromium/%s/chromium-with-symbols-win64.zip',
},
'firefox': {
'<unknown>': undefined,
'ubuntu18.04-x64': undefined,
@ -368,9 +345,9 @@ function readDescriptors(browsersJSON: BrowsersJSON) {
}
export type BrowserName = 'chromium' | 'firefox' | 'webkit';
type InternalTool = 'ffmpeg' | 'firefox-beta' | 'firefox-asan' | 'chromium-with-symbols' | 'chromium-tip-of-tree' | 'android';
type InternalTool = 'ffmpeg' | 'firefox-beta' | 'firefox-asan' | 'chromium-tip-of-tree' | 'android';
type ChromiumChannel = 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary' | 'msedge' | 'msedge-beta' | 'msedge-dev' | 'msedge-canary';
const allDownloadable = ['chromium', 'firefox', 'webkit', 'ffmpeg', 'firefox-beta', 'chromium-with-symbols', 'chromium-tip-of-tree'];
const allDownloadable = ['chromium', 'firefox', 'webkit', 'ffmpeg', 'firefox-beta', 'chromium-tip-of-tree'];
export interface Executable {
type: 'browser' | 'tool' | 'channel';
@ -453,24 +430,6 @@ export class Registry {
_isHermeticInstallation: true,
});
const chromiumWithSymbols = descriptors.find(d => d.name === 'chromium-with-symbols')!;
const chromiumWithSymbolsExecutable = findExecutablePath(chromiumWithSymbols.dir, 'chromium');
this._executables.push({
type: 'tool',
name: 'chromium-with-symbols',
browserName: 'chromium',
directory: chromiumWithSymbols.dir,
executablePath: () => chromiumWithSymbolsExecutable,
executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('chromium-with-symbols', chromiumWithSymbolsExecutable, chromiumWithSymbols.installByDefault, sdkLanguage),
installType: chromiumWithSymbols.installByDefault ? 'download-by-default' : 'download-on-demand',
_validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, 'chromium', chromiumWithSymbols.dir, ['chrome-linux'], [], ['chrome-win']),
downloadURLs: this._downloadURLs(chromiumWithSymbols),
browserVersion: chromiumWithSymbols.browserVersion,
_install: () => this._downloadExecutable(chromiumWithSymbols, chromiumWithSymbolsExecutable),
_dependencyGroup: 'chromium',
_isHermeticInstallation: true,
});
const chromiumTipOfTree = descriptors.find(d => d.name === 'chromium-tip-of-tree')!;
const chromiumTipOfTreeExecutable = findExecutablePath(chromiumTipOfTree.dir, 'chromium');
this._executables.push({

View file

@ -43,6 +43,7 @@ import { Snapshotter } from './snapshotter';
import { yazl } from '../../../zipBundle';
import type { ConsoleMessage } from '../../console';
import { Dispatcher } from '../../dispatchers/dispatcher';
import { serializeError } from '../../errors';
const version: trace.VERSION = 6;
@ -182,6 +183,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
this._context.instrumentation.addListener(this, this._context);
this._eventListeners.push(
eventsHelper.addEventListener(this._context, BrowserContext.Events.Console, this._onConsoleMessage.bind(this)),
eventsHelper.addEventListener(this._context, BrowserContext.Events.PageError, this._onPageError.bind(this)),
);
if (this._state.options.screenshots)
this._startScreencast();
@ -396,18 +398,6 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
return this._captureSnapshot(event.afterSnapshot, sdkObject, metadata);
}
onEvent(sdkObject: SdkObject, event: trace.EventTraceEvent) {
if (!sdkObject.attribution.context)
return;
if (event.method === 'console' ||
(event.method === '__create__' && event.class === 'ConsoleMessage') ||
(event.method === '__create__' && event.class === 'JSHandle')) {
// Console messages are handled separately.
return;
}
this._appendTraceEvent(event);
}
onEntryStarted(entry: har.Entry) {
this._pendingHarEntries.add(entry);
}
@ -456,6 +446,18 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
this._appendTraceEvent(event);
}
private _onPageError(error: Error, page: Page) {
const event: trace.EventTraceEvent = {
type: 'event',
time: monotonicTime(),
class: 'BrowserContext',
method: 'pageError',
params: { error: serializeError(error) },
pageId: page.guid,
};
this._appendTraceEvent(event);
}
private _startScreencastInPage(page: Page) {
page.setScreencastOptions(kScreencastOptions);
const prefix = page.guid;

View file

@ -17,34 +17,35 @@
import path from 'path';
import fs from 'fs';
import { HttpServer } from '../../../utils/httpServer';
import { createGuid, gracefullyProcessExitDoNotHang, isUnderTest } from '../../../utils';
import type { Transport } from '../../../utils/httpServer';
import { gracefullyProcessExitDoNotHang, isUnderTest } from '../../../utils';
import { syncLocalStorageWithSettings } from '../../launchApp';
import { serverSideCallMetadata } from '../../instrumentation';
import { createPlaywright } from '../../playwright';
import { ProgressController } from '../../progress';
import { open, wsServer } from '../../../utilsBundle';
import { open } from '../../../utilsBundle';
import type { Page } from '../../page';
import type { BrowserType } from '../../browserType';
import { launchApp } from '../../launchApp';
export type Transport = {
sendEvent?: (method: string, params: any) => void;
dispatch: (method: string, params: any) => Promise<any>;
close?: () => void;
onclose: () => void;
};
export type OpenTraceViewerOptions = {
app?: string;
headless?: boolean;
export type TraceViewerServerOptions = {
host?: string;
port?: number;
isServer?: boolean;
transport?: Transport;
};
export type TraceViewerRedirectOptions = {
webApp?: string;
isServer?: boolean;
};
export type TraceViewerAppOptions = {
headless?: boolean;
persistentContextOptions?: Parameters<BrowserType['launchPersistentContext']>[2];
};
async function startTraceViewerServer(traceUrls: string[], options?: OpenTraceViewerOptions): Promise<{ server: HttpServer, url: string }> {
async function validateTraceUrls(traceUrls: string[]) {
for (const traceUrl of traceUrls) {
let traceFile = traceUrl;
// If .json is requested, we'll synthesize it.
@ -54,10 +55,13 @@ async function startTraceViewerServer(traceUrls: string[], options?: OpenTraceVi
if (!traceUrl.startsWith('http://') && !traceUrl.startsWith('https://') && !fs.existsSync(traceFile) && !fs.existsSync(traceFile + '.trace')) {
// eslint-disable-next-line no-console
console.error(`Trace file ${traceUrl} does not exist!`);
gracefullyProcessExitDoNotHang(1);
return false;
}
}
return true;
}
export async function startTraceViewerServer(options?: TraceViewerServerOptions): Promise<HttpServer> {
const server = new HttpServer();
server.routePrefix('/trace', (request, response) => {
const url = new URL('http://localhost' + request.url!);
@ -88,36 +92,25 @@ async function startTraceViewerServer(traceUrls: string[], options?: OpenTraceVi
return server.serveFile(request, response, absolutePath);
});
const params = traceUrls.map(t => `trace=${encodeURIComponent(t)}`);
const transport = options?.transport || (options?.isServer ? new StdinServer() : undefined);
if (transport)
server.createWebSocket(transport);
if (transport) {
const guid = createGuid();
params.push('ws=' + guid);
const wss = new wsServer({ server: server.server(), path: '/' + guid });
wss.on('connection', ws => {
transport.sendEvent = (method, params) => ws.send(JSON.stringify({ method, params }));
transport.close = () => ws.close();
ws.on('message', async (message: string) => {
const { id, method, params } = JSON.parse(message);
const result = await transport.dispatch(method, params);
ws.send(JSON.stringify({ id, result }));
});
ws.on('close', () => transport.onclose());
ws.on('error', () => transport.onclose());
});
}
const { host, port } = options || {};
await server.start({ preferredPort: port, host });
return server;
}
export async function installRootRedirect(server: HttpServer, traceUrls: string[], options: TraceViewerRedirectOptions) {
const params = (traceUrls || []).map(t => `trace=${encodeURIComponent(t)}`);
if (server.wsGuid())
params.push('ws=' + server.wsGuid());
if (options?.isServer)
params.push('isServer');
if (isUnderTest())
params.push('isUnderTest=true');
const { host, port } = options || {};
const url = await server.start({ preferredPort: port, host });
const { app } = options || {};
const searchQuery = params.length ? '?' + params.join('&') : '';
const urlPath = `/trace/${app || 'index.html'}${searchQuery}`;
const urlPath = `/trace/${options.webApp || 'index.html'}${searchQuery}`;
server.routePath('/', (request, response) => {
response.statusCode = 302;
@ -125,12 +118,28 @@ async function startTraceViewerServer(traceUrls: string[], options?: OpenTraceVi
response.end();
return true;
});
return { server, url };
}
export async function openTraceViewerApp(traceUrls: string[], browserName: string, options?: OpenTraceViewerOptions): Promise<Page> {
const { url } = await startTraceViewerServer(traceUrls, options);
export async function runTraceViewerApp(traceUrls: string[], browserName: string, options: TraceViewerServerOptions & { headless?: boolean }, exitOnClose?: boolean) {
if (!validateTraceUrls(traceUrls))
return;
const server = await startTraceViewerServer(options);
await installRootRedirect(server, traceUrls, options);
const page = await openTraceViewerApp(server.urlPrefix(), browserName, options);
if (exitOnClose)
page.on('close', () => gracefullyProcessExitDoNotHang(0));
return page;
}
export async function runTraceInBrowser(traceUrls: string[], options: TraceViewerServerOptions) {
if (!validateTraceUrls(traceUrls))
return;
const server = await startTraceViewerServer(options);
await installRootRedirect(server, traceUrls, options);
await openTraceInBrowser(server.urlPrefix());
}
export async function openTraceViewerApp(url: string, browserName: string, options?: TraceViewerAppOptions): Promise<Page> {
const traceViewerPlaywright = createPlaywright({ sdkLanguage: 'javascript', isInternalPlaywright: true });
const traceViewerBrowser = isUnderTest() ? 'chromium' : browserName;
@ -141,7 +150,7 @@ export async function openTraceViewerApp(traceUrls: string[], browserName: strin
persistentContextOptions: {
...options?.persistentContextOptions,
useWebSocket: isUnderTest(),
headless: options?.headless,
headless: !!options?.headless,
},
});
@ -163,8 +172,7 @@ export async function openTraceViewerApp(traceUrls: string[], browserName: strin
return page;
}
export async function openTraceInBrowser(traceUrls: string[], options?: OpenTraceViewerOptions) {
const { url } = await startTraceViewerServer(traceUrls, options);
export async function openTraceInBrowser(url: string) {
// eslint-disable-next-line no-console
console.log('\nListening on ' + url);
if (!isUnderTest())
@ -202,10 +210,10 @@ class StdinServer implements Transport {
sendEvent?: (method: string, params: any) => void;
close?: () => void;
private _loadTrace(url: string) {
this._traceUrl = url;
private _loadTrace(traceUrl: string) {
this._traceUrl = traceUrl;
clearTimeout(this._pollTimer);
this.sendEvent?.('loadTrace', { url });
this.sendEvent?.('loadTraceRequested', { traceUrl });
}
private _pollLoadTrace(url: string) {

View file

@ -966,7 +966,7 @@ export class WKPage implements PageDelegate {
await this._session.send('DOM.setInputFiles', { objectId, files: protocolFiles });
}
async setInputFilePaths(progress: Progress, handle: dom.ElementHandle<HTMLInputElement>, paths: string[]): Promise<void> {
async setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, paths: string[]): Promise<void> {
const pageProxyId = this._pageProxySession.sessionId;
const objectId = handle._objectId;
await Promise.all([

View file

@ -17,19 +17,28 @@
import type http from 'http';
import fs from 'fs';
import path from 'path';
import { mime } from '../utilsBundle';
import { mime, wsServer } from '../utilsBundle';
import { assert } from './debug';
import { createHttpServer } from './network';
import { ManualPromise } from './manualPromise';
import { createGuid } from './crypto';
export type ServerRouteHandler = (request: http.IncomingMessage, response: http.ServerResponse) => boolean;
export type Transport = {
sendEvent?: (method: string, params: any) => void;
dispatch: (method: string, params: any) => Promise<any>;
close?: () => void;
onclose: () => void;
};
export class HttpServer {
private _server: http.Server;
private _urlPrefix: string;
private _port: number = 0;
private _started = false;
private _routes: { prefix?: string, exact?: string, handler: ServerRouteHandler }[] = [];
private _wsGuid: string | undefined;
constructor(address: string = '') {
this._urlPrefix = address;
@ -68,6 +77,31 @@ export class HttpServer {
}
}
createWebSocket(transport: Transport, guid?: string) {
assert(!this._wsGuid, 'can only create one main websocket transport per server');
this._wsGuid = guid || createGuid();
const wss = new wsServer({ server: this._server, path: '/' + this._wsGuid });
wss.on('connection', ws => {
transport.sendEvent = (method, params) => ws.send(JSON.stringify({ method, params }));
transport.close = () => ws.close();
ws.on('message', async message => {
const { id, method, params } = JSON.parse(String(message));
try {
const result = await transport.dispatch(method, params);
ws.send(JSON.stringify({ id, result }));
} catch (e) {
ws.send(JSON.stringify({ id, error: String(e) }));
}
});
ws.on('close', () => transport.onclose());
ws.on('error', () => transport.onclose());
});
}
wsGuid(): string | undefined {
return this._wsGuid;
}
async start(options: { port?: number, preferredPort?: number, host?: string } = {}): Promise<string> {
assert(!this._started, 'server already started');
this._started = true;

View file

@ -48,8 +48,10 @@ function preprocess(str: string): number[] {
if (code === 0xd && str.charCodeAt(i + 1) === 0xa) {
code = 0xa; i++;
}
if (code === 0xd || code === 0xc) code = 0xa;
if (code === 0x0) code = 0xfffd;
if (code === 0xd || code === 0xc)
code = 0xa;
if (code === 0x0)
code = 0xfffd;
if (between(code, 0xd800, 0xdbff) && between(str.charCodeAt(i + 1), 0xdc00, 0xdfff)) {
// Decode a surrogate pair into an astral codepoint.
const lead = code - 0xd800;
@ -63,7 +65,8 @@ function preprocess(str: string): number[] {
}
function stringFromCode(code: number) {
if (code <= 0xffff) return String.fromCharCode(code);
if (code <= 0xffff)
return String.fromCharCode(code);
// Otherwise, encode astral char as surrogate pair.
code -= Math.pow(2, 16);
const lead = Math.floor(code / Math.pow(2, 10)) + 0xd800;
@ -107,8 +110,10 @@ export function tokenize(str1: string): CSSTokenInterface[] {
num = 1;
i += num;
code = codepoint(i);
if (newline(code)) incrLineno();
else column += num;
if (newline(code))
incrLineno();
else
column += num;
// console.log('Consume '+i+' '+String.fromCharCode(code) + ' 0x' + code.toString(16));
return true;
};
@ -125,7 +130,8 @@ export function tokenize(str1: string): CSSTokenInterface[] {
return true;
};
const eof = function(codepoint?: number): boolean {
if (codepoint === undefined) codepoint = code;
if (codepoint === undefined)
codepoint = code;
return codepoint === -1;
};
const donothing = function() { };
@ -138,12 +144,14 @@ export function tokenize(str1: string): CSSTokenInterface[] {
consumeComments();
consume();
if (whitespace(code)) {
while (whitespace(next())) consume();
while (whitespace(next()))
consume();
return new WhitespaceToken();
} else if (code === 0x22) {return consumeAStringToken();} else if (code === 0x23) {
if (namechar(next()) || areAValidEscape(next(1), next(2))) {
const token = new HashToken('');
if (wouldStartAnIdentifier(next(1), next(2), next(3))) token.type = 'id';
if (wouldStartAnIdentifier(next(1), next(2), next(3)))
token.type = 'id';
token.value = consumeAName();
return token;
} else {
@ -288,7 +296,8 @@ export function tokenize(str1: string): CSSTokenInterface[] {
const str = consumeAName();
if (str.toLowerCase() === 'url' && next() === 0x28) {
consume();
while (whitespace(next(1)) && whitespace(next(2))) consume();
while (whitespace(next(1)) && whitespace(next(2)))
consume();
if (next() === 0x22 || next() === 0x27)
return new FunctionToken(str);
else if (whitespace(next()) && (next(2) === 0x22 || next(2) === 0x27))
@ -305,7 +314,8 @@ export function tokenize(str1: string): CSSTokenInterface[] {
};
const consumeAStringToken = function(endingCodePoint?: number): CSSParserToken {
if (endingCodePoint === undefined) endingCodePoint = code;
if (endingCodePoint === undefined)
endingCodePoint = code;
let string = '';
while (consume()) {
if (code === endingCodePoint || eof()) {
@ -331,13 +341,16 @@ export function tokenize(str1: string): CSSTokenInterface[] {
const consumeAURLToken = function(): CSSTokenInterface {
const token = new URLToken('');
while (whitespace(next())) consume();
if (eof(next())) return token;
while (whitespace(next()))
consume();
if (eof(next()))
return token;
while (consume()) {
if (code === 0x29 || eof()) {
return token;
} else if (whitespace(code)) {
while (whitespace(next())) consume();
while (whitespace(next()))
consume();
if (next() === 0x29 || eof(next())) {
consume();
return token;
@ -379,9 +392,11 @@ export function tokenize(str1: string): CSSTokenInterface[] {
break;
}
}
if (whitespace(next())) consume();
if (whitespace(next()))
consume();
let value = parseInt(digits.map(function(x) { return String.fromCharCode(x); }).join(''), 16);
if (value > maximumallowedcodepoint) value = 0xfffd;
if (value > maximumallowedcodepoint)
value = 0xfffd;
return value;
} else if (eof()) {
return 0xfffd;
@ -391,8 +406,10 @@ export function tokenize(str1: string): CSSTokenInterface[] {
};
const areAValidEscape = function(c1: number, c2: number) {
if (c1 !== 0x5c) return false;
if (newline(c2)) return false;
if (c1 !== 0x5c)
return false;
if (newline(c2))
return false;
return true;
};
const startsWithAValidEscape = function() {
@ -416,11 +433,14 @@ export function tokenize(str1: string): CSSTokenInterface[] {
const wouldStartANumber = function(c1: number, c2: number, c3: number) {
if (c1 === 0x2b || c1 === 0x2d) {
if (digit(c2)) return true;
if (c2 === 0x2e && digit(c3)) return true;
if (digit(c2))
return true;
if (c2 === 0x2e && digit(c3))
return true;
return false;
} else if (c1 === 0x2e) {
if (digit(c2)) return true;
if (digit(c2))
return true;
return false;
} else if (digit(c1)) {
return true;
@ -519,7 +539,8 @@ export function tokenize(str1: string): CSSTokenInterface[] {
while (!eof(next())) {
tokens.push(consumeAToken());
iterationCount++;
if (iterationCount > str.length * 2) throw new Error("I'm infinite-looping!");
if (iterationCount > str.length * 2)
throw new Error("I'm infinite-looping!");
}
return tokens;
}

View file

@ -181,9 +181,8 @@ export async function isURLAvailable(url: URL, ignoreHTTPSErrors: boolean, onLog
async function httpStatusCode(url: URL, ignoreHTTPSErrors: boolean, onLog?: (data: string) => void, onStdErr?: (data: string) => void): Promise<number> {
return new Promise(resolve => {
onLog?.(`HTTP HEAD: ${url}`);
onLog?.(`HTTP GET: ${url}`);
httpRequest({
method: 'HEAD',
url: url.toString(),
headers: { Accept: '*/*' },
rejectUnauthorized: !ignoreHTTPSErrors

View file

@ -14,108 +14,22 @@
* limitations under the License.
*/
import { ManualPromise } from './manualPromise';
import { monotonicTime } from './';
export class TimeoutRunnerError extends Error {}
type TimeoutRunnerData = {
lastElapsedSync: number,
timer: NodeJS.Timeout | undefined,
timeoutPromise: ManualPromise<any>,
};
export const MaxTime = 2147483647; // 2^31-1
export class TimeoutRunner {
private _running: TimeoutRunnerData | undefined;
private _timeout: number;
private _elapsed: number;
private _deadline = MaxTime;
constructor(timeout: number) {
this._timeout = timeout;
this._elapsed = 0;
}
async run<T>(cb: () => Promise<T>): Promise<T> {
const running = this._running = {
lastElapsedSync: monotonicTime(),
timer: undefined,
timeoutPromise: new ManualPromise(),
};
try {
const resultPromise = Promise.race([
cb(),
running.timeoutPromise
]);
this._updateTimeout(running, this._timeout);
return await resultPromise;
} finally {
this._updateTimeout(running, 0);
if (this._running === running)
this._running = undefined;
}
}
interrupt() {
if (this._running)
this._updateTimeout(this._running, -1);
}
elapsed() {
this._syncElapsedAndStart();
return this._elapsed;
}
deadline(): number {
return this._deadline;
}
updateTimeout(timeout: number, elapsed?: number) {
this._timeout = timeout;
if (elapsed !== undefined) {
this._syncElapsedAndStart();
this._elapsed = elapsed;
}
if (this._running)
this._updateTimeout(this._running, timeout);
}
private _syncElapsedAndStart() {
if (this._running) {
const now = monotonicTime();
this._elapsed += now - this._running.lastElapsedSync;
this._running.lastElapsedSync = now;
}
}
private _updateTimeout(running: TimeoutRunnerData, timeout: number) {
if (running.timer) {
clearTimeout(running.timer);
running.timer = undefined;
}
this._syncElapsedAndStart();
this._deadline = timeout ? monotonicTime() + timeout : MaxTime;
if (timeout === 0)
return;
timeout = timeout - this._elapsed;
if (timeout <= 0)
running.timeoutPromise.reject(new TimeoutRunnerError());
else
running.timer = setTimeout(() => running.timeoutPromise.reject(new TimeoutRunnerError()), timeout);
}
}
export async function raceAgainstDeadline<T>(cb: () => Promise<T>, deadline: number): Promise<{ result: T, timedOut: false } | { timedOut: true }> {
const runner = new TimeoutRunner((deadline || MaxTime) - monotonicTime());
try {
return { result: await runner.run(cb), timedOut: false };
} catch (e) {
if (e instanceof TimeoutRunnerError)
return { timedOut: true };
throw e;
}
let timer: NodeJS.Timeout | undefined;
return Promise.race([
cb().then(result => {
return { result, timedOut: false };
}),
new Promise<{ timedOut: true }>(resolve => {
const kMaxDeadline = 2147483647; // 2^31-1
const timeout = (deadline || kMaxDeadline) - monotonicTime();
timer = setTimeout(() => resolve({ timedOut: true }), timeout);
}),
]).finally(() => {
clearTimeout(timer);
});
}
export async function pollAgainstDeadline<T>(callback: () => Promise<{ continuePolling: boolean, result: T }>, deadline: number, pollIntervals: number[] = [100, 250, 500, 1000]): Promise<{ result?: T, timedOut: boolean }> {

View file

@ -831,7 +831,7 @@ CORS RFC1918 enforcement.
resourceIPAddressSpace?: Network.IPAddressSpace;
clientSecurityState?: Network.ClientSecurityState;
}
export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation";
export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation"|"InvalidInfoHeader"|"NoRegisterSourceHeader"|"NoRegisterTriggerHeader"|"NoRegisterOsSourceHeader"|"NoRegisterOsTriggerHeader";
/**
* Details for issues around "Attribution Reporting API" usage.
Explainer: https://github.com/WICG/attribution-reporting-api
@ -2493,6 +2493,28 @@ stylesheet rules) this rule came from.
*/
tryRules: CSSTryRule[];
}
/**
* CSS @position-try rule representation.
*/
export interface CSSPositionTryRule {
/**
* The prelude dashed-ident name
*/
name: Value;
/**
* The css style sheet identifier (absent for user agent stylesheet and user-specified
stylesheet rules) this rule came from.
*/
styleSheetId?: StyleSheetId;
/**
* Parent stylesheet's origin.
*/
origin: StyleSheetOrigin;
/**
* Associated style declaration.
*/
style: CSSStyle;
}
/**
* CSS keyframes rule representation.
*/
@ -2820,6 +2842,10 @@ attributes) for a DOM node identified by `nodeId`.
* 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-options property.
*/
cssPositionTryRules?: CSSPositionTryRule[];
/**
* A list of CSS at-property rules matching this node.
*/
@ -2882,6 +2908,17 @@ the full layer tree for the tree scope and their ordering.
export type getLayersForNodeReturnValue = {
rootLayer: CSSLayerData;
}
/**
* Given a CSS selector text and a style sheet ID, getLocationForSelector
returns an array of locations of the CSS selector in the style sheet.
*/
export type getLocationForSelectorParameters = {
styleSheetId: StyleSheetId;
selectorText: string;
}
export type getLocationForSelectorReturnValue = {
ranges: SourceRange[];
}
/**
* Starts tracking the given computed styles for updates. The specified array of properties
replaces the one previously specified. Pass empty array to disable tracking.
@ -8052,6 +8089,7 @@ milliseconds relatively to this requestTime.
headers: Headers;
/**
* HTTP POST request data.
Use postDataEntries instead.
*/
postData?: string;
/**
@ -8059,7 +8097,7 @@ milliseconds relatively to this requestTime.
*/
hasPostData?: boolean;
/**
* Request body elements. This will be converted from base64 to binary
* Request body elements (post data broken into individual entries).
*/
postDataEntries?: PostDataEntry[];
/**
@ -9787,6 +9825,18 @@ matches provided URL.
* Connection type if known.
*/
connectionType?: ConnectionType;
/**
* WebRTC packet loss (percent, 0-100). 0 disables packet loss emulation, 100 drops all the packets.
*/
packetLoss?: number;
/**
* WebRTC packet queue length (packet). 0 removes any queue length limitations.
*/
packetQueueLength?: number;
/**
* WebRTC packetReordering feature.
*/
packetReordering?: boolean;
}
export type emulateNetworkConditionsReturnValue = {
}
@ -11065,7 +11115,7 @@ as an ad.
* All Permissions Policy features. This enum should match the one defined
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-factor"|"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"|"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"|"window-placement"|"xr-spatial-tracking";
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"|"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"|"window-placement"|"xr-spatial-tracking";
/**
* Reason for a permissions policy feature to be disabled.
*/
@ -11534,7 +11584,7 @@ Example URLs: http://www.google.com/file.html -> "google.com"
/**
* List of not restored reasons for back-forward cache.
*/
export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"Portal"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame";
export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"Portal"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame";
/**
* Types of not restored reasons for back-forward cache.
*/
@ -11811,7 +11861,7 @@ open.
*/
type: DialogType;
/**
* True if browser is capable showing or acting on the given dialog. When browser has no
* True iff browser is capable showing or acting on the given dialog. When browser has no
dialog handler for given target, calling alert while Page domain is engaged will stall
the page execution. Execution can be resumed via calling Page.handleJavaScriptDialog.
*/
@ -13533,34 +13583,10 @@ Tokens from that issuer.
* Enum of network fetches auctions can do.
*/
export type InterestGroupAuctionFetchType = "bidderJs"|"bidderWasm"|"sellerJs"|"bidderTrustedSignals"|"sellerTrustedSignals";
/**
* Ad advertising element inside an interest group.
*/
export interface InterestGroupAd {
renderURL: string;
metadata?: string;
}
/**
* The full details of an interest group.
*/
export interface InterestGroupDetails {
ownerOrigin: string;
name: string;
expirationTime: Network.TimeSinceEpoch;
joiningOrigin: string;
biddingLogicURL?: string;
biddingWasmHelperURL?: string;
updateURL?: string;
trustedBiddingSignalsURL?: string;
trustedBiddingSignalsKeys: string[];
userBiddingSignals?: string;
ads: InterestGroupAd[];
adComponents: InterestGroupAd[];
}
/**
* Enum of shared storage access types.
*/
export type SharedStorageAccessType = "documentAddModule"|"documentSelectURL"|"documentRun"|"documentSet"|"documentAppend"|"documentDelete"|"documentClear"|"workletSet"|"workletAppend"|"workletDelete"|"workletClear"|"workletGet"|"workletKeys"|"workletEntries"|"workletLength"|"workletRemainingBudget";
export type SharedStorageAccessType = "documentAddModule"|"documentSelectURL"|"documentRun"|"documentSet"|"documentAppend"|"documentDelete"|"documentClear"|"documentGet"|"workletSet"|"workletAppend"|"workletDelete"|"workletClear"|"workletGet"|"workletKeys"|"workletEntries"|"workletLength"|"workletRemainingBudget"|"headerSet"|"headerAppend"|"headerDelete"|"headerClear";
/**
* Struct for a single key-value pair in an origin's shared storage.
*/
@ -13644,22 +13670,28 @@ SharedStorageAccessType.documentAppend,
SharedStorageAccessType.documentDelete,
SharedStorageAccessType.workletSet,
SharedStorageAccessType.workletAppend,
SharedStorageAccessType.workletDelete, and
SharedStorageAccessType.workletGet.
SharedStorageAccessType.workletDelete,
SharedStorageAccessType.workletGet,
SharedStorageAccessType.headerSet,
SharedStorageAccessType.headerAppend, and
SharedStorageAccessType.headerDelete.
*/
key?: string;
/**
* Value for a specific entry in an origin's shared storage.
Present only for SharedStorageAccessType.documentSet,
SharedStorageAccessType.documentAppend,
SharedStorageAccessType.workletSet, and
SharedStorageAccessType.workletAppend.
SharedStorageAccessType.workletSet,
SharedStorageAccessType.workletAppend,
SharedStorageAccessType.headerSet, and
SharedStorageAccessType.headerAppend.
*/
value?: string;
/**
* Whether or not to set an entry for a key if that key is already present.
Present only for SharedStorageAccessType.documentSet and
SharedStorageAccessType.workletSet.
Present only for SharedStorageAccessType.documentSet,
SharedStorageAccessType.workletSet, and
SharedStorageAccessType.headerSet.
*/
ignoreIfPresent?: boolean;
}
@ -13789,6 +13821,23 @@ int
}
export type AttributionReportingEventLevelResult = "success"|"successDroppedLowerPriority"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"deduplicated"|"excessiveAttributions"|"priorityTooLow"|"neverAttributedSource"|"excessiveReportingOrigins"|"noMatchingSourceFilterData"|"prohibitedByBrowserPolicy"|"noMatchingConfigurations"|"excessiveReports"|"falselyAttributedSource"|"reportWindowPassed"|"notRegistered"|"reportWindowNotStarted"|"noMatchingTriggerData";
export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports";
/**
* A single Related Website Set object.
*/
export interface RelatedWebsiteSet {
/**
* The primary site of this set, along with the ccTLDs if there is any.
*/
primarySites: string[];
/**
* The associated sites of this set, along with the ccTLDs if there is any.
*/
associatedSites: string[];
/**
* The service sites of this set, along with the ccTLDs if there is any.
*/
serviceSites: string[];
}
/**
* A cache's contents have been modified.
@ -14216,7 +14265,13 @@ Leaves other stored data, including the issuer's Redemption Records, intact.
name: string;
}
export type getInterestGroupDetailsReturnValue = {
details: InterestGroupDetails;
/**
* This largely corresponds to:
https://wicg.github.io/turtledove/#dictdef-generatebidinterestgroup
but has absolute expirationTime instead of relative lifetimeMs and
also adds joiningOrigin.
*/
details: { [key: string]: string };
}
/**
* Enables/Disables issuing of interestGroupAccessed events.
@ -14345,6 +14400,15 @@ interestGroupAuctionNetworkRequestCreated.
}
export type setAttributionReportingTrackingReturnValue = {
}
/**
* Returns the effective Related Website Sets in use by this profile for the browser
session. The effective Related Website Sets will not change during a browser session.
*/
export type getRelatedWebsiteSetsParameters = {
}
export type getRelatedWebsiteSetsReturnValue = {
sets: RelatedWebsiteSet[];
}
}
/**
@ -14582,10 +14646,10 @@ supported.
export type SessionID = string;
export interface TargetInfo {
targetId: TargetID;
type: string;
/**
* List of types: https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/devtools_agent_host_impl.cc?ss=chromium&q=f:devtools%20-f:out%20%22::kTypeTab%5B%5D%22
*/
type: string;
title: string;
url: string;
/**
@ -16385,7 +16449,7 @@ See also:
requestId?: Network.RequestId;
/**
* Error information
`errorMessage` is null if `errorType` is null.
`errorMessage` is null iff `errorType` is null.
*/
errorType?: RuleSetErrorType;
/**
@ -19515,6 +19579,7 @@ Error was thrown.
"CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeParameters;
"CSS.getStyleSheetText": CSS.getStyleSheetTextParameters;
"CSS.getLayersForNode": CSS.getLayersForNodeParameters;
"CSS.getLocationForSelector": CSS.getLocationForSelectorParameters;
"CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesParameters;
"CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesParameters;
"CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeParameters;
@ -19885,6 +19950,7 @@ Error was thrown.
"Storage.runBounceTrackingMitigations": Storage.runBounceTrackingMitigationsParameters;
"Storage.setAttributionReportingLocalTestingMode": Storage.setAttributionReportingLocalTestingModeParameters;
"Storage.setAttributionReportingTracking": Storage.setAttributionReportingTrackingParameters;
"Storage.getRelatedWebsiteSets": Storage.getRelatedWebsiteSetsParameters;
"SystemInfo.getInfo": SystemInfo.getInfoParameters;
"SystemInfo.getFeatureState": SystemInfo.getFeatureStateParameters;
"SystemInfo.getProcessInfo": SystemInfo.getProcessInfoParameters;
@ -20097,6 +20163,7 @@ Error was thrown.
"CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeReturnValue;
"CSS.getStyleSheetText": CSS.getStyleSheetTextReturnValue;
"CSS.getLayersForNode": CSS.getLayersForNodeReturnValue;
"CSS.getLocationForSelector": CSS.getLocationForSelectorReturnValue;
"CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesReturnValue;
"CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesReturnValue;
"CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeReturnValue;
@ -20467,6 +20534,7 @@ Error was thrown.
"Storage.runBounceTrackingMitigations": Storage.runBounceTrackingMitigationsReturnValue;
"Storage.setAttributionReportingLocalTestingMode": Storage.setAttributionReportingLocalTestingModeReturnValue;
"Storage.setAttributionReportingTracking": Storage.setAttributionReportingTrackingReturnValue;
"Storage.getRelatedWebsiteSets": Storage.getRelatedWebsiteSetsReturnValue;
"SystemInfo.getInfo": SystemInfo.getInfoReturnValue;
"SystemInfo.getFeatureState": SystemInfo.getFeatureStateReturnValue;
"SystemInfo.getProcessInfo": SystemInfo.getProcessInfoReturnValue;

View file

@ -40,6 +40,6 @@ export type Unboxed<Arg> =
export type PageFunction0<R> = string | (() => R | Promise<R>);
export type PageFunction<Arg, R> = string | ((arg: Unboxed<Arg>) => R | Promise<R>);
export type PageFunctionOn<On, Arg2, R> = string | ((on: On, arg2: Unboxed<Arg2>) => R | Promise<R>);
export type SmartHandle<T> = T extends Node ? ElementHandle<T> : JSHandle<T>;
export type SmartHandle<T> = [T] extends [Node] ? ElementHandle<T> : JSHandle<T>;
export type ElementHandleForTag<K extends keyof HTMLElementTagNameMap> = ElementHandle<HTMLElementTagNameMap[K]>;
export type BindingSource = { context: BrowserContext, page: Page, frame: Frame };

View file

@ -1781,26 +1781,32 @@ export interface Page {
prependListener(event: 'worker', listener: (worker: Worker) => void): this;
/**
* Sometimes, the web page can show an overlay that obstructs elements behind it and prevents certain actions, like
* click, from completing. When such an overlay is shown predictably, we recommend dismissing it as a part of your
* test flow. However, sometimes such an overlay may appear non-deterministically, for example certain cookies consent
* dialogs behave this way. In this case,
* [page.addLocatorHandler(locator, handler)](https://playwright.dev/docs/api/class-page#page-add-locator-handler)
* allows handling an overlay during an action that it would block.
* **NOTE** This method is experimental and its behavior may change in the upcoming releases.
*
* This method registers a handler for an overlay that is executed once the locator is visible on the page. The
* handler should get rid of the overlay so that actions blocked by it can proceed. This is useful for
* nondeterministic interstitial pages or dialogs, like a cookie consent dialog.
* When testing a web page, sometimes unexpected overlays like a "Sign up" dialog appear and block actions you want to
* automate, e.g. clicking a button. These overlays don't always show up in the same way or at the same time, making
* them tricky to handle in automated tests.
*
* Note that execution time of the handler counts towards the timeout of the action/assertion that executed the
* handler.
* This method lets you set up a special function, called a handler, that activates when it detects that overlay is
* visible. The handler's job is to remove the overlay, allowing your test to continue as if the overlay wasn't there.
*
* You can register multiple handlers. However, only a single handler will be running at a time. Any actions inside a
* handler must not require another handler to run.
* Things to keep in mind:
* - When an overlay is shown predictably, we recommend explicitly waiting for it in your test and dismissing it as
* a part of your normal test flow, instead of using
* [page.addLocatorHandler(locator, handler)](https://playwright.dev/docs/api/class-page#page-add-locator-handler).
* - Playwright checks for the overlay every time before executing or retrying an action that requires an
* [actionability check](https://playwright.dev/docs/actionability), or before performing an auto-waiting assertion check. When overlay
* is visible, Playwright calls the handler first, and then proceeds with the action/assertion. Note that the
* handler is only called when you perform an action/assertion - if the overlay becomes visible but you don't
* perform any actions, the handler will not be triggered.
* - The execution time of the handler counts towards the timeout of the action/assertion that executed the handler.
* If your handler takes too long, it might cause timeouts.
* - You can register multiple handlers. However, only a single handler will be running at a time. Make sure the
* actions within a handler don't depend on another handler.
*
* **NOTE** Running the interceptor will alter your page state mid-test. For example it will change the currently
* focused element and move the mouse. Make sure that the actions that run after the interceptor are self-contained
* and do not rely on the focus and mouse state. <br /> <br /> For example, consider a test that calls
* **NOTE** Running the handler will alter your page state mid-test. For example it will change the currently focused
* element and move the mouse. Make sure that actions that run after the handler are self-contained and do not rely on
* the focus and mouse state being unchanged. <br /> <br /> For example, consider a test that calls
* [locator.focus([options])](https://playwright.dev/docs/api/class-locator#locator-focus) followed by
* [keyboard.press(key[, options])](https://playwright.dev/docs/api/class-keyboard#keyboard-press). If your handler
* clicks a button between these two actions, the focused element most likely will be wrong, and key press will happen
@ -1809,17 +1815,18 @@ export interface Page {
* problem. <br /> <br /> Another example is a series of mouse actions, where
* [mouse.move(x, y[, options])](https://playwright.dev/docs/api/class-mouse#mouse-move) is followed by
* [mouse.down([options])](https://playwright.dev/docs/api/class-mouse#mouse-down). Again, when the handler runs
* between these two actions, the mouse position will be wrong during the mouse down. Prefer methods like
* [locator.click([options])](https://playwright.dev/docs/api/class-locator#locator-click) that are self-contained.
* between these two actions, the mouse position will be wrong during the mouse down. Prefer self-contained actions
* like [locator.click([options])](https://playwright.dev/docs/api/class-locator#locator-click) that do not rely on
* the state being unchanged by a handler.
*
* **Usage**
*
* An example that closes a cookie dialog when it appears:
* An example that closes a "Sign up to the newsletter" dialog when it appears:
*
* ```js
* // Setup the handler.
* await page.addLocatorHandler(page.getByRole('button', { name: 'Accept all cookies' }), async () => {
* await page.getByRole('button', { name: 'Reject all cookies' }).click();
* await page.addLocatorHandler(page.getByText('Sign up to the newsletter'), async () => {
* await page.getByRole('button', { name: 'No thanks' }).click();
* });
*
* // Write the test as usual.
@ -1832,7 +1839,7 @@ export interface Page {
* ```js
* // Setup the handler.
* await page.addLocatorHandler(page.getByText('Confirm your security details'), async () => {
* await page.getByRole('button', 'Remind me later').click();
* await page.getByRole('button', { name: 'Remind me later' }).click();
* });
*
* // Write the test as usual.
@ -8438,6 +8445,28 @@ export interface BrowserContext {
*/
pages(): Array<Page>;
/**
* Removes cookies from context. At least one of the removal criteria should be provided.
*
* **Usage**
*
* ```js
* await browserContext.removeCookies({ name: 'session-id' });
* await browserContext.removeCookies({ domain: 'my-origin.com' });
* await browserContext.removeCookies({ path: '/api/v1' });
* await browserContext.removeCookies({ name: 'session-id', domain: 'my-origin.com' });
* ```
*
* @param filter
*/
removeCookies(filter: {
name?: string;
domain?: string;
path?: string;
}): Promise<void>;
/**
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once
* route is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
@ -11433,6 +11462,24 @@ export interface Locator {
*/
elementHandles(): Promise<Array<ElementHandle>>;
/**
* Returns a {@link FrameLocator} object pointing to the same `iframe` as this locator.
*
* Useful when you have a {@link Locator} object obtained somewhere, and later on would like to interact with the
* content inside the frame.
*
* **Usage**
*
* ```js
* const locator = page.locator('iframe[name="embedded"]');
* // ...
* const frameLocator = locator.enterFrame();
* await frameLocator.getByRole('button').click();
* ```
*
*/
enterFrame(): FrameLocator;
/**
* Set a value to the input field.
*
@ -13125,6 +13172,7 @@ export interface BrowserType<Unused = {}> {
/**
* **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the
* `headless` option will be set `false`.
* @deprecated Use [debugging tools](https://playwright.dev/docs/debug) instead.
*/
devtools?: boolean;
@ -13529,6 +13577,7 @@ export interface BrowserType<Unused = {}> {
/**
* **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the
* `headless` option will be set `false`.
* @deprecated Use [debugging tools](https://playwright.dev/docs/debug) instead.
*/
devtools?: boolean;
@ -17734,14 +17783,32 @@ export interface FileChooser {
* **Converting Locator to FrameLocator**
*
* If you have a {@link Locator} object pointing to an `iframe` it can be converted to {@link FrameLocator} using
* [`:scope`](https://developer.mozilla.org/en-US/docs/Web/CSS/:scope) CSS selector:
* [locator.enterFrame()](https://playwright.dev/docs/api/class-locator#locator-enter-frame).
*
* ```js
* const frameLocator = locator.frameLocator(':scope');
* ```
* **Converting FrameLocator to Locator**
*
* If you have a {@link FrameLocator} object it can be converted to {@link Locator} pointing to the same `iframe`
* using [frameLocator.exitFrame()](https://playwright.dev/docs/api/class-framelocator#frame-locator-exit-frame).
*/
export interface FrameLocator {
/**
* Returns a {@link Locator} object pointing to the same `iframe` as this frame locator.
*
* Useful when you have a {@link FrameLocator} object obtained somewhere, and later on would like to interact with the
* `iframe` element.
*
* **Usage**
*
* ```js
* const frameLocator = page.frameLocator('iframe[name="embedded"]');
* // ...
* const locator = frameLocator.exitFrame();
* await expect(locator).toBeVisible();
* ```
*
*/
exitFrame(): Locator;
/**
* Returns locator to the first matching frame.
*/
@ -18313,7 +18380,7 @@ export interface Keyboard {
* (async () => {
* const browser = await chromium.launch({
* logger: {
* isEnabled: (name, severity) => name === 'browser',
* isEnabled: (name, severity) => name === 'api',
* log: (name, severity, message, args) => console.log(`${name} ${message}`)
* }
* });
@ -20205,6 +20272,7 @@ export interface LaunchOptions {
/**
* **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the
* `headless` option will be set `false`.
* @deprecated Use [debugging tools](https://playwright.dev/docs/debug) instead.
*/
devtools?: boolean;

View file

@ -1,3 +1,3 @@
module.exports = {
extends: "../.eslintrc-with-ts-config.js",
extends: "../../.eslintrc-with-ts-config.js",
};

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-core",
"version": "1.42.0-next",
"version": "1.43.0-next",
"description": "Playwright Component Testing Helpers",
"repository": {
"type": "git",
@ -26,9 +26,9 @@
}
},
"dependencies": {
"playwright-core": "1.42.0-next",
"playwright-core": "1.43.0-next",
"vite": "^5.0.12",
"playwright": "1.42.0-next"
"playwright": "1.43.0-next"
},
"bin": {
"playwright": "cli.js"

View file

@ -75,7 +75,7 @@ export default declare((api: BabelAPI) => {
const ext = path.extname(importNode.source.value);
// Convert all non-JS imports into refs.
if (!allJsExtensions.has(ext)) {
if (artifactExtensions.has(ext)) {
for (const specifier of importNode.specifiers) {
if (t.isImportNamespaceSpecifier(specifier))
continue;
@ -171,4 +171,29 @@ export function importInfo(importNode: T.ImportDeclaration, specifier: T.ImportS
return { localName: specifier.local.name, info: result };
}
const allJsExtensions = new Set(['.js', '.jsx', '.cjs', '.mjs', '.ts', '.tsx', '.cts', '.mts', '']);
const artifactExtensions = new Set([
// Frameworks
'.vue',
'.svelte',
// Images
'.jpg', '.jpeg',
'.png',
'.gif',
'.svg',
'.bmp',
'.webp',
'.ico',
// CSS
'.css',
// Fonts
'.woff', '.woff2',
'.ttf',
'.otf',
'.eot',
// Other assets
'.json',
]);

Some files were not shown because too many files have changed in this diff Show more