diff --git a/.github/workflows/tests_secondary.yml b/.github/workflows/tests_secondary.yml index de9434d7df..5afd2f5491 100644 --- a/.github/workflows/tests_secondary.yml +++ b/.github/workflows/tests_secondary.yml @@ -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 }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb06a17e77..ebcabc8a27 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,7 +35,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 diff --git a/README.md b/README.md index 6203130f2e..913aac3269 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-132.0.6834.32-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-132.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-132.0.6834.46-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-132.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](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 132.0.6834.32 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 132.0.6834.46 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 18.2 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox 132.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 121ce1fdf6..85cbc4f62a 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -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 diff --git a/docs/src/api/class-clock.md b/docs/src/api/class-clock.md index 38ca329769..e5610b4242 100644 --- a/docs/src/api/class-clock.md +++ b/docs/src/api/class-clock.md @@ -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 diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index e61b119c69..e93d02d9d8 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -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** diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 7a640c4ef6..7e61e1b3a5 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -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` -### 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 diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index f44bd18696..ec58c027b3 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -3970,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 diff --git a/docs/src/aria-snapshots.md b/docs/src/aria-snapshots.md index e190fd3022..27d1c0ebce 100644 --- a/docs/src/aria-snapshots.md +++ b/docs/src/aria-snapshots.md @@ -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. diff --git a/docs/src/browsers.md b/docs/src/browsers.md index 83ea0f60c3..90c0b2850b 100644 --- a/docs/src/browsers.md +++ b/docs/src/browsers.md @@ -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 diff --git a/docs/src/clock.md b/docs/src/clock.md index 2f846e4fd3..44582f450b 100644 --- a/docs/src/clock.md +++ b/docs/src/clock.md @@ -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. diff --git a/docs/src/docker.md b/docs/src/docker.md index b2a3e906b9..abb647e35b 100644 --- a/docs/src/docker.md +++ b/docs/src/docker.md @@ -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]. diff --git a/docs/src/release-notes-csharp.md b/docs/src/release-notes-csharp.md index 7c1ffc87ab..0d2681c874 100644 --- a/docs/src/release-notes-csharp.md +++ b/docs/src/release-notes-csharp.md @@ -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"); diff --git a/docs/src/release-notes-java.md b/docs/src/release-notes-java.md index 8130c77f07..317c1c5398 100644 --- a/docs/src/release-notes-java.md +++ b/docs/src/release-notes-java.md @@ -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"); diff --git a/docs/src/release-notes-js.md b/docs/src/release-notes-js.md index 06a0fe3b19..0595933b80 100644 --- a/docs/src/release-notes-js.md +++ b/docs/src/release-notes-js.md @@ -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'); diff --git a/docs/src/release-notes-python.md b/docs/src/release-notes-python.md index fc7dd1ccd1..f2002f15fd 100644 --- a/docs/src/release-notes-python.md +++ b/docs/src/release-notes-python.md @@ -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") diff --git a/docs/src/test-sharding-js.md b/docs/src/test-sharding-js.md index d4b474ca9b..3b0f55e333 100644 --- a/docs/src/test-sharding-js.md +++ b/docs/src/test-sharding-js.md @@ -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:** diff --git a/package-lock.json b/package-lock.json index 6a8526b8c2..0c4d97edad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" } @@ -2184,126 +2192,126 @@ } }, "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": { @@ -3326,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", @@ -5236,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": { @@ -5396,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" }, @@ -5772,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", @@ -5827,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", @@ -5844,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": { @@ -6720,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", @@ -6888,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" @@ -7450,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": "*" diff --git a/package.json b/package.json index f36e9039e3..5eafd4d805 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/html-reporter/package.json b/packages/html-reporter/package.json index c1beb75b89..0b5ad5fb24 100644 --- a/packages/html-reporter/package.json +++ b/packages/html-reporter/package.json @@ -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" } diff --git a/packages/html-reporter/src/headerView.spec.tsx b/packages/html-reporter/src/headerView.spec.tsx index f783a33c1d..202cd5fa7b 100644 --- a/packages/html-reporter/src/headerView.spec.tsx +++ b/packages/html-reporter/src/headerView.spec.tsx @@ -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']); }); diff --git a/packages/html-reporter/src/headerView.tsx b/packages/html-reporter/src/headerView.tsx index 01f92f1ecb..8384f9e31d 100644 --- a/packages/html-reporter/src/headerView.tsx +++ b/packages/html-reporter/src/headerView.tsx @@ -49,12 +49,14 @@ export const HeaderView: React.FC { 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 */} - { + { setFilterText(e.target.value); }}> diff --git a/packages/html-reporter/src/index.tsx b/packages/html-reporter/src/index.tsx index 16c1ae1a88..4ae3b02591 100644 --- a/packages/html-reporter/src/index.tsx +++ b/packages/html-reporter/src/index.tsx @@ -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'; diff --git a/packages/html-reporter/src/links.tsx b/packages/html-reporter/src/links.tsx index 4beb7b65e0..1e5cad48c1 100644 --- a/packages/html-reporter/src/links.tsx +++ b/packages/html-reporter/src/links.tsx @@ -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); diff --git a/packages/playwright-core/ThirdPartyNotices.txt b/packages/playwright-core/ThirdPartyNotices.txt index 89a6418ab8..2dc33e8c8f 100644 --- a/packages/playwright-core/ThirdPartyNotices.txt +++ b/packages/playwright-core/ThirdPartyNotices.txt @@ -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 ========================================= diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 6f5d925d12..a4d1786903 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -3,21 +3,15 @@ "browsers": [ { "name": "chromium", - "revision": "1151", + "revision": "1152", "installByDefault": true, - "browserVersion": "132.0.6834.32" - }, - { - "name": "chromium-headless-shell", - "revision": "1151", - "installByDefault": true, - "browserVersion": "132.0.6834.32" + "browserVersion": "132.0.6834.46" }, { "name": "chromium-tip-of-tree", - "revision": "1284", + "revision": "1286", "installByDefault": false, - "browserVersion": "133.0.6878.0" + "browserVersion": "133.0.6891.0" }, { "name": "firefox", @@ -33,7 +27,7 @@ }, { "name": "webkit", - "revision": "2113", + "revision": "2119", "installByDefault": true, "revisionOverrides": { "debian11-x64": "2105", diff --git a/packages/playwright-core/bundles/utils/package-lock.json b/packages/playwright-core/bundles/utils/package-lock.json index 6d9b0562e3..44aabaca9d 100644 --- a/packages/playwright-core/bundles/utils/package-lock.json +++ b/packages/playwright-core/bundles/utils/package-lock.json @@ -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" } diff --git a/packages/playwright-core/bundles/utils/package.json b/packages/playwright-core/bundles/utils/package.json index 1900d423f5..a409cca037 100644 --- a/packages/playwright-core/bundles/utils/package.json +++ b/packages/playwright-core/bundles/utils/package.json @@ -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" diff --git a/packages/playwright-core/src/androidServerImpl.ts b/packages/playwright-core/src/androidServerImpl.ts index a0d7bb5496..f0108e67c7 100644 --- a/packages/playwright-core/src/androidServerImpl.ts +++ b/packages/playwright-core/src/androidServerImpl.ts @@ -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) diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index 7d0659d26f..7ce1c4f928 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -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 [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(', '); } @@ -291,7 +275,7 @@ program }); program - .command('run-server', { hidden: true }) + .command('run-server') .option('--port ', 'Server port') .option('--host ', 'Server host') .option('--path ', 'Endpoint Path', '/') diff --git a/packages/playwright-core/src/client/electron.ts b/packages/playwright-core/src/client/electron.ts index fcabfa494f..fe5b6e189e 100644 --- a/packages/playwright-core/src/client/electron.ts +++ b/packages/playwright-core/src/client/electron.ts @@ -103,7 +103,7 @@ export class ElectronApplication extends ChannelOwner { if (this._windows.size) - return this._windows.values().next().value; + return this._windows.values().next().value!; return await this.waitForEvent('window', options); } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 7fe92a7947..f4db833e02 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -385,6 +385,7 @@ scheme.DebugControllerInitializer = tOptional(tObject({})); scheme.DebugControllerInspectRequestedEvent = tObject({ selector: tString, locator: tString, + ariaSnapshot: tString, }); scheme.DebugControllerSetModeRequestedEvent = tObject({ mode: tString, diff --git a/packages/playwright-core/src/server/bidi/bidiBrowser.ts b/packages/playwright-core/src/server/bidi/bidiBrowser.ts index 9861fc80cf..acefc08fb2 100644 --- a/packages/playwright-core/src/server/bidi/bidiBrowser.ts +++ b/packages/playwright-core/src/server/bidi/bidiBrowser.ts @@ -211,6 +211,10 @@ export class BidiBrowserContext extends BrowserContext { return this._bidiPages().map(bidiPage => bidiPage._initializedPage).filter(Boolean) as Page[]; } + pagesOrErrors() { + return this._bidiPages().map(bidiPage => bidiPage.pageOrError()); + } + async newPageDelegate(): Promise { assertBrowserContextIsNotOwned(this); const { context } = await this._browser._browserSession.send('browsingContext.create', { diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 025bd0f388..6d9227c0a7 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -259,6 +259,7 @@ export abstract class BrowserContext extends SdkObject { // BrowserContext methods. abstract pages(): Page[]; + abstract pagesOrErrors(): Promise[]; abstract newPageDelegate(): Promise; abstract addCookies(cookies: channels.SetNetworkCookie[]): Promise; abstract setGeolocation(geolocation?: types.Geolocation): Promise; @@ -358,29 +359,36 @@ export abstract class BrowserContext extends SdkObject { this._timeoutSettings.setDefaultTimeout(timeout); } - async _loadDefaultContextAsIs(progress: Progress): Promise { - if (!this.pages().length) { + async _loadDefaultContextAsIs(progress: Progress): Promise { + let pageOrError; + if (!this.pagesOrErrors().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 + pageOrError = await Promise.race([ + waitForEvent.promise as Promise, + this._closePromise, + ]); + // Consider Page initialization errors + if (pageOrError instanceof Page) + pageOrError = await pageOrError._delegate.pageOrError(); + } else { + pageOrError = await this.pagesOrErrors()[0]; } - const pages = this.pages(); - if (pages[0]._pageIsError) - throw pages[0]._pageIsError; - await pages[0].mainFrame()._waitForLoadState(progress, 'load'); - return pages; + if (pageOrError instanceof Error) + throw pageOrError; + await pageOrError.mainFrame()._waitForLoadState(progress, 'load'); + return pageOrError; } async _loadDefaultContext(progress: Progress) { - const pages = await this._loadDefaultContextAsIs(progress); + const defaultPage = await this._loadDefaultContextAsIs(progress); 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]; + const oldPage = defaultPage; await this.newPage(progress.metadata); await oldPage.close(progress.metadata); } diff --git a/packages/playwright-core/src/server/chromium/crAccessibility.ts b/packages/playwright-core/src/server/chromium/crAccessibility.ts index 30d02ee7bf..4114663a0e 100644 --- a/packages/playwright-core/src/server/chromium/crAccessibility.ts +++ b/packages/playwright-core/src/server/chromium/crAccessibility.ts @@ -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!; } } diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index a6d77e4ae7..1aba1ed8cb 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -368,6 +368,10 @@ export class CRBrowserContext extends BrowserContext { return this._crPages().map(crPage => crPage._initializedPage).filter(Boolean) as Page[]; } + pagesOrErrors() { + return this._crPages().map(crPage => crPage.pageOrError()); + } + async newPageDelegate(): Promise { assertBrowserContextIsNotOwned(this); diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index bfad678f1c..a7ee34d2c5 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -432,6 +432,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 { diff --git a/packages/playwright-core/src/server/debugController.ts b/packages/playwright-core/src/server/debugController.ts index 251dce7e03..8878ecb59c 100644 --- a/packages/playwright-core/src/server/debugController.ts +++ b/packages/playwright-core/src/server/debugController.ts @@ -228,7 +228,7 @@ class InspectingRecorderApp extends EmptyRecorderApp { override async elementPicked(elementInfo: ElementInfo): Promise { 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 { diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index 179eb52f7b..49d9edde41 100644 --- a/packages/playwright-core/src/server/deviceDescriptorsSource.json +++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json @@ -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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 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.32 Safari/537.36 Edg/132.0.6834.32", + "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.32 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.32 Safari/537.36 Edg/132.0.6834.32", + "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 diff --git a/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts b/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts index 77d7b503ab..fc722d1bd3 100644 --- a/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts @@ -32,8 +32,8 @@ export class DebugControllerDispatcher extends Dispatcher { 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 })); diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index b26a2850ee..afc0671f70 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -271,6 +271,10 @@ export class FFBrowserContext extends BrowserContext { return this._ffPages().map(ffPage => ffPage._initializedPage).filter(pageOrNull => !!pageOrNull) as Page[]; } + pagesOrErrors() { + return this._ffPages().map(ffPage => ffPage.pageOrError()); + } + async newPageDelegate(): Promise { assertBrowserContextIsNotOwned(this); const { targetId } = await this._browser.session.send('Browser.newPage', { diff --git a/packages/playwright-core/src/server/injected/ariaSnapshot.ts b/packages/playwright-core/src/server/injected/ariaSnapshot.ts index d541646f0e..84537cddd7 100644 --- a/packages/playwright-core/src/server/injected/ariaSnapshot.ts +++ b/packages/playwright-core/src/server/injected/ariaSnapshot.ts @@ -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; } diff --git a/packages/playwright-core/src/server/injected/selectorGenerator.ts b/packages/playwright-core/src/server/injected/selectorGenerator.ts index 7aa22e6f0c..04f1598762 100644 --- a/packages/playwright-core/src/server/injected/selectorGenerator.ts +++ b/packages/playwright-core/src/server/injected/selectorGenerator.ts @@ -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; } diff --git a/packages/playwright-core/src/server/injected/selectorUtils.ts b/packages/playwright-core/src/server/injected/selectorUtils.ts index 746e12af9d..da0f869fa0 100644 --- a/packages/playwright-core/src/server/injected/selectorUtils.ts +++ b/packages/playwright-core/src/server/injected/selectorUtils.ts @@ -73,6 +73,8 @@ export function elementText(cache: Map, 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); diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index d626b1ed3c..48c0827c08 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -164,7 +164,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; @@ -208,7 +207,7 @@ export class Page extends SdkObject { // context/browser closure. Just ignore the page. if (this._browserContext.isClosingOrClosed()) return; - this._setIsError(error); + this._frameManager.createDummyMainFrameIfNeeded(); } this._initialized = true; this.emitOnContext(contextEvent, this); @@ -709,11 +708,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'; } diff --git a/packages/playwright-core/src/server/recorder/recorderCollection.ts b/packages/playwright-core/src/server/recorder/recorderCollection.ts index b5216d7382..2643dece91 100644 --- a/packages/playwright-core/src/server/recorder/recorderCollection.ts +++ b/packages/playwright-core/src/server/recorder/recorderCollection.ts @@ -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)); } } diff --git a/packages/playwright-core/src/server/registry/index.ts b/packages/playwright-core/src/server/registry/index.ts index cd2f2f1d32..4bb27bcaea 100644 --- a/packages/playwright-core/src/server/registry/index.ts +++ b/packages/playwright-core/src/server/registry/index.ts @@ -174,6 +174,35 @@ const DOWNLOAD_PATHS: Record = { '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': { + '': 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': { '': 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({ diff --git a/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts b/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts index 4d575ef0e9..4730c3e5dd 100644 --- a/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts +++ b/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts @@ -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); diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index b8dc3e5314..f852f6c996 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -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); diff --git a/packages/playwright-core/src/server/transport.ts b/packages/playwright-core/src/server/transport.ts index e213e030e7..507225b04c 100644 --- a/packages/playwright-core/src/server/transport.ts +++ b/packages/playwright-core/src/server/transport.ts @@ -24,6 +24,7 @@ import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy- import type { HeadersArray } from './types'; export const perMessageDeflate = { + clientNoContextTakeover: true, zlibDeflateOptions: { level: 3, }, diff --git a/packages/playwright-core/src/server/webkit/wkBrowser.ts b/packages/playwright-core/src/server/webkit/wkBrowser.ts index f4f9f732a5..4e5467fd17 100644 --- a/packages/playwright-core/src/server/webkit/wkBrowser.ts +++ b/packages/playwright-core/src/server/webkit/wkBrowser.ts @@ -243,6 +243,10 @@ export class WKBrowserContext extends BrowserContext { return this._wkPages().map(wkPage => wkPage._initializedPage).filter(pageOrNull => !!pageOrNull) as Page[]; } + pagesOrErrors() { + return this._wkPages().map(wkPage => wkPage.pageOrError()); + } + async newPageDelegate(): Promise { assertBrowserContextIsNotOwned(this); const { pageProxyId } = await this._browser._browserSession.send('Playwright.createPage', { browserContextId: this._browserContextId }); diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index a6e2145ce4..a1e69e1267 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -849,7 +849,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; diff --git a/packages/playwright-core/src/utils/isomorphic/locatorParser.ts b/packages/playwright-core/src/utils/isomorphic/locatorParser.ts index e0c10b53fd..9bae0a62bd 100644 --- a/packages/playwright-core/src/utils/isomorphic/locatorParser.ts +++ b/packages/playwright-core/src/utils/isomorphic/locatorParser.ts @@ -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('.'); @@ -233,6 +235,6 @@ export function locatorOrSelectorAsSelector(language: Language, locator: string, 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; } diff --git a/packages/playwright-core/src/utils/wsServer.ts b/packages/playwright-core/src/utils/wsServer.ts index 63c0ed1fed..636a57c3cc 100644 --- a/packages/playwright-core/src/utils/wsServer.ts +++ b/packages/playwright-core/src/utils/wsServer.ts @@ -25,6 +25,7 @@ let lastConnectionId = 0; const kConnectionSymbol = Symbol('kConnection'); export const perMessageDeflate = { + serverNoContextTakeover: true, zlibDeflateOptions: { level: 3, }, diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 34b83cfc52..cb83bb4ccd 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -4294,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; @@ -9193,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; @@ -12428,7 +12428,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** @@ -18590,6 +18590,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; diff --git a/packages/playwright-ct-core/index.d.ts b/packages/playwright-ct-core/index.d.ts index 9c323ef6df..f6710ee1f6 100644 --- a/packages/playwright-ct-core/index.d.ts +++ b/packages/playwright-ct-core/index.d.ts @@ -52,7 +52,7 @@ export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig; export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig; export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig; -export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig; -export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig; +export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig; +export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig; export { expect, devices, Locator } from 'playwright/test'; diff --git a/packages/playwright-ct-react/hooks.d.ts b/packages/playwright-ct-react/hooks.d.ts index 1093f1a3a3..f05ad1ffdf 100644 --- a/packages/playwright-ct-react/hooks.d.ts +++ b/packages/playwright-ct-react/hooks.d.ts @@ -14,8 +14,10 @@ * limitations under the License. */ +import type React from 'react'; + export declare function beforeMount( - callback: (params: { hooksConfig?: HooksConfig; App: () => JSX.Element }) => Promise + callback: (params: { hooksConfig?: HooksConfig; App: () => React.JSX.Element }) => Promise ): void; export declare function afterMount( callback: (params: { hooksConfig?: HooksConfig }) => Promise diff --git a/packages/playwright-ct-react/index.d.ts b/packages/playwright-ct-react/index.d.ts index c5e6d6d2da..d7000c5c85 100644 --- a/packages/playwright-ct-react/index.d.ts +++ b/packages/playwright-ct-react/index.d.ts @@ -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 { @@ -22,12 +24,12 @@ export interface MountOptions { export interface MountResult extends Locator { unmount(): Promise; - update(component: JSX.Element): Promise; + update(component: React.JSX.Element): Promise; } export const test: TestType<{ mount( - component: JSX.Element, + component: React.JSX.Element, options?: MountOptions ): Promise; }>; diff --git a/packages/playwright/src/common/testLoader.ts b/packages/playwright/src/common/testLoader.ts index a85d93545d..6a8940ee19 100644 --- a/packages/playwright/src/common/testLoader.ts +++ b/packages/playwright/src/common/testLoader.ts @@ -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)) diff --git a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts index 552ad2e36d..152ceb6ba9 100644 --- a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts +++ b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts @@ -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> { 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; +}; diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index 406be15e83..302d532641 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -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') }; } diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 981a1580f0..30f724c03e 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -479,7 +479,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 }; } diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index fed6a1bdeb..520bcb30d3 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -1891,7 +1891,7 @@ type ConditionBody = (args: TestArgs) => boolean; * ``` * */ -export interface TestType { +export interface TestType { /** * Declares a test. * - `test(title, body)` @@ -5632,7 +5632,7 @@ export interface TestType(fixtures: Fixtures): TestType; + extend(fixtures: Fixtures): TestType; /** * Returns information about the currently running test. This method can only be called during the test execution, * otherwise it throws. @@ -5653,19 +5653,18 @@ export interface TestType = (args: Args, use: (r: R) => Promise, testInfo: TestInfo) => any; -export type WorkerFixture = (args: Args, use: (r: R) => Promise, workerInfo: WorkerInfo) => any; -type TestFixtureValue = Exclude | TestFixture; -type WorkerFixtureValue = Exclude | WorkerFixture; -export type Fixtures = { +export type TestFixture = (args: Args, use: (r: R) => Promise, testInfo: TestInfo) => any; +export type WorkerFixture = (args: Args, use: (r: R) => Promise, workerInfo: WorkerInfo) => any; +type TestFixtureValue = Exclude | TestFixture; +type WorkerFixtureValue = Exclude | WorkerFixture; +export type Fixtures = { [K in keyof PW]?: WorkerFixtureValue | [WorkerFixtureValue, { scope: 'worker', timeout?: number | undefined, title?: string, box?: boolean }]; } & { [K in keyof PT]?: TestFixtureValue | [TestFixtureValue, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean }]; } & { - [K in keyof W]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }]; + [K in Exclude]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }]; } & { - [K in keyof T]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }]; + [K in Exclude]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }]; }; type BrowserName = 'chromium' | 'firefox' | 'webkit'; @@ -7487,8 +7486,8 @@ export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig; export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig; export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig; -export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig; -export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig; +export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig; +export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig; type MergedT = List extends [TestType, ...(infer Rest)] ? T & MergedT : {}; type MergedW = List extends [TestType, ...(infer Rest)] ? W & MergedW : {}; @@ -8431,6 +8430,37 @@ interface LocatorAssertions { timeout?: number; }): Promise; + /** + * Asserts that the target element matches the given [accessibility snapshot](https://playwright.dev/docs/aria-snapshots). + * + * **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' }); + * ``` + * + * @param options + */ + toMatchAriaSnapshot(options?: { + /** + * Name of the snapshot to store in the snapshot folder corresponding to this test. Generates ordinal name if not + * specified. + */ + name?: string; + + /** + * Path to the YAML snapshot file. + */ + path?: string; + + /** + * Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + }): Promise; + /** * Asserts that the target element matches the given [accessibility snapshot](https://playwright.dev/docs/aria-snapshots). * diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 27c4242fe5..100baf59f9 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -691,6 +691,7 @@ export interface DebugControllerChannel extends DebugControllerEventTarget, Chan export type DebugControllerInspectRequestedEvent = { selector: string, locator: string, + ariaSnapshot: string, }; export type DebugControllerSetModeRequestedEvent = { mode: string, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index d9597c2295..9997989f77 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -807,6 +807,7 @@ DebugController: parameters: selector: string locator: string + ariaSnapshot: string setModeRequested: parameters: diff --git a/packages/recorder/package.json b/packages/recorder/package.json index 0bad170fee..97e52eb9a5 100644 --- a/packages/recorder/package.json +++ b/packages/recorder/package.json @@ -3,11 +3,6 @@ "private": true, "version": "0.0.0", "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build && tsc", - "preview": "vite preview" - }, "dependencies": { "yaml": "^2.6.0" } diff --git a/packages/trace-viewer/package.json b/packages/trace-viewer/package.json index e1b745bf83..66cf4e1e58 100644 --- a/packages/trace-viewer/package.json +++ b/packages/trace-viewer/package.json @@ -2,11 +2,5 @@ "name": "trace-viewer", "private": true, "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build && tsc", - "build-sw": "vite --config vite.sw.config.ts build && tsc", - "preview": "vite preview" - } + "type": "module" } diff --git a/packages/trace-viewer/src/sw/lruCache.ts b/packages/trace-viewer/src/sw/lruCache.ts index 573a15ea49..2d5eae8542 100644 --- a/packages/trace-viewer/src/sw/lruCache.ts +++ b/packages/trace-viewer/src/sw/lruCache.ts @@ -37,7 +37,7 @@ export class LRUCache { const result = compute(); while (this._map.size && this._size + result.size > this._maxSize) { - const [firstKey, firstValue] = this._map.entries().next().value; + const [firstKey, firstValue] = this._map.entries().next().value!; this._size -= firstValue.size; this._map.delete(firstKey); } diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index 4d01ef2a61..cda3ba8cad 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -43,10 +43,8 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, client: const clientId = client?.id ?? ''; let data = clientIdToTraceUrls.get(clientId); if (!data) { - let traceViewerServerBaseUrl = new URL('../', client?.url ?? self.registration.scope); - if (traceViewerServerBaseUrl.searchParams.has('server')) - traceViewerServerBaseUrl = new URL(traceViewerServerBaseUrl.searchParams.get('server')!, traceViewerServerBaseUrl); - + const clientURL = new URL(client?.url ?? self.registration.scope); + const traceViewerServerBaseUrl = new URL(clientURL.searchParams.get('server') ?? '../', clientURL); data = { limit, traceUrls: new Set(), traceViewerServer: new TraceViewerServer(traceViewerServerBaseUrl) }; clientIdToTraceUrls.set(clientId, data); } diff --git a/packages/trace-viewer/src/sw/snapshotRenderer.ts b/packages/trace-viewer/src/sw/snapshotRenderer.ts index 08e994ea6d..722ef69e40 100644 --- a/packages/trace-viewer/src/sw/snapshotRenderer.ts +++ b/packages/trace-viewer/src/sw/snapshotRenderer.ts @@ -152,7 +152,7 @@ export class SnapshotRenderer { const html = prefix + [ // Hide the document in order to prevent flickering. We will unhide once script has processed shadow. '', - `` + `` ].join('') + result.join(''); return { value: html, size: html.length }; }); @@ -236,12 +236,41 @@ function snapshotNodes(snapshot: FrameSnapshot): NodeSnapshot[] { return (snapshot as any)._nodes; } -function snapshotScript(...targetIds: (string | undefined)[]) { - function applyPlaywrightAttributes(unwrapPopoutUrl: (url: string) => string, ...targetIds: (string | undefined)[]) { +type ViewportSize = { width: number, height: number }; +type BoundingRect = { left: number, top: number, right: number, bottom: number }; +type FrameBoundingRectsInfo = { + viewport: ViewportSize; + frames: WeakMap; +}; + +declare global { + interface Window { + __playwright_frame_bounding_rects__: FrameBoundingRectsInfo; + } +} + +function snapshotScript(viewport: ViewportSize, ...targetIds: (string | undefined)[]) { + function applyPlaywrightAttributes(unwrapPopoutUrl: (url: string) => string, viewport: ViewportSize, ...targetIds: (string | undefined)[]) { const searchParams = new URLSearchParams(location.search); const shouldPopulateCanvasFromScreenshot = searchParams.has('shouldPopulateCanvasFromScreenshot'); const isUnderTest = searchParams.has('isUnderTest'); + // info to recursively compute canvas position relative to the top snapshot frame. + // Before rendering each iframe, its parent extracts the '__playwright_canvas_render_info__' attribute + // value and keeps in this variable. It can then remove the attribute and render the element, + // which will eventually trigger the same process inside the iframe recursively. + // When there's a canvas to render, we iterate over its ancestor frames to compute + // its position relative to the top snapshot frame. + const frameBoundingRectsInfo = { + viewport, + frames: new WeakMap(), + }; + window['__playwright_frame_bounding_rects__'] = frameBoundingRectsInfo; + const kPointerWarningTitle = 'Recorded click position in absolute coordinates did not' + ' match the center of the clicked element. This is likely due to a difference between' + ' the test runner and the trace viewer operating systems.'; @@ -251,6 +280,10 @@ function snapshotScript(...targetIds: (string | undefined)[]) { const targetElements: Element[] = []; const canvasElements: HTMLCanvasElement[] = []; + let topSnapshotWindow: Window = window; + while (topSnapshotWindow !== topSnapshotWindow.parent && !topSnapshotWindow.location.pathname.match(/\/page@[a-z0-9]+$/)) + topSnapshotWindow = topSnapshotWindow.parent; + const visit = (root: Document | ShadowRoot) => { // Collect all scrolled elements for later use. for (const e of root.querySelectorAll(`[__playwright_scroll_top_]`)) @@ -290,6 +323,11 @@ function snapshotScript(...targetIds: (string | undefined)[]) { } for (const iframe of root.querySelectorAll('iframe, frame')) { + const boundingRectJson = iframe.getAttribute('__playwright_bounding_rect__'); + iframe.removeAttribute('__playwright_bounding_rect__'); + const boundingRect = boundingRectJson ? JSON.parse(boundingRectJson) : undefined; + if (boundingRect) + frameBoundingRectsInfo.frames.set(iframe, { boundingRect, scrollLeft: 0, scrollTop: 0 }); const src = iframe.getAttribute('__playwright_src__'); if (!src) { iframe.setAttribute('src', 'data:text/html,'); @@ -341,16 +379,20 @@ function snapshotScript(...targetIds: (string | undefined)[]) { for (const element of scrollTops) { element.scrollTop = +element.getAttribute('__playwright_scroll_top_')!; element.removeAttribute('__playwright_scroll_top_'); + if (frameBoundingRectsInfo.frames.has(element)) + frameBoundingRectsInfo.frames.get(element)!.scrollTop = element.scrollTop; } for (const element of scrollLefts) { element.scrollLeft = +element.getAttribute('__playwright_scroll_left_')!; element.removeAttribute('__playwright_scroll_left_'); + if (frameBoundingRectsInfo.frames.has(element)) + frameBoundingRectsInfo.frames.get(element)!.scrollLeft = element.scrollTop; } document.styleSheets[0].disabled = true; const search = new URL(window.location.href).searchParams; - const isTopFrame = window.location.pathname.match(/\/page@[a-z0-9]+$/); + const isTopFrame = window === topSnapshotWindow; if (search.get('pointX') && search.get('pointY')) { const pointX = +search.get('pointX')!; @@ -421,16 +463,6 @@ function snapshotScript(...targetIds: (string | undefined)[]) { context.fillRect(0, 0, canvas.width, canvas.height); } - - if (!isTopFrame) { - for (const canvas of canvasElements) { - const context = canvas.getContext('2d')!; - drawCheckerboard(context, canvas); - canvas.title = `Playwright displays canvas contents on a best-effort basis. It doesn't support canvas elements inside an iframe yet. If this impacts your workflow, please open an issue so we can prioritize.`; - } - return; - } - const img = new Image(); img.onload = () => { for (const canvas of canvasElements) { @@ -448,6 +480,31 @@ function snapshotScript(...targetIds: (string | undefined)[]) { continue; } + let currWindow: Window = window; + while (currWindow !== topSnapshotWindow) { + const iframe = currWindow.frameElement!; + currWindow = currWindow.parent; + + const iframeInfo = currWindow['__playwright_frame_bounding_rects__']?.frames.get(iframe); + if (!iframeInfo?.boundingRect) + break; + + const leftOffset = iframeInfo.boundingRect.left - iframeInfo.scrollLeft; + const topOffset = iframeInfo.boundingRect.top - iframeInfo.scrollTop; + + boundingRect.left += leftOffset; + boundingRect.top += topOffset; + boundingRect.right += leftOffset; + boundingRect.bottom += topOffset; + } + + const { width, height } = topSnapshotWindow['__playwright_frame_bounding_rects__'].viewport; + + boundingRect.left = boundingRect.left / width; + boundingRect.top = boundingRect.top / height; + boundingRect.right = boundingRect.right / width; + boundingRect.bottom = boundingRect.bottom / height; + const partiallyUncaptured = boundingRect.right > 1 || boundingRect.bottom > 1; const fullyUncaptured = boundingRect.left > 1 || boundingRect.top > 1; if (fullyUncaptured) { @@ -490,7 +547,7 @@ function snapshotScript(...targetIds: (string | undefined)[]) { window.addEventListener('DOMContentLoaded', onDOMContentLoaded); } - return `\n(${applyPlaywrightAttributes.toString()})(${unwrapPopoutUrl.toString()}${targetIds.map(id => `, "${id}"`).join('')})`; + return `\n(${applyPlaywrightAttributes.toString()})(${unwrapPopoutUrl.toString()}, ${JSON.stringify(viewport)}${targetIds.map(id => `, "${id}"`).join('')})`; } diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.tsx b/packages/trace-viewer/src/ui/networkResourceDetails.tsx index 9805d42c6f..0c4af4e969 100644 --- a/packages/trace-viewer/src/ui/networkResourceDetails.tsx +++ b/packages/trace-viewer/src/ui/networkResourceDetails.tsx @@ -120,7 +120,7 @@ const ResponseTab: React.FunctionComponent<{ const BodyTab: React.FunctionComponent<{ resource: ResourceSnapshot; }> = ({ resource }) => { - const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string, mimeType?: string, font?: BinaryData } | null>(null); + const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string, mimeType?: string, font?: BufferSource } | null>(null); React.useEffect(() => { const readResources = async () => { @@ -158,7 +158,7 @@ const BodyTab: React.FunctionComponent<{ }; const FontPreview: React.FunctionComponent<{ - font: BinaryData; + font: BufferSource; }> = ({ font }) => { const [isError, setIsError] = React.useState(false); diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index 70eec996fb..91ac2dff11 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -351,7 +351,8 @@ export function extendSnapshot(snapshot: Snapshot, shouldPopulateCanvasFromScree const popoutParams = new URLSearchParams(); popoutParams.set('r', snapshotUrl); - popoutParams.set('server', serverParam ?? ''); + if (serverParam) + popoutParams.set('server', serverParam); popoutParams.set('trace', context(snapshot.action).traceUrl); if (snapshot.point) { popoutParams.set('pointX', String(snapshot.point.x)); diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index 09bdb2089d..4375018765 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -47,9 +47,7 @@ const xtermDataSource: XtermDataSource = { }; const searchParams = new URLSearchParams(window.location.search); -let testServerBaseUrl = new URL('../', window.location.href); -if (testServerBaseUrl.searchParams.has('server')) - testServerBaseUrl = new URL(testServerBaseUrl.searchParams.get('server')!, testServerBaseUrl); +const testServerBaseUrl = new URL(searchParams.get('server') ?? '../', window.location.href); const wsURL = new URL(searchParams.get('ws')!, testServerBaseUrl); wsURL.protocol = (wsURL.protocol === 'https:' ? 'wss:' : 'ws:'); const queryParams = { diff --git a/packages/web/src/components/treeView.tsx b/packages/web/src/components/treeView.tsx index 242e834ede..14f2e35195 100644 --- a/packages/web/src/components/treeView.tsx +++ b/packages/web/src/components/treeView.tsx @@ -180,7 +180,7 @@ export function TreeView({ const itemData = treeItems.get(child as T); return itemData && { const testWithServer = test.extend(serverFixtures); testWithServer( 'components routing should go through context', + // @ts-ignore "serverFixtures" are imported from the impl without any types async ({ mount, context, server }) => { server.setRoute('/hello', (req: any, res: any) => { res.write('served via server'); diff --git a/tests/components/ct-vue-cli/package.json b/tests/components/ct-vue-cli/package.json index 6abe7531cb..1c3653d027 100644 --- a/tests/components/ct-vue-cli/package.json +++ b/tests/components/ct-vue-cli/package.json @@ -10,8 +10,8 @@ }, "dependencies": { "core-js": "^3.8.3", - "vue": "3.2.36", - "vue-router": "^4.1.5" + "vue": "^3.2.36", + "vue-router": "^4.5.0" }, "devDependencies": { "@babel/core": "^7.23.2", @@ -20,7 +20,7 @@ "@vue/cli-plugin-eslint": "~5.0.0", "@vue/cli-plugin-router": "~5.0.0", "@vue/cli-service": "~5.0.0", - "@vue/tsconfig": "^0.1.3", + "@vue/tsconfig": "^0.7.0", "eslint": "^7.32.0", "eslint-plugin-vue": "^8.0.3" }, diff --git a/tests/components/ct-vue-cli/tsconfig.app.json b/tests/components/ct-vue-cli/tsconfig.app.json index 5e1fc21721..4d8e68abb8 100644 --- a/tests/components/ct-vue-cli/tsconfig.app.json +++ b/tests/components/ct-vue-cli/tsconfig.app.json @@ -1,5 +1,5 @@ { - "extends": "@vue/tsconfig/tsconfig.web.json", + "extends": "@vue/tsconfig/tsconfig.dom.json", "include": ["src/**/*", "src/**/*.vue"], "exclude": ["src/**/*.spec.*/*"], "compilerOptions": { diff --git a/tests/components/ct-vue-cli/tsconfig.config.json b/tests/components/ct-vue-cli/tsconfig.config.json index d554caf51b..4c52107add 100644 --- a/tests/components/ct-vue-cli/tsconfig.config.json +++ b/tests/components/ct-vue-cli/tsconfig.config.json @@ -1,5 +1,5 @@ { - "extends": "@vue/tsconfig/tsconfig.node.json", + "extends": "@vue/tsconfig/tsconfig.dom.json", "include": ["playwright.config.*"], "compilerOptions": { "composite": true, diff --git a/tests/components/ct-vue-vite/tsconfig.node.json b/tests/components/ct-vue-vite/tsconfig.node.json deleted file mode 100644 index 43cc105313..0000000000 --- a/tests/components/ct-vue-vite/tsconfig.node.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "@vue/tsconfig/tsconfig.dom.json", - "include": ["vite.config.*", "playwright.config.*"], - "compilerOptions": { - "composite": true, - "types": ["node"], - "ignoreDeprecations": "5.0" - } -} \ No newline at end of file diff --git a/tests/config/browserTest.ts b/tests/config/browserTest.ts index c729052715..8477359407 100644 --- a/tests/config/browserTest.ts +++ b/tests/config/browserTest.ts @@ -99,7 +99,7 @@ const test = baseTest.extend isWebView2: [false, { scope: 'worker' }], isHeadlessShell: [async ({ browserName, channel, headless }, use) => { - await use(browserName === 'chromium' && (channel === 'chromium-headless-shell' || (!channel && headless))); + await use(browserName === 'chromium' && (channel === 'chromium-headless-shell' || channel === 'chromium-tip-of-tree-headless-shell' || (!channel && headless))); }, { scope: 'worker' }], contextFactory: async ({ _contextFactory }: any, run) => { diff --git a/tests/config/remoteServer.ts b/tests/config/remoteServer.ts index 04305f31a4..6d9710fd44 100644 --- a/tests/config/remoteServer.ts +++ b/tests/config/remoteServer.ts @@ -92,7 +92,7 @@ export class RemoteServer implements PlaywrightServer { handleSIGINT: true, handleSIGTERM: true, handleSIGHUP: true, - executablePath: browserOptions.channel ? undefined : browserOptions.executablePath || browserType.executablePath(), + executablePath: browserOptions.channel ? undefined : browserOptions.executablePath, logger: undefined, }; const options = { diff --git a/tests/installation/playwright-cli-install-should-work.spec.ts b/tests/installation/playwright-cli-install-should-work.spec.ts index 1c0cea6277..d1a8bd3d04 100755 --- a/tests/installation/playwright-cli-install-should-work.spec.ts +++ b/tests/installation/playwright-cli-install-should-work.spec.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { test, expect } from './npmTest'; +import { chromium } from '@playwright/test'; import path from 'path'; test.use({ isolateBrowsers: true }); @@ -95,3 +96,46 @@ test('install playwright-chromium should work', async ({ exec, installedSoftware await exec('npx playwright install chromium'); await exec('node sanity.js playwright-chromium chromium'); }); + +test('should print error if recording video without ffmpeg', async ({ exec, writeFiles }) => { + await exec('npm i playwright'); + + await writeFiles({ + 'launch.js': ` + const playwright = require('playwright'); + (async () => { + const browser = await playwright.chromium.launch({ executablePath: ${JSON.stringify(chromium.executablePath())} }); + try { + const context = await browser.newContext({ recordVideo: { dir: 'videos' } }); + const page = await context.newPage(); + } finally { + await browser.close(); + } + })().catch(e => { + console.error(e); + process.exit(1); + }); + `, + 'launchPersistentContext.js': ` + const playwright = require('playwright'); + process.on('unhandledRejection', (e) => console.error('unhandledRejection', e)); + (async () => { + const context = await playwright.chromium.launchPersistentContext('', { executablePath: ${JSON.stringify(chromium.executablePath())}, recordVideo: { dir: 'videos' } }); + })().catch(e => { + console.error(e); + process.exit(1); + }); + `, + }); + + await test.step('BrowserType.launch', async () => { + const result = await exec('node', 'launch.js', { expectToExitWithError: true }); + expect(result).toContain(`browserContext.newPage: Executable doesn't exist at`); + }); + + await test.step('BrowserType.launchPersistentContext', async () => { + const result = await exec('node', 'launchPersistentContext.js', { expectToExitWithError: true }); + expect(result).not.toContain('unhandledRejection'); + expect(result).toContain(`browserType.launchPersistentContext: Executable doesn't exist at`); + }); +}); diff --git a/tests/library/debug-controller.spec.ts b/tests/library/debug-controller.spec.ts index 6050d4e645..c3fb643665 100644 --- a/tests/library/debug-controller.spec.ts +++ b/tests/library/debug-controller.spec.ts @@ -84,9 +84,11 @@ test('should pick element', async ({ backend, connectedBrowser }) => { expect(events).toEqual([ { + ariaSnapshot: '- button "Submit"', selector: 'internal:role=button[name=\"Submit\"i]', locator: 'getByRole(\'button\', { name: \'Submit\' })', }, { + ariaSnapshot: '- button "Submit"', selector: 'internal:role=button[name=\"Submit\"i]', locator: 'getByRole(\'button\', { name: \'Submit\' })', }, diff --git a/tests/library/headful.spec.ts b/tests/library/headful.spec.ts index d0c9f99450..b74563850a 100644 --- a/tests/library/headful.spec.ts +++ b/tests/library/headful.spec.ts @@ -19,7 +19,7 @@ import { PNG } from 'playwright-core/lib/utilsBundle'; import { expect, playwrightTest as it } from '../config/browserTest'; it.use({ headless: false }); -it.skip(({ channel }) => channel === 'chromium-headless-shell', 'shell is never headed'); +it.skip(({ channel }) => channel === 'chromium-headless-shell' || channel === 'chromium-tip-of-tree-headless-shell', 'shell is never headed'); it('should have default url when launching browser @smoke', async ({ launchPersistent }) => { const { context } = await launchPersistent(); diff --git a/tests/library/inspector/cli-codegen-1.spec.ts b/tests/library/inspector/cli-codegen-1.spec.ts index a876a25e47..0fd5f69d0b 100644 --- a/tests/library/inspector/cli-codegen-1.spec.ts +++ b/tests/library/inspector/cli-codegen-1.spec.ts @@ -926,4 +926,34 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`) const predicate = (msg: ConsoleMessage) => msg.type() === 'error' && /Content[\- ]Security[\- ]Policy/i.test(msg.text()); await expect(page.waitForEvent('console', { predicate, timeout: 1000 })).rejects.toThrow(); }); + + test('should clear when recording is disabled', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33802' } }, async ({ openRecorder }) => { + const { recorder } = await openRecorder(); + + await recorder.setContentAndWait(` + + + `); + + await recorder.hoverOverElement('#foo'); + let [sources] = await Promise.all([ + recorder.waitForOutput('JavaScript', 'click'), + recorder.trustedClick(), + ]); + + expect(sources.get('JavaScript').text).toContain(`getByRole('button', { name: 'Foo' }).click()`); + + await recorder.recorderPage.getByRole('button', { name: 'Record' }).click(); + await recorder.recorderPage.getByRole('button', { name: 'Clear' }).click(); + await recorder.recorderPage.getByRole('button', { name: 'Record' }).click(); + + await recorder.hoverOverElement('#bar'); + [sources] = await Promise.all([ + recorder.waitForOutput('JavaScript', 'click'), + recorder.trustedClick(), + ]); + + expect(sources.get('JavaScript').text).toContain(`getByRole('button', { name: 'Bar' }).click()`); + expect(sources.get('JavaScript').text).not.toContain(`getByRole('button', { name: 'Foo' })`); + }); }); diff --git a/tests/library/inspector/cli-codegen-3.spec.ts b/tests/library/inspector/cli-codegen-3.spec.ts index a57e7d5405..22032a8f26 100644 --- a/tests/library/inspector/cli-codegen-3.spec.ts +++ b/tests/library/inspector/cli-codegen-3.spec.ts @@ -472,7 +472,7 @@ await page.GetByTestId("testid").ClickAsync();`); await recorder.setContentAndWait(``); const locator = await recorder.hoverOverElement('input'); - expect(locator).toBe(`getByPlaceholder('Country')`); + expect(locator).toBe(`getByRole('textbox', { name: 'Country' })`); const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'click'), @@ -480,19 +480,19 @@ await page.GetByTestId("testid").ClickAsync();`); ]); expect.soft(sources.get('JavaScript')!.text).toContain(` - await page.getByPlaceholder('Country').click();`); + await page.getByRole('textbox', { name: 'Country' }).click();`); expect.soft(sources.get('Python')!.text).toContain(` - page.get_by_placeholder("Country").click()`); + page.get_by_role("textbox", name="Country").click()`); expect.soft(sources.get('Python Async')!.text).toContain(` - await page.get_by_placeholder("Country").click()`); + await page.get_by_role("textbox", name="Country").click()`); expect.soft(sources.get('Java')!.text).toContain(` - page.getByPlaceholder("Country").click()`); + page.getByRole(AriaRole.TEXTBOX, new Page.GetByRoleOptions().setName("Country")).click()`); expect.soft(sources.get('C#')!.text).toContain(` -await page.GetByPlaceholder("Country").ClickAsync();`); +await page.GetByRole(AriaRole.Textbox, new() { Name = "Country" }).ClickAsync();`); }); test('should generate getByAltText', async ({ openRecorder }) => { @@ -530,7 +530,7 @@ await page.GetByAltText("Country").ClickAsync();`); await recorder.setContentAndWait(``); const locator = await recorder.hoverOverElement('input'); - expect(locator).toBe(`getByLabel('Country')`); + expect(locator).toBe(`getByRole('textbox', { name: 'Country' })`); const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'click'), @@ -538,19 +538,19 @@ await page.GetByAltText("Country").ClickAsync();`); ]); expect.soft(sources.get('JavaScript')!.text).toContain(` - await page.getByLabel('Country').click();`); + await page.getByRole('textbox', { name: 'Country' }).click();`); expect.soft(sources.get('Python')!.text).toContain(` - page.get_by_label("Country").click()`); + page.get_by_role("textbox", name="Country").click()`); expect.soft(sources.get('Python Async')!.text).toContain(` - await page.get_by_label("Country").click()`); + await page.get_by_role("textbox", name="Country").click()`); expect.soft(sources.get('Java')!.text).toContain(` - page.getByLabel("Country").click()`); + page.getByRole(AriaRole.TEXTBOX, new Page.GetByRoleOptions().setName("Country")).click()`); expect.soft(sources.get('C#')!.text).toContain(` -await page.GetByLabel("Country").ClickAsync();`); +await page.GetByRole(AriaRole.Textbox, new() { Name = "Country" }).ClickAsync();`); }); test('should generate getByLabel without regex', async ({ openRecorder }) => { @@ -559,7 +559,7 @@ await page.GetByLabel("Country").ClickAsync();`); await recorder.setContentAndWait(``); const locator = await recorder.hoverOverElement('input'); - expect(locator).toBe(`getByLabel('Coun"try')`); + expect(locator).toBe(`getByRole('textbox', { name: 'Coun"try' })`); const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'click'), @@ -567,19 +567,19 @@ await page.GetByLabel("Country").ClickAsync();`); ]); expect.soft(sources.get('JavaScript')!.text).toContain(` - await page.getByLabel('Coun\"try').click();`); + await page.getByRole('textbox', { name: 'Coun\"try' }).click();`); expect.soft(sources.get('Python')!.text).toContain(` - page.get_by_label("Coun\\"try").click()`); + page.get_by_role("textbox", name="Coun\\"try").click()`); expect.soft(sources.get('Python Async')!.text).toContain(` - await page.get_by_label("Coun\\"try").click()`); + await page.get_by_role("textbox", name="Coun\\"try").click()`); expect.soft(sources.get('Java')!.text).toContain(` - page.getByLabel("Coun\\"try").click()`); + page.getByRole(AriaRole.TEXTBOX, new Page.GetByRoleOptions().setName(\"Coun\\\"try\")).click();`); expect.soft(sources.get('C#')!.text).toContain(` -await page.GetByLabel("Coun\\"try").ClickAsync();`); +await page.GetByRole(AriaRole.Textbox, new() { Name = \"Coun\\\"try\" }).ClickAsync();`); }); test('should consume pointer events', async ({ openRecorder }) => { diff --git a/tests/library/launcher.spec.ts b/tests/library/launcher.spec.ts index bb244c0b60..049e9e1834 100644 --- a/tests/library/launcher.spec.ts +++ b/tests/library/launcher.spec.ts @@ -45,6 +45,7 @@ it('should throw a friendly error if its headed and there is no xserver on linux it.skip(platform !== 'linux'); it.skip(mode.startsWith('service')); it.skip(channel === 'chromium-headless-shell', 'shell is never headed'); + it.skip(channel === 'chromium-tip-of-tree-headless-shell', 'shell is never headed'); const error: Error = await browserType.launch({ headless: false, diff --git a/tests/library/locator-generator.spec.ts b/tests/library/locator-generator.spec.ts index 4df72977f4..cceff133d4 100644 --- a/tests/library/locator-generator.spec.ts +++ b/tests/library/locator-generator.spec.ts @@ -196,6 +196,12 @@ it('reverse engineer getByRole', async ({ page }) => { java: `getByRole(AriaRole.BUTTON)`, csharp: `GetByRole(AriaRole.Button)`, }); + expect.soft(generate(page.getByRole('heading', {}))).toEqual({ + javascript: "getByRole('heading')", + python: 'get_by_role("heading")', + java: 'getByRole(AriaRole.HEADING)', + csharp: 'GetByRole(AriaRole.Heading)' + }); expect.soft(generate(page.getByRole('button', { name: 'Hello' }))).toEqual({ javascript: `getByRole('button', { name: 'Hello' })`, python: `get_by_role("button", name="Hello")`, @@ -559,6 +565,12 @@ it('parseLocator css', async () => { expect.soft(parseLocator('csharp', `Locator("css=.foo")`, '')).toBe(`css=.foo`); }); + +it('parseLocator options', async () => { + expect.soft(parseLocator('javascript', `getByRole('heading', {})`, '')).toBe(`internal:role=heading`); + expect.soft(parseLocator('javascript', `getByRole('checkbox', { checked:false, includeHidden: true })`, '')).toBe(`internal:role=checkbox[checked=false][include-hidden=true]`); +}); + it('parse locators strictly', () => { const selector = 'div >> internal:has-text=\"Goodbye world\"i >> span'; diff --git a/tests/library/selector-generator.spec.ts b/tests/library/selector-generator.spec.ts index e9df1afc6e..1feaf538e7 100644 --- a/tests/library/selector-generator.spec.ts +++ b/tests/library/selector-generator.spec.ts @@ -58,7 +58,7 @@ it.describe('selector generator', () => { it('should not escape spaces inside named attr selectors', async ({ page }) => { await page.setContent(``); - expect(await generate(page, 'input')).toBe('internal:attr=[placeholder=\"Foo b ar\"i]'); + expect(await generate(page, 'input')).toBe('internal:role=textbox[name=\"Foo b ar\"i]'); }); it('should generate text for ', async ({ page }) => { @@ -91,7 +91,7 @@ it.describe('selector generator', () => { it('should try to improve label text by shortening', async ({ page }) => { await page.setContent(``); - expect(await generate(page, 'input')).toBe('internal:label="Longest verbose description"i'); + expect(await generate(page, 'input')).toBe('internal:role=textbox[name=\"Longest verbose description\"i]'); }); it('should not improve guid text', async ({ page }) => { @@ -309,14 +309,14 @@ it.describe('selector generator', () => { expect(await generate(page, 'input[mark="1"]')).toBe('internal:role=textbox >> nth=1'); }); - it.describe('should prioritise attributes correctly', () => { + it.describe('should prioritize attributes correctly', () => { it('role', async ({ page }) => { await page.setContent(``); expect(await generate(page, 'input')).toBe('internal:role=textbox'); }); it('placeholder', async ({ page }) => { await page.setContent(``); - expect(await generate(page, 'input')).toBe('internal:attr=[placeholder=\"foobar\"i]'); + expect(await generate(page, 'input')).toBe('internal:role=textbox[name=\"foobar\"i]'); }); it('name', async ({ page }) => { await page.setContent(` @@ -437,7 +437,7 @@ it.describe('selector generator', () => { it('should accept valid aria-label for candidate consideration', async ({ page }) => { await page.setContent(``); - expect(await generate(page, 'button')).toBe('internal:label="ariaLabel"i'); + expect(await generate(page, 'button')).toBe('internal:role=button[name=\"ariaLabel\"i]'); }); it('should ignore empty role for candidate consideration', async ({ page }) => { @@ -469,15 +469,15 @@ it.describe('selector generator', () => {
text
`); - expect(await generate(page, '#target1')).toBe('internal:label="Target1"i'); - expect(await generate(page, '#target2')).toBe('internal:label="Target2"i'); - expect(await generate(page, '#target3')).toBe('internal:label="Target3"i'); - expect(await generate(page, '#target4')).toBe('internal:label="Target4"i'); - expect(await generate(page, '#target5')).toBe('#target5'); - expect(await generate(page, '#target6')).toBe('internal:text="text"i'); + expect.soft(await generate(page, '#target1')).toBe('internal:role=textbox[name=\"Target1\"i]'); + expect.soft(await generate(page, '#target2')).toBe('internal:role=button[name=\"Target2\"i]'); + expect.soft(await generate(page, '#target3')).toBe('internal:label=\"Target3\"i'); + expect.soft(await generate(page, '#target4')).toBe('internal:label=\"Target4\"i'); + expect.soft(await generate(page, '#target5')).toBe('#target5'); + expect.soft(await generate(page, '#target6')).toBe('internal:text="text"i'); await page.setContent(``); - expect(await generate(page, 'input')).toBe('internal:label="Coun\\\"try"i'); + expect(await generate(page, 'input')).toBe('internal:role=textbox[name=\"Coun\\\"try\"i]'); }); it('should prefer role other input[type]', async ({ page }) => { @@ -514,7 +514,7 @@ it.describe('selector generator', () => { `); - expect(await generate(page, 'input')).toBe('internal:attr=[placeholder=\"Text\"s]'); + expect(await generate(page, 'input')).toBe('internal:role=textbox[name=\"Text\"s]'); }); it('should generate exact role when necessary', async ({ page }) => { @@ -530,7 +530,7 @@ it.describe('selector generator', () => { `); - expect(await generate(page, 'input')).toBe('internal:label=\"Text\"s'); + expect(await generate(page, 'input')).toBe('internal:role=textbox[name=\"Text\"s]'); }); it('should generate relative selector', async ({ page }) => { @@ -596,4 +596,23 @@ it.describe('selector generator', () => { `span >> nth=1`, ]); }); + + it('should prefer role with hasText to css with hasText', async ({ page }) => { + await page.setContent(` +
    +
  • + + buy flowers +
  • +
  • + + sell milk +
  • +
+ `); + expect(await generateMultiple(page, 'input')).toEqual([ + `internal:role=listitem >> internal:has-text=\"buy flowers\"i >> internal:label=\"Toggle Todo\"i`, + `internal:label=\"Toggle Todo\"i >> nth=0`, + ]); + }); }); diff --git a/tests/library/snapshotter.spec.ts b/tests/library/snapshotter.spec.ts index 7ada643dcc..3e9cfce46f 100644 --- a/tests/library/snapshotter.spec.ts +++ b/tests/library/snapshotter.spec.ts @@ -268,10 +268,12 @@ it.describe('snapshots', () => { }); }); -function distillSnapshot(snapshot, distillTarget = true) { +function distillSnapshot(snapshot, options: { distillTarget: boolean, distillBoundingRect: boolean } = { distillTarget: true, distillBoundingRect: true }) { let { html } = snapshot.render(); - if (distillTarget) + if (options.distillTarget) html = html.replace(/\s__playwright_target__="[^"]+"/g, ''); + if (options.distillBoundingRect) + html = html.replace(/\s__playwright_bounding_rect__="[^"]+"/g, ''); return html .replace(/