Merge branch 'main' of https://github.com/JacksonLei123/playwright
This commit is contained in:
commit
92e2d01329
21
.github/workflows/tests_secondary.yml
vendored
21
.github/workflows/tests_secondary.yml
vendored
|
|
@ -234,6 +234,27 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: chromium-tip-of-tree
|
||||
|
||||
chromium_tot_headless_shell:
|
||||
name: Chromium tip-of-tree headless-shell-${{ matrix.os }}
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/run-test
|
||||
with:
|
||||
browsers-to-install: chromium-tip-of-tree-headless-shell
|
||||
command: npm run ctest
|
||||
bot-name: "chromium-tip-of-tree-headless-shell-${{ matrix.os }}"
|
||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
||||
env:
|
||||
PWTEST_CHANNEL: chromium-tip-of-tree-headless-shell
|
||||
|
||||
firefox_beta:
|
||||
name: Firefox Beta ${{ matrix.os }}
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,16 @@ npm run watch
|
|||
npx playwright install
|
||||
```
|
||||
|
||||
**Experimental dev mode with Hot Module Replacement for recorder/trace-viewer/UI Mode**
|
||||
|
||||
```
|
||||
PW_HMR=1 npm run watch
|
||||
PW_HMR=1 npx playwright show-trace
|
||||
PW_HMR=1 npm run ctest -- --ui
|
||||
PW_HMR=1 npx playwright codegen
|
||||
PW_HMR=1 npx playwright show-report
|
||||
```
|
||||
|
||||
Playwright is a multi-package repository that uses npm workspaces. For browser APIs, look at [`packages/playwright-core`](https://github.com/microsoft/playwright/blob/main/packages/playwright-core). For test runner, see [`packages/playwright`](https://github.com/microsoft/playwright/blob/main/packages/playwright).
|
||||
|
||||
Note that some files are generated by the build, so the watch process might override your changes if done in the wrong file. For example, TypeScript types for the API are generated from the [`docs/src`](https://github.com/microsoft/playwright/blob/main/docs/src).
|
||||
|
|
@ -35,7 +45,7 @@ Coding style is fully defined in [.eslintrc](https://github.com/microsoft/playwr
|
|||
npm run lint
|
||||
```
|
||||
|
||||
Comments should be generally avoided. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory.
|
||||
Comments should have an explicit purpose and should improve readability rather than hinder it. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory.
|
||||
|
||||
### Write documentation
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 🎭 Playwright
|
||||
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
|
||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
|||
|
||||
| | Linux | macOS | Windows |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Chromium <!-- GEN:chromium-version -->132.0.6834.15<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Chromium <!-- GEN:chromium-version -->132.0.6834.46<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Firefox <!-- GEN:firefox-version -->132.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ In case this browser is connected to, clears all created contexts belonging to t
|
|||
browser server.
|
||||
|
||||
:::note
|
||||
This is similar to force quitting the browser. Therefore, you should call [`method: BrowserContext.close`] on any [BrowserContext]'s you explicitly created earlier with [`method: Browser.newContext`] **before** calling [`method: Browser.close`].
|
||||
This is similar to force-quitting the browser. To close pages gracefully and ensure you receive page close events, call [`method: BrowserContext.close`] on any [BrowserContext] instances you explicitly created earlier using [`method: Browser.newContext`] **before** calling [`method: Browser.close`].
|
||||
:::
|
||||
|
||||
The [Browser] object itself is considered to be disposed and cannot be used anymore.
|
||||
|
|
|
|||
|
|
@ -1412,7 +1412,7 @@ This setting will change the default maximum time for all the methods accepting
|
|||
* since: v1.8
|
||||
- `timeout` <[float]>
|
||||
|
||||
Maximum time in milliseconds
|
||||
Maximum time in milliseconds. Pass `0` to disable timeout.
|
||||
|
||||
## async method: BrowserContext.setExtraHTTPHeaders
|
||||
* since: v1.8
|
||||
|
|
|
|||
|
|
@ -161,6 +161,41 @@ await page.Clock.PauseAtAsync(DateTime.Parse("2020-02-02"));
|
|||
await page.Clock.PauseAtAsync("2020-02-02");
|
||||
```
|
||||
|
||||
For best results, install the clock before navigating the page and set it to a time slightly before the intended test time. This ensures that all timers run normally during page loading, preventing the page from getting stuck. Once the page has fully loaded, you can safely use [`method: Clock.pauseAt`] to pause the clock.
|
||||
|
||||
```js
|
||||
// Initialize clock with some time before the test time and let the page load
|
||||
// naturally. `Date.now` will progress as the timers fire.
|
||||
await page.clock.install({ time: new Date('2024-12-10T08:00:00') });
|
||||
await page.goto('http://localhost:3333');
|
||||
await page.clock.pauseAt(new Date('2024-12-10T10:00:00'));
|
||||
```
|
||||
|
||||
```python async
|
||||
# Initialize clock with some time before the test time and let the page load
|
||||
# naturally. `Date.now` will progress as the timers fire.
|
||||
await page.clock.install(time=datetime.datetime(2024, 12, 10, 8, 0, 0))
|
||||
await page.goto("http://localhost:3333")
|
||||
await page.clock.pause_at(datetime.datetime(2024, 12, 10, 10, 0, 0))
|
||||
```
|
||||
|
||||
```python sync
|
||||
# Initialize clock with some time before the test time and let the page load
|
||||
# naturally. `Date.now` will progress as the timers fire.
|
||||
page.clock.install(time=datetime.datetime(2024, 12, 10, 8, 0, 0))
|
||||
page.goto("http://localhost:3333")
|
||||
page.clock.pause_at(datetime.datetime(2024, 12, 10, 10, 0, 0))
|
||||
```
|
||||
|
||||
```java
|
||||
// Initialize clock with some time before the test time and let the page load
|
||||
// naturally. `Date.now` will progress as the timers fire.
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
|
||||
page.clock().install(new Clock.InstallOptions().setTime(format.parse("2024-12-10T08:00:00")));
|
||||
page.navigate("http://localhost:3333");
|
||||
page.clock().pauseAt(format.parse("2024-12-10T10:00:00"));
|
||||
```
|
||||
|
||||
### param: Clock.pauseAt.time
|
||||
* langs: js, java
|
||||
* since: v1.45
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ Additional locator to match.
|
|||
- returns: <[string]>
|
||||
|
||||
Captures the aria snapshot of the given element.
|
||||
Read more about [aria snapshots](../aria-snapshots.md) and [`method: LocatorAssertions.toMatchAriaSnapshot`] for the corresponding assertion.
|
||||
Read more about [aria snapshots](../aria-snapshots.md) and [`method: LocatorAssertions.toMatchAriaSnapshot#2`] for the corresponding assertion.
|
||||
|
||||
**Usage**
|
||||
|
||||
|
|
|
|||
|
|
@ -446,7 +446,7 @@ Expected options currently selected.
|
|||
* since: v1.49
|
||||
* langs: python
|
||||
|
||||
The opposite of [`method: LocatorAssertions.toMatchAriaSnapshot`].
|
||||
The opposite of [`method: LocatorAssertions.toMatchAriaSnapshot#2`].
|
||||
|
||||
### param: LocatorAssertions.NotToMatchAriaSnapshot.expected
|
||||
* since: v1.49
|
||||
|
|
@ -2121,7 +2121,58 @@ Expected options currently selected.
|
|||
* since: v1.23
|
||||
|
||||
|
||||
## async method: LocatorAssertions.toMatchAriaSnapshot
|
||||
## async method: LocatorAssertions.toMatchAriaSnapshot#1
|
||||
* since: v1.50
|
||||
* langs:
|
||||
- alias-java: matchesAriaSnapshot
|
||||
|
||||
Asserts that the target element matches the given [accessibility snapshot](../aria-snapshots.md).
|
||||
|
||||
**Usage**
|
||||
|
||||
```js
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot();
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot({ path: '/path/to/snapshot.yml' });
|
||||
```
|
||||
|
||||
```python async
|
||||
await expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml')
|
||||
```
|
||||
|
||||
```python sync
|
||||
expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml')
|
||||
```
|
||||
|
||||
```csharp
|
||||
await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(new { Path = "/path/to/snapshot.yml" });
|
||||
```
|
||||
|
||||
```java
|
||||
assertThat(page.locator("body")).matchesAriaSnapshot(new LocatorAssertions.MatchesAriaSnapshotOptions().setPath("/path/to/snapshot.yml"));
|
||||
```
|
||||
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot#1.name
|
||||
* since: v1.50
|
||||
* langs: js
|
||||
- `name` <[string]>
|
||||
|
||||
Name of the snapshot to store in the snapshot folder corresponding to this test. Generates ordinal name if not specified.
|
||||
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot#1.path
|
||||
* since: v1.50
|
||||
- `path` <[string]>
|
||||
|
||||
Path to the YAML snapshot file.
|
||||
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot#1.timeout = %%-js-assertions-timeout-%%
|
||||
* since: v1.50
|
||||
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot#1.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
* since: v1.50
|
||||
|
||||
|
||||
## async method: LocatorAssertions.toMatchAriaSnapshot#2
|
||||
* since: v1.49
|
||||
* langs:
|
||||
- alias-java: matchesAriaSnapshot
|
||||
|
|
@ -2170,12 +2221,12 @@ assertThat(page.locator("body")).matchesAriaSnapshot("""
|
|||
""");
|
||||
```
|
||||
|
||||
### param: LocatorAssertions.toMatchAriaSnapshot.expected
|
||||
### param: LocatorAssertions.toMatchAriaSnapshot#2.expected
|
||||
* since: v1.49
|
||||
- `expected` <string>
|
||||
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%%
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-js-assertions-timeout-%%
|
||||
* since: v1.49
|
||||
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
* since: v1.49
|
||||
|
|
|
|||
|
|
@ -2762,10 +2762,6 @@ This method requires Playwright to be started in a headed mode, with a falsy [`o
|
|||
|
||||
Returns the PDF buffer.
|
||||
|
||||
:::note
|
||||
Generating a pdf is currently only supported in Chromium headless.
|
||||
:::
|
||||
|
||||
`page.pdf()` generates a pdf of the page with `print` css media. To generate a pdf with `screen` media, call
|
||||
[`method: Page.emulateMedia`] before calling `page.pdf()`:
|
||||
|
||||
|
|
@ -3974,7 +3970,7 @@ This setting will change the default maximum time for all the methods accepting
|
|||
* since: v1.8
|
||||
- `timeout` <[float]>
|
||||
|
||||
Maximum time in milliseconds
|
||||
Maximum time in milliseconds. Pass `0` to disable timeout.
|
||||
|
||||
## async method: Page.setExtraHTTPHeaders
|
||||
* since: v1.8
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ structure of a page, use the [Chrome DevTools Accessibility Pane](https://develo
|
|||
|
||||
## Snapshot matching
|
||||
|
||||
The [`method: LocatorAssertions.toMatchAriaSnapshot`] assertion method in Playwright compares the accessible
|
||||
The [`method: LocatorAssertions.toMatchAriaSnapshot#2`] assertion method in Playwright compares the accessible
|
||||
structure of the locator scope with a predefined aria snapshot template, helping validate the page's state against
|
||||
testing requirements.
|
||||
|
||||
|
|
|
|||
|
|
@ -342,7 +342,7 @@ Playwright ships a regular Chromium build for headed operations and a separate [
|
|||
|
||||
#### Optimize download size on CI
|
||||
|
||||
If you are only running tests in headless mode, for example on CI, you can avoid downloading a regular version of Chromium by passing `--only-shell` during installation.
|
||||
If you are only running tests in headless shell (i.e. the `channel` option is not specified), for example on CI, you can avoid downloading the full Chromium browser by passing `--only-shell` during installation.
|
||||
|
||||
```bash js
|
||||
# only running tests headlessly
|
||||
|
|
|
|||
|
|
@ -214,16 +214,15 @@ def test_popup_page(page: Page, extension_id: str) -> None:
|
|||
|
||||
## Headless mode
|
||||
|
||||
By default, Chrome's headless mode in Playwright does not support Chrome extensions. To overcome this limitation, you can run Chrome's persistent context with a new headless mode by using the following code:
|
||||
By default, Chrome's headless mode in Playwright does not support Chrome extensions. To overcome this limitation, you can run Chrome's persistent context with a new headless mode by using [channel `chromium`](./browsers.md#opt-in-to-new-headless-mode):
|
||||
|
||||
```js title="fixtures.ts"
|
||||
// ...
|
||||
|
||||
const pathToExtension = path.join(__dirname, 'my-extension');
|
||||
const context = await chromium.launchPersistentContext('', {
|
||||
headless: false,
|
||||
channel: 'chromium',
|
||||
args: [
|
||||
`--headless=new`,
|
||||
`--disable-extensions-except=${pathToExtension}`,
|
||||
`--load-extension=${pathToExtension}`,
|
||||
],
|
||||
|
|
@ -235,9 +234,8 @@ const context = await chromium.launchPersistentContext('', {
|
|||
path_to_extension = Path(__file__).parent.joinpath("my-extension")
|
||||
context = playwright.chromium.launch_persistent_context(
|
||||
"",
|
||||
headless=False,
|
||||
channel="chromium",
|
||||
args=[
|
||||
"--headless=new",
|
||||
f"--disable-extensions-except={path_to_extension}",
|
||||
f"--load-extension={path_to_extension}",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -454,11 +454,11 @@ jobs:
|
|||
|
||||
### Docker
|
||||
|
||||
We have a [pre-built Docker image](./docker.md) which can either be used directly, or as a reference to update your existing Docker definitions.
|
||||
We have a [pre-built Docker image](./docker.md) which can either be used directly or as a reference to update your existing Docker definitions.
|
||||
|
||||
Suggested configuration
|
||||
1. Using `--ipc=host` is also recommended when using Chromium. Without it Chromium can run out of memory
|
||||
and crash. Learn more about this option in [Docker docs](https://docs.docker.com/engine/reference/run/#ipc-settings---ipc).
|
||||
and crash. Learn more about this option in [Docker docs](https://docs.docker.com/reference/cli/docker/container/run/#ipc).
|
||||
1. Seeing other weird errors when launching Chromium? Try running your container
|
||||
with `docker run --cap-add=SYS_ADMIN` when developing locally.
|
||||
1. Using `--init` Docker flag or [dumb-init](https://github.com/Yelp/dumb-init) is recommended to avoid special
|
||||
|
|
@ -466,7 +466,7 @@ Suggested configuration
|
|||
|
||||
### Azure Pipelines
|
||||
|
||||
For Windows or macOS agents, no additional configuration required, just install Playwright and run your tests.
|
||||
For Windows or macOS agents, no additional configuration is required, just install Playwright and run your tests.
|
||||
|
||||
For Linux agents, you can use [our Docker container](./docker.md) with Azure
|
||||
Pipelines support [running containerized
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ The recommended approach is to use `setFixedTime` to set the time to a specific
|
|||
- `Event.timeStamp`
|
||||
:::
|
||||
|
||||
:::warning
|
||||
If you call `install` at any point in your test, the call _MUST_ occur before any other clock related calls (see note above for list). Calling these methods out of order will result in undefined behavior. For example, you cannot call `setInterval`, followed by `install`, then `clearInterval`, as `install` overrides the native definition of the clock functions.
|
||||
:::
|
||||
|
||||
## Test with predefined time
|
||||
|
||||
Often you only need to fake `Date.now` while keeping the timers going.
|
||||
|
|
|
|||
|
|
@ -164,19 +164,19 @@ You can use the test generator to generate tests using emulation so as to genera
|
|||
Playwright opens a browser window with its viewport set to a specific width and height and is not responsive as tests need to be run under the same conditions. Use the `--viewport` option to generate tests with a different viewport size.
|
||||
|
||||
```bash js
|
||||
npx playwright codegen --viewport-size=800,600 playwright.dev
|
||||
npx playwright codegen --viewport-size="800,600" playwright.dev
|
||||
```
|
||||
|
||||
```bash java
|
||||
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="codegen --viewport-size=800,600 playwright.dev"
|
||||
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="codegen --viewport-size='800,600' playwright.dev"
|
||||
```
|
||||
|
||||
```bash python
|
||||
playwright codegen --viewport-size=800,600 playwright.dev
|
||||
playwright codegen --viewport-size="800,600" playwright.dev
|
||||
```
|
||||
|
||||
```bash csharp
|
||||
pwsh bin/Debug/netX/playwright.ps1 codegen --viewport-size=800,600 playwright.dev
|
||||
pwsh bin/Debug/netX/playwright.ps1 codegen --viewport-size="800,600" playwright.dev
|
||||
```
|
||||
######
|
||||
* langs: js
|
||||
|
|
|
|||
|
|
@ -103,6 +103,88 @@ Using `--ipc=host` is recommended when using Chrome ([Docker docs](https://docs.
|
|||
|
||||
See our [Continuous Integration guides](./ci.md) for sample configs.
|
||||
|
||||
### Remote Connection
|
||||
|
||||
You can run Playwright Server in Docker while keeping your tests running on the host system or another machine. This is useful for running tests on unsupported Linux distributions or remote execution scenarios.
|
||||
|
||||
#### Running the Playwright Server
|
||||
|
||||
Start the Playwright Server in Docker:
|
||||
|
||||
```bash
|
||||
docker run -p 3000:3000 --rm --init -it --workdir /home/pwuser --user pwuser mcr.microsoft.com/playwright:v%%VERSION%%-noble /bin/sh -c "npx -y playwright@%%VERSION%% run-server --port 3000 --host 0.0.0.0"
|
||||
```
|
||||
|
||||
#### Connecting to the Server
|
||||
* langs: js
|
||||
|
||||
There are two ways to connect to the remote Playwright server:
|
||||
|
||||
1. Using environment variable with `@playwright/test`:
|
||||
|
||||
```bash
|
||||
PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:3000/ npx playwright test
|
||||
```
|
||||
|
||||
2. Using the [`method: BrowserType.connect`] API for other applications:
|
||||
|
||||
```js
|
||||
const browser = await playwright['chromium'].connect('ws://127.0.0.1:3000/');
|
||||
```
|
||||
|
||||
#### Connecting to the Server
|
||||
* langs: python, csharp, java
|
||||
|
||||
```python sync
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.connect("ws://127.0.0.1:3000/")
|
||||
```
|
||||
|
||||
```python async
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.connect("ws://127.0.0.1:3000/")
|
||||
```
|
||||
|
||||
```csharp
|
||||
using Microsoft.Playwright;
|
||||
|
||||
using var playwright = await Playwright.CreateAsync();
|
||||
await using var browser = await playwright.Chromium.ConnectAsync("ws://127.0.0.1:3000/");
|
||||
```
|
||||
|
||||
```java
|
||||
package org.example;
|
||||
|
||||
import com.microsoft.playwright.*;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class App {
|
||||
public static void main(String[] args) {
|
||||
try (Playwright playwright = Playwright.create()) {
|
||||
Browser browser = playwright.chromium().connect("ws://127.0.0.1:3000/");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Network Configuration
|
||||
|
||||
If you need to access local servers from within the Docker container:
|
||||
|
||||
```bash
|
||||
docker run --add-host=hostmachine:host-gateway -p 3000:3000 --rm --init -it --workdir /home/pwuser --user pwuser mcr.microsoft.com/playwright:v%%VERSION%%-noble /bin/sh -c "npx -y playwright@%%VERSION%% run-server --port 3000 --host 0.0.0.0"
|
||||
```
|
||||
|
||||
This makes `hostmachine` point to the host's localhost. Your tests should use `hostmachine` instead of `localhost` when accessing local servers.
|
||||
|
||||
:::note
|
||||
When running tests remotely, ensure the Playwright version in your tests matches the version running in the Docker container.
|
||||
:::
|
||||
|
||||
## Image tags
|
||||
|
||||
See [all available image tags].
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ title: "Installation"
|
|||
|
||||
Playwright was created specifically to accommodate the needs of end-to-end testing. Playwright supports all modern rendering engines including Chromium, WebKit, and Firefox. Test on Windows, Linux, and macOS, locally or on CI, headless or headed with native mobile emulation.
|
||||
|
||||
You can choose to use [MSTest base classes](./test-runners.md) or [NUnit base classes](./test-runners.md) that Playwright provides to write end-to-end tests. These classes support running tests on multiple browser engines, parallelizing tests, adjusting launch/context options and getting a [Page]/[BrowserContext] instance per test out of the box. Alternatively you can use the [library](./library.md) to manually write the testing infrastructure.
|
||||
You can choose to use MSTest, NUnit, or xUnit [base classes](./test-runners.md) that Playwright provides to write end-to-end tests. These classes support running tests on multiple browser engines, parallelizing tests, adjusting launch/context options and getting a [Page]/[BrowserContext] instance per test out of the box. Alternatively you can use the [library](./library.md) to manually write the testing infrastructure.
|
||||
|
||||
1. Start by creating a new project with `dotnet new`. This will create the `PlaywrightTests` directory which includes a `UnitTest1.cs` file:
|
||||
|
||||
|
|
@ -17,6 +17,7 @@ You can choose to use [MSTest base classes](./test-runners.md) or [NUnit base cl
|
|||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
{label: 'xUnit', value: 'xunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
|
@ -34,6 +35,14 @@ dotnet new mstest -n PlaywrightTests
|
|||
cd PlaywrightTests
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="xunit">
|
||||
|
||||
```bash
|
||||
dotnet new xunit -n PlaywrightTests
|
||||
cd PlaywrightTests
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
|
@ -45,6 +54,7 @@ cd PlaywrightTests
|
|||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
{label: 'xUnit', value: 'xunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
|
@ -60,6 +70,13 @@ dotnet add package Microsoft.Playwright.NUnit
|
|||
dotnet add package Microsoft.Playwright.MSTest
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="xunit">
|
||||
|
||||
```bash
|
||||
dotnet add package Microsoft.Playwright.Xunit
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
|
@ -87,6 +104,7 @@ Edit the `UnitTest1.cs` file with the code below to create an example end-to-end
|
|||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
{label: 'xUnit', value: 'xunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
|
@ -164,6 +182,41 @@ public class ExampleTest : PageTest
|
|||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="xunit">
|
||||
|
||||
```csharp title="UnitTest1.cs"
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Playwright;
|
||||
using Microsoft.Playwright.Xunit;
|
||||
|
||||
namespace PlaywrightTests;
|
||||
|
||||
public class UnitTest1: PageTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task HasTitle()
|
||||
{
|
||||
await Page.GotoAsync("https://playwright.dev");
|
||||
|
||||
// Expect a title "to contain" a substring.
|
||||
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStartedLink()
|
||||
{
|
||||
await Page.GotoAsync("https://playwright.dev");
|
||||
|
||||
// Click the get started link.
|
||||
await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
|
||||
|
||||
// Expects page to have a heading with the name of Installation.
|
||||
await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Installation" })).ToBeVisibleAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Running the Example Tests
|
||||
|
|
@ -190,4 +243,4 @@ See our doc on [Running and Debugging Tests](./running-tests.md) to learn more a
|
|||
- [Generate tests with Codegen](./codegen-intro.md)
|
||||
- [See a trace of your tests](./trace-viewer-intro.md)
|
||||
- [Run tests on CI](./ci-intro.md)
|
||||
- [Learn more about the MSTest and NUnit base classes](./test-runners.md)
|
||||
- [Learn more about the MSTest, NUnit, and xUnit base classes](./test-runners.md)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ You can choose any testing framework such as JUnit or TestNG based on your proje
|
|||
|
||||
## .NET
|
||||
|
||||
Playwright for .NET comes with [MSTest base classes](https://playwright.dev/dotnet/docs/test-runners) and [NUnit base classes](https://playwright.dev/dotnet/docs/test-runners) for writing end-to-end tests.
|
||||
Playwright for .NET comes with MSTest, NUnit, and xUnit [base classes](https://playwright.dev/dotnet/docs/test-runners) for writing end-to-end tests.
|
||||
|
||||
* [Documentation](https://playwright.dev/dotnet/docs/intro)
|
||||
* [GitHub repo](https://github.com/microsoft/playwright-dotnet)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ title: "Getting started - Library"
|
|||
|
||||
## Introduction
|
||||
|
||||
Playwright can either be used with the [MSTest](./test-runners.md) or [NUnit](./test-runners.md), or as a Playwright Library (this guide). If you are working on an application that utilizes Playwright capabilities or you are using Playwright with another test runner, read on.
|
||||
Playwright can either be used with the [MSTest, NUnit, or xUnit base classes](./test-runners.md) or as a Playwright Library (this guide). If you are working on an application that utilizes Playwright capabilities or you are using Playwright with another test runner, read on.
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
|
|||
|
|
@ -115,8 +115,7 @@ await page.GotoAsync("https://example.com");
|
|||
You can configure pages to load over the HTTP(S) proxy or SOCKSv5. Proxy can be either set globally
|
||||
for the entire browser, or for each browser context individually.
|
||||
|
||||
You can optionally specify username and password for HTTP(S) proxy, you can also specify hosts to
|
||||
bypass proxy for.
|
||||
You can optionally specify username and password for HTTP(S) proxy, you can also specify hosts to bypass the [`option: Browser.newContext.proxy`] for.
|
||||
|
||||
Here is an example of a global proxy:
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ toc_max_heading_level: 2
|
|||
|
||||
### Aria snapshots
|
||||
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#2`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
|
||||
```csharp
|
||||
await page.GotoAsync("https://playwright.dev");
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ toc_max_heading_level: 2
|
|||
|
||||
### Aria snapshots
|
||||
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#2`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
|
||||
```java
|
||||
page.navigate("https://playwright.dev");
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
|
|||
|
||||
### Aria snapshots
|
||||
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#2`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
|
||||
```js
|
||||
await page.goto('https://playwright.dev');
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ toc_max_heading_level: 2
|
|||
|
||||
### Aria snapshots
|
||||
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#2`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
|
||||
```python
|
||||
page.goto("https://playwright.dev")
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ dotnet test --filter "Name~GetStartedLink"
|
|||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
{label: 'xUnit', value: 'xunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
|
@ -128,6 +129,19 @@ dotnet test -- NUnit.NumberOfTestWorkers=5
|
|||
dotnet test -- MSTest.Parallelize.Workers=5
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="xunit">
|
||||
|
||||
```bash
|
||||
dotnet test -- xUnit.MaxParallelThreads=5
|
||||
```
|
||||
|
||||
See [here](https://xunit.net/docs/running-tests-in-parallel.html) for more information to run tests in parallel with xUnit.
|
||||
|
||||
:::note
|
||||
We recommend xUnit 2.8+ which uses the [`conservative` parallelism algorithm](https://xunit.net/docs/running-tests-in-parallel.html#algorithms) by default.
|
||||
:::
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
|
|
|||
|
|
@ -392,8 +392,11 @@ export default defineConfig({
|
|||
});
|
||||
```
|
||||
|
||||
## property: TestOptions.locale = %%-context-option-locale-%%
|
||||
## property: TestOptions.locale
|
||||
* since: v1.10
|
||||
- type: <[string]>
|
||||
|
||||
Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, `Accept-Language` request header value as well as number and date formatting rules. Defaults to `en-US`. Learn more about emulation in our [emulation guide](../emulation.md#locale--timezone).
|
||||
|
||||
**Usage**
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ expect.set_options(timeout=10_000)
|
|||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
{label: 'xUnit', value: 'xunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
|
@ -127,6 +128,24 @@ public class UnitTest1 : PageTest
|
|||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="xunit">
|
||||
|
||||
```csharp title="UnitTest1.cs"
|
||||
using Microsoft.Playwright;
|
||||
using Microsoft.Playwright.Xunit;
|
||||
|
||||
namespace PlaywrightTests;
|
||||
|
||||
public class UnitTest1: PageTest
|
||||
{
|
||||
UnitTest1()
|
||||
{
|
||||
SetDefaultExpectTimeout(10_000);
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ Complete set of Playwright Test options is available in the [configuration file]
|
|||
| Option | Description |
|
||||
| :- | :- |
|
||||
| Non-option arguments | Each argument is treated as a regular expression matched against the full test file path. Only tests from files matching the pattern will be executed. Special symbols like `$` or `*` should be escaped with `\`. In many shells/terminals you may need to quote the arguments. |
|
||||
| `-c <file>` or `--config <file>` | Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}". Defaults to `playwright.config.ts` or `playwright.config.js` in the current directory. |
|
||||
| `-c <file>` or `--config <file>` | Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}". Defaults to `playwright.config.ts` or `playwright.config.js` in the current directory. |
|
||||
| `--debug` | Run tests with Playwright Inspector. Shortcut for `PWDEBUG=1` environment variable and `--timeout=0 --max-failures=1 --headed --workers=1` options. |
|
||||
| `--fail-on-flaky-tests` | Fail if any test is flagged as flaky (default: false). |
|
||||
| `--forbid-only` | Fail if `test.only` is called (default: false). Useful on CI. |
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ title: "Test Runners"
|
|||
|
||||
## Introduction
|
||||
|
||||
While Playwright for .NET isn't tied to a particular test runner or testing framework, in our experience the easiest way of getting started is by using the base classes we provide for MSTest and NUnit. These classes support running tests on multiple browser engines, adjusting launch/context options and getting a [Page]/[BrowserContext] instance per test out of the box.
|
||||
While Playwright for .NET isn't tied to a particular test runner or testing framework, in our experience the easiest way of getting started is by using the base classes we provide for MSTest, NUnit, or xUnit. These classes support running tests on multiple browser engines, adjusting launch/context options and getting a [Page]/[BrowserContext] instance per test out of the box.
|
||||
|
||||
Playwright and Browser instances will be reused between tests for better performance. We
|
||||
recommend running each test case in a new BrowserContext, this way browser state will be
|
||||
|
|
@ -17,6 +17,7 @@ isolated between the tests.
|
|||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
{label: 'xUnit', value: 'xunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
|
@ -28,6 +29,11 @@ Playwright provides base classes to write tests with NUnit via the [`Microsoft.P
|
|||
|
||||
Playwright provides base classes to write tests with MSTest via the [`Microsoft.Playwright.MSTest`](https://www.nuget.org/packages/Microsoft.Playwright.MSTest) package.
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="xunit">
|
||||
|
||||
Playwright provides base classes to write tests with xUnit via the [`Microsoft.Playwright.Xunit`](https://www.nuget.org/packages/Microsoft.Playwright.Xunit) package.
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
|
@ -41,6 +47,7 @@ Check out the [installation guide](./intro.md) to get started.
|
|||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
{label: 'xUnit', value: 'xunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
|
@ -64,6 +71,20 @@ Running tests in parallel at the method level (`ExecutionScope.MethodLevel`) is
|
|||
dotnet test --settings:.runsettings -- MSTest.Parallelize.Workers=4
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="xunit">
|
||||
|
||||
By default xUnit will run all classes in parallel, while running tests inside each class sequentially.
|
||||
It will create by default as many processes as there are cores on the system. You can adjust this behavior by using the following CLI parameter or using a `.runsettings` file, see below.
|
||||
|
||||
```bash
|
||||
dotnet test -- xUnit.MaxParallelThreads=5
|
||||
```
|
||||
|
||||
:::note
|
||||
We recommend xUnit 2.8+ which uses the [`conservative` parallelism algorithm](https://xunit.net/docs/running-tests-in-parallel.html#algorithms) by default.
|
||||
:::
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
|
@ -76,6 +97,7 @@ dotnet test --settings:.runsettings -- MSTest.Parallelize.Workers=4
|
|||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
{label: 'xUnit', value: 'xunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
|
@ -154,6 +176,41 @@ public class ExampleTest : PageTest
|
|||
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="xunit">
|
||||
|
||||
To customize context options, you can override the `ContextOptions` method of your test class derived from `Microsoft.Playwright.Xunit.PageTest` or `Microsoft.Playwright.Xunit.ContextTest`. See the following example:
|
||||
|
||||
```csharp
|
||||
using Microsoft.Playwright;
|
||||
using Microsoft.Playwright.Xunit;
|
||||
|
||||
namespace PlaywrightTests;
|
||||
|
||||
public class UnitTest1 : PageTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task TestWithCustomContextOptions()
|
||||
{
|
||||
// The following Page (and BrowserContext) instance has the custom colorScheme, viewport and baseURL set:
|
||||
await Page.GotoAsync("/login");
|
||||
}
|
||||
public override BrowserNewContextOptions ContextOptions()
|
||||
{
|
||||
return new BrowserNewContextOptions()
|
||||
{
|
||||
ColorScheme = ColorScheme.Light,
|
||||
ViewportSize = new()
|
||||
{
|
||||
Width = 1920,
|
||||
Height = 1080
|
||||
},
|
||||
BaseURL = "https://github.com",
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
|
@ -194,6 +251,7 @@ When running tests from Visual Studio, you can take advantage of the `.runsettin
|
|||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
{label: 'xUnit', value: 'xunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
|
@ -259,6 +317,36 @@ For example, to specify the number of workers, you can use `MSTest.Parallelize.W
|
|||
</RunSettings>
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="xunit">
|
||||
|
||||
For example, to specify the number of workers, you can use `xUnit.MaxParallelThreads`. You can also enable `DEBUG` logs using `RunConfiguration.EnvironmentVariables`.
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RunSettings>
|
||||
<!-- See https://xunit.net/docs/runsettings -->
|
||||
<xUnit>
|
||||
<MaxParallelThreads>1</MaxParallelThreads>
|
||||
</xUnit>
|
||||
<!-- General run configuration -->
|
||||
<RunConfiguration>
|
||||
<EnvironmentVariables>
|
||||
<!-- For debugging selectors, it's recommend to set the following environment variable -->
|
||||
<DEBUG>pw:api</DEBUG>
|
||||
</EnvironmentVariables>
|
||||
</RunConfiguration>
|
||||
<!-- Playwright -->
|
||||
<Playwright>
|
||||
<BrowserName>chromium</BrowserName>
|
||||
<ExpectTimeout>5000</ExpectTimeout>
|
||||
<LaunchOptions>
|
||||
<Headless>false</Headless>
|
||||
<Channel>msedge</Channel>
|
||||
</LaunchOptions>
|
||||
</Playwright>
|
||||
</RunSettings>
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
|
@ -270,6 +358,7 @@ For example, to specify the number of workers, you can use `MSTest.Parallelize.W
|
|||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
{label: 'xUnit', value: 'xunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
|
@ -281,6 +370,11 @@ There are a few base classes available to you in `Microsoft.Playwright.NUnit` na
|
|||
|
||||
There are a few base classes available to you in `Microsoft.Playwright.MSTest` namespace:
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="xunit">
|
||||
|
||||
There are a few base classes available to you in `Microsoft.Playwright.Xunit` namespace:
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
|
@ -290,8 +384,3 @@ There are a few base classes available to you in `Microsoft.Playwright.MSTest` n
|
|||
|ContextTest |Each test will get a fresh copy of a [BrowserContext]. You can create as many pages in this context as you'd like. Using this test is the easiest way to test multi-page scenarios where you need more than one tab.<br></br><br></br>Note: You can override the `ContextOptions` method in each test file to control context options, the ones typically passed into the [`method: Browser.newContext`] method. That way you can specify all kinds of emulation options for your test file individually.|
|
||||
|BrowserTest |Each test will get a browser and can create as many contexts as it likes. Each test is responsible for cleaning up all the contexts it created.|
|
||||
|PlaywrightTest|This gives each test a Playwright object so that the test could start and stop as many browsers as it likes.|
|
||||
|
||||
## xUnit support
|
||||
|
||||
While using xUnit is also supported, we do not support running parallel tests. This is a well known problem/design limitation
|
||||
outlined by the maintainers across [several](https://github.com/xunit/xunit/issues/2003) [issues](https://github.com/xunit/xunit/issues/2111#issuecomment-650004247).
|
||||
|
|
|
|||
|
|
@ -259,3 +259,18 @@ def test_bing_is_working(page):
|
|||
## Deploy to CI
|
||||
|
||||
See the [guides for CI providers](./ci.md) to deploy your tests to CI/CD.
|
||||
|
||||
## Async Fixtures
|
||||
|
||||
If you want to use async fixtures, you can use the [`pytest-playwright-asyncio`](https://pypi.org/project/pytest-playwright-asyncio/) plugin.
|
||||
Make sure to use `pytest-asyncio>=0.24.0` and make your tests use of [`loop_scope=sesion`](https://pytest-asyncio.readthedocs.io/en/latest/how-to-guides/run_session_tests_in_same_loop.html).
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from playwright.async_api import Page
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_foo(page: Page):
|
||||
await page.goto("https://github.com")
|
||||
# ...
|
||||
```
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ When `fullyParallel: true` is enabled, Playwright Test runs individual tests in
|
|||
|
||||
**Sharding without fullyParallel**
|
||||
|
||||
Without the fullyParallel setting, Playwright Test defaults to file-level granularity, meaning entire test files are assigned to shards. In this case, the number of tests per file can greatly influence shard distribution. If your test files are not evenly sized (i.e., some files contain many more tests than others), certain shards may end up running significantly more tests, while others may run fewer or even none.
|
||||
Without the fullyParallel setting, Playwright Test defaults to file-level granularity, meaning entire test files are assigned to shards (note that the same file may be assigned to different shards across different projects). In this case, the number of tests per file can greatly influence shard distribution. If your test files are not evenly sized (i.e., some files contain many more tests than others), certain shards may end up running significantly more tests, while others may run fewer or even none.
|
||||
|
||||
**Key Takeaways:**
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
|
|||
|
||||
## Introduction
|
||||
|
||||
UI Mode lets you explore, run and debug tests with a time travel experience complete with watch mode. All test files are loaded into the testing sidebar where you can expand each file and describe block to individually run, view, watch and debug each test. Filter tests by **text** or **@tag** or by **passed**, **failed** and **skipped** tests as well as by [**projects**](./test-projects) as set in your `playwright.config` file. See a full trace of your tests and hover back and forward over each action to see what was happening during each step and pop out the DOM snapshot to a separate window for a better debugging experience.
|
||||
UI Mode lets you explore, run, and debug tests with a time travel experience complete with a watch mode. All test files are displayed in the testing sidebar, allowing you to expand each file and describe block to individually run, view, watch, and debug each test. Filter tests by **name**, [**projects**](./test-projects) (set in your `playwright.config` file), **@tag**, or by the execution status of **passed**, **failed**, and **skipped**. See a full trace of your tests and hover back and forward over each action to see what was happening during each step. You can also pop out the DOM snapshot of a given moment into a separate window for a better debugging experience.
|
||||
|
||||
<LiteYouTube
|
||||
id="d0u6XhXknzU"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ Traces can be recorded using the [`property: BrowserContext.tracing`] API as fol
|
|||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
{label: 'xUnit', value: 'xunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
|
@ -112,6 +113,69 @@ public class ExampleTest : PageTest
|
|||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="xunit">
|
||||
|
||||
```csharp
|
||||
using System.Reflection;
|
||||
using Microsoft.Playwright;
|
||||
using Microsoft.Playwright.Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace PlaywrightTests;
|
||||
|
||||
[WithTestName]
|
||||
public class UnitTest1 : PageTest
|
||||
{
|
||||
public override async Task InitializeAsync()
|
||||
{
|
||||
await base.InitializeAsync().ConfigureAwait(false);
|
||||
await Context.Tracing.StartAsync(new()
|
||||
{
|
||||
Title = $"{WithTestNameAttribute.CurrentClassName}.{WithTestNameAttribute.CurrentTestName}",
|
||||
Screenshots = true,
|
||||
Snapshots = true,
|
||||
Sources = true
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task DisposeAsync()
|
||||
{
|
||||
await Context.Tracing.StopAsync(new()
|
||||
{
|
||||
Path = Path.Combine(
|
||||
Environment.CurrentDirectory,
|
||||
"playwright-traces",
|
||||
$"{WithTestNameAttribute.CurrentClassName}.{WithTestNameAttribute.CurrentTestName}.zip"
|
||||
)
|
||||
});
|
||||
await base.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStartedLink()
|
||||
{
|
||||
// ...
|
||||
await Page.GotoAsync("https://playwright.dev/dotnet/docs/intro");
|
||||
}
|
||||
}
|
||||
|
||||
public class WithTestNameAttribute : BeforeAfterTestAttribute
|
||||
{
|
||||
public static string CurrentTestName = string.Empty;
|
||||
public static string CurrentClassName = string.Empty;
|
||||
|
||||
public override void Before(MethodInfo methodInfo)
|
||||
{
|
||||
CurrentTestName = methodInfo.Name;
|
||||
CurrentClassName = methodInfo.DeclaringType!.Name;
|
||||
}
|
||||
|
||||
public override void After(MethodInfo methodInfo)
|
||||
{
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
|
@ -134,4 +198,4 @@ Check out our detailed guide on [Trace Viewer](/trace-viewer.md) to learn more a
|
|||
## What's next
|
||||
|
||||
- [Run tests on CI with GitHub Actions](/ci-intro.md)
|
||||
- [Learn more about the MSTest and NUnit base classes](./test-runners.md)
|
||||
- [Learn more about the MSTest, NUnit, and xUnit base classes](./test-runners.md)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ Playwright Trace Viewer is a GUI tool that lets you explore recorded Playwright
|
|||
|
||||
## Recording a Trace
|
||||
|
||||
By default the [playwright.config](./trace-viewer.md#recording-a-trace-on-ci) file will contain the configuration needed to create a `trace.zip` file for each test. Traces are setup to run `on-first-retry` meaning they will be run on the first retry of a failed test. Also `retries` are set to 2 when running on CI and 0 locally. This means the traces will be recorded on the first retry of a failed test but not on the first run and not on the second retry.
|
||||
By default the [playwright.config](./trace-viewer.md#tracing-on-ci) file will contain the configuration needed to create a `trace.zip` file for each test. Traces are setup to run `on-first-retry` meaning they will be run on the first retry of a failed test. Also `retries` are set to 2 when running on CI and 0 locally. This means the traces will be recorded on the first retry of a failed test but not on the first run and not on the second retry.
|
||||
|
||||
```js title="playwright.config.ts"
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
|
|
|||
|
|
@ -17,106 +17,67 @@ Playwright Trace Viewer is a GUI tool that helps you explore recorded Playwright
|
|||
title="Viewing Playwright Traces"
|
||||
/>
|
||||
|
||||
## Trace Viewer features
|
||||
### Actions
|
||||
## Opening Trace Viewer
|
||||
|
||||
In the Actions tab you can see what locator was used for every action and how long each one took to run. Hover over each action of your test and visually see the change in the DOM snapshot. Go back and forward in time and click an action to inspect and debug. Use the Before and After tabs to visually see what happened before and after the action.
|
||||
You can open a saved trace using either the Playwright CLI or in the browser at [trace.playwright.dev](https://trace.playwright.dev). Make sure to add the full path to where your `trace.zip` file is located.
|
||||
|
||||

|
||||
```bash js
|
||||
npx playwright show-trace path/to/trace.zip
|
||||
```
|
||||
|
||||
**Selecting each action reveals:**
|
||||
- action snapshots
|
||||
- action log
|
||||
- source code location
|
||||
```bash java
|
||||
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace trace.zip"
|
||||
```
|
||||
|
||||
### Screenshots
|
||||
```bash python
|
||||
playwright show-trace trace.zip
|
||||
```
|
||||
|
||||
When tracing with the [`option: Tracing.start.screenshots`] option turned on (default), each trace records a screencast and renders it as a film strip. You can hover over the film strip to see a magnified image of for each action and state which helps you easily find the action you want to inspect.
|
||||
```bash csharp
|
||||
pwsh bin/Debug/netX/playwright.ps1 show-trace trace.zip
|
||||
```
|
||||
|
||||
Double click on an action to see the time range for that action. You can use the slider in the timeline to increase the actions selected and these will be shown in the Actions tab and all console logs and network logs will be filtered to only show the logs for the actions selected.
|
||||
### Using [trace.playwright.dev](https://trace.playwright.dev)
|
||||
|
||||

|
||||
[trace.playwright.dev](https://trace.playwright.dev) is a statically hosted variant of the Trace Viewer. You can upload trace files using drag and drop or via the `Select file(s)` button.
|
||||
|
||||
Trace Viewer loads the trace entirely in your browser and does not transmit any data externally.
|
||||
|
||||
### Snapshots
|
||||
<img width="1119" alt="Drop Playwright Trace to load" src="https://user-images.githubusercontent.com/13063165/194577918-b4d45726-2692-4093-8a28-9e73552617ef.png" />
|
||||
|
||||
When tracing with the [`option: Tracing.start.snapshots`] option turned on (default), Playwright captures a set of complete DOM snapshots for each action. Depending on the type of the action, it will capture:
|
||||
### Viewing remote traces
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
|Before|A snapshot at the time action is called.|
|
||||
|Action|A snapshot at the moment of the performed input. This type of snapshot is especially useful when exploring where exactly Playwright clicked.|
|
||||
|After|A snapshot after the action.|
|
||||
You can open remote traces directly using its URL. This makes it easy to view the remote trace without having to manually download the file from CI runs, for example.
|
||||
|
||||
Here is what the typical Action snapshot looks like:
|
||||
```bash js
|
||||
npx playwright show-trace https://example.com/trace.zip
|
||||
```
|
||||
|
||||

|
||||
```bash java
|
||||
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace https://example.com/trace.zip"
|
||||
```
|
||||
|
||||
Notice how it highlights both, the DOM Node as well as the exact click position.
|
||||
```bash python
|
||||
playwright show-trace https://example.com/trace.zip
|
||||
```
|
||||
|
||||
### Source
|
||||
```bash csharp
|
||||
pwsh bin/Debug/netX/playwright.ps1 show-trace https://example.com/trace.zip
|
||||
```
|
||||
|
||||
When you click on an action in the sidebar, the line of code for that action is highlighted in the source panel.
|
||||
When using [trace.playwright.dev](https://trace.playwright.dev), you can also pass the URL of your uploaded trace at some accessible storage (e.g. inside your CI) as a query parameter. CORS (Cross-Origin Resource Sharing) rules might apply.
|
||||
|
||||

|
||||
```txt
|
||||
https://trace.playwright.dev/?trace=https://demo.playwright.dev/reports/todomvc/data/cb0fa77ebd9487a5c899f3ae65a7ffdbac681182.zip
|
||||
```
|
||||
|
||||
### Call
|
||||
|
||||
The call tab shows you information about the action such as the time it took, what locator was used, if in strict mode and what key was used.
|
||||
|
||||

|
||||
|
||||
### Log
|
||||
|
||||
See a full log of your test to better understand what Playwright is doing behind the scenes such as scrolling into view, waiting for element to be visible, enabled and stable and performing actions such as click, fill, press etc.
|
||||
|
||||

|
||||
|
||||
### Errors
|
||||
|
||||
If your test fails you will see the error messages for each test in the Errors tab. The timeline will also show a red line highlighting where the error occurred. You can also click on the source tab to see on which line of the source code the error is.
|
||||
|
||||

|
||||
|
||||
### Console
|
||||
|
||||
See console logs from the browser as well as from your test. Different icons are displayed to show you if the console log came from the browser or from the test file.
|
||||
|
||||

|
||||
|
||||
Double click on an action from your test in the actions sidebar. This will filter the console to only show the logs that were made during that action. Click the *Show all* button to see all console logs again.
|
||||
|
||||
Use the timeline to filter actions, by clicking a start point and dragging to an ending point. The console tab will also be filtered to only show the logs that were made during the actions selected.
|
||||
|
||||
|
||||
### Network
|
||||
|
||||
The Network tab shows you all the network requests that were made during your test. You can sort by different types of requests, status code, method, request, content type, duration and size. Click on a request to see more information about it such as the request headers, response headers, request body and response body.
|
||||
|
||||

|
||||
|
||||
Double click on an action from your test in the actions sidebar. This will filter the network requests to only show the requests that were made during that action. Click the *Show all* button to see all network requests again.
|
||||
|
||||
Use the timeline to filter actions, by clicking a start point and dragging to an ending point. The network tab will also be filtered to only show the network requests that were made during the actions selected.
|
||||
|
||||
### Metadata
|
||||
|
||||
Next to the Actions tab you will find the Metadata tab which will show you more information on your test such as the Browser, viewport size, test duration and more.
|
||||
|
||||

|
||||
|
||||
### Attachments
|
||||
## Recording a trace
|
||||
* langs: js
|
||||
|
||||
The "Attachments" tab allows you to explore attachments. If you're doing [visual regression testing](./test-snapshots.md), you'll be able to compare screenshots by examining the image diff, the actual image and the expected image. When you click on the expected image you can use the slider to slide one image over the other so you can easily see the differences in your screenshots.
|
||||
|
||||

|
||||
|
||||
|
||||
## Recording a trace locally
|
||||
### Tracing locally
|
||||
* langs: js
|
||||
|
||||
To record a trace during development mode set the `--trace` flag to `on` when running your tests. You can also use [UI Mode](./test-ui-mode.md) for a better developer experience.
|
||||
To record a trace during development mode set the `--trace` flag to `on` when running your tests. You can also use [UI Mode](./test-ui-mode.md) for a better developer experience, as it traces each test automatically.
|
||||
|
||||
```bash
|
||||
npx playwright test --trace on
|
||||
|
|
@ -126,7 +87,7 @@ You can then open the HTML report and click on the trace icon to open the trace.
|
|||
```bash
|
||||
npx playwright show-report
|
||||
```
|
||||
## Recording a trace on CI
|
||||
### Tracing on CI
|
||||
* langs: js
|
||||
|
||||
Traces should be run on continuous integration on the first retry of a failed test
|
||||
|
|
@ -254,6 +215,7 @@ Traces can be recorded using the [`property: BrowserContext.tracing`] API as fol
|
|||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
{label: 'xUnit', value: 'xunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
|
@ -348,6 +310,70 @@ public class UnitTest1 : PageTest
|
|||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="xunit">
|
||||
|
||||
```csharp
|
||||
using System.Reflection;
|
||||
using Microsoft.Playwright;
|
||||
using Microsoft.Playwright.Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace PlaywrightTests;
|
||||
|
||||
[WithTestName]
|
||||
public class UnitTest1 : PageTest
|
||||
{
|
||||
public override async Task InitializeAsync()
|
||||
{
|
||||
await base.InitializeAsync().ConfigureAwait(false);
|
||||
await Context.Tracing.StartAsync(new()
|
||||
{
|
||||
Title = $"{WithTestNameAttribute.CurrentClassName}.{WithTestNameAttribute.CurrentTestName}",
|
||||
Screenshots = true,
|
||||
Snapshots = true,
|
||||
Sources = true
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task DisposeAsync()
|
||||
{
|
||||
await Context.Tracing.StopAsync(new()
|
||||
{
|
||||
Path = Path.Combine(
|
||||
Environment.CurrentDirectory,
|
||||
"playwright-traces",
|
||||
$"{WithTestNameAttribute.CurrentClassName}.{WithTestNameAttribute.CurrentTestName}.zip"
|
||||
)
|
||||
});
|
||||
await base.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStartedLink()
|
||||
{
|
||||
// ...
|
||||
await Page.GotoAsync("https://playwright.dev/dotnet/docs/intro");
|
||||
}
|
||||
}
|
||||
|
||||
public class WithTestNameAttribute : BeforeAfterTestAttribute
|
||||
{
|
||||
public static string CurrentTestName = string.Empty;
|
||||
public static string CurrentClassName = string.Empty;
|
||||
|
||||
public override void Before(MethodInfo methodInfo)
|
||||
{
|
||||
CurrentTestName = methodInfo.Name;
|
||||
CurrentClassName = methodInfo.DeclaringType!.Name;
|
||||
}
|
||||
|
||||
public override void After(MethodInfo methodInfo)
|
||||
{
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
|
@ -365,6 +391,7 @@ Setup your tests to record a trace only when the test fails:
|
|||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
{label: 'xUnit', value: 'xunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
|
@ -459,60 +486,165 @@ public class ExampleTest : PageTest
|
|||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="xunit">
|
||||
|
||||
```csharp
|
||||
using System.Reflection;
|
||||
using Microsoft.Playwright;
|
||||
using Microsoft.Playwright.Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace PlaywrightTests;
|
||||
|
||||
[WithTestName]
|
||||
public class UnitTest1 : PageTest
|
||||
{
|
||||
public override async Task InitializeAsync()
|
||||
{
|
||||
await base.InitializeAsync().ConfigureAwait(false);
|
||||
await Context.Tracing.StartAsync(new()
|
||||
{
|
||||
Title = $"{WithTestNameAttribute.CurrentClassName}.{WithTestNameAttribute.CurrentTestName}",
|
||||
Screenshots = true,
|
||||
Snapshots = true,
|
||||
Sources = true
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task DisposeAsync()
|
||||
{
|
||||
await Context.Tracing.StopAsync(new()
|
||||
{
|
||||
Path = !TestOk ? Path.Combine(
|
||||
Environment.CurrentDirectory,
|
||||
"playwright-traces",
|
||||
$"{WithTestNameAttribute.CurrentClassName}.{WithTestNameAttribute.CurrentTestName}.zip"
|
||||
) : null
|
||||
});
|
||||
await base.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStartedLink()
|
||||
{
|
||||
// ...
|
||||
await Page.GotoAsync("https://playwright.dev/dotnet/docs/intro");
|
||||
}
|
||||
}
|
||||
|
||||
public class WithTestNameAttribute : BeforeAfterTestAttribute
|
||||
{
|
||||
public static string CurrentTestName = string.Empty;
|
||||
public static string CurrentClassName = string.Empty;
|
||||
|
||||
public override void Before(MethodInfo methodInfo)
|
||||
{
|
||||
CurrentTestName = methodInfo.Name;
|
||||
CurrentClassName = methodInfo.DeclaringType!.Name;
|
||||
}
|
||||
|
||||
public override void After(MethodInfo methodInfo)
|
||||
{
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Opening the trace
|
||||
## Trace Viewer features
|
||||
### Actions
|
||||
|
||||
You can open the saved trace using the Playwright CLI or in your browser on [`trace.playwright.dev`](https://trace.playwright.dev). Make sure to add the full path to where your `trace.zip` file is located.
|
||||
In the Actions tab you can see what locator was used for every action and how long each one took to run. Hover over each action of your test and visually see the change in the DOM snapshot. Go back and forward in time and click an action to inspect and debug. Use the Before and After tabs to visually see what happened before and after the action.
|
||||
|
||||
```bash js
|
||||
npx playwright show-trace path/to/trace.zip
|
||||
```
|
||||

|
||||
|
||||
```bash java
|
||||
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace trace.zip"
|
||||
```
|
||||
**Selecting each action reveals:**
|
||||
- Action snapshots
|
||||
- Action log
|
||||
- Source code location
|
||||
|
||||
```bash python
|
||||
playwright show-trace trace.zip
|
||||
```
|
||||
### Screenshots
|
||||
|
||||
```bash csharp
|
||||
pwsh bin/Debug/netX/playwright.ps1 show-trace trace.zip
|
||||
```
|
||||
When tracing with the [`option: Tracing.start.screenshots`] option turned on (default), each trace records a screencast and renders it as a film strip. You can hover over the film strip to see a magnified image of for each action and state which helps you easily find the action you want to inspect.
|
||||
|
||||
## Using [trace.playwright.dev](https://trace.playwright.dev)
|
||||
Double click on an action to see the time range for that action. You can use the slider in the timeline to increase the actions selected and these will be shown in the Actions tab and all console logs and network logs will be filtered to only show the logs for the actions selected.
|
||||
|
||||
[trace.playwright.dev](https://trace.playwright.dev) is a statically hosted variant of the Trace Viewer. You can upload trace files using drag and drop.
|
||||

|
||||
|
||||
|
||||
<img width="1119" alt="Drop Playwright Trace to load" src="https://user-images.githubusercontent.com/13063165/194577918-b4d45726-2692-4093-8a28-9e73552617ef.png" />
|
||||
### Snapshots
|
||||
|
||||
## Viewing remote traces
|
||||
When tracing with the [`option: Tracing.start.snapshots`] option turned on (default), Playwright captures a set of complete DOM snapshots for each action. Depending on the type of the action, it will capture:
|
||||
|
||||
You can open remote traces using its URL. They could be generated on a CI run which makes it easy to view the remote trace without having to manually download the file.
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
|Before|A snapshot at the time action is called.|
|
||||
|Action|A snapshot at the moment of the performed input. This type of snapshot is especially useful when exploring where exactly Playwright clicked.|
|
||||
|After|A snapshot after the action.|
|
||||
|
||||
```bash js
|
||||
npx playwright show-trace https://example.com/trace.zip
|
||||
```
|
||||
Here is what the typical Action snapshot looks like:
|
||||
|
||||
```bash java
|
||||
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace https://example.com/trace.zip"
|
||||
```
|
||||

|
||||
|
||||
```bash python
|
||||
playwright show-trace https://example.com/trace.zip
|
||||
```
|
||||
Notice how it highlights both, the DOM Node as well as the exact click position.
|
||||
|
||||
```bash csharp
|
||||
pwsh bin/Debug/netX/playwright.ps1 show-trace https://example.com/trace.zip
|
||||
```
|
||||
### Source
|
||||
|
||||
When you click on an action in the sidebar, the line of code for that action is highlighted in the source panel.
|
||||
|
||||

|
||||
|
||||
### Call
|
||||
|
||||
The call tab shows you information about the action such as the time it took, what locator was used, if in strict mode and what key was used.
|
||||
|
||||

|
||||
|
||||
### Log
|
||||
|
||||
See a full log of your test to better understand what Playwright is doing behind the scenes such as scrolling into view, waiting for element to be visible, enabled and stable and performing actions such as click, fill, press etc.
|
||||
|
||||

|
||||
|
||||
### Errors
|
||||
|
||||
If your test fails you will see the error messages for each test in the Errors tab. The timeline will also show a red line highlighting where the error occurred. You can also click on the source tab to see on which line of the source code the error is.
|
||||
|
||||

|
||||
|
||||
### Console
|
||||
|
||||
See console logs from the browser as well as from your test. Different icons are displayed to show you if the console log came from the browser or from the test file.
|
||||
|
||||

|
||||
|
||||
Double click on an action from your test in the actions sidebar. This will filter the console to only show the logs that were made during that action. Click the *Show all* button to see all console logs again.
|
||||
|
||||
Use the timeline to filter actions, by clicking a start point and dragging to an ending point. The console tab will also be filtered to only show the logs that were made during the actions selected.
|
||||
|
||||
|
||||
You can also pass the URL of your uploaded trace (e.g. inside your CI) from some accessible storage as a parameter. CORS (Cross-Origin Resource Sharing) rules might apply.
|
||||
### Network
|
||||
|
||||
```txt
|
||||
https://trace.playwright.dev/?trace=https://demo.playwright.dev/reports/todomvc/data/cb0fa77ebd9487a5c899f3ae65a7ffdbac681182.zip
|
||||
```
|
||||
The Network tab shows you all the network requests that were made during your test. You can sort by different types of requests, status code, method, request, content type, duration and size. Click on a request to see more information about it such as the request headers, response headers, request body and response body.
|
||||
|
||||

|
||||
|
||||
Double click on an action from your test in the actions sidebar. This will filter the network requests to only show the requests that were made during that action. Click the *Show all* button to see all network requests again.
|
||||
|
||||
Use the timeline to filter actions, by clicking a start point and dragging to an ending point. The network tab will also be filtered to only show the network requests that were made during the actions selected.
|
||||
|
||||
### Metadata
|
||||
|
||||
Next to the Actions tab you will find the Metadata tab which will show you more information on your test such as the Browser, viewport size, test duration and more.
|
||||
|
||||

|
||||
|
||||
### Attachments
|
||||
* langs: js
|
||||
|
||||
The "Attachments" tab allows you to explore attachments. If you're doing [visual regression testing](./test-snapshots.md), you'll be able to compare screenshots by examining the image diff, the actual image and the expected image. When you click on the expected image you can use the slider to slide one image over the other so you can easily see the differences in your screenshots.
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Take a look at the following example to see how to write a test.
|
|||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
{label: 'xUnit', value: 'xunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
|
@ -117,6 +118,40 @@ public class ExampleTest : PageTest
|
|||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="xunit">
|
||||
|
||||
```csharp title="UnitTest1.cs"
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Playwright;
|
||||
using Microsoft.Playwright.Xunit;
|
||||
|
||||
namespace PlaywrightTests;
|
||||
|
||||
public class UnitTest1: PageTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task HasTitle()
|
||||
{
|
||||
await Page.GotoAsync("https://playwright.dev");
|
||||
|
||||
// Expect a title "to contain" a substring.
|
||||
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStartedLink()
|
||||
{
|
||||
await Page.GotoAsync("https://playwright.dev");
|
||||
|
||||
// Click the get started link.
|
||||
await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
|
||||
|
||||
// Expects page to have a heading with the name of Installation.
|
||||
await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Installation" })).ToBeVisibleAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
|
@ -204,6 +239,7 @@ The Playwright NUnit and MSTest test framework base classes will isolate each te
|
|||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
{label: 'xUnit', value: 'xunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
|
@ -248,23 +284,43 @@ public class ExampleTest : PageTest
|
|||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="xunit">
|
||||
|
||||
```csharp title="UnitTest1.cs"
|
||||
using Microsoft.Playwright;
|
||||
using Microsoft.Playwright.Xunit;
|
||||
|
||||
namespace PlaywrightTests;
|
||||
|
||||
public class UnitTest1: PageTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task BasicTest()
|
||||
{
|
||||
await Page.GotoAsync("https://playwright.dev");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Using Test Hooks
|
||||
|
||||
You can use `SetUp`/`TearDown` in NUnit or `TestInitialize`/`TestCleanup` in MSTest to prepare and clean up your test environment:
|
||||
|
||||
<Tabs
|
||||
groupId="test-runners"
|
||||
defaultValue="mstest"
|
||||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
{label: 'xUnit', value: 'xunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
||||
You can use `SetUp`/`TearDown` to prepare and clean up your test environment:
|
||||
|
||||
```csharp title="UnitTest1.cs"
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Playwright.NUnit;
|
||||
|
|
@ -294,6 +350,8 @@ public class ExampleTest : PageTest
|
|||
</TabItem>
|
||||
<TabItem value="mstest">
|
||||
|
||||
You can use `TestInitialize`/`TestCleanup` to prepare and clean up your test environment:
|
||||
|
||||
```csharp title="UnitTest1.cs"
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Playwright.MSTest;
|
||||
|
|
@ -319,6 +377,39 @@ public class ExampleTest : PageTest
|
|||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="xunit">
|
||||
|
||||
You can use `InitializeAsync`/`DisposeAsync` to prepare and clean up your test environment:
|
||||
|
||||
```csharp title="UnitTest1.cs"
|
||||
using Microsoft.Playwright;
|
||||
using Microsoft.Playwright.Xunit;
|
||||
|
||||
namespace PlaywrightTests;
|
||||
|
||||
public class UnitTest1: PageTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task MainNavigation()
|
||||
{
|
||||
// Assertions use the expect API.
|
||||
await Expect(Page).ToHaveURLAsync("https://playwright.dev/");
|
||||
}
|
||||
|
||||
override public async Task InitializeAsync()
|
||||
{
|
||||
await base.InitializeAsync();
|
||||
await Page.GotoAsync("https://playwright.dev");
|
||||
}
|
||||
|
||||
public override async Task DisposeAsync()
|
||||
{
|
||||
Console.WriteLine("After each test cleanup");
|
||||
await base.DisposeAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
|
@ -328,4 +419,4 @@ public class ExampleTest : PageTest
|
|||
- [Generate tests with Codegen](./codegen-intro.md)
|
||||
- [See a trace of your tests](./trace-viewer-intro.md)
|
||||
- [Run tests on CI](./ci-intro.md)
|
||||
- [Learn more about the MSTest and NUnit base classes](./test-runners.md)
|
||||
- [Learn more about the MSTest, NUnit, or xUnit base classes](./test-runners.md)
|
||||
|
|
|
|||
271
package-lock.json
generated
271
package-lock.json
generated
|
|
@ -27,7 +27,7 @@
|
|||
"@types/codemirror": "^5.60.7",
|
||||
"@types/formidable": "^2.0.4",
|
||||
"@types/immutable": "^3.8.7",
|
||||
"@types/node": "^18.19.39",
|
||||
"@types/node": "^18.19.68",
|
||||
"@types/react": "^18.0.12",
|
||||
"@types/react-dom": "^18.0.5",
|
||||
"@types/ws": "^8.5.3",
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"ssim.js": "^3.5.0",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.4.6",
|
||||
"ws": "^8.17.1",
|
||||
"xml2js": "^0.5.0",
|
||||
|
|
@ -397,17 +397,19 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.23.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
|
||||
"integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
||||
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
|
||||
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
|
|
@ -447,9 +449,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.23.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz",
|
||||
"integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==",
|
||||
"version": "7.26.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
|
||||
"integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.26.3"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
|
|
@ -810,13 +816,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.23.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
|
||||
"integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
|
||||
"version": "7.26.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
|
||||
"integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.23.4",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
"@babel/helper-string-parser": "^7.25.9",
|
||||
"@babel/helper-validator-identifier": "^7.25.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -1409,9 +1415,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.21",
|
||||
|
|
@ -1851,10 +1858,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.19.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.39.tgz",
|
||||
"integrity": "sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==",
|
||||
"version": "18.19.68",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.68.tgz",
|
||||
"integrity": "sha512-QGtpFH1vB99ZmTa63K4/FU8twThj4fuVSBkGddTp7uIL/cuoLWIUSL2RcOaigBhfR+hg5pgGkBnkoOxrTVBMKw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
|
|
@ -2171,138 +2179,139 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
|
||||
"integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.0.tgz",
|
||||
"integrity": "sha512-7n7KdUEtx/7Yl7I/WVAMZ1bEb0eVvXF3ummWTeLcs/9gvo9pJhuLdouSXGjdZ/MKD1acf1I272+X0RMua4/R3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^4.0.0 || ^5.0.0",
|
||||
"vite": "^5.0.0",
|
||||
"vue": "^3.2.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.15.tgz",
|
||||
"integrity": "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
|
||||
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.23.6",
|
||||
"@vue/shared": "3.4.15",
|
||||
"@babel/parser": "^7.25.3",
|
||||
"@vue/shared": "3.5.13",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core/node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core/node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz",
|
||||
"integrity": "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
|
||||
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.4.15",
|
||||
"@vue/shared": "3.4.15"
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz",
|
||||
"integrity": "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
|
||||
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.23.6",
|
||||
"@vue/compiler-core": "3.4.15",
|
||||
"@vue/compiler-dom": "3.4.15",
|
||||
"@vue/compiler-ssr": "3.4.15",
|
||||
"@vue/shared": "3.4.15",
|
||||
"@babel/parser": "^7.25.3",
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/compiler-ssr": "3.5.13",
|
||||
"@vue/shared": "3.5.13",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.5",
|
||||
"postcss": "^8.4.33",
|
||||
"source-map-js": "^1.0.2"
|
||||
"magic-string": "^0.30.11",
|
||||
"postcss": "^8.4.48",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc/node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz",
|
||||
"integrity": "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
|
||||
"integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.4.15",
|
||||
"@vue/shared": "3.4.15"
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.15.tgz",
|
||||
"integrity": "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
|
||||
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.4.15"
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.15.tgz",
|
||||
"integrity": "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
|
||||
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.4.15",
|
||||
"@vue/shared": "3.4.15"
|
||||
"@vue/reactivity": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.15.tgz",
|
||||
"integrity": "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
|
||||
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/runtime-core": "3.4.15",
|
||||
"@vue/shared": "3.4.15",
|
||||
"@vue/reactivity": "3.5.13",
|
||||
"@vue/runtime-core": "3.5.13",
|
||||
"@vue/shared": "3.5.13",
|
||||
"csstype": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.15.tgz",
|
||||
"integrity": "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
|
||||
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.4.15",
|
||||
"@vue/shared": "3.4.15"
|
||||
"@vue/compiler-ssr": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.4.15"
|
||||
"vue": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
|
||||
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
|
||||
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@zip.js/zip.js": {
|
||||
|
|
@ -3325,6 +3334,19 @@
|
|||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/env-paths": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||
|
|
@ -5235,14 +5257,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.5",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
|
||||
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
|
||||
"version": "0.30.15",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.15.tgz",
|
||||
"integrity": "sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
|
|
@ -5395,15 +5415,16 @@
|
|||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
|
|
@ -5771,9 +5792,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
||||
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
|
|
@ -5826,9 +5848,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.47",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
|
||||
"version": "8.4.49",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
|
||||
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
|
@ -5843,9 +5865,10 @@
|
|||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.1.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -6719,14 +6742,6 @@
|
|||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
|
|
@ -6887,10 +6902,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
|
||||
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
|
||||
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -7449,16 +7465,17 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.4.15",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.15.tgz",
|
||||
"integrity": "sha512-jC0GH4KkWLWJOEQjOpkqU1bQsBwf4R1rsFtw5GQJbjHVKWDzO6P0nWWBTmjp1xSemAioDFj1jdaK1qa3DnMQoQ==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
|
||||
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.4.15",
|
||||
"@vue/compiler-sfc": "3.4.15",
|
||||
"@vue/runtime-dom": "3.4.15",
|
||||
"@vue/server-renderer": "3.4.15",
|
||||
"@vue/shared": "3.4.15"
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/compiler-sfc": "3.5.13",
|
||||
"@vue/runtime-dom": "3.5.13",
|
||||
"@vue/server-renderer": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
|
|
@ -7876,7 +7893,7 @@
|
|||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@vitejs/plugin-vue": "^4.2.1"
|
||||
"@vitejs/plugin-vue": "^5.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@
|
|||
"@types/codemirror": "^5.60.7",
|
||||
"@types/formidable": "^2.0.4",
|
||||
"@types/immutable": "^3.8.7",
|
||||
"@types/node": "^18.19.39",
|
||||
"@types/node": "^18.19.68",
|
||||
"@types/react": "^18.0.12",
|
||||
"@types/react-dom": "^18.0.5",
|
||||
"@types/ws": "^8.5.3",
|
||||
|
|
@ -99,7 +99,7 @@
|
|||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"ssim.js": "^3.5.0",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.4.6",
|
||||
"ws": "^8.17.1",
|
||||
"xml2js": "^0.5.0",
|
||||
|
|
|
|||
|
|
@ -2,10 +2,5 @@
|
|||
"name": "html-reporter",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build && tsc",
|
||||
"preview": "vite preview"
|
||||
}
|
||||
"type": "module"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import './colors.css';
|
|||
import './common.css';
|
||||
import * as icons from './icons';
|
||||
import { clsx } from '@web/uiUtils';
|
||||
import { useAnchor } from './links';
|
||||
import { type AnchorID, useAnchor } from './links';
|
||||
|
||||
export const Chip: React.FC<{
|
||||
header: JSX.Element | string,
|
||||
|
|
@ -53,7 +53,7 @@ export const AutoChip: React.FC<{
|
|||
noInsets?: boolean,
|
||||
children?: any,
|
||||
dataTestId?: string,
|
||||
revealOnAnchorId?: string,
|
||||
revealOnAnchorId?: AnchorID,
|
||||
}> = ({ header, initialExpanded, noInsets, children, dataTestId, revealOnAnchorId }) => {
|
||||
const [expanded, setExpanded] = React.useState(initialExpanded ?? true);
|
||||
const onReveal = React.useCallback(() => setExpanded(true), []);
|
||||
|
|
|
|||
|
|
@ -64,6 +64,6 @@ test('should toggle filters', async ({ page, mount }) => {
|
|||
await expect(page).toHaveURL(/#\?q=s:flaky/);
|
||||
await component.locator('a', { hasText: 'Skipped' }).click();
|
||||
await expect(page).toHaveURL(/#\?q=s:skipped/);
|
||||
await component.getByRole('searchbox').fill('annot:annotation type=annotation description');
|
||||
await component.getByRole('textbox').fill('annot:annotation type=annotation description');
|
||||
expect(filters).toEqual(['', 's:passed', 's:failed', 's:flaky', 's:skipped', 'annot:annotation type=annotation description']);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,12 +49,14 @@ export const HeaderView: React.FC<React.PropsWithChildren<{
|
|||
<form className='subnav-search' onSubmit={
|
||||
event => {
|
||||
event.preventDefault();
|
||||
navigate(`#?q=${filterText ? encodeURIComponent(filterText) : ''}`);
|
||||
const url = new URL(window.location.href);
|
||||
url.hash = filterText ? '?' + new URLSearchParams({ q: filterText }) : '';
|
||||
navigate(url);
|
||||
}
|
||||
}>
|
||||
{icons.search()}
|
||||
{/* Use navigationId to reset defaultValue */}
|
||||
<input type='search' spellCheck={false} className='form-control subnav-search-input input-contrast width-full' value={filterText} onChange={e => {
|
||||
<input spellCheck={false} className='form-control subnav-search-input input-contrast width-full' value={filterText} onChange={e => {
|
||||
setFilterText(e.target.value);
|
||||
}}></input>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import type { HTMLReport } from './types';
|
||||
import type zip from '@zip.js/zip.js';
|
||||
import type * as zip from '@zip.js/zip.js';
|
||||
// @ts-ignore
|
||||
import * as zipImport from '@zip.js/zip.js/lib/zip-no-worker-inflate.js';
|
||||
import * as React from 'react';
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { TestAttachment } from './types';
|
||||
import type { TestAttachment, TestCase, TestCaseSummary, TestResult, TestResultSummary } from './types';
|
||||
import * as React from 'react';
|
||||
import * as icons from './icons';
|
||||
import { TreeItem } from './treeItem';
|
||||
|
|
@ -23,7 +23,7 @@ import './links.css';
|
|||
import { linkifyText } from '@web/renderUtils';
|
||||
import { clsx } from '@web/uiUtils';
|
||||
|
||||
export function navigate(href: string) {
|
||||
export function navigate(href: string | URL) {
|
||||
window.history.pushState({}, '', href);
|
||||
const navEvent = new PopStateEvent('popstate');
|
||||
window.dispatchEvent(navEvent);
|
||||
|
|
@ -72,6 +72,7 @@ export const AttachmentLink: React.FunctionComponent<{
|
|||
linkName?: string,
|
||||
openInNewTab?: boolean,
|
||||
}> = ({ attachment, href, linkName, openInNewTab }) => {
|
||||
const isAnchored = useIsAnchored('attachment-' + attachment.name);
|
||||
return <TreeItem title={<span>
|
||||
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
|
||||
{attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>}
|
||||
|
|
@ -82,7 +83,7 @@ export const AttachmentLink: React.FunctionComponent<{
|
|||
)}
|
||||
</span>} loadChildren={attachment.body ? () => {
|
||||
return [<div key={1} className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>];
|
||||
} : undefined} depth={0} style={{ lineHeight: '32px' }}></TreeItem>;
|
||||
} : undefined} depth={0} style={{ lineHeight: '32px' }} selected={isAnchored}></TreeItem>;
|
||||
};
|
||||
|
||||
export const SearchParamsContext = React.createContext<URLSearchParams>(new URLSearchParams(window.location.hash.slice(1)));
|
||||
|
|
@ -114,23 +115,29 @@ export function generateTraceUrl(traces: TestAttachment[]) {
|
|||
|
||||
const kMissingContentType = 'x-playwright/missing';
|
||||
|
||||
type AnchorID = string | ((id: string | null) => boolean) | undefined;
|
||||
export type AnchorID = string | string[] | ((id: string) => boolean) | undefined;
|
||||
|
||||
export function useAnchor(id: AnchorID, onReveal: () => void) {
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
const isAnchored = useIsAnchored(id);
|
||||
React.useEffect(() => {
|
||||
if (typeof id === 'undefined')
|
||||
return;
|
||||
if (isAnchored)
|
||||
onReveal();
|
||||
}, [isAnchored, onReveal, searchParams]);
|
||||
}
|
||||
|
||||
const listener = () => {
|
||||
const params = new URLSearchParams(window.location.hash.slice(1));
|
||||
const anchor = params.get('anchor');
|
||||
const isRevealed = typeof id === 'function' ? id(anchor) : anchor === id;
|
||||
if (isRevealed)
|
||||
onReveal();
|
||||
};
|
||||
window.addEventListener('popstate', listener);
|
||||
return () => window.removeEventListener('popstate', listener);
|
||||
}, [id, onReveal]);
|
||||
export function useIsAnchored(id: AnchorID) {
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
const anchor = searchParams.get('anchor');
|
||||
if (anchor === null)
|
||||
return false;
|
||||
if (typeof id === 'undefined')
|
||||
return false;
|
||||
if (typeof id === 'string')
|
||||
return id === anchor;
|
||||
if (Array.isArray(id))
|
||||
return id.includes(anchor);
|
||||
return id(anchor);
|
||||
}
|
||||
|
||||
export function Anchor({ id, children }: React.PropsWithChildren<{ id: AnchorID }>) {
|
||||
|
|
@ -142,3 +149,14 @@ export function Anchor({ id, children }: React.PropsWithChildren<{ id: AnchorID
|
|||
|
||||
return <div ref={ref}>{children}</div>;
|
||||
}
|
||||
|
||||
export function testResultHref({ test, result, anchor }: { test?: TestCase | TestCaseSummary, result?: TestResult | TestResultSummary, anchor?: string }) {
|
||||
const params = new URLSearchParams();
|
||||
if (test)
|
||||
params.set('testId', test.testId);
|
||||
if (test && result)
|
||||
params.set('run', '' + test.results.indexOf(result as any));
|
||||
if (anchor)
|
||||
params.set('anchor', anchor);
|
||||
return `#?` + params;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import * as React from 'react';
|
|||
import { TabbedPane } from './tabbedPane';
|
||||
import { AutoChip } from './chip';
|
||||
import './common.css';
|
||||
import { Link, ProjectLink, SearchParamsContext } from './links';
|
||||
import { Link, ProjectLink, SearchParamsContext, testResultHref } from './links';
|
||||
import { statusIcon } from './statusIcon';
|
||||
import './testCaseView.css';
|
||||
import { TestResultView } from './testResultView';
|
||||
|
|
@ -53,9 +53,9 @@ export const TestCaseView: React.FC<{
|
|||
{test && <div className='hbox'>
|
||||
<div className='test-case-path'>{test.path.join(' › ')}</div>
|
||||
<div style={{ flex: 'auto' }}></div>
|
||||
<div className={clsx(!prev && 'hidden')}><Link href={`#?testId=${prev?.testId}${filterParam}`}>« previous</Link></div>
|
||||
<div className={clsx(!prev && 'hidden')}><Link href={testResultHref({ test: prev }) + filterParam}>« previous</Link></div>
|
||||
<div style={{ width: 10 }}></div>
|
||||
<div className={clsx(!next && 'hidden')}><Link href={`#?testId=${next?.testId}${filterParam}`}>next »</Link></div>
|
||||
<div className={clsx(!next && 'hidden')}><Link href={testResultHref({ test: next }) + filterParam}>next »</Link></div>
|
||||
</div>}
|
||||
{test && <div className='test-case-title'>{test?.title}</div>}
|
||||
{test && <div className='hbox'>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import * as React from 'react';
|
|||
import { hashStringToInt, msToString } from './utils';
|
||||
import { Chip } from './chip';
|
||||
import { filterWithToken } from './filter';
|
||||
import { generateTraceUrl, Link, navigate, ProjectLink, SearchParamsContext } from './links';
|
||||
import { generateTraceUrl, Link, navigate, ProjectLink, SearchParamsContext, testResultHref } from './links';
|
||||
import { statusIcon } from './statusIcon';
|
||||
import './testFileView.css';
|
||||
import { video, image, trace } from './icons';
|
||||
|
|
@ -48,7 +48,7 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
|
|||
{statusIcon(test.outcome)}
|
||||
</span>
|
||||
<span>
|
||||
<Link href={`#?testId=${test.testId}${filterParam}`} title={[...test.path, test.title].join(' › ')}>
|
||||
<Link href={testResultHref({ test }) + filterParam} title={[...test.path, test.title].join(' › ')}>
|
||||
<span className='test-file-title'>{[...test.path, test.title].join(' › ')}</span>
|
||||
</Link>
|
||||
{projectNames.length > 1 && !!test.projectName &&
|
||||
|
|
@ -59,7 +59,7 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
|
|||
<span data-testid='test-duration' style={{ minWidth: '50px', textAlign: 'right' }}>{msToString(test.duration)}</span>
|
||||
</div>
|
||||
<div className='test-file-details-row'>
|
||||
<Link href={`#?testId=${test.testId}`} title={[...test.path, test.title].join(' › ')} className='test-file-path-link'>
|
||||
<Link href={testResultHref({ test })} title={[...test.path, test.title].join(' › ')} className='test-file-path-link'>
|
||||
<span className='test-file-path'>{test.location.file}:{test.location.line}</span>
|
||||
</Link>
|
||||
{imageDiffBadge(test)}
|
||||
|
|
@ -72,15 +72,17 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
|
|||
};
|
||||
|
||||
function imageDiffBadge(test: TestCaseSummary): JSX.Element | undefined {
|
||||
const resultWithImageDiff = test.results.find(result => result.attachments.some(attachment => {
|
||||
return attachment.contentType.startsWith('image/') && !!attachment.name.match(/-(expected|actual|diff)/);
|
||||
}));
|
||||
return resultWithImageDiff ? <Link href={`#?testId=${test.testId}&anchor=diff-0&run=${test.results.indexOf(resultWithImageDiff)}`} title='View images' className='test-file-badge'>{image()}</Link> : undefined;
|
||||
for (const result of test.results) {
|
||||
for (const attachment of result.attachments) {
|
||||
if (attachment.contentType.startsWith('image/') && !!attachment.name.match(/-(expected|actual|diff)/))
|
||||
return <Link href={testResultHref({ test, result, anchor: `attachment-${attachment.name}` })} title='View images' className='test-file-badge'>{image()}</Link>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function videoBadge(test: TestCaseSummary): JSX.Element | undefined {
|
||||
const resultWithVideo = test.results.find(result => result.attachments.some(attachment => attachment.name === 'video'));
|
||||
return resultWithVideo ? <Link href={`#?testId=${test.testId}&anchor=videos&run=${test.results.indexOf(resultWithVideo)}`} title='View video' className='test-file-badge'>{video()}</Link> : undefined;
|
||||
return resultWithVideo ? <Link href={testResultHref({ test, result: resultWithVideo, anchor: 'attachment-video' })} title='View video' className='test-file-badge'>{video()}</Link> : undefined;
|
||||
}
|
||||
|
||||
function traceBadge(test: TestCaseSummary): JSX.Element | undefined {
|
||||
|
|
|
|||
|
|
@ -20,15 +20,20 @@ import { TreeItem } from './treeItem';
|
|||
import { msToString } from './utils';
|
||||
import { AutoChip } from './chip';
|
||||
import { traceImage } from './images';
|
||||
import { Anchor, AttachmentLink, generateTraceUrl } from './links';
|
||||
import { Anchor, AttachmentLink, generateTraceUrl, testResultHref } from './links';
|
||||
import { statusIcon } from './statusIcon';
|
||||
import type { ImageDiff } from '@web/shared/imageDiffView';
|
||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||
import { TestErrorView, TestScreenshotErrorView } from './testErrorView';
|
||||
import * as icons from './icons';
|
||||
import './testResultView.css';
|
||||
|
||||
function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiff[] {
|
||||
const snapshotNameToImageDiff = new Map<string, ImageDiff>();
|
||||
interface ImageDiffWithAnchors extends ImageDiff {
|
||||
anchors: string[];
|
||||
}
|
||||
|
||||
function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiffWithAnchors[] {
|
||||
const snapshotNameToImageDiff = new Map<string, ImageDiffWithAnchors>();
|
||||
for (const attachment of screenshots) {
|
||||
const match = attachment.name.match(/^(.*)-(expected|actual|diff|previous)(\.[^.]+)?$/);
|
||||
if (!match)
|
||||
|
|
@ -37,9 +42,10 @@ function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiff[] {
|
|||
const snapshotName = name + extension;
|
||||
let imageDiff = snapshotNameToImageDiff.get(snapshotName);
|
||||
if (!imageDiff) {
|
||||
imageDiff = { name: snapshotName };
|
||||
imageDiff = { name: snapshotName, anchors: [`attachment-${name}`] };
|
||||
snapshotNameToImageDiff.set(snapshotName, imageDiff);
|
||||
}
|
||||
imageDiff.anchors.push(`attachment-${attachment.name}`);
|
||||
if (category === 'actual')
|
||||
imageDiff.actual = { attachment };
|
||||
if (category === 'expected')
|
||||
|
|
@ -64,18 +70,19 @@ function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiff[] {
|
|||
export const TestResultView: React.FC<{
|
||||
test: TestCase,
|
||||
result: TestResult,
|
||||
}> = ({ result }) => {
|
||||
const { screenshots, videos, traces, otherAttachments, diffs, errors, htmls } = React.useMemo(() => {
|
||||
}> = ({ test, result }) => {
|
||||
const { screenshots, videos, traces, otherAttachments, diffs, errors, otherAttachmentAnchors, screenshotAnchors } = React.useMemo(() => {
|
||||
const attachments = result?.attachments || [];
|
||||
const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/')));
|
||||
const screenshotAnchors = [...screenshots].map(a => `attachment-${a.name}`);
|
||||
const videos = attachments.filter(a => a.contentType.startsWith('video/'));
|
||||
const traces = attachments.filter(a => a.name === 'trace');
|
||||
const htmls = attachments.filter(a => a.contentType.startsWith('text/html'));
|
||||
const otherAttachments = new Set<TestAttachment>(attachments);
|
||||
[...screenshots, ...videos, ...traces, ...htmls].forEach(a => otherAttachments.delete(a));
|
||||
[...screenshots, ...videos, ...traces].forEach(a => otherAttachments.delete(a));
|
||||
const otherAttachmentAnchors = [...otherAttachments].map(a => `attachment-${a.name}`);
|
||||
const diffs = groupImageDiffs(screenshots);
|
||||
const errors = classifyErrors(result.errors, diffs);
|
||||
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, errors, htmls };
|
||||
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, errors, otherAttachmentAnchors, screenshotAnchors };
|
||||
}, [result]);
|
||||
|
||||
return <div className='test-result'>
|
||||
|
|
@ -87,29 +94,29 @@ export const TestResultView: React.FC<{
|
|||
})}
|
||||
</AutoChip>}
|
||||
{!!result.steps.length && <AutoChip header='Test Steps'>
|
||||
{result.steps.map((step, i) => <StepTreeItem key={`step-${i}`} step={step} depth={0}></StepTreeItem>)}
|
||||
{result.steps.map((step, i) => <StepTreeItem key={`step-${i}`} step={step} result={result} test={test} depth={0}/>)}
|
||||
</AutoChip>}
|
||||
|
||||
{diffs.map((diff, index) =>
|
||||
<Anchor key={`diff-${index}`} id={`diff-${index}`}>
|
||||
<AutoChip dataTestId='test-results-image-diff' header={`Image mismatch: ${diff.name}`} revealOnAnchorId={`diff-${index}`}>
|
||||
<Anchor key={`diff-${index}`} id={diff.anchors}>
|
||||
<AutoChip dataTestId='test-results-image-diff' header={`Image mismatch: ${diff.name}`} revealOnAnchorId={diff.anchors}>
|
||||
<ImageDiffView diff={diff}/>
|
||||
</AutoChip>
|
||||
</Anchor>
|
||||
)}
|
||||
|
||||
{!!screenshots.length && <AutoChip header='Screenshots'>
|
||||
{!!screenshots.length && <AutoChip header='Screenshots' revealOnAnchorId={screenshotAnchors}>
|
||||
{screenshots.map((a, i) => {
|
||||
return <div key={`screenshot-${i}`}>
|
||||
return <Anchor key={`screenshot-${i}`} id={`attachment-${a.name}`}>
|
||||
<a href={a.path}>
|
||||
<img className='screenshot' src={a.path} />
|
||||
</a>
|
||||
<AttachmentLink attachment={a}></AttachmentLink>
|
||||
</div>;
|
||||
</Anchor>;
|
||||
})}
|
||||
</AutoChip>}
|
||||
|
||||
{!!traces.length && <Anchor id='traces'><AutoChip header='Traces' revealOnAnchorId='traces'>
|
||||
{!!traces.length && <Anchor id='attachment-trace'><AutoChip header='Traces' revealOnAnchorId='attachment-trace'>
|
||||
{<div>
|
||||
<a href={generateTraceUrl(traces)}>
|
||||
<img className='screenshot' src={traceImage} style={{ width: 192, height: 117, marginLeft: 20 }} />
|
||||
|
|
@ -118,7 +125,7 @@ export const TestResultView: React.FC<{
|
|||
</div>}
|
||||
</AutoChip></Anchor>}
|
||||
|
||||
{!!videos.length && <Anchor id='videos'><AutoChip header='Videos' revealOnAnchorId='videos'>
|
||||
{!!videos.length && <Anchor id='attachment-video'><AutoChip header='Videos' revealOnAnchorId='attachment-video'>
|
||||
{videos.map((a, i) => <div key={`video-${i}`}>
|
||||
<video controls>
|
||||
<source src={a.path} type={a.contentType}/>
|
||||
|
|
@ -127,11 +134,12 @@ export const TestResultView: React.FC<{
|
|||
</div>)}
|
||||
</AutoChip></Anchor>}
|
||||
|
||||
{!!(otherAttachments.size + htmls.length) && <AutoChip header='Attachments'>
|
||||
{[...htmls].map((a, i) => (
|
||||
<AttachmentLink key={`html-link-${i}`} attachment={a} openInNewTab />)
|
||||
{!!otherAttachments.size && <AutoChip header='Attachments' revealOnAnchorId={otherAttachmentAnchors}>
|
||||
{[...otherAttachments].map((a, i) =>
|
||||
<Anchor key={`attachment-link-${i}`} id={`attachment-${a.name}`}>
|
||||
<AttachmentLink attachment={a} openInNewTab={a.contentType.startsWith('text/html')} />
|
||||
</Anchor>
|
||||
)}
|
||||
{[...otherAttachments].map((a, i) => <AttachmentLink key={`attachment-link-${i}`} attachment={a}></AttachmentLink>)}
|
||||
</AutoChip>}
|
||||
</div>;
|
||||
};
|
||||
|
|
@ -161,19 +169,23 @@ function classifyErrors(testErrors: string[], diffs: ImageDiff[]) {
|
|||
}
|
||||
|
||||
const StepTreeItem: React.FC<{
|
||||
test: TestCase;
|
||||
result: TestResult;
|
||||
step: TestStep;
|
||||
depth: number,
|
||||
}> = ({ step, depth }) => {
|
||||
return <TreeItem title={<span>
|
||||
}> = ({ test, step, result, depth }) => {
|
||||
const attachmentName = step.title.match(/^attach "(.*)"$/)?.[1];
|
||||
return <TreeItem title={<span aria-label={step.title}>
|
||||
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
|
||||
{attachmentName && <a style={{ float: 'right' }} title='link to attachment' href={testResultHref({ test, result, anchor: `attachment-${attachmentName}` })} onClick={evt => { evt.stopPropagation(); }}>{icons.attachment()}</a>}
|
||||
{statusIcon(step.error || step.duration === -1 ? 'failed' : 'passed')}
|
||||
<span>{step.title}</span>
|
||||
{step.count > 1 && <> ✕ <span className='test-result-counter'>{step.count}</span></>}
|
||||
{step.location && <span className='test-result-path'>— {step.location.file}:{step.location.line}</span>}
|
||||
</span>} loadChildren={step.steps.length + (step.snippet ? 1 : 0) ? () => {
|
||||
const children = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1}></StepTreeItem>);
|
||||
const children = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1} result={result} test={test} />);
|
||||
if (step.snippet)
|
||||
children.unshift(<TestErrorView testId='test-snippet' key='line' error={step.snippet}></TestErrorView>);
|
||||
children.unshift(<TestErrorView testId='test-snippet' key='line' error={step.snippet}/>);
|
||||
return children;
|
||||
} : undefined} depth={depth}></TreeItem>;
|
||||
} : undefined} depth={depth}/>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tree-item-title.selected {
|
||||
text-decoration: underline var(--color-underlinenav-icon);
|
||||
text-decoration-thickness: 1.5px;
|
||||
}
|
||||
|
||||
.tree-item-body {
|
||||
min-height: 18px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
import * as React from 'react';
|
||||
import './treeItem.css';
|
||||
import * as icons from './icons';
|
||||
import { clsx } from '@web/uiUtils';
|
||||
|
||||
export const TreeItem: React.FunctionComponent<{
|
||||
title: JSX.Element,
|
||||
|
|
@ -28,9 +29,8 @@ export const TreeItem: React.FunctionComponent<{
|
|||
style?: React.CSSProperties,
|
||||
}> = ({ title, loadChildren, onClick, expandByDefault, depth, selected, style }) => {
|
||||
const [expanded, setExpanded] = React.useState(expandByDefault || false);
|
||||
const className = selected ? 'tree-item-title selected' : 'tree-item-title';
|
||||
return <div className={'tree-item'} style={style}>
|
||||
<span className={className} style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
|
||||
<span className={clsx('tree-item-title', selected && 'selected')} style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
|
||||
{loadChildren && !!expanded && icons.downArrow()}
|
||||
{loadChildren && !expanded && icons.rightArrow()}
|
||||
{!loadChildren && <span style={{ visibility: 'hidden' }}>{icons.rightArrow()}</span>}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ This project incorporates components from the projects listed below. The origina
|
|||
|
||||
- @types/node@17.0.24 (https://github.com/DefinitelyTyped/DefinitelyTyped)
|
||||
- @types/yauzl@2.10.0 (https://github.com/DefinitelyTyped/DefinitelyTyped)
|
||||
- agent-base@7.1.1 (https://github.com/TooTallNate/proxy-agents)
|
||||
- agent-base@7.1.3 (https://github.com/TooTallNate/proxy-agents)
|
||||
- balanced-match@1.0.2 (https://github.com/juliangruber/balanced-match)
|
||||
- brace-expansion@1.1.11 (https://github.com/juliangruber/brace-expansion)
|
||||
- buffer-crc32@0.2.13 (https://github.com/brianloveswords/buffer-crc32)
|
||||
|
|
@ -24,7 +24,7 @@ This project incorporates components from the projects listed below. The origina
|
|||
- fd-slicer@1.1.0 (https://github.com/andrewrk/node-fd-slicer)
|
||||
- get-stream@5.2.0 (https://github.com/sindresorhus/get-stream)
|
||||
- graceful-fs@4.2.10 (https://github.com/isaacs/node-graceful-fs)
|
||||
- https-proxy-agent@7.0.5 (https://github.com/TooTallNate/proxy-agents)
|
||||
- https-proxy-agent@7.0.6 (https://github.com/TooTallNate/proxy-agents)
|
||||
- ip-address@9.0.5 (https://github.com/beaugunderson/ip-address)
|
||||
- is-docker@2.2.1 (https://github.com/sindresorhus/is-docker)
|
||||
- is-wsl@2.2.0 (https://github.com/sindresorhus/is-wsl)
|
||||
|
|
@ -43,7 +43,7 @@ This project incorporates components from the projects listed below. The origina
|
|||
- retry@0.12.0 (https://github.com/tim-kos/node-retry)
|
||||
- signal-exit@3.0.7 (https://github.com/tapjs/signal-exit)
|
||||
- smart-buffer@4.2.0 (https://github.com/JoshGlazebrook/smart-buffer)
|
||||
- socks-proxy-agent@8.0.4 (https://github.com/TooTallNate/proxy-agents)
|
||||
- socks-proxy-agent@8.0.5 (https://github.com/TooTallNate/proxy-agents)
|
||||
- socks@2.8.3 (https://github.com/JoshGlazebrook/socks)
|
||||
- sprintf-js@1.1.3 (https://github.com/alexei/sprintf.js)
|
||||
- stack-utils@2.0.5 (https://github.com/tapjs/stack-utils)
|
||||
|
|
@ -105,7 +105,7 @@ MIT License
|
|||
=========================================
|
||||
END OF @types/yauzl@2.10.0 AND INFORMATION
|
||||
|
||||
%% agent-base@7.1.1 NOTICES AND INFORMATION BEGIN HERE
|
||||
%% agent-base@7.1.3 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
(The MIT License)
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
=========================================
|
||||
END OF agent-base@7.1.1 AND INFORMATION
|
||||
END OF agent-base@7.1.3 AND INFORMATION
|
||||
|
||||
%% balanced-match@1.0.2 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
|
|
@ -542,7 +542,7 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|||
=========================================
|
||||
END OF graceful-fs@4.2.10 AND INFORMATION
|
||||
|
||||
%% https-proxy-agent@7.0.5 NOTICES AND INFORMATION BEGIN HERE
|
||||
%% https-proxy-agent@7.0.6 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
(The MIT License)
|
||||
|
||||
|
|
@ -567,7 +567,7 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
=========================================
|
||||
END OF https-proxy-agent@7.0.5 AND INFORMATION
|
||||
END OF https-proxy-agent@7.0.6 AND INFORMATION
|
||||
|
||||
%% ip-address@9.0.5 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
|
|
@ -1005,7 +1005,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|||
=========================================
|
||||
END OF smart-buffer@4.2.0 AND INFORMATION
|
||||
|
||||
%% socks-proxy-agent@8.0.4 NOTICES AND INFORMATION BEGIN HERE
|
||||
%% socks-proxy-agent@8.0.5 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
(The MIT License)
|
||||
|
||||
|
|
@ -1030,7 +1030,7 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
=========================================
|
||||
END OF socks-proxy-agent@8.0.4 AND INFORMATION
|
||||
END OF socks-proxy-agent@8.0.5 AND INFORMATION
|
||||
|
||||
%% socks@2.8.3 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
|
|
|
|||
|
|
@ -3,21 +3,15 @@
|
|||
"browsers": [
|
||||
{
|
||||
"name": "chromium",
|
||||
"revision": "1150",
|
||||
"revision": "1152",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "132.0.6834.15"
|
||||
},
|
||||
{
|
||||
"name": "chromium-headless-shell",
|
||||
"revision": "1150",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "132.0.6834.15"
|
||||
"browserVersion": "132.0.6834.46"
|
||||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree",
|
||||
"revision": "1280",
|
||||
"revision": "1286",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "133.0.6850.0"
|
||||
"browserVersion": "133.0.6891.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
|
|
@ -33,7 +27,7 @@
|
|||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2110",
|
||||
"revision": "2119",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"debian11-x64": "2105",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
"diff": "^7.0.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"graceful-fs": "4.2.10",
|
||||
"https-proxy-agent": "7.0.5",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"jpeg-js": "0.4.4",
|
||||
"mime": "^3.0.0",
|
||||
"minimatch": "^3.1.2",
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
"proxy-from-env": "1.1.0",
|
||||
"retry": "0.12.0",
|
||||
"signal-exit": "3.0.7",
|
||||
"socks-proxy-agent": "8.0.4",
|
||||
"socks-proxy-agent": "8.0.5",
|
||||
"stack-utils": "2.0.5",
|
||||
"ws": "8.17.1",
|
||||
"yaml": "^2.6.0"
|
||||
|
|
@ -140,13 +140,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
|
||||
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
||||
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
|
|
@ -244,12 +241,12 @@
|
|||
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
|
||||
"integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.0.2",
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -403,12 +400,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/socks-proxy-agent": {
|
||||
"version": "8.0.4",
|
||||
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz",
|
||||
"integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==",
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
|
||||
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.1",
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "^4.3.4",
|
||||
"socks": "^2.8.3"
|
||||
},
|
||||
|
|
@ -562,12 +559,9 @@
|
|||
}
|
||||
},
|
||||
"agent-base": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
|
||||
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
|
||||
"requires": {
|
||||
"debug": "^4.3.4"
|
||||
}
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
||||
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.2",
|
||||
|
|
@ -632,11 +626,11 @@
|
|||
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
|
||||
},
|
||||
"https-proxy-agent": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
|
||||
"integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"requires": {
|
||||
"agent-base": "^7.0.2",
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
}
|
||||
},
|
||||
|
|
@ -740,11 +734,11 @@
|
|||
}
|
||||
},
|
||||
"socks-proxy-agent": {
|
||||
"version": "8.0.4",
|
||||
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz",
|
||||
"integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==",
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
|
||||
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
|
||||
"requires": {
|
||||
"agent-base": "^7.1.1",
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "^4.3.4",
|
||||
"socks": "^2.8.3"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"diff": "^7.0.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"graceful-fs": "4.2.10",
|
||||
"https-proxy-agent": "7.0.5",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"jpeg-js": "0.4.4",
|
||||
"mime": "^3.0.0",
|
||||
"minimatch": "^3.1.2",
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
"proxy-from-env": "1.1.0",
|
||||
"retry": "0.12.0",
|
||||
"signal-exit": "3.0.7",
|
||||
"socks-proxy-agent": "8.0.4",
|
||||
"socks-proxy-agent": "8.0.5",
|
||||
"stack-utils": "2.0.5",
|
||||
"ws": "8.17.1",
|
||||
"yaml": "^2.6.0"
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export class AndroidServerLauncherImpl {
|
|||
if (options.deviceSerialNumber) {
|
||||
devices = devices.filter(d => d.serial === options.deviceSerialNumber);
|
||||
if (devices.length === 0)
|
||||
throw new Error(`No device with serial number '${options.deviceSerialNumber}' not found`);
|
||||
throw new Error(`No device with serial number '${options.deviceSerialNumber}' was found`);
|
||||
}
|
||||
|
||||
if (devices.length > 1)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ import type { Browser } from '../client/browser';
|
|||
import type { Page } from '../client/page';
|
||||
import type { BrowserType } from '../client/browserType';
|
||||
import type { BrowserContextOptions, LaunchOptions } from '../client/types';
|
||||
import { spawn } from 'child_process';
|
||||
import { wrapInASCIIBox, isLikelyNpxGlobal, assert, gracefullyProcessExitDoNotHang, getPackageManagerExecCommand } from '../utils';
|
||||
import type { Executable } from '../server';
|
||||
import { registry, writeDockerVersion } from '../server';
|
||||
|
|
@ -77,21 +76,6 @@ Examples:
|
|||
$ codegen --target=python
|
||||
$ codegen -b webkit https://example.com`);
|
||||
|
||||
program
|
||||
.command('debug <app> [args...]', { hidden: true })
|
||||
.description('run command in debug mode: disable timeout, open inspector')
|
||||
.allowUnknownOption(true)
|
||||
.action(function(app, options) {
|
||||
spawn(app, options, {
|
||||
env: { ...process.env, PWDEBUG: '1' },
|
||||
stdio: 'inherit'
|
||||
});
|
||||
}).addHelpText('afterAll', `
|
||||
Examples:
|
||||
|
||||
$ debug node test.js
|
||||
$ debug npm run test`);
|
||||
|
||||
function suggestedBrowsersToInstall() {
|
||||
return registry.executables().filter(e => e.installType !== 'none' && e.type !== 'tool').map(e => e.name).join(', ');
|
||||
}
|
||||
|
|
@ -293,7 +277,7 @@ program
|
|||
});
|
||||
|
||||
program
|
||||
.command('run-server', { hidden: true })
|
||||
.command('run-server')
|
||||
.option('--port <port>', 'Server port')
|
||||
.option('--host <host>', 'Server host')
|
||||
.option('--path <path>', 'Endpoint Path', '/')
|
||||
|
|
@ -451,10 +435,12 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
|||
// Viewport size
|
||||
if (options.viewportSize) {
|
||||
try {
|
||||
const [width, height] = options.viewportSize.split(',').map(n => parseInt(n, 10));
|
||||
const [width, height] = options.viewportSize.split(',').map(n => +n);
|
||||
if (isNaN(width) || isNaN(height))
|
||||
throw new Error('bad values');
|
||||
contextOptions.viewport = { width, height };
|
||||
} catch (e) {
|
||||
throw new Error('Invalid viewport size format: use "width, height", for example --viewport-size=800,600');
|
||||
throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
|
|||
|
||||
async firstWindow(options?: { timeout?: number }): Promise<Page> {
|
||||
if (this._windows.size)
|
||||
return this._windows.values().next().value;
|
||||
return this._windows.values().next().value!;
|
||||
return await this.waitForEvent('window', options);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -385,6 +385,7 @@ scheme.DebugControllerInitializer = tOptional(tObject({}));
|
|||
scheme.DebugControllerInspectRequestedEvent = tObject({
|
||||
selector: tString,
|
||||
locator: tString,
|
||||
ariaSnapshot: tString,
|
||||
});
|
||||
scheme.DebugControllerSetModeRequestedEvent = tObject({
|
||||
mode: tString,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { Browser } from '../browser';
|
|||
import { assertBrowserContextIsNotOwned, BrowserContext } from '../browserContext';
|
||||
import type { SdkObject } from '../instrumentation';
|
||||
import * as network from '../network';
|
||||
import type { InitScript, Page, PageDelegate } from '../page';
|
||||
import type { InitScript, Page } from '../page';
|
||||
import type { ConnectionTransport } from '../transport';
|
||||
import type * as types from '../types';
|
||||
import type { BidiSession } from './bidiConnection';
|
||||
|
|
@ -99,8 +99,8 @@ export class BidiBrowser extends Browser {
|
|||
browser._defaultContext = new BidiBrowserContext(browser, undefined, options.persistent);
|
||||
await (browser._defaultContext as BidiBrowserContext)._initialize();
|
||||
// Create default page as we cannot get access to the existing one.
|
||||
const pageDelegate = await browser._defaultContext.newPageDelegate();
|
||||
await pageDelegate.pageOrError();
|
||||
const page = await browser._defaultContext.doCreateNewPage();
|
||||
await page.waitForInitializedOrError();
|
||||
}
|
||||
return browser;
|
||||
}
|
||||
|
|
@ -207,17 +207,17 @@ export class BidiBrowserContext extends BrowserContext {
|
|||
return [...this._browser._bidiPages.values()].filter(bidiPage => bidiPage._browserContext === this);
|
||||
}
|
||||
|
||||
pages(): Page[] {
|
||||
return this._bidiPages().map(bidiPage => bidiPage._initializedPage).filter(Boolean) as Page[];
|
||||
override possiblyUninitializedPages(): Page[] {
|
||||
return this._bidiPages().map(bidiPage => bidiPage._page);
|
||||
}
|
||||
|
||||
async newPageDelegate(): Promise<PageDelegate> {
|
||||
override async doCreateNewPage(): Promise<Page> {
|
||||
assertBrowserContextIsNotOwned(this);
|
||||
const { context } = await this._browser._browserSession.send('browsingContext.create', {
|
||||
type: bidi.BrowsingContext.CreateType.Window,
|
||||
userContext: this._browserContextId,
|
||||
});
|
||||
return this._browser._bidiPages.get(context)!;
|
||||
return this._browser._bidiPages.get(context)!._page;
|
||||
}
|
||||
|
||||
async doGetCookies(urls: string[]): Promise<channels.NetworkCookie[]> {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ export class BidiPage implements PageDelegate {
|
|||
readonly rawKeyboard: RawKeyboardImpl;
|
||||
readonly rawTouchscreen: RawTouchscreenImpl;
|
||||
readonly _page: Page;
|
||||
private readonly _pagePromise: Promise<Page | Error>;
|
||||
readonly _session: BidiSession;
|
||||
readonly _opener: BidiPage | null;
|
||||
private readonly _realmToContext: Map<string, dom.FrameExecutionContext>;
|
||||
|
|
@ -51,7 +50,6 @@ export class BidiPage implements PageDelegate {
|
|||
readonly _browserContext: BidiBrowserContext;
|
||||
readonly _networkManager: BidiNetworkManager;
|
||||
private readonly _pdf: BidiPDF;
|
||||
_initializedPage: Page | null = null;
|
||||
private _initScriptIds: string[] = [];
|
||||
|
||||
constructor(browserContext: BidiBrowserContext, bidiSession: BidiSession, opener: BidiPage | null) {
|
||||
|
|
@ -81,16 +79,10 @@ export class BidiPage implements PageDelegate {
|
|||
];
|
||||
|
||||
// Initialize main frame.
|
||||
this._pagePromise = this._initialize().finally(async () => {
|
||||
await this._page.initOpener(this._opener);
|
||||
}).then(() => {
|
||||
this._initializedPage = this._page;
|
||||
this._page.reportAsNew();
|
||||
return this._page;
|
||||
}).catch(e => {
|
||||
this._page.reportAsNew(e);
|
||||
return e;
|
||||
});
|
||||
// TODO: Wait for first execution context to be created and maybe about:blank navigated.
|
||||
this._initialize().then(
|
||||
() => this._page.reportAsNew(this._opener?._page),
|
||||
error => this._page.reportAsNew(this._opener?._page, error));
|
||||
}
|
||||
|
||||
private async _initialize() {
|
||||
|
|
@ -109,21 +101,12 @@ export class BidiPage implements PageDelegate {
|
|||
return Promise.all(this._page.allInitScripts().map(initScript => this.addInitScript(initScript)));
|
||||
}
|
||||
|
||||
potentiallyUninitializedPage(): Page {
|
||||
return this._page;
|
||||
}
|
||||
|
||||
didClose() {
|
||||
this._session.dispose();
|
||||
eventsHelper.removeEventListeners(this._sessionListeners);
|
||||
this._page._didClose();
|
||||
}
|
||||
|
||||
async pageOrError(): Promise<Page | Error> {
|
||||
// TODO: Wait for first execution context to be created and maybe about:blank navigated.
|
||||
return this._pagePromise;
|
||||
}
|
||||
|
||||
private _onFrameAttached(frameId: string, parentFrameId: string | null): frames.Frame {
|
||||
return this._page._frameManager.frameAttached(frameId, parentFrameId);
|
||||
}
|
||||
|
|
@ -372,7 +355,7 @@ export class BidiPage implements PageDelegate {
|
|||
private async _onScriptMessage(event: bidi.Script.MessageParameters) {
|
||||
if (event.channel !== kPlaywrightBindingChannel)
|
||||
return;
|
||||
const pageOrError = await this.pageOrError();
|
||||
const pageOrError = await this._page.waitForInitializedOrError();
|
||||
if (pageOrError instanceof Error)
|
||||
return;
|
||||
const context = this._realmToContext.get(event.source.realm);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import type * as frames from './frames';
|
|||
import { helper } from './helper';
|
||||
import * as network from './network';
|
||||
import { InitScript } from './page';
|
||||
import type { PageDelegate } from './page';
|
||||
import { Page, PageBinding } from './page';
|
||||
import type { Progress, ProgressController } from './progress';
|
||||
import type { Selectors } from './selectors';
|
||||
|
|
@ -257,9 +256,13 @@ export abstract class BrowserContext extends SdkObject {
|
|||
this.emit(BrowserContext.Events.Close);
|
||||
}
|
||||
|
||||
pages(): Page[] {
|
||||
return this.possiblyUninitializedPages().filter(page => page.initializedOrUndefined());
|
||||
}
|
||||
|
||||
// BrowserContext methods.
|
||||
abstract pages(): Page[];
|
||||
abstract newPageDelegate(): Promise<PageDelegate>;
|
||||
abstract possiblyUninitializedPages(): Page[];
|
||||
abstract doCreateNewPage(): Promise<Page>;
|
||||
abstract addCookies(cookies: channels.SetNetworkCookie[]): Promise<void>;
|
||||
abstract setGeolocation(geolocation?: types.Geolocation): Promise<void>;
|
||||
abstract setExtraHTTPHeaders(headers: types.HeadersArray): Promise<void>;
|
||||
|
|
@ -358,31 +361,34 @@ export abstract class BrowserContext extends SdkObject {
|
|||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
}
|
||||
|
||||
async _loadDefaultContextAsIs(progress: Progress): Promise<Page[]> {
|
||||
if (!this.pages().length) {
|
||||
async _loadDefaultContextAsIs(progress: Progress): Promise<Page | undefined> {
|
||||
if (!this.possiblyUninitializedPages().length) {
|
||||
const waitForEvent = helper.waitForEvent(progress, this, BrowserContext.Events.Page);
|
||||
progress.cleanupWhenAborted(() => waitForEvent.dispose);
|
||||
const page = (await waitForEvent.promise) as Page;
|
||||
if (page._pageIsError)
|
||||
throw page._pageIsError;
|
||||
// Race against BrowserContext.close
|
||||
await Promise.race([waitForEvent.promise, this._closePromise]);
|
||||
}
|
||||
const pages = this.pages();
|
||||
if (pages[0]._pageIsError)
|
||||
throw pages[0]._pageIsError;
|
||||
await pages[0].mainFrame()._waitForLoadState(progress, 'load');
|
||||
return pages;
|
||||
const page = this.possiblyUninitializedPages()[0];
|
||||
if (!page)
|
||||
return;
|
||||
const pageOrError = await page.waitForInitializedOrError();
|
||||
if (pageOrError instanceof Error)
|
||||
throw pageOrError;
|
||||
await page.mainFrame()._waitForLoadState(progress, 'load');
|
||||
return page;
|
||||
}
|
||||
|
||||
async _loadDefaultContext(progress: Progress) {
|
||||
const pages = await this._loadDefaultContextAsIs(progress);
|
||||
const defaultPage = await this._loadDefaultContextAsIs(progress);
|
||||
if (!defaultPage)
|
||||
return;
|
||||
const browserName = this._browser.options.name;
|
||||
if ((this._options.isMobile && browserName === 'chromium') || (this._options.locale && browserName === 'webkit')) {
|
||||
// Workaround for:
|
||||
// - chromium fails to change isMobile for existing page;
|
||||
// - webkit fails to change locale for existing page.
|
||||
const oldPage = pages[0];
|
||||
await this.newPage(progress.metadata);
|
||||
await oldPage.close(progress.metadata);
|
||||
await defaultPage.close(progress.metadata);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -480,10 +486,10 @@ export abstract class BrowserContext extends SdkObject {
|
|||
}
|
||||
|
||||
async newPage(metadata: CallMetadata): Promise<Page> {
|
||||
const pageDelegate = await this.newPageDelegate();
|
||||
const page = await this.doCreateNewPage();
|
||||
if (metadata.isServerSide)
|
||||
pageDelegate.potentiallyUninitializedPage().markAsServerSideOnly();
|
||||
const pageOrError = await pageDelegate.pageOrError();
|
||||
page.markAsServerSideOnly();
|
||||
const pageOrError = await page.waitForInitializedOrError();
|
||||
if (pageOrError instanceof Page) {
|
||||
if (pageOrError.isClosed())
|
||||
throw new Error('Page has been closed.');
|
||||
|
|
|
|||
|
|
@ -299,6 +299,6 @@ class CRAXNode implements accessibility.AXNode {
|
|||
for (const childId of node._payload.childIds || [])
|
||||
node._children.push(nodeById.get(childId)!);
|
||||
}
|
||||
return nodeById.values().next().value;
|
||||
return nodeById.values().next().value!;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { Browser } from '../browser';
|
|||
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
|
||||
import { assert, createGuid } from '../../utils';
|
||||
import * as network from '../network';
|
||||
import type { InitScript, PageDelegate, Worker } from '../page';
|
||||
import type { InitScript, Worker } from '../page';
|
||||
import { Page } from '../page';
|
||||
import { Frame } from '../frames';
|
||||
import type { Dialog } from '../dialog';
|
||||
|
|
@ -146,7 +146,7 @@ export class CRBrowser extends Browser {
|
|||
}
|
||||
|
||||
async _waitForAllPagesToBeInitialized() {
|
||||
await Promise.all([...this._crPages.values()].map(page => page.pageOrError()));
|
||||
await Promise.all([...this._crPages.values()].map(crPage => crPage._page.waitForInitializedOrError()));
|
||||
}
|
||||
|
||||
_onAttachedToTarget({ targetInfo, sessionId, waitingForDebugger }: Protocol.Target.attachedToTargetPayload) {
|
||||
|
|
@ -259,10 +259,10 @@ export class CRBrowser extends Browser {
|
|||
}
|
||||
page.willBeginDownload();
|
||||
|
||||
let originPage = page._initializedPage;
|
||||
let originPage = page._page.initializedOrUndefined();
|
||||
// If it's a new window download, report it on the opener page.
|
||||
if (!originPage && page._opener)
|
||||
originPage = page._opener._initializedPage;
|
||||
originPage = page._opener._page.initializedOrUndefined();
|
||||
if (!originPage)
|
||||
return;
|
||||
this._downloadCreated(originPage, payload.guid, payload.url, payload.suggestedFilename);
|
||||
|
|
@ -364,11 +364,11 @@ export class CRBrowserContext extends BrowserContext {
|
|||
return [...this._browser._crPages.values()].filter(crPage => crPage._browserContext === this);
|
||||
}
|
||||
|
||||
pages(): Page[] {
|
||||
return this._crPages().map(crPage => crPage._initializedPage).filter(Boolean) as Page[];
|
||||
override possiblyUninitializedPages(): Page[] {
|
||||
return this._crPages().map(crPage => crPage._page);
|
||||
}
|
||||
|
||||
async newPageDelegate(): Promise<PageDelegate> {
|
||||
override async doCreateNewPage(): Promise<Page> {
|
||||
assertBrowserContextIsNotOwned(this);
|
||||
|
||||
const oldKeys = this._browser.isClank() ? new Set(this._browser._crPages.keys()) : undefined;
|
||||
|
|
@ -391,7 +391,7 @@ export class CRBrowserContext extends BrowserContext {
|
|||
assert(newKeys.size === 1);
|
||||
[targetId] = [...newKeys];
|
||||
}
|
||||
return this._browser._crPages.get(targetId)!;
|
||||
return this._browser._crPages.get(targetId)!._page;
|
||||
}
|
||||
|
||||
async doGetCookies(urls: string[]): Promise<channels.NetworkCookie[]> {
|
||||
|
|
@ -544,7 +544,7 @@ export class CRBrowserContext extends BrowserContext {
|
|||
// When persistent context is closed, we do not necessary get Target.detachedFromTarget
|
||||
// for all the background pages.
|
||||
for (const [targetId, backgroundPage] of this._browser._backgroundPages.entries()) {
|
||||
if (backgroundPage._browserContext === this && backgroundPage._initializedPage) {
|
||||
if (backgroundPage._browserContext === this && backgroundPage._page.initializedOrUndefined()) {
|
||||
backgroundPage.didClose();
|
||||
this._browser._backgroundPages.delete(targetId);
|
||||
}
|
||||
|
|
@ -569,8 +569,8 @@ export class CRBrowserContext extends BrowserContext {
|
|||
backgroundPages(): Page[] {
|
||||
const result: Page[] = [];
|
||||
for (const backgroundPage of this._browser._backgroundPages.values()) {
|
||||
if (backgroundPage._browserContext === this && backgroundPage._initializedPage)
|
||||
result.push(backgroundPage._initializedPage);
|
||||
if (backgroundPage._browserContext === this && backgroundPage._page.initializedOrUndefined())
|
||||
result.push(backgroundPage._page);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
|||
|
||||
function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue {
|
||||
if (error.message.includes('Object reference chain is too long'))
|
||||
return { result: { type: 'undefined' } };
|
||||
throw new Error('Cannot serialize result: object reference chain is too long.');
|
||||
if (error.message.includes('Object couldn\'t be returned by value'))
|
||||
return { result: { type: 'undefined' } };
|
||||
|
||||
|
|
|
|||
|
|
@ -65,8 +65,6 @@ export class CRPage implements PageDelegate {
|
|||
private readonly _pdf: CRPDF;
|
||||
private readonly _coverage: CRCoverage;
|
||||
readonly _browserContext: CRBrowserContext;
|
||||
private readonly _pagePromise: Promise<Page | Error>;
|
||||
_initializedPage: Page | null = null;
|
||||
private _isBackgroundPage: boolean;
|
||||
|
||||
// Holds window features for the next popup being opened via window.open,
|
||||
|
|
@ -108,30 +106,11 @@ export class CRPage implements PageDelegate {
|
|||
if (viewportSize)
|
||||
this._page._emulatedSize = { viewport: viewportSize, screen: viewportSize };
|
||||
}
|
||||
// Note: it is important to call |reportAsNew| before resolving pageOrError promise,
|
||||
// so that anyone who awaits pageOrError got a ready and reported page.
|
||||
this._pagePromise = this._mainFrameSession._initialize(bits.hasUIWindow).then(async r => {
|
||||
await this._page.initOpener(this._opener);
|
||||
return r;
|
||||
}).catch(async e => {
|
||||
await this._page.initOpener(this._opener);
|
||||
throw e;
|
||||
}).then(() => {
|
||||
this._initializedPage = this._page;
|
||||
this._reportAsNew();
|
||||
return this._page;
|
||||
}).catch(e => {
|
||||
this._reportAsNew(e);
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
potentiallyUninitializedPage(): Page {
|
||||
return this._page;
|
||||
}
|
||||
|
||||
private _reportAsNew(error?: Error) {
|
||||
this._page.reportAsNew(error, this._isBackgroundPage ? CRBrowserContext.CREvents.BackgroundPage : BrowserContext.Events.Page);
|
||||
const createdEvent = this._isBackgroundPage ? CRBrowserContext.CREvents.BackgroundPage : BrowserContext.Events.Page;
|
||||
this._mainFrameSession._initialize(bits.hasUIWindow).then(
|
||||
() => this._page.reportAsNew(this._opener?._page, undefined, createdEvent),
|
||||
error => this._page.reportAsNew(this._opener?._page, error, createdEvent));
|
||||
}
|
||||
|
||||
private async _forAllFrameSessions(cb: (frame: FrameSession) => Promise<any>) {
|
||||
|
|
@ -168,10 +147,6 @@ export class CRPage implements PageDelegate {
|
|||
this._mainFrameSession._willBeginDownload();
|
||||
}
|
||||
|
||||
async pageOrError(): Promise<Page | Error> {
|
||||
return this._pagePromise;
|
||||
}
|
||||
|
||||
didClose() {
|
||||
for (const session of this._sessions.values())
|
||||
session.dispose();
|
||||
|
|
@ -432,6 +407,9 @@ class FrameSession {
|
|||
this._firstNonInitialNavigationCommittedFulfill = f;
|
||||
this._firstNonInitialNavigationCommittedReject = r;
|
||||
});
|
||||
// The Promise is not always awaited (e.g. FrameSession._initialize can throw)
|
||||
// so we catch errors here to prevent unhandled promise rejection.
|
||||
this._firstNonInitialNavigationCommittedPromise.catch(() => {});
|
||||
}
|
||||
|
||||
_isMainFrame(): boolean {
|
||||
|
|
@ -489,7 +467,7 @@ class FrameSession {
|
|||
// Note: it is important to start video recorder before sending Page.startScreencast,
|
||||
// and it is equally important to send Page.startScreencast before sending Runtime.runIfWaitingForDebugger.
|
||||
await this._createVideoRecorder(screencastId, screencastOptions);
|
||||
this._crPage.pageOrError().then(p => {
|
||||
this._crPage._page.waitForInitializedOrError().then(p => {
|
||||
if (p instanceof Error)
|
||||
this._stopVideoRecording().catch(() => {});
|
||||
});
|
||||
|
|
@ -830,7 +808,7 @@ class FrameSession {
|
|||
}
|
||||
|
||||
async _onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) {
|
||||
const pageOrError = await this._crPage.pageOrError();
|
||||
const pageOrError = await this._crPage._page.waitForInitializedOrError();
|
||||
if (!(pageOrError instanceof Error)) {
|
||||
const context = this._contextIdToContext.get(event.executionContextId);
|
||||
if (context)
|
||||
|
|
@ -846,6 +824,9 @@ class FrameSession {
|
|||
event.type,
|
||||
event.message,
|
||||
async (accept: boolean, promptText?: string) => {
|
||||
// TODO: this should actually be a CDP event that notifies about a cancelled navigation attempt.
|
||||
if (this._isMainFrame() && event.type === 'beforeunload' && !accept)
|
||||
this._page._frameManager.frameAbortedNavigation(this._page.mainFrame()._id, 'navigation cancelled by beforeunload dialog');
|
||||
await this._client.send('Page.handleJavaScriptDialog', { accept, promptText });
|
||||
},
|
||||
event.defaultPrompt));
|
||||
|
|
@ -892,8 +873,7 @@ class FrameSession {
|
|||
}
|
||||
|
||||
_willBeginDownload() {
|
||||
const originPage = this._crPage._initializedPage;
|
||||
if (!originPage) {
|
||||
if (!this._crPage._page.initializedOrUndefined()) {
|
||||
// Resume the page creation with an error. The page will automatically close right
|
||||
// after the download begins.
|
||||
this._firstNonInitialNavigationCommittedReject(new Error('Starting new page download'));
|
||||
|
|
@ -933,7 +913,7 @@ class FrameSession {
|
|||
});
|
||||
// Wait for the first frame before reporting video to the client.
|
||||
gotFirstFrame.then(() => {
|
||||
this._crPage._browserContext._browser._videoStarted(this._crPage._browserContext, screencastId, options.outputFile, this._crPage.pageOrError());
|
||||
this._crPage._browserContext._browser._videoStarted(this._crPage._browserContext, screencastId, options.outputFile, this._crPage._page.waitForInitializedOrError());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ import { Recorder } from './recorder';
|
|||
import { EmptyRecorderApp } from './recorder/recorderApp';
|
||||
import { asLocator, type Language } from '../utils';
|
||||
import { parseYamlForAriaSnapshot } from './ariaSnapshot';
|
||||
import type { ParsedYaml } from '../utils/isomorphic/ariaSnapshot';
|
||||
import { parseYamlTemplate } from '../utils/isomorphic/ariaSnapshot';
|
||||
import { unsafeLocatorOrSelectorAsSelector } from '../utils/isomorphic/locatorParser';
|
||||
|
||||
const internalMetadata = serverSideCallMetadata();
|
||||
|
||||
|
|
@ -144,9 +147,17 @@ export class DebugController extends SdkObject {
|
|||
}
|
||||
|
||||
async highlight(params: { selector?: string, ariaTemplate?: string }) {
|
||||
// Assert parameters validity.
|
||||
if (params.selector)
|
||||
unsafeLocatorOrSelectorAsSelector(this._sdkLanguage, params.selector, 'data-testid');
|
||||
let parsedYaml: ParsedYaml | undefined;
|
||||
if (params.ariaTemplate) {
|
||||
parsedYaml = parseYamlForAriaSnapshot(params.ariaTemplate);
|
||||
parseYamlTemplate(parsedYaml);
|
||||
}
|
||||
for (const recorder of await this._allRecorders()) {
|
||||
if (params.ariaTemplate)
|
||||
recorder.setHighlightedAriaTemplate(parseYamlForAriaSnapshot(params.ariaTemplate));
|
||||
if (parsedYaml)
|
||||
recorder.setHighlightedAriaTemplate(parsedYaml);
|
||||
else if (params.selector)
|
||||
recorder.setHighlightedSelector(this._sdkLanguage, params.selector);
|
||||
}
|
||||
|
|
@ -228,7 +239,7 @@ class InspectingRecorderApp extends EmptyRecorderApp {
|
|||
|
||||
override async elementPicked(elementInfo: ElementInfo): Promise<void> {
|
||||
const locator: string = asLocator(this._debugController._sdkLanguage, elementInfo.selector);
|
||||
this._debugController.emit(DebugController.Events.InspectRequested, { selector: elementInfo.selector, locator });
|
||||
this._debugController.emit(DebugController.Events.InspectRequested, { selector: elementInfo.selector, locator, ariaSnapshot: elementInfo.ariaSnapshot });
|
||||
}
|
||||
|
||||
override async setSources(sources: Source[]): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -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/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.15 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.15 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 1138,
|
||||
"height": 712
|
||||
|
|
@ -1098,7 +1098,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/132.0.6834.15 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/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 384,
|
||||
"height": 640
|
||||
|
|
@ -1109,7 +1109,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/132.0.6834.15 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/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 384
|
||||
|
|
@ -1120,7 +1120,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/132.0.6834.15 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/132.0.6834.46 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1131,7 +1131,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/132.0.6834.15 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/132.0.6834.46 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1142,7 +1142,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/132.0.6834.15 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/132.0.6834.46 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1153,7 +1153,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/132.0.6834.15 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/132.0.6834.46 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1164,7 +1164,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/132.0.6834.15 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 800,
|
||||
"height": 1280
|
||||
|
|
@ -1175,7 +1175,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/132.0.6834.15 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 1280,
|
||||
"height": 800
|
||||
|
|
@ -1186,7 +1186,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/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 384,
|
||||
"height": 640
|
||||
|
|
@ -1197,7 +1197,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/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 384
|
||||
|
|
@ -1208,7 +1208,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1219,7 +1219,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/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1230,7 +1230,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/132.0.6834.15 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/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1241,7 +1241,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/132.0.6834.15 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/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1252,7 +1252,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/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1263,7 +1263,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/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1274,7 +1274,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/132.0.6834.15 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/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1285,7 +1285,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/132.0.6834.15 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/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1296,7 +1296,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/132.0.6834.15 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 600,
|
||||
"height": 960
|
||||
|
|
@ -1307,7 +1307,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/132.0.6834.15 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 960,
|
||||
"height": 600
|
||||
|
|
@ -1362,7 +1362,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/132.0.6834.15 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/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 411,
|
||||
"height": 731
|
||||
|
|
@ -1373,7 +1373,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/132.0.6834.15 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/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 731,
|
||||
"height": 411
|
||||
|
|
@ -1384,7 +1384,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/132.0.6834.15 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/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 411,
|
||||
"height": 823
|
||||
|
|
@ -1395,7 +1395,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/132.0.6834.15 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/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 823,
|
||||
"height": 411
|
||||
|
|
@ -1406,7 +1406,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/132.0.6834.15 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/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 393,
|
||||
"height": 786
|
||||
|
|
@ -1417,7 +1417,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/132.0.6834.15 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/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 786,
|
||||
"height": 393
|
||||
|
|
@ -1428,7 +1428,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 353,
|
||||
"height": 745
|
||||
|
|
@ -1439,7 +1439,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 745,
|
||||
"height": 353
|
||||
|
|
@ -1450,7 +1450,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4a (5G)": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 412,
|
||||
"height": 892
|
||||
|
|
@ -1465,7 +1465,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4a (5G) landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"height": 892,
|
||||
"width": 412
|
||||
|
|
@ -1480,7 +1480,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 393,
|
||||
"height": 851
|
||||
|
|
@ -1495,7 +1495,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 5 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 851,
|
||||
"height": 393
|
||||
|
|
@ -1510,7 +1510,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 7": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 412,
|
||||
"height": 915
|
||||
|
|
@ -1525,7 +1525,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 7 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 915,
|
||||
"height": 412
|
||||
|
|
@ -1540,7 +1540,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Moto G4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1551,7 +1551,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Moto G4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1562,7 +1562,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Chrome HiDPI": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36",
|
||||
"screen": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1577,7 +1577,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Edge HiDPI": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Safari/537.36 Edg/132.0.6834.15",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36 Edg/132.0.6834.46",
|
||||
"screen": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1622,7 +1622,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Desktop Chrome": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36",
|
||||
"screen": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
@ -1637,7 +1637,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Edge": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Safari/537.36 Edg/132.0.6834.15",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36 Edg/132.0.6834.46",
|
||||
"screen": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ export class DebugControllerDispatcher extends Dispatcher<DebugController, chann
|
|||
eventsHelper.addEventListener(this._object, DebugController.Events.StateChanged, params => {
|
||||
this._dispatchEvent('stateChanged', params);
|
||||
}),
|
||||
eventsHelper.addEventListener(this._object, DebugController.Events.InspectRequested, ({ selector, locator }) => {
|
||||
this._dispatchEvent('inspectRequested', { selector, locator });
|
||||
eventsHelper.addEventListener(this._object, DebugController.Events.InspectRequested, ({ selector, locator, ariaSnapshot }) => {
|
||||
this._dispatchEvent('inspectRequested', { selector, locator, ariaSnapshot });
|
||||
}),
|
||||
eventsHelper.addEventListener(this._object, DebugController.Events.SourceChanged, ({ text, header, footer, actions }) => {
|
||||
this._dispatchEvent('sourceChanged', ({ text, header, footer, actions }));
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import type { BrowserOptions } from '../browser';
|
|||
import { Browser } from '../browser';
|
||||
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
|
||||
import * as network from '../network';
|
||||
import type { InitScript, Page, PageDelegate } from '../page';
|
||||
import type { InitScript, Page } from '../page';
|
||||
import { PageBinding } from '../page';
|
||||
import type { ConnectionTransport } from '../transport';
|
||||
import type * as types from '../types';
|
||||
|
|
@ -136,14 +136,14 @@ export class FFBrowser extends Browser {
|
|||
// Abort the navigation that turned into download.
|
||||
ffPage._page._frameManager.frameAbortedNavigation(payload.frameId, 'Download is starting');
|
||||
|
||||
let originPage = ffPage._initializedPage;
|
||||
let originPage = ffPage._page.initializedOrUndefined();
|
||||
// If it's a new window download, report it on the opener page.
|
||||
if (!originPage) {
|
||||
// Resume the page creation with an error. The page will automatically close right
|
||||
// after the download begins.
|
||||
ffPage._markAsError(new Error('Starting new page download'));
|
||||
if (ffPage._opener)
|
||||
originPage = ffPage._opener._initializedPage;
|
||||
originPage = ffPage._opener._page.initializedOrUndefined();
|
||||
}
|
||||
if (!originPage)
|
||||
return;
|
||||
|
|
@ -267,11 +267,11 @@ export class FFBrowserContext extends BrowserContext {
|
|||
return Array.from(this._browser._ffPages.values()).filter(ffPage => ffPage._browserContext === this);
|
||||
}
|
||||
|
||||
pages(): Page[] {
|
||||
return this._ffPages().map(ffPage => ffPage._initializedPage).filter(pageOrNull => !!pageOrNull) as Page[];
|
||||
override possiblyUninitializedPages(): Page[] {
|
||||
return this._ffPages().map(ffPage => ffPage._page);
|
||||
}
|
||||
|
||||
async newPageDelegate(): Promise<PageDelegate> {
|
||||
override async doCreateNewPage(): Promise<Page> {
|
||||
assertBrowserContextIsNotOwned(this);
|
||||
const { targetId } = await this._browser.session.send('Browser.newPage', {
|
||||
browserContextId: this._browserContextId
|
||||
|
|
@ -280,7 +280,7 @@ export class FFBrowserContext extends BrowserContext {
|
|||
throw new Error(`Invalid timezone ID: ${this._options.timezoneId}`);
|
||||
throw e;
|
||||
});
|
||||
return this._browser._ffPages.get(targetId)!;
|
||||
return this._browser._ffPages.get(targetId)!._page;
|
||||
}
|
||||
|
||||
async doGetCookies(urls: string[]): Promise<channels.NetworkCookie[]> {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ import type { Protocol } from './protocol';
|
|||
import type { Progress } from '../progress';
|
||||
import { splitErrorMessage } from '../../utils/stackTrace';
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
import { ManualPromise } from '../../utils/manualPromise';
|
||||
import { BrowserContext } from '../browserContext';
|
||||
import { TargetClosedError } from '../errors';
|
||||
|
||||
|
|
@ -49,9 +48,7 @@ export class FFPage implements PageDelegate {
|
|||
readonly _page: Page;
|
||||
readonly _networkManager: FFNetworkManager;
|
||||
readonly _browserContext: FFBrowserContext;
|
||||
private _pagePromise = new ManualPromise<Page | Error>();
|
||||
_initializedPage: Page | null = null;
|
||||
private _initializationFailed = false;
|
||||
private _reportedAsNew = false;
|
||||
readonly _opener: FFPage | null;
|
||||
private readonly _contextIdToContext: Map<string, dom.FrameExecutionContext>;
|
||||
private _eventListeners: RegisteredListener[];
|
||||
|
|
@ -102,40 +99,23 @@ export class FFPage implements PageDelegate {
|
|||
eventsHelper.addEventListener(this._session, 'Page.screencastFrame', this._onScreencastFrame.bind(this)),
|
||||
|
||||
];
|
||||
this._session.once('Page.ready', async () => {
|
||||
await this._page.initOpener(this._opener);
|
||||
if (this._initializationFailed)
|
||||
this._session.once('Page.ready', () => {
|
||||
if (this._reportedAsNew)
|
||||
return;
|
||||
// Note: it is important to call |reportAsNew| before resolving pageOrError promise,
|
||||
// so that anyone who awaits pageOrError got a ready and reported page.
|
||||
this._initializedPage = this._page;
|
||||
this._page.reportAsNew();
|
||||
this._pagePromise.resolve(this._page);
|
||||
this._reportedAsNew = true;
|
||||
this._page.reportAsNew(this._opener?._page);
|
||||
});
|
||||
// Ideally, we somehow ensure that utility world is created before Page.ready arrives, but currently it is racy.
|
||||
// Therefore, we can end up with an initialized page without utility world, although very unlikely.
|
||||
this.addInitScript(new InitScript('', true), UTILITY_WORLD_NAME).catch(e => this._markAsError(e));
|
||||
}
|
||||
|
||||
potentiallyUninitializedPage(): Page {
|
||||
return this._page;
|
||||
}
|
||||
|
||||
async _markAsError(error: Error) {
|
||||
// Same error may be report twice: channer disconnected and session.send fails.
|
||||
if (this._initializationFailed)
|
||||
// Same error may be reported twice: channel disconnected and session.send fails.
|
||||
if (this._reportedAsNew)
|
||||
return;
|
||||
this._initializationFailed = true;
|
||||
|
||||
if (!this._initializedPage) {
|
||||
await this._page.initOpener(this._opener);
|
||||
this._page.reportAsNew(error);
|
||||
this._pagePromise.resolve(error);
|
||||
}
|
||||
}
|
||||
|
||||
async pageOrError(): Promise<Page | Error> {
|
||||
return this._pagePromise;
|
||||
this._reportedAsNew = true;
|
||||
this._page.reportAsNew(this._opener?._page, error);
|
||||
}
|
||||
|
||||
_onWebSocketCreated(event: Protocol.Page.webSocketCreatedPayload) {
|
||||
|
|
@ -268,7 +248,7 @@ export class FFPage implements PageDelegate {
|
|||
}
|
||||
|
||||
async _onBindingCalled(event: Protocol.Page.bindingCalledPayload) {
|
||||
const pageOrError = await this.pageOrError();
|
||||
const pageOrError = await this._page.waitForInitializedOrError();
|
||||
if (!(pageOrError instanceof Error)) {
|
||||
const context = this._contextIdToContext.get(event.executionContextId);
|
||||
if (context)
|
||||
|
|
@ -333,7 +313,7 @@ export class FFPage implements PageDelegate {
|
|||
}
|
||||
|
||||
_onVideoRecordingStarted(event: Protocol.Page.videoRecordingStartedPayload) {
|
||||
this._browserContext._browser._videoStarted(this._browserContext, event.screencastId, event.file, this.pageOrError());
|
||||
this._browserContext._browser._videoStarted(this._browserContext, event.screencastId, event.file, this._page.waitForInitializedOrError());
|
||||
}
|
||||
|
||||
didClose() {
|
||||
|
|
|
|||
|
|
@ -158,8 +158,10 @@ function toAriaNode(element: Element): AriaNode | null {
|
|||
if (roleUtils.kAriaSelectedRoles.includes(role))
|
||||
result.selected = roleUtils.getAriaSelected(element);
|
||||
|
||||
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)
|
||||
result.children = [element.value];
|
||||
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
||||
if (element.type !== 'checkbox' && element.type !== 'radio')
|
||||
result.children = [element.value];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,21 +149,24 @@ x-pw-tools-list {
|
|||
|
||||
x-pw-tool-item {
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
x-pw-tool-item:not(.disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
x-pw-tool-item:not(.disabled):hover {
|
||||
background-color: hsl(0, 0%, 86%);
|
||||
}
|
||||
|
||||
x-pw-tool-item.active {
|
||||
x-pw-tool-item.toggled {
|
||||
background-color: rgba(138, 202, 228, 0.5);
|
||||
}
|
||||
|
||||
x-pw-tool-item.active:not(.disabled):hover {
|
||||
x-pw-tool-item.toggled:not(.disabled):hover {
|
||||
background-color: #8acae4c4;
|
||||
}
|
||||
|
||||
|
|
@ -179,18 +182,22 @@ x-pw-tool-item.disabled > x-div {
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
x-pw-tool-item.record.active {
|
||||
x-pw-tool-item.record.toggled {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
x-pw-tool-item.record.active:hover {
|
||||
x-pw-tool-item.record.toggled:not(.disabled):hover {
|
||||
background-color: hsl(0, 0%, 86%);
|
||||
}
|
||||
|
||||
x-pw-tool-item.record.active > x-div {
|
||||
x-pw-tool-item.record.toggled > x-div {
|
||||
background-color: #a1260d;
|
||||
}
|
||||
|
||||
x-pw-tool-item.record.disabled.toggled > x-div {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
x-pw-tool-item.accept > x-div {
|
||||
background-color: #388a34;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -457,7 +457,8 @@ export class InjectedScript {
|
|||
const queryAll = (root: SelectorRoot, body: string) => {
|
||||
if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
|
||||
return [];
|
||||
return isElementVisible(root as Element) === Boolean(body) ? [root as Element] : [];
|
||||
const visible = body === 'true';
|
||||
return isElementVisible(root as Element) === visible ? [root as Element] : [];
|
||||
};
|
||||
return { queryAll };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -883,9 +883,13 @@ class Overlay {
|
|||
this._dragState = { offsetX: this._offsetX, dragStart: { x: (event as MouseEvent).clientX, y: 0 } };
|
||||
}),
|
||||
addEventListener(this._recordToggle, 'click', () => {
|
||||
if (this._recordToggle.classList.contains('disabled'))
|
||||
return;
|
||||
this._recorder.setMode(this._recorder.state.mode === 'none' || this._recorder.state.mode === 'standby' || this._recorder.state.mode === 'inspecting' ? 'recording' : 'standby');
|
||||
}),
|
||||
addEventListener(this._pickLocatorToggle, 'click', () => {
|
||||
if (this._pickLocatorToggle.classList.contains('disabled'))
|
||||
return;
|
||||
const newMode: Record<Mode, Mode> = {
|
||||
'inspecting': 'standby',
|
||||
'none': 'inspecting',
|
||||
|
|
@ -929,15 +933,15 @@ class Overlay {
|
|||
}
|
||||
|
||||
setUIState(state: UIState) {
|
||||
this._recordToggle.classList.toggle('active', state.mode === 'recording' || state.mode === 'assertingText' || state.mode === 'assertingVisibility' || state.mode === 'assertingValue' || state.mode === 'recording-inspecting');
|
||||
this._pickLocatorToggle.classList.toggle('active', state.mode === 'inspecting' || state.mode === 'recording-inspecting');
|
||||
this._assertVisibilityToggle.classList.toggle('active', state.mode === 'assertingVisibility');
|
||||
this._recordToggle.classList.toggle('toggled', state.mode === 'recording' || state.mode === 'assertingText' || state.mode === 'assertingVisibility' || state.mode === 'assertingValue' || state.mode === 'assertingSnapshot' || state.mode === 'recording-inspecting');
|
||||
this._pickLocatorToggle.classList.toggle('toggled', state.mode === 'inspecting' || state.mode === 'recording-inspecting');
|
||||
this._assertVisibilityToggle.classList.toggle('toggled', state.mode === 'assertingVisibility');
|
||||
this._assertVisibilityToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting');
|
||||
this._assertTextToggle.classList.toggle('active', state.mode === 'assertingText');
|
||||
this._assertTextToggle.classList.toggle('toggled', state.mode === 'assertingText');
|
||||
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('toggled', state.mode === 'assertingValue');
|
||||
this._assertValuesToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting');
|
||||
this._assertSnapshotToggle.classList.toggle('active', state.mode === 'assertingSnapshot');
|
||||
this._assertSnapshotToggle.classList.toggle('toggled', state.mode === 'assertingSnapshot');
|
||||
this._assertSnapshotToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting');
|
||||
if (this._offsetX !== state.overlay.offsetX) {
|
||||
this._offsetX = state.overlay.offsetX;
|
||||
|
|
@ -1157,7 +1161,6 @@ export class Recorder {
|
|||
}
|
||||
|
||||
clearHighlight() {
|
||||
this._currentTool.cleanup?.();
|
||||
this.updateHighlight(null, false);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ const kOtherTestIdScore = 2; // other data-test* attributes
|
|||
const kIframeByAttributeScore = 10;
|
||||
|
||||
const kBeginPenalizedScore = 50;
|
||||
const kPlaceholderScore = 100;
|
||||
const kLabelScore = 120;
|
||||
const kRoleWithNameScore = 140;
|
||||
const kRoleWithNameScore = 100;
|
||||
const kPlaceholderScore = 120;
|
||||
const kLabelScore = 140;
|
||||
const kAltTextScore = 160;
|
||||
const kTextScore = 180;
|
||||
const kTitleScore = 200;
|
||||
|
|
@ -268,7 +268,7 @@ function buildNoTextCandidates(injectedScript: InjectedScript, element: Element,
|
|||
if (input.placeholder) {
|
||||
candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(input.placeholder, true)}]`, score: kPlaceholderScoreExact });
|
||||
for (const alternative of suitableTextAlternatives(input.placeholder))
|
||||
candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(alternative.text, false)}]`, score: kPlaceholderScore - alternative.scoreBouns });
|
||||
candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(alternative.text, false)}]`, score: kPlaceholderScore - alternative.scoreBonus });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -277,7 +277,7 @@ function buildNoTextCandidates(injectedScript: InjectedScript, element: Element,
|
|||
const labelText = label.normalized;
|
||||
candidates.push({ engine: 'internal:label', selector: escapeForTextSelector(labelText, true), score: kLabelScoreExact });
|
||||
for (const alternative of suitableTextAlternatives(labelText))
|
||||
candidates.push({ engine: 'internal:label', selector: escapeForTextSelector(alternative.text, false), score: kLabelScore - alternative.scoreBouns });
|
||||
candidates.push({ engine: 'internal:label', selector: escapeForTextSelector(alternative.text, false), score: kLabelScore - alternative.scoreBonus });
|
||||
}
|
||||
|
||||
const ariaRole = getAriaRole(element);
|
||||
|
|
@ -308,28 +308,28 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, i
|
|||
if (title) {
|
||||
candidates.push([{ engine: 'internal:attr', selector: `[title=${escapeForAttributeSelector(title, true)}]`, score: kTitleScoreExact }]);
|
||||
for (const alternative of suitableTextAlternatives(title))
|
||||
candidates.push([{ engine: 'internal:attr', selector: `[title=${escapeForAttributeSelector(alternative.text, false)}]`, score: kTitleScore - alternative.scoreBouns }]);
|
||||
candidates.push([{ engine: 'internal:attr', selector: `[title=${escapeForAttributeSelector(alternative.text, false)}]`, score: kTitleScore - alternative.scoreBonus }]);
|
||||
}
|
||||
|
||||
const alt = element.getAttribute('alt');
|
||||
if (alt && ['APPLET', 'AREA', 'IMG', 'INPUT'].includes(element.nodeName)) {
|
||||
candidates.push([{ engine: 'internal:attr', selector: `[alt=${escapeForAttributeSelector(alt, true)}]`, score: kAltTextScoreExact }]);
|
||||
for (const alternative of suitableTextAlternatives(alt))
|
||||
candidates.push([{ engine: 'internal:attr', selector: `[alt=${escapeForAttributeSelector(alternative.text, false)}]`, score: kAltTextScore - alternative.scoreBouns }]);
|
||||
candidates.push([{ engine: 'internal:attr', selector: `[alt=${escapeForAttributeSelector(alternative.text, false)}]`, score: kAltTextScore - alternative.scoreBonus }]);
|
||||
}
|
||||
|
||||
const text = elementText(injectedScript._evaluator._cacheText, element).normalized;
|
||||
const textAlternatives = text ? suitableTextAlternatives(text) : [];
|
||||
if (text) {
|
||||
const alternatives = suitableTextAlternatives(text);
|
||||
if (isTargetNode) {
|
||||
if (text.length <= 80)
|
||||
candidates.push([{ engine: 'internal:text', selector: escapeForTextSelector(text, true), score: kTextScoreExact }]);
|
||||
for (const alternative of alternatives)
|
||||
candidates.push([{ engine: 'internal:text', selector: escapeForTextSelector(alternative.text, false), score: kTextScore - alternative.scoreBouns }]);
|
||||
for (const alternative of textAlternatives)
|
||||
candidates.push([{ engine: 'internal:text', selector: escapeForTextSelector(alternative.text, false), score: kTextScore - alternative.scoreBonus }]);
|
||||
}
|
||||
const cssToken: SelectorToken = { engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: kCSSTagNameScore };
|
||||
for (const alternative of alternatives)
|
||||
candidates.push([cssToken, { engine: 'internal:has-text', selector: escapeForTextSelector(alternative.text, false), score: kTextScore - alternative.scoreBouns }]);
|
||||
for (const alternative of textAlternatives)
|
||||
candidates.push([cssToken, { engine: 'internal:has-text', selector: escapeForTextSelector(alternative.text, false), score: kTextScore - alternative.scoreBonus }]);
|
||||
if (text.length <= 80) {
|
||||
const re = new RegExp('^' + escapeRegExp(text) + '$');
|
||||
candidates.push([cssToken, { engine: 'internal:has-text', selector: escapeForTextSelector(re, false), score: kTextScoreRegex }]);
|
||||
|
|
@ -340,9 +340,18 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, i
|
|||
if (ariaRole && !['none', 'presentation'].includes(ariaRole)) {
|
||||
const ariaName = getElementAccessibleName(element, false);
|
||||
if (ariaName) {
|
||||
candidates.push([{ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: kRoleWithNameScoreExact }]);
|
||||
const roleToken = { engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: kRoleWithNameScoreExact };
|
||||
candidates.push([roleToken]);
|
||||
for (const alternative of suitableTextAlternatives(ariaName))
|
||||
candidates.push([{ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(alternative.text, false)}]`, score: kRoleWithNameScore - alternative.scoreBouns }]);
|
||||
candidates.push([{ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(alternative.text, false)}]`, score: kRoleWithNameScore - alternative.scoreBonus }]);
|
||||
} else {
|
||||
const roleToken = { engine: 'internal:role', selector: `${ariaRole}`, score: kRoleWithoutNameScore };
|
||||
for (const alternative of textAlternatives)
|
||||
candidates.push([roleToken, { engine: 'internal:has-text', selector: escapeForTextSelector(alternative.text, false), score: kTextScore - alternative.scoreBonus }]);
|
||||
if (text.length <= 80) {
|
||||
const re = new RegExp('^' + escapeRegExp(text) + '$');
|
||||
candidates.push([roleToken, { engine: 'internal:has-text', selector: escapeForTextSelector(re, false), score: kTextScoreRegex }]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -527,14 +536,14 @@ function trimWordBoundary(text: string, maxLength: number) {
|
|||
}
|
||||
|
||||
function suitableTextAlternatives(text: string) {
|
||||
let result: { text: string, scoreBouns: number }[] = [];
|
||||
let result: { text: string, scoreBonus: number }[] = [];
|
||||
|
||||
{
|
||||
const match = text.match(/^([\d.,]+)[^.,\w]/);
|
||||
const leadingNumberLength = match ? match[1].length : 0;
|
||||
if (leadingNumberLength) {
|
||||
const alt = trimWordBoundary(text.substring(leadingNumberLength).trimStart(), 80);
|
||||
result.push({ text: alt, scoreBouns: alt.length <= 30 ? 2 : 1 });
|
||||
result.push({ text: alt, scoreBonus: alt.length <= 30 ? 2 : 1 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -543,20 +552,20 @@ function suitableTextAlternatives(text: string) {
|
|||
const trailingNumberLength = match ? match[1].length : 0;
|
||||
if (trailingNumberLength) {
|
||||
const alt = trimWordBoundary(text.substring(0, text.length - trailingNumberLength).trimEnd(), 80);
|
||||
result.push({ text: alt, scoreBouns: alt.length <= 30 ? 2 : 1 });
|
||||
result.push({ text: alt, scoreBonus: alt.length <= 30 ? 2 : 1 });
|
||||
}
|
||||
}
|
||||
|
||||
if (text.length <= 30) {
|
||||
result.push({ text, scoreBouns: 0 });
|
||||
result.push({ text, scoreBonus: 0 });
|
||||
} else {
|
||||
result.push({ text: trimWordBoundary(text, 80), scoreBouns: 0 });
|
||||
result.push({ text: trimWordBoundary(text, 30), scoreBouns: 1 });
|
||||
result.push({ text: trimWordBoundary(text, 80), scoreBonus: 0 });
|
||||
result.push({ text: trimWordBoundary(text, 30), scoreBonus: 1 });
|
||||
}
|
||||
|
||||
result = result.filter(r => r.text);
|
||||
if (!result.length)
|
||||
result.push({ text: text.substring(0, 80), scoreBouns: 0 });
|
||||
result.push({ text: text.substring(0, 80), scoreBonus: 0 });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ export function elementText(cache: Map<Element | ShadowRoot, ElementText>, root:
|
|||
if (child.nodeType === Node.TEXT_NODE) {
|
||||
value.full += child.nodeValue || '';
|
||||
currentImmediate += child.nodeValue || '';
|
||||
} else if (child.nodeType === Node.COMMENT_NODE) {
|
||||
continue;
|
||||
} else {
|
||||
if (currentImmediate)
|
||||
value.immediate.push(currentImmediate);
|
||||
|
|
|
|||
|
|
@ -62,12 +62,8 @@ function yamlStringNeedsQuotes(str: string): boolean {
|
|||
if (/^-\s/.test(str))
|
||||
return true;
|
||||
|
||||
// Strings that start with a special indicator character need quotes
|
||||
if (/^[&*\],].*/.test(str))
|
||||
return true;
|
||||
|
||||
// Strings containing ':' followed by a space or at the end need quotes
|
||||
if (/:(\s|$)/.test(str))
|
||||
// Strings containing ':' or '\n' followed by a space or at the end need quotes
|
||||
if (/[\n:](\s|$)/.test(str))
|
||||
return true;
|
||||
|
||||
// Strings containing '#' preceded by a space need quotes (comment indicator)
|
||||
|
|
@ -78,21 +74,17 @@ function yamlStringNeedsQuotes(str: string): boolean {
|
|||
if (/[\n\r]/.test(str))
|
||||
return true;
|
||||
|
||||
// Strings starting with '?' or '!' (directives) need quotes
|
||||
if (/^[?!]/.test(str))
|
||||
return true;
|
||||
|
||||
// Strings starting with '>' or '|' (block scalar indicators) need quotes
|
||||
if (/^[>|]/.test(str))
|
||||
return true;
|
||||
|
||||
// Strings starting with quotes need quotes
|
||||
if (/^["']/.test(str))
|
||||
// Strings starting with indicator characters or quotes need quotes
|
||||
if (/^[&*\],?!>|@"'#%]/.test(str))
|
||||
return true;
|
||||
|
||||
// Strings containing special characters that could cause ambiguity
|
||||
if (/[{}`]/.test(str))
|
||||
return true;
|
||||
|
||||
// Non-string types recognized by YAML
|
||||
if (!isNaN(Number(str)) || ['y', 'n', 'yes', 'no', 'true', 'false', 'on', 'off', 'null'].includes(str.toLowerCase()))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,8 +59,6 @@ export interface PageDelegate {
|
|||
addInitScript(initScript: InitScript): Promise<void>;
|
||||
removeNonInternalInitScripts(): Promise<void>;
|
||||
closePage(runBeforeUnload: boolean): Promise<void>;
|
||||
potentiallyUninitializedPage(): Page;
|
||||
pageOrError(): Promise<Page | Error>;
|
||||
|
||||
navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult>;
|
||||
|
||||
|
|
@ -139,7 +137,8 @@ export class Page extends SdkObject {
|
|||
|
||||
private _closedState: 'open' | 'closing' | 'closed' = 'open';
|
||||
private _closedPromise = new ManualPromise<void>();
|
||||
private _initialized = false;
|
||||
private _initialized: Page | Error | undefined;
|
||||
private _initializedPromise = new ManualPromise<Page | Error>();
|
||||
private _eventsToEmitAfterInitialized: { event: string | symbol, args: any[] }[] = [];
|
||||
private _crashed = false;
|
||||
readonly openScope = new LongStandingScope();
|
||||
|
|
@ -164,7 +163,6 @@ export class Page extends SdkObject {
|
|||
_clientRequestInterceptor: network.RouteHandler | undefined;
|
||||
_serverRequestInterceptor: network.RouteHandler | undefined;
|
||||
_ownedContext: BrowserContext | undefined;
|
||||
_pageIsError: Error | undefined;
|
||||
_video: Artifact | null = null;
|
||||
_opener: Page | undefined;
|
||||
private _isServerSideOnly = false;
|
||||
|
|
@ -194,23 +192,24 @@ export class Page extends SdkObject {
|
|||
this.coverage = delegate.coverage ? delegate.coverage() : null;
|
||||
}
|
||||
|
||||
async initOpener(opener: PageDelegate | null) {
|
||||
if (!opener)
|
||||
return;
|
||||
const openerPage = await opener.pageOrError();
|
||||
if (openerPage instanceof Page && !openerPage.isClosed())
|
||||
this._opener = openerPage;
|
||||
async reportAsNew(opener: Page | undefined, error: Error | undefined = undefined, contextEvent: string = BrowserContext.Events.Page) {
|
||||
if (opener) {
|
||||
const openerPageOrError = await opener.waitForInitializedOrError();
|
||||
if (openerPageOrError instanceof Page && !openerPageOrError.isClosed())
|
||||
this._opener = openerPageOrError;
|
||||
}
|
||||
this._markInitialized(error, contextEvent);
|
||||
}
|
||||
|
||||
reportAsNew(error: Error | undefined = undefined, contextEvent: string = BrowserContext.Events.Page) {
|
||||
private _markInitialized(error: Error | undefined = undefined, contextEvent: string = BrowserContext.Events.Page) {
|
||||
if (error) {
|
||||
// Initialization error could have happened because of
|
||||
// context/browser closure. Just ignore the page.
|
||||
if (this._browserContext.isClosingOrClosed())
|
||||
return;
|
||||
this._setIsError(error);
|
||||
this._frameManager.createDummyMainFrameIfNeeded();
|
||||
}
|
||||
this._initialized = true;
|
||||
this._initialized = error || this;
|
||||
this.emitOnContext(contextEvent, this);
|
||||
|
||||
for (const { event, args } of this._eventsToEmitAfterInitialized)
|
||||
|
|
@ -224,12 +223,20 @@ export class Page extends SdkObject {
|
|||
this.emit(Page.Events.Close);
|
||||
else
|
||||
this.instrumentation.onPageOpen(this);
|
||||
|
||||
// Note: it is important to resolve _initializedPromise at the end,
|
||||
// so that anyone who awaits waitForInitializedOrError got a ready and reported page.
|
||||
this._initializedPromise.resolve(this._initialized);
|
||||
}
|
||||
|
||||
initializedOrUndefined() {
|
||||
initializedOrUndefined(): Page | undefined {
|
||||
return this._initialized ? this : undefined;
|
||||
}
|
||||
|
||||
waitForInitializedOrError(): Promise<Page | Error> {
|
||||
return this._initializedPromise;
|
||||
}
|
||||
|
||||
emitOnContext(event: string | symbol, ...args: any[]) {
|
||||
if (this._isServerSideOnly)
|
||||
return;
|
||||
|
|
@ -709,11 +716,6 @@ export class Page extends SdkObject {
|
|||
await this._ownedContext.close(options);
|
||||
}
|
||||
|
||||
private _setIsError(error: Error) {
|
||||
this._pageIsError = error;
|
||||
this._frameManager.createDummyMainFrameIfNeeded();
|
||||
}
|
||||
|
||||
isClosed(): boolean {
|
||||
return this._closedState === 'closed';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -300,7 +300,6 @@ async function generateFrameSelectorInParent(parent: Frame, frame: Frame): Promi
|
|||
}, frameElement);
|
||||
return selector;
|
||||
} catch (e) {
|
||||
return e.toString();
|
||||
}
|
||||
}, monotonicTime() + 2000);
|
||||
if (!result.timedOut && result.result)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export class RecorderCollection extends EventEmitter {
|
|||
|
||||
restart() {
|
||||
this._actions = [];
|
||||
this._fireChange();
|
||||
this.emit('change', []);
|
||||
}
|
||||
|
||||
setEnabled(enabled: boolean) {
|
||||
|
|
@ -128,6 +128,7 @@ export class RecorderCollection extends EventEmitter {
|
|||
private _fireChange() {
|
||||
if (!this._enabled)
|
||||
return;
|
||||
|
||||
this.emit('change', collapseActions(this._actions));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,6 +174,35 @@ const DOWNLOAD_PATHS: Record<BrowserName | InternalTool, DownloadPaths> = {
|
|||
'mac15-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-tip-of-tree-headless-shell': {
|
||||
'<unknown>': undefined,
|
||||
'ubuntu18.04-x64': undefined,
|
||||
'ubuntu20.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
|
||||
'ubuntu22.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
|
||||
'ubuntu24.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
|
||||
'ubuntu18.04-arm64': undefined,
|
||||
'ubuntu20.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
|
||||
'ubuntu22.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
|
||||
'ubuntu24.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
|
||||
'debian11-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
|
||||
'debian11-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
|
||||
'debian12-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
|
||||
'debian12-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
|
||||
'mac10.13': undefined,
|
||||
'mac10.14': undefined,
|
||||
'mac10.15': undefined,
|
||||
'mac11': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
|
||||
'mac11-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
|
||||
'mac12': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
|
||||
'mac12-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
|
||||
'mac13': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
|
||||
'mac13-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
|
||||
'mac14': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
|
||||
'mac14-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
|
||||
'mac15': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
|
||||
'mac15-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
|
||||
'win64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-win64.zip',
|
||||
},
|
||||
'firefox': {
|
||||
'<unknown>': undefined,
|
||||
'ubuntu18.04-x64': undefined,
|
||||
|
|
@ -386,7 +415,14 @@ type BrowsersJSONDescriptor = {
|
|||
};
|
||||
|
||||
function readDescriptors(browsersJSON: BrowsersJSON): BrowsersJSONDescriptor[] {
|
||||
return (browsersJSON['browsers']).map(obj => {
|
||||
const headlessShells: BrowsersJSON['browsers'] = [];
|
||||
for (const browserName of ['chromium', 'chromium-tip-of-tree']) {
|
||||
headlessShells.push({
|
||||
...browsersJSON.browsers.find(browser => browser.name === browserName)!,
|
||||
name: `${browserName}-headless-shell`,
|
||||
});
|
||||
}
|
||||
return [...browsersJSON.browsers, ...headlessShells].map(obj => {
|
||||
const name = obj.name;
|
||||
const revisionOverride = (obj.revisionOverrides || {})[hostPlatform];
|
||||
const revision = revisionOverride || obj.revision;
|
||||
|
|
@ -410,10 +446,10 @@ function readDescriptors(browsersJSON: BrowsersJSON): BrowsersJSONDescriptor[] {
|
|||
}
|
||||
|
||||
export type BrowserName = 'chromium' | 'firefox' | 'webkit' | 'bidi';
|
||||
type InternalTool = 'ffmpeg' | 'firefox-beta' | 'chromium-tip-of-tree' | 'chromium-headless-shell' | 'android';
|
||||
type InternalTool = 'ffmpeg' | 'firefox-beta' | 'chromium-tip-of-tree' | 'chromium-headless-shell' | 'chromium-tip-of-tree-headless-shell' | 'android';
|
||||
type BidiChannel = 'bidi-firefox-stable' | 'bidi-firefox-beta' | 'bidi-firefox-nightly' | 'bidi-chrome-canary' | 'bidi-chrome-stable' | 'bidi-chromium';
|
||||
type ChromiumChannel = 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary' | 'msedge' | 'msedge-beta' | 'msedge-dev' | 'msedge-canary';
|
||||
const allDownloadable = ['android', 'chromium', 'firefox', 'webkit', 'ffmpeg', 'firefox-beta', 'chromium-tip-of-tree', 'chromium-headless-shell'];
|
||||
const allDownloadable = ['android', 'chromium', 'firefox', 'webkit', 'ffmpeg', 'firefox-beta', 'chromium-tip-of-tree', 'chromium-headless-shell', 'chromium-tip-of-tree-headless-shell'];
|
||||
|
||||
export interface Executable {
|
||||
type: 'browser' | 'tool' | 'channel';
|
||||
|
|
@ -514,6 +550,24 @@ export class Registry {
|
|||
_isHermeticInstallation: true,
|
||||
});
|
||||
|
||||
const chromiumTipOfTreeHeadlessShell = descriptors.find(d => d.name === 'chromium-tip-of-tree-headless-shell')!;
|
||||
const chromiumTipOfTreeHeadlessShellExecutable = findExecutablePath(chromiumTipOfTreeHeadlessShell.dir, 'chromium-headless-shell');
|
||||
this._executables.push({
|
||||
type: 'channel',
|
||||
name: 'chromium-tip-of-tree-headless-shell',
|
||||
browserName: 'chromium',
|
||||
directory: chromiumTipOfTreeHeadlessShell.dir,
|
||||
executablePath: () => chromiumTipOfTreeHeadlessShellExecutable,
|
||||
executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('chromium', chromiumTipOfTreeHeadlessShellExecutable, chromiumTipOfTreeHeadlessShell.installByDefault, sdkLanguage),
|
||||
installType: chromiumTipOfTreeHeadlessShell.installByDefault ? 'download-by-default' : 'download-on-demand',
|
||||
_validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, chromiumTipOfTreeHeadlessShell.dir, ['chrome-linux'], [], ['chrome-win']),
|
||||
downloadURLs: this._downloadURLs(chromiumTipOfTreeHeadlessShell),
|
||||
browserVersion: chromium.browserVersion,
|
||||
_install: () => this._downloadExecutable(chromiumTipOfTreeHeadlessShell, chromiumTipOfTreeHeadlessShellExecutable),
|
||||
_dependencyGroup: 'chromium',
|
||||
_isHermeticInstallation: true,
|
||||
});
|
||||
|
||||
const chromiumTipOfTree = descriptors.find(d => d.name === 'chromium-tip-of-tree')!;
|
||||
const chromiumTipOfTreeExecutable = findExecutablePath(chromiumTipOfTree.dir, 'chromium');
|
||||
this._executables.push({
|
||||
|
|
|
|||
|
|
@ -438,13 +438,13 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
|
|||
expectValue(value);
|
||||
attrs[kSelectedAttribute] = value;
|
||||
}
|
||||
if (nodeName === 'CANVAS') {
|
||||
const boundingRect = (element as HTMLCanvasElement).getBoundingClientRect();
|
||||
if (nodeName === 'CANVAS' || nodeName === 'IFRAME' || nodeName === 'FRAME') {
|
||||
const boundingRect = (element as HTMLElement).getBoundingClientRect();
|
||||
const value = JSON.stringify({
|
||||
left: boundingRect.left / window.innerWidth,
|
||||
top: boundingRect.top / window.innerHeight,
|
||||
right: boundingRect.right / window.innerWidth,
|
||||
bottom: boundingRect.bottom / window.innerHeight
|
||||
left: boundingRect.left,
|
||||
top: boundingRect.top,
|
||||
right: boundingRect.right,
|
||||
bottom: boundingRect.bottom
|
||||
});
|
||||
expectValue(kBoundingRectAttribute);
|
||||
expectValue(value);
|
||||
|
|
|
|||
|
|
@ -125,7 +125,13 @@ export async function installRootRedirect(server: HttpServer, traceUrls: string[
|
|||
for (const reporter of options.reporter || [])
|
||||
params.append('reporter', reporter);
|
||||
|
||||
const urlPath = `./trace/${options.webApp || 'index.html'}?${params.toString()}`;
|
||||
let baseUrl = '.';
|
||||
if (process.env.PW_HMR) {
|
||||
baseUrl = 'http://localhost:44223'; // port is hardcoded in build.js
|
||||
params.set('server', server.urlPrefix('precise'));
|
||||
}
|
||||
|
||||
const urlPath = `${baseUrl}/trace/${options.webApp || 'index.html'}?${params.toString()}`;
|
||||
server.routePath('/', (_, response) => {
|
||||
response.statusCode = 302;
|
||||
response.setHeader('Location', urlPath);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-
|
|||
import type { HeadersArray } from './types';
|
||||
|
||||
export const perMessageDeflate = {
|
||||
clientNoContextTakeover: true,
|
||||
zlibDeflateOptions: {
|
||||
level: 3,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { Browser } from '../browser';
|
|||
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
|
||||
import { assert } from '../../utils';
|
||||
import * as network from '../network';
|
||||
import type { InitScript, Page, PageDelegate } from '../page';
|
||||
import type { InitScript, Page } from '../page';
|
||||
import type { ConnectionTransport } from '../transport';
|
||||
import type * as types from '../types';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
|
@ -121,14 +121,14 @@ export class WKBrowser extends Browser {
|
|||
// abort navigation that is still running. We should be able to fix this by
|
||||
// instrumenting policy decision start/proceed/cancel.
|
||||
page._page._frameManager.frameAbortedNavigation(payload.frameId, 'Download is starting');
|
||||
let originPage = page._initializedPage;
|
||||
let originPage = page._page.initializedOrUndefined();
|
||||
// If it's a new window download, report it on the opener page.
|
||||
if (!originPage) {
|
||||
// Resume the page creation with an error. The page will automatically close right
|
||||
// after the download begins.
|
||||
page._firstNonInitialNavigationCommittedReject(new Error('Starting new page download'));
|
||||
if (page._opener)
|
||||
originPage = page._opener._initializedPage;
|
||||
originPage = page._opener._page.initializedOrUndefined();
|
||||
}
|
||||
if (!originPage)
|
||||
return;
|
||||
|
|
@ -239,14 +239,14 @@ export class WKBrowserContext extends BrowserContext {
|
|||
return Array.from(this._browser._wkPages.values()).filter(wkPage => wkPage._browserContext === this);
|
||||
}
|
||||
|
||||
pages(): Page[] {
|
||||
return this._wkPages().map(wkPage => wkPage._initializedPage).filter(pageOrNull => !!pageOrNull) as Page[];
|
||||
override possiblyUninitializedPages(): Page[] {
|
||||
return this._wkPages().map(wkPage => wkPage._page);
|
||||
}
|
||||
|
||||
async newPageDelegate(): Promise<PageDelegate> {
|
||||
override async doCreateNewPage(): Promise<Page> {
|
||||
assertBrowserContextIsNotOwned(this);
|
||||
const { pageProxyId } = await this._browser._browserSession.send('Playwright.createPage', { browserContextId: this._browserContextId });
|
||||
return this._browser._wkPages.get(pageProxyId)!;
|
||||
return this._browser._wkPages.get(pageProxyId)!._page;
|
||||
}
|
||||
|
||||
async doGetCookies(urls: string[]): Promise<channels.NetworkCookie[]> {
|
||||
|
|
|
|||
|
|
@ -115,6 +115,8 @@ function potentiallyUnserializableValue(remoteObject: Protocol.Runtime.RemoteObj
|
|||
}
|
||||
|
||||
function rewriteError(error: Error): Error {
|
||||
if (error.message.includes('Object has too long reference chain'))
|
||||
throw new Error('Cannot serialize result: object reference chain is too long.');
|
||||
if (!js.isJavaScriptErrorInEvaluate(error) && !isSessionClosedError(error))
|
||||
return new Error('Execution context was destroyed, most likely because of a navigation.');
|
||||
return error;
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ import { WKInterceptableRequest, WKRouteImpl } from './wkInterceptableRequest';
|
|||
import { WKProvisionalPage } from './wkProvisionalPage';
|
||||
import { WKWorkers } from './wkWorkers';
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
import { ManualPromise } from '../../utils/manualPromise';
|
||||
import { BrowserContext } from '../browserContext';
|
||||
import { TargetClosedError } from '../errors';
|
||||
|
||||
|
|
@ -56,7 +55,6 @@ export class WKPage implements PageDelegate {
|
|||
_session: WKSession;
|
||||
private _provisionalPage: WKProvisionalPage | null = null;
|
||||
readonly _page: Page;
|
||||
private readonly _pagePromise = new ManualPromise<Page | Error>();
|
||||
private readonly _pageProxySession: WKSession;
|
||||
readonly _opener: WKPage | null;
|
||||
private readonly _requestIdToRequest = new Map<string, WKInterceptableRequest>();
|
||||
|
|
@ -66,7 +64,6 @@ export class WKPage implements PageDelegate {
|
|||
private _sessionListeners: RegisteredListener[] = [];
|
||||
private _eventListeners: RegisteredListener[];
|
||||
readonly _browserContext: WKBrowserContext;
|
||||
_initializedPage: Page | null = null;
|
||||
private _firstNonInitialNavigationCommittedPromise: Promise<void>;
|
||||
private _firstNonInitialNavigationCommittedFulfill = () => {};
|
||||
_firstNonInitialNavigationCommittedReject = (e: Error) => {};
|
||||
|
|
@ -111,10 +108,6 @@ export class WKPage implements PageDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
potentiallyUninitializedPage(): Page {
|
||||
return this._page;
|
||||
}
|
||||
|
||||
private async _initializePageProxySession() {
|
||||
if (this._page._browserContext.isSettingStorageState())
|
||||
return;
|
||||
|
|
@ -283,7 +276,7 @@ export class WKPage implements PageDelegate {
|
|||
}
|
||||
|
||||
handleProvisionalLoadFailed(event: Protocol.Playwright.provisionalLoadFailedPayload) {
|
||||
if (!this._initializedPage) {
|
||||
if (!this._page.initializedOrUndefined()) {
|
||||
this._firstNonInitialNavigationCommittedReject(new Error('Initial load failed'));
|
||||
return;
|
||||
}
|
||||
|
|
@ -300,10 +293,6 @@ export class WKPage implements PageDelegate {
|
|||
this._nextWindowOpenPopupFeatures = event.windowFeatures;
|
||||
}
|
||||
|
||||
async pageOrError(): Promise<Page | Error> {
|
||||
return this._pagePromise;
|
||||
}
|
||||
|
||||
private async _onTargetCreated(event: Protocol.Target.targetCreatedPayload) {
|
||||
const { targetInfo } = event;
|
||||
const session = new WKSession(this._pageProxySession.connection, targetInfo.targetId, (message: any) => {
|
||||
|
|
@ -316,7 +305,7 @@ export class WKPage implements PageDelegate {
|
|||
assert(targetInfo.type === 'page', 'Only page targets are expected in WebKit, received: ' + targetInfo.type);
|
||||
|
||||
if (!targetInfo.isProvisional) {
|
||||
assert(!this._initializedPage);
|
||||
assert(!this._page.initializedOrUndefined());
|
||||
let pageOrError: Page | Error;
|
||||
try {
|
||||
this._setSession(session);
|
||||
|
|
@ -343,12 +332,7 @@ export class WKPage implements PageDelegate {
|
|||
// Avoid rejection on disconnect.
|
||||
this._firstNonInitialNavigationCommittedPromise.catch(() => {});
|
||||
}
|
||||
await this._page.initOpener(this._opener);
|
||||
// Note: it is important to call |reportAsNew| before resolving pageOrError promise,
|
||||
// so that anyone who awaits pageOrError got a ready and reported page.
|
||||
this._initializedPage = pageOrError instanceof Page ? pageOrError : null;
|
||||
this._page.reportAsNew(pageOrError instanceof Page ? undefined : pageOrError);
|
||||
this._pagePromise.resolve(pageOrError);
|
||||
this._page.reportAsNew(this._opener?._page, pageOrError instanceof Page ? undefined : pageOrError);
|
||||
} else {
|
||||
assert(targetInfo.isProvisional);
|
||||
assert(!this._provisionalPage);
|
||||
|
|
@ -515,7 +499,7 @@ export class WKPage implements PageDelegate {
|
|||
}
|
||||
|
||||
private async _onBindingCalled(contextId: Protocol.Runtime.ExecutionContextId, argument: string) {
|
||||
const pageOrError = await this.pageOrError();
|
||||
const pageOrError = await this._page.waitForInitializedOrError();
|
||||
if (!(pageOrError instanceof Error)) {
|
||||
const context = this._contextIdToContext.get(contextId);
|
||||
if (context)
|
||||
|
|
@ -611,6 +595,9 @@ export class WKPage implements PageDelegate {
|
|||
event.type as dialog.DialogType,
|
||||
event.message,
|
||||
async (accept: boolean, promptText?: string) => {
|
||||
// TODO: this should actually be a RDP event that notifies about a cancelled navigation attempt.
|
||||
if (event.type === 'beforeunload' && !accept)
|
||||
this._page._frameManager.frameAbortedNavigation(this._page.mainFrame()._id, 'navigation cancelled by beforeunload dialog');
|
||||
await this._pageProxySession.send('Dialog.handleJavaScriptDialog', { accept, promptText });
|
||||
},
|
||||
event.defaultPrompt));
|
||||
|
|
@ -818,7 +805,7 @@ export class WKPage implements PageDelegate {
|
|||
toolbarHeight: this._toolbarHeight()
|
||||
});
|
||||
this._recordingVideoFile = options.outputFile;
|
||||
this._browserContext._browser._videoStarted(this._browserContext, screencastId, options.outputFile, this.pageOrError());
|
||||
this._browserContext._browser._videoStarted(this._browserContext, screencastId, options.outputFile, this._page.waitForInitializedOrError());
|
||||
}
|
||||
|
||||
async _stopVideo(): Promise<void> {
|
||||
|
|
@ -846,7 +833,7 @@ export class WKPage implements PageDelegate {
|
|||
this.validateScreenshotDimension(rect.height, omitDeviceScaleFactor);
|
||||
const result = await this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: documentRect ? 'Page' : 'Viewport', omitDeviceScaleFactor });
|
||||
const prefix = 'data:image/png;base64,';
|
||||
let buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
|
||||
let buffer: Buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
|
||||
if (format === 'jpeg')
|
||||
buffer = jpegjs.encode(PNG.sync.read(buffer), quality).data;
|
||||
return buffer;
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@ function parseLocator(locator: string, testIdAttributeName: string): { selector:
|
|||
.replace(/newregex\(([^)]+)\)/g, 'r$1')
|
||||
.replace(/string=/g, '=')
|
||||
.replace(/regex=/g, '=')
|
||||
.replace(/,,/g, ',');
|
||||
.replace(/,,/g, ',')
|
||||
.replace(/,\)/g, ')');
|
||||
|
||||
const preferredQuote = params.map(p => p.quote).filter(quote => '\'"`'.includes(quote))[0] as Quote | undefined;
|
||||
return { selector: transform(template, params, testIdAttributeName), preferredQuote };
|
||||
|
|
@ -174,6 +175,7 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
|
|||
.replace(/filter\(,?hasnot2=([^)]+)\)/g, 'internal:has-not=$1')
|
||||
.replace(/,exact=false/g, '')
|
||||
.replace(/,exact=true/g, 's')
|
||||
.replace(/,includehidden=/g, ',include-hidden=')
|
||||
.replace(/\,/g, '][');
|
||||
|
||||
const parts = template.split('.');
|
||||
|
|
@ -214,25 +216,30 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
|
|||
}
|
||||
|
||||
export function locatorOrSelectorAsSelector(language: Language, locator: string, testIdAttributeName: string): string {
|
||||
try {
|
||||
return unsafeLocatorOrSelectorAsSelector(language, locator, testIdAttributeName);
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function unsafeLocatorOrSelectorAsSelector(language: Language, locator: string, testIdAttributeName: string): string {
|
||||
try {
|
||||
parseSelector(locator);
|
||||
return locator;
|
||||
} catch (e) {
|
||||
}
|
||||
try {
|
||||
const { selector, preferredQuote } = parseLocator(locator, testIdAttributeName);
|
||||
const locators = asLocators(language, selector, undefined, undefined, preferredQuote);
|
||||
const digest = digestForComparison(language, locator);
|
||||
if (locators.some(candidate => digestForComparison(language, candidate) === digest))
|
||||
return selector;
|
||||
} catch (e) {
|
||||
}
|
||||
const { selector, preferredQuote } = parseLocator(locator, testIdAttributeName);
|
||||
const locators = asLocators(language, selector, undefined, undefined, preferredQuote);
|
||||
const digest = digestForComparison(language, locator);
|
||||
if (locators.some(candidate => digestForComparison(language, candidate) === digest))
|
||||
return selector;
|
||||
return '';
|
||||
}
|
||||
|
||||
function digestForComparison(language: Language, locator: string) {
|
||||
locator = locator.replace(/\s/g, '');
|
||||
if (language === 'javascript')
|
||||
locator = locator.replace(/\\?["`]/g, '\'');
|
||||
locator = locator.replace(/\\?["`]/g, '\'').replace(/,{}/g, '');
|
||||
return locator;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ let lastConnectionId = 0;
|
|||
const kConnectionSymbol = Symbol('kConnection');
|
||||
|
||||
export const perMessageDeflate = {
|
||||
serverNoContextTakeover: true,
|
||||
zlibDeflateOptions: {
|
||||
level: 3,
|
||||
},
|
||||
|
|
|
|||
28
packages/playwright-core/types/types.d.ts
vendored
28
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -3609,8 +3609,6 @@ export interface Page {
|
|||
/**
|
||||
* Returns the PDF buffer.
|
||||
*
|
||||
* **NOTE** Generating a pdf is currently only supported in Chromium headless.
|
||||
*
|
||||
* `page.pdf()` generates a pdf of the page with `print` css media. To generate a pdf with `screen` media, call
|
||||
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) before calling
|
||||
* `page.pdf()`:
|
||||
|
|
@ -4296,7 +4294,7 @@ export interface Page {
|
|||
* takes priority over
|
||||
* [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout).
|
||||
*
|
||||
* @param timeout Maximum time in milliseconds
|
||||
* @param timeout Maximum time in milliseconds. Pass `0` to disable timeout.
|
||||
*/
|
||||
setDefaultTimeout(timeout: number): void;
|
||||
|
||||
|
|
@ -9195,7 +9193,7 @@ export interface BrowserContext {
|
|||
* take priority over
|
||||
* [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout).
|
||||
*
|
||||
* @param timeout Maximum time in milliseconds
|
||||
* @param timeout Maximum time in milliseconds. Pass `0` to disable timeout.
|
||||
*/
|
||||
setDefaultTimeout(timeout: number): void;
|
||||
|
||||
|
|
@ -9591,10 +9589,11 @@ export interface Browser {
|
|||
* In case this browser is connected to, clears all created contexts belonging to this browser and disconnects from
|
||||
* the browser server.
|
||||
*
|
||||
* **NOTE** This is similar to force quitting the browser. Therefore, you should call
|
||||
* **NOTE** This is similar to force-quitting the browser. To close pages gracefully and ensure you receive page close
|
||||
* events, call
|
||||
* [browserContext.close([options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-close) on
|
||||
* any [BrowserContext](https://playwright.dev/docs/api/class-browsercontext)'s you explicitly created earlier with
|
||||
* [browser.newContext([options])](https://playwright.dev/docs/api/class-browser#browser-new-context) **before**
|
||||
* any [BrowserContext](https://playwright.dev/docs/api/class-browsercontext) instances you explicitly created earlier
|
||||
* using [browser.newContext([options])](https://playwright.dev/docs/api/class-browser#browser-new-context) **before**
|
||||
* calling [browser.close([options])](https://playwright.dev/docs/api/class-browser#browser-close).
|
||||
*
|
||||
* The [Browser](https://playwright.dev/docs/api/class-browser) object itself is considered to be disposed and cannot
|
||||
|
|
@ -12430,7 +12429,7 @@ export interface Locator {
|
|||
|
||||
/**
|
||||
* Captures the aria snapshot of the given element. Read more about [aria snapshots](https://playwright.dev/docs/aria-snapshots) and
|
||||
* [expect(locator).toMatchAriaSnapshot(expected[, options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot)
|
||||
* [expect(locator).toMatchAriaSnapshot(expected[, options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
|
||||
* for the corresponding assertion.
|
||||
*
|
||||
* **Usage**
|
||||
|
|
@ -18592,6 +18591,19 @@ export interface Clock {
|
|||
* await page.clock.pauseAt('2020-02-02');
|
||||
* ```
|
||||
*
|
||||
* For best results, install the clock before navigating the page and set it to a time slightly before the intended
|
||||
* test time. This ensures that all timers run normally during page loading, preventing the page from getting stuck.
|
||||
* Once the page has fully loaded, you can safely use
|
||||
* [clock.pauseAt(time)](https://playwright.dev/docs/api/class-clock#clock-pause-at) to pause the clock.
|
||||
*
|
||||
* ```js
|
||||
* // Initialize clock with some time before the test time and let the page load
|
||||
* // naturally. `Date.now` will progress as the timers fire.
|
||||
* await page.clock.install({ time: new Date('2024-12-10T08:00:00') });
|
||||
* await page.goto('http://localhost:3333');
|
||||
* await page.clock.pauseAt(new Date('2024-12-10T10:00:00'));
|
||||
* ```
|
||||
*
|
||||
* @param time Time to pause at.
|
||||
*/
|
||||
pauseAt(time: number|string|Date): Promise<void>;
|
||||
|
|
|
|||
4
packages/playwright-ct-core/index.d.ts
vendored
4
packages/playwright-ct-core/index.d.ts
vendored
|
|
@ -52,7 +52,7 @@ export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig
|
|||
export function defineConfig<T>(config: PlaywrightTestConfig<T>): PlaywrightTestConfig<T>;
|
||||
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>): PlaywrightTestConfig<T, W>;
|
||||
export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig;
|
||||
export function defineConfig<T>(config: PlaywrightTestConfig<T>, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig<T>;
|
||||
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig<T, W>;
|
||||
export function defineConfig<T>(config: PlaywrightTestConfig<T>, ...configs: PlaywrightTestConfig<T>[]): PlaywrightTestConfig<T>;
|
||||
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>, ...configs: PlaywrightTestConfig<T, W>[]): PlaywrightTestConfig<T, W>;
|
||||
|
||||
export { expect, devices, Locator } from 'playwright/test';
|
||||
|
|
|
|||
4
packages/playwright-ct-react/hooks.d.ts
vendored
4
packages/playwright-ct-react/hooks.d.ts
vendored
|
|
@ -14,8 +14,10 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
export declare function beforeMount<HooksConfig>(
|
||||
callback: (params: { hooksConfig?: HooksConfig; App: () => JSX.Element }) => Promise<void | JSX.Element>
|
||||
callback: (params: { hooksConfig?: HooksConfig; App: () => React.JSX.Element }) => Promise<void | React.JSX.Element>
|
||||
): void;
|
||||
export declare function afterMount<HooksConfig>(
|
||||
callback: (params: { hooksConfig?: HooksConfig }) => Promise<void>
|
||||
|
|
|
|||
6
packages/playwright-ct-react/index.d.ts
vendored
6
packages/playwright-ct-react/index.d.ts
vendored
|
|
@ -14,6 +14,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
import type { TestType, Locator } from '@playwright/experimental-ct-core';
|
||||
|
||||
export interface MountOptions<HooksConfig> {
|
||||
|
|
@ -22,12 +24,12 @@ export interface MountOptions<HooksConfig> {
|
|||
|
||||
export interface MountResult extends Locator {
|
||||
unmount(): Promise<void>;
|
||||
update(component: JSX.Element): Promise<void>;
|
||||
update(component: React.JSX.Element): Promise<void>;
|
||||
}
|
||||
|
||||
export const test: TestType<{
|
||||
mount<HooksConfig>(
|
||||
component: JSX.Element,
|
||||
component: React.JSX.Element,
|
||||
options?: MountOptions<HooksConfig>
|
||||
): Promise<MountResult>;
|
||||
}>;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@vitejs/plugin-vue": "^4.2.1"
|
||||
"@vitejs/plugin-vue": "^5.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ export type ConfigCLIOverrides = {
|
|||
timeout?: number;
|
||||
tsconfig?: string;
|
||||
ignoreSnapshots?: boolean;
|
||||
updateSnapshots?: 'all'|'changed'|'missing'|'none';
|
||||
updateSourceMethod?: 'overwrite'|'patch'|'3way';
|
||||
updateSnapshots?: 'all' | 'changed' | 'missing' | 'none';
|
||||
updateSourceMethod?: 'overwrite' | 'patch' | '3way';
|
||||
workers?: number | string;
|
||||
projects?: { name: string, use?: any }[],
|
||||
use?: any;
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export async function loadTestFile(file: string, rootDir: string, testErrors?: T
|
|||
suite.allTests().map(t => files.add(t.location.file));
|
||||
if (files.size === 1) {
|
||||
// All tests point to one file.
|
||||
const mappedFile = files.values().next().value;
|
||||
const mappedFile = files.values().next().value!;
|
||||
if (suite.location.file !== mappedFile) {
|
||||
// The file is different, check for a likely source map case.
|
||||
if (path.extname(mappedFile) !== path.extname(suite.location.file))
|
||||
|
|
|
|||
|
|
@ -94,7 +94,8 @@ export interface TestServerInterface {
|
|||
testIds?: string[];
|
||||
headed?: boolean;
|
||||
workers?: number | string;
|
||||
updateSnapshots?: 'all' | 'none' | 'missing';
|
||||
updateSnapshots?: 'all' | 'changed' | 'missing' | 'none';
|
||||
updateSourceMethod?: 'overwrite' | 'patch' | '3way';
|
||||
reporters?: string[],
|
||||
trace?: 'on' | 'off';
|
||||
video?: 'on' | 'off';
|
||||
|
|
|
|||
|
|
@ -18,19 +18,25 @@
|
|||
import type { LocatorEx } from './matchers';
|
||||
import type { ExpectMatcherState } from '../../types/test';
|
||||
import { kNoElementsFoundError, matcherHint, type MatcherResult } from './matcherHint';
|
||||
import { colors } from 'playwright-core/lib/utilsBundle';
|
||||
import { EXPECTED_COLOR } from '../common/expectBundle';
|
||||
import { callLogText } from '../util';
|
||||
import { callLogText, sanitizeFilePathBeforeExtension, trimLongString } from '../util';
|
||||
import { printReceivedStringContainExpectedSubstring } from './expect';
|
||||
import { currentTestInfo } from '../common/globals';
|
||||
import type { MatcherReceived } from '@injected/ariaSnapshot';
|
||||
import { escapeTemplateString } from 'playwright-core/lib/utils';
|
||||
import { escapeTemplateString, isString, sanitizeForFilePath } from 'playwright-core/lib/utils';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
type ToMatchAriaSnapshotExpected = {
|
||||
name?: string;
|
||||
path?: string;
|
||||
} | string;
|
||||
|
||||
export async function toMatchAriaSnapshot(
|
||||
this: ExpectMatcherState,
|
||||
receiver: LocatorEx,
|
||||
expected: string,
|
||||
options: { timeout?: number, matchSubstring?: boolean } = {},
|
||||
expectedParam: ToMatchAriaSnapshotExpected,
|
||||
options: { timeout?: number } = {},
|
||||
): Promise<MatcherResult<string | RegExp, string>> {
|
||||
const matcherName = 'toMatchAriaSnapshot';
|
||||
|
||||
|
|
@ -39,7 +45,7 @@ export async function toMatchAriaSnapshot(
|
|||
throw new Error(`toMatchAriaSnapshot() must be called during the test`);
|
||||
|
||||
if (testInfo._projectInternal.ignoreSnapshots)
|
||||
return { pass: !this.isNot, message: () => '', name: 'toMatchAriaSnapshot', expected };
|
||||
return { pass: !this.isNot, message: () => '', name: 'toMatchAriaSnapshot', expected: '' };
|
||||
|
||||
const updateSnapshots = testInfo.config.updateSnapshots;
|
||||
|
||||
|
|
@ -48,12 +54,25 @@ export async function toMatchAriaSnapshot(
|
|||
promise: this.promise,
|
||||
};
|
||||
|
||||
if (typeof expected !== 'string') {
|
||||
throw new Error([
|
||||
matcherHint(this, receiver, matcherName, receiver, expected, matcherOptions),
|
||||
`${colors.bold('Matcher error')}: ${EXPECTED_COLOR('expected',)} value must be a string`,
|
||||
this.utils.printWithType('Expected', expected, this.utils.printExpected)
|
||||
].join('\n\n'));
|
||||
let expected: string;
|
||||
let expectedPath: string | undefined;
|
||||
if (isString(expectedParam)) {
|
||||
expected = expectedParam;
|
||||
} else {
|
||||
if (expectedParam?.path) {
|
||||
expectedPath = expectedParam.path;
|
||||
} else if (expectedParam?.name) {
|
||||
expectedPath = testInfo.snapshotPath(sanitizeFilePathBeforeExtension(expectedParam.name));
|
||||
} else {
|
||||
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
|
||||
if (!snapshotNames) {
|
||||
snapshotNames = { anonymousSnapshotIndex: 0 };
|
||||
(testInfo as any)[snapshotNamesSymbol] = snapshotNames;
|
||||
}
|
||||
const fullTitleWithoutSpec = [...testInfo.titlePath.slice(1), ++snapshotNames.anonymousSnapshotIndex].join(' ');
|
||||
expectedPath = testInfo.snapshotPath(sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.yml');
|
||||
}
|
||||
expected = await fs.promises.readFile(expectedPath, 'utf8').catch(() => '');
|
||||
}
|
||||
|
||||
const generateMissingBaseline = updateSnapshots === 'missing' && !expected;
|
||||
|
|
@ -102,8 +121,24 @@ export async function toMatchAriaSnapshot(
|
|||
if ((updateSnapshots === 'all') ||
|
||||
(updateSnapshots === 'changed' && pass === this.isNot) ||
|
||||
generateMissingBaseline) {
|
||||
const suggestedRebaseline = `toMatchAriaSnapshot(\`\n${escapeTemplateString(indent(typedReceived.regex, '{indent} '))}\n{indent}\`)`;
|
||||
return { pass: this.isNot, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline };
|
||||
if (expectedPath) {
|
||||
await fs.promises.mkdir(path.dirname(expectedPath), { recursive: true });
|
||||
await fs.promises.writeFile(expectedPath, typedReceived.regex, 'utf8');
|
||||
const relativePath = path.relative(process.cwd(), expectedPath);
|
||||
if (updateSnapshots === 'missing') {
|
||||
const message = `A snapshot doesn't exist at ${relativePath}, writing actual.`;
|
||||
testInfo._hasNonRetriableError = true;
|
||||
testInfo._failWithError(new Error(message));
|
||||
} else {
|
||||
const message = `A snapshot is generated at ${relativePath}.`;
|
||||
/* eslint-disable no-console */
|
||||
console.log(message);
|
||||
}
|
||||
return { pass: true, message: () => '', name: 'toMatchAriaSnapshot' };
|
||||
} else {
|
||||
const suggestedRebaseline = `toMatchAriaSnapshot(\`\n${escapeTemplateString(indent(typedReceived.regex, '{indent} '))}\n{indent}\`)`;
|
||||
return { pass: false, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -134,3 +169,9 @@ function unshift(snapshot: string): string {
|
|||
function indent(snapshot: string, indent: string): string {
|
||||
return snapshot.split('\n').map(line => indent + line).join('\n');
|
||||
}
|
||||
|
||||
const snapshotNamesSymbol = Symbol('snapshotNames');
|
||||
|
||||
type SnapshotNames = {
|
||||
anonymousSnapshotIndex: number;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ export class BaseReporter implements ReporterV2 {
|
|||
console.log(colors.yellow(' Slow test file: ') + file + colors.yellow(` (${milliseconds(duration)})`));
|
||||
});
|
||||
if (slowTests.length)
|
||||
console.log(colors.yellow(' Consider splitting slow test files to speed up parallel execution'));
|
||||
console.log(colors.yellow(' Consider running tests from slow files in parallel, see https://playwright.dev/docs/test-parallel.'));
|
||||
}
|
||||
|
||||
private _printSummary(summary: string) {
|
||||
|
|
@ -381,7 +381,7 @@ export function formatTestTitle(config: FullConfig, test: TestCase, step?: TestS
|
|||
if (omitLocation)
|
||||
location = `${relativeTestPath(config, test)}`;
|
||||
else
|
||||
location = `${relativeTestPath(config, test)}:${step?.location?.line ?? test.location.line}:${step?.location?.column ?? test.location.column}`;
|
||||
location = `${relativeTestPath(config, test)}:${test.location.line}:${test.location.column}`;
|
||||
const projectTitle = projectName ? `[${projectName}] › ` : '';
|
||||
const testTitle = `${projectTitle}${location} › ${titles.join(' › ')}`;
|
||||
const extraTags = test.tags.filter(t => !testTitle.includes(t));
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ class HtmlBuilder {
|
|||
|
||||
let singleTestId: string | undefined;
|
||||
if (htmlReport.stats.total === 1) {
|
||||
const testFile: TestFile = data.values().next().value.testFile;
|
||||
const testFile: TestFile = data.values().next().value!.testFile;
|
||||
singleTestId = testFile.tests[0].testId;
|
||||
}
|
||||
|
||||
|
|
@ -602,17 +602,10 @@ type JsonAttachment = {
|
|||
};
|
||||
|
||||
function stdioAttachment(chunk: Buffer | string, type: 'stdout' | 'stderr'): JsonAttachment {
|
||||
if (typeof chunk === 'string') {
|
||||
return {
|
||||
name: type,
|
||||
contentType: 'text/plain',
|
||||
body: chunk
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: type,
|
||||
contentType: 'application/octet-stream',
|
||||
body: chunk
|
||||
contentType: 'text/plain',
|
||||
body: typeof chunk === 'string' ? chunk : chunk.toString('utf-8')
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -311,6 +311,7 @@ export class TestServerDispatcher implements TestServerInterface {
|
|||
_optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined,
|
||||
},
|
||||
...(params.updateSnapshots ? { updateSnapshots: params.updateSnapshots } : {}),
|
||||
...(params.updateSourceMethod ? { updateSourceMethod: params.updateSourceMethod } : {}),
|
||||
...(params.workers ? { workers: params.workers } : {}),
|
||||
};
|
||||
if (params.trace === 'on')
|
||||
|
|
@ -479,7 +480,7 @@ type StdioPayload = {
|
|||
};
|
||||
|
||||
function chunkToPayload(type: 'stdout' | 'stderr', chunk: Buffer | string): StdioPayload {
|
||||
if (chunk instanceof Buffer)
|
||||
if (chunk instanceof Uint8Array)
|
||||
return { type, buffer: chunk.toString('base64') };
|
||||
return { type, text: chunk };
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue