diff --git a/.eslintignore b/.eslintignore
index f7365e0082..152cbbe228 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -19,4 +19,5 @@ tests/components/
tests/installation/fixture-scripts/
examples/
DEPS
-.cache/
\ No newline at end of file
+.cache/
+utils/
diff --git a/.eslintrc-with-ts-config.js b/.eslintrc-with-ts-config.js
new file mode 100644
index 0000000000..b06ec00195
--- /dev/null
+++ b/.eslintrc-with-ts-config.js
@@ -0,0 +1,15 @@
+module.exports = {
+ extends: "./.eslintrc.js",
+ parserOptions: {
+ ecmaVersion: 9,
+ sourceType: "module",
+ project: "./tsconfig.json",
+ },
+ rules: {
+ "@typescript-eslint/no-base-to-string": "error",
+ "@typescript-eslint/no-unnecessary-boolean-literal-compare": 2,
+ },
+ parserOptions: {
+ project: "./tsconfig.json"
+ },
+};
diff --git a/.eslintrc.js b/.eslintrc.js
index 7bc5a0868f..bff9ffeeb4 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -48,6 +48,7 @@ module.exports = {
"arrow-parens": [2, "as-needed"],
"prefer-const": 2,
"quote-props": [2, "consistent"],
+ "nonblock-statement-body-position": [2, "below"],
// anti-patterns
"no-var": 2,
diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
index 5baad382eb..062d7c7e74 100644
--- a/.github/ISSUE_TEMPLATE/bug.yml
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -23,7 +23,7 @@ body:
value: |
## Make a minimal reproduction
To file the report, you will need a GitHub repository with a minimal (but complete) example and simple/clear steps on how to reproduce the bug.
- The simpler you can make it, the more likely we are to successfully verify and fix the bug.
+ The simpler you can make it, the more likely we are to successfully verify and fix the bug. You can create a new project with `npm init playwright@latest new-project` and then add the test code there.
- type: markdown
attributes:
value: |
diff --git a/.github/workflows/cherry_pick_into_release_branch.yml b/.github/workflows/cherry_pick_into_release_branch.yml
index 6350d7ea0d..f48028b14b 100644
--- a/.github/workflows/cherry_pick_into_release_branch.yml
+++ b/.github/workflows/cherry_pick_into_release_branch.yml
@@ -12,6 +12,9 @@ on:
description: Comma-separated list of commit hashes to cherry-pick
required: true
+permissions:
+ contents: write
+
jobs:
roll:
runs-on: ubuntu-22.04
diff --git a/.github/workflows/tests_primary.yml b/.github/workflows/tests_primary.yml
index 0ad4b68294..8c491514ae 100644
--- a/.github/workflows/tests_primary.yml
+++ b/.github/workflows/tests_primary.yml
@@ -55,7 +55,7 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
- - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}
+ - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-*
- run: node tests/config/checkCoverage.js ${{ matrix.browser }}
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: always()
@@ -87,7 +87,7 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps chromium-tip-of-tree
- - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=chromium
+ - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=chromium-*
env:
PWTEST_CHANNEL: chromium-tip-of-tree
PWTEST_BOT_NAME: "${{ matrix.os }}-chromium-tip-of-tree"
diff --git a/.github/workflows/tests_secondary.yml b/.github/workflows/tests_secondary.yml
index 816bf5c3f9..7659c338bc 100644
--- a/.github/workflows/tests_secondary.yml
+++ b/.github/workflows/tests_secondary.yml
@@ -42,7 +42,7 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
- - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}
+ - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-*
- run: node tests/config/checkCoverage.js ${{ matrix.browser }}
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: always()
@@ -75,7 +75,7 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
- - run: npm run test -- --project=${{ matrix.browser }}
+ - run: npm run test -- --project=${{ matrix.browser }}-*
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: always()
shell: bash
@@ -106,10 +106,10 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
- - run: npm run test -- --project=${{ matrix.browser }} --workers=1
+ - run: npm run test -- --project=${{ matrix.browser }}-* --workers=1
if: matrix.browser == 'firefox'
shell: bash
- - run: npm run test -- --project=${{ matrix.browser }}
+ - run: npm run test -- --project=${{ matrix.browser }}-*
if: matrix.browser != 'firefox'
shell: bash
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
@@ -175,9 +175,9 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
- - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }} --headed
+ - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-* --headed
if: always() && startsWith(matrix.os, 'ubuntu-')
- - run: npm run test -- --project=${{ matrix.browser }} --headed
+ - run: npm run test -- --project=${{ matrix.browser }}-* --headed
if: always() && !startsWith(matrix.os, 'ubuntu-')
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: always()
@@ -247,7 +247,7 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium ${{ matrix.channel }}
- - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}
+ - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-*
env:
PWTEST_TRACE: 1
PWTEST_CHANNEL: ${{ matrix.channel }}
@@ -868,7 +868,7 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps chromium
- - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=chromium
+ - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=chromium-*
env:
PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW: 1
- run: node tests/config/checkCoverage.js chromium
diff --git a/.github/workflows/tests_service.yml b/.github/workflows/tests_service.yml
index 9c932f38e4..b8c192f988 100644
--- a/.github/workflows/tests_service.yml
+++ b/.github/workflows/tests_service.yml
@@ -23,7 +23,7 @@ jobs:
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }} --workers=10 --retries=0
+ - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-* --workers=10 --retries=0
env:
PWTEST_MODE: service2
PWTEST_TRACE: 1
diff --git a/.github/workflows/tests_video.yml b/.github/workflows/tests_video.yml
index f39b544f93..3acda7cec7 100644
--- a/.github/workflows/tests_video.yml
+++ b/.github/workflows/tests_video.yml
@@ -32,7 +32,7 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
- - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}
+ - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-*
env:
PWTEST_VIDEO: 1
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
diff --git a/.github/workflows/tests_webview2.yml b/.github/workflows/tests_webview2.yml
index 425a7bbc8a..26c97f6062 100644
--- a/.github/workflows/tests_webview2.yml
+++ b/.github/workflows/tests_webview2.yml
@@ -38,6 +38,12 @@ jobs:
- run: npm run build
- run: dotnet build
working-directory: tests/webview2/webview2-app/
+ - name: Update to Evergreen WebView2 Runtime
+ shell: pwsh
+ run: |
+ # See here: https://developer.microsoft.com/en-us/microsoft-edge/webview2/
+ Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'setup.exe'
+ Start-Process -FilePath setup.exe -Verb RunAs -Wait
- run: npm run webview2test
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: always()
diff --git a/.github/workflows/trigger_build_chromium_with_symbols.yml b/.github/workflows/trigger_build_chromium_with_symbols.yml
deleted file mode 100644
index e0e31acd57..0000000000
--- a/.github/workflows/trigger_build_chromium_with_symbols.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-name: "Trigger: Chromium with Symbols Builds"
-
-on:
- workflow_dispatch:
- release:
- types: [published]
-
-jobs:
- trigger:
- name: "trigger"
- runs-on: ubuntu-22.04
- steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v2
- with:
- node-version: 18
- - name: Get Chromium revision
- id: chromium-version
- run: |
- REVISION=$(node -e "console.log(require('./packages/playwright-core/browsers.json').browsers.find(b => b.name === 'chromium-with-symbols').revision)")
- echo "REVISION=$REVISION" >> $GITHUB_OUTPUT
- - run: |
- curl -X POST \
- -H "Accept: application/vnd.github.v3+json" \
- -H "Authorization: token ${GH_TOKEN}" \
- --data "{\"event_type\": \"build_chromium_with_symbols\", \"client_payload\": {\"revision\": \"${CHROMIUM_REVISION}\"}}" \
- https://api.github.com/repos/microsoft/playwright-browsers/dispatches
- env:
- GH_TOKEN: ${{ secrets.REPOSITORY_DISPATCH_PERSONAL_ACCESS_TOKEN }}
- CHROMIUM_REVISION: ${{ steps.chromium-version.outputs.REVISION }}
diff --git a/README.md b/README.md
index 14737d10d4..93be49146e 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# 🎭 Playwright
-[](https://www.npmjs.com/package/playwright) [](https://www.chromium.org/Home) [](https://www.mozilla.org/en-US/firefox/new/) [](https://webkit.org/)
+[](https://www.npmjs.com/package/playwright) [](https://www.chromium.org/Home) [](https://www.mozilla.org/en-US/firefox/new/) [](https://webkit.org/)
## [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 123.0.6312.4 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
+| Chromium 124.0.6367.8 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit 17.4 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox 123.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
diff --git a/docs/src/accessibility-testing-java.md b/docs/src/accessibility-testing-java.md
index a14064452c..15bb998481 100644
--- a/docs/src/accessibility-testing-java.md
+++ b/docs/src/accessibility-testing-java.md
@@ -238,3 +238,5 @@ public class HomepageTests extends AxeTestFixtures {
}
}
```
+
+See experimental [JUnit integration](./junit.md) to automatically initialize Playwright objects and more.
diff --git a/docs/src/api-testing-java.md b/docs/src/api-testing-java.md
index 80faa968d4..41265776a5 100644
--- a/docs/src/api-testing-java.md
+++ b/docs/src/api-testing-java.md
@@ -375,6 +375,8 @@ public class TestGitHubAPI {
}
```
+See experimental [JUnit integration](./junit.md) to automatically initialize Playwright objects and more.
+
## Prepare server state via API calls
The following test creates a new issue via API and then navigates to the list of all issues in the
diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md
index 87f6369b5c..11df502854 100644
--- a/docs/src/api/class-browsercontext.md
+++ b/docs/src/api/class-browsercontext.md
@@ -186,7 +186,10 @@ context.on("dialog", lambda dialog: dialog.accept())
```
```csharp
-context.Dialog += (_, dialog) => dialog.AcceptAsync();
+Context.Dialog += async (_, dialog) =>
+{
+ await dialog.AcceptAsync();
+};
```
:::note
@@ -390,7 +393,7 @@ browser_context.add_init_script(path="preload.js")
```
```csharp
-await context.AddInitScriptAsync(scriptPath: "preload.js");
+await Context.AddInitScriptAsync(scriptPath: "preload.js");
```
:::note
@@ -1010,6 +1013,27 @@ Creates a new page in the browser context.
Returns all open pages in the context.
+## async method: BrowserContext.removeCookies
+* since: v1.43
+
+Removes cookies from context. At least one of the removal criteria should be provided.
+
+**Usage**
+
+```js
+await browserContext.removeCookies({ name: 'session-id' });
+await browserContext.removeCookies({ domain: 'my-origin.com' });
+await browserContext.removeCookies({ path: '/api/v1' });
+await browserContext.removeCookies({ name: 'session-id', domain: 'my-origin.com' });
+```
+
+### param: BrowserContext.removeCookies.filter
+* since: v1.43
+- `filter` <[Object]>
+ - `name` ?<[string]>
+ - `domain` ?<[string]>
+ - `path` ?<[string]>
+
## property: BrowserContext.request
* since: v1.16
* langs:
@@ -1159,7 +1183,7 @@ context.route("/api/**", handle_route)
await page.RouteAsync("/api/**", async r =>
{
if (r.Request.PostData.Contains("my-string"))
- await r.FulfillAsync(body: "mocked-data");
+ await r.FulfillAsync(new() { Body = "mocked-data" });
else
await r.ContinueAsync();
});
diff --git a/docs/src/api/class-framelocator.md b/docs/src/api/class-framelocator.md
index c90ff1fa19..cf75fc8422 100644
--- a/docs/src/api/class-framelocator.md
+++ b/docs/src/api/class-framelocator.md
@@ -74,28 +74,59 @@ await page.FrameLocator(".result-frame").First.getByRole(AriaRole.Button).ClickA
**Converting Locator to FrameLocator**
-If you have a [Locator] object pointing to an `iframe` it can be converted to [FrameLocator] using [`:scope`](https://developer.mozilla.org/en-US/docs/Web/CSS/:scope) CSS selector:
+If you have a [Locator] object pointing to an `iframe` it can be converted to [FrameLocator] using [`method: Locator.enterFrame`].
+
+**Converting FrameLocator to Locator**
+
+If you have a [FrameLocator] object it can be converted to [Locator] pointing to the same `iframe` using [`method: FrameLocator.exitFrame`].
+
+
+## method: FrameLocator.exitFrame
+* since: v1.43
+- returns: <[Locator]>
+
+Returns a [Locator] object pointing to the same `iframe` as this frame locator.
+
+Useful when you have a [FrameLocator] object obtained somewhere, and later on would like to interact with the `iframe` element.
+
+**Usage**
```js
-const frameLocator = locator.frameLocator(':scope');
+const frameLocator = page.frameLocator('iframe[name="embedded"]');
+// ...
+const locator = frameLocator.exitFrame();
+await expect(locator).toBeVisible();
```
```java
-Locator frameLocator = locator.frameLocator(':scope');
+FrameLocator frameLocator = page.frameLocator("iframe[name=\"embedded\"]");
+// ...
+Locator locator = frameLocator.exitFrame();
+assertThat(locator).isVisible();
```
```python async
-frameLocator = locator.frame_locator(":scope")
+frame_locator = page.frame_locator("iframe[name=\"embedded\"]")
+# ...
+locator = frame_locator.exit_frame
+await expect(locator).to_be_visible()
```
```python sync
-frameLocator = locator.frame_locator(":scope")
+frame_locator = page.frame_locator("iframe[name=\"embedded\"]")
+# ...
+locator = frame_locator.exit_frame
+expect(locator).to_be_visible()
```
```csharp
-var frameLocator = locator.FrameLocator(":scope");
+var frameLocator = Page.FrameLocator("iframe[name=\"embedded\"]");
+// ...
+var locator = frameLocator.ExitFrame;
+await Expect(locator).ToBeVisibleAsync();
```
+
## method: FrameLocator.first
* since: v1.17
- returns: <[FrameLocator]>
diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md
index bea8507def..c254adc1c0 100644
--- a/docs/src/api/class-locator.md
+++ b/docs/src/api/class-locator.md
@@ -747,6 +747,51 @@ Resolves given locator to the first matching DOM element. If there are no matchi
Resolves given locator to all matching DOM elements. If there are no matching elements, returns an empty list.
+## method: Locator.enterFrame
+* since: v1.43
+- returns: <[FrameLocator]>
+
+Returns a [FrameLocator] object pointing to the same `iframe` as this locator.
+
+Useful when you have a [Locator] object obtained somewhere, and later on would like to interact with the content inside the frame.
+
+**Usage**
+
+```js
+const locator = page.locator('iframe[name="embedded"]');
+// ...
+const frameLocator = locator.enterFrame();
+await frameLocator.getByRole('button').click();
+```
+
+```java
+Locator locator = page.locator("iframe[name=\"embedded\"]");
+// ...
+FrameLocator frameLocator = locator.enterFrame();
+frameLocator.getByRole(AriaRole.BUTTON).click();
+```
+
+```python async
+locator = page.locator("iframe[name=\"embedded\"]")
+# ...
+frame_locator = locator.enter_frame
+await frame_locator.get_by_role("button").click()
+```
+
+```python sync
+locator = page.locator("iframe[name=\"embedded\"]")
+# ...
+frame_locator = locator.enter_frame
+frame_locator.get_by_role("button").click()
+```
+
+```csharp
+var locator = Page.Locator("iframe[name=\"embedded\"]");
+// ...
+var frameLocator = locator.EnterFrame;
+await frameLocator.GetByRole(AriaRole.Button).ClickAsync();
+```
+
## async method: Locator.evaluate
* since: v1.14
- returns: <[Serializable]>
diff --git a/docs/src/api/class-logger.md b/docs/src/api/class-logger.md
index a3a4b7c463..5ab4f3128a 100644
--- a/docs/src/api/class-logger.md
+++ b/docs/src/api/class-logger.md
@@ -10,7 +10,7 @@ const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
(async () => {
const browser = await chromium.launch({
logger: {
- isEnabled: (name, severity) => name === 'browser',
+ isEnabled: (name, severity) => name === 'api',
log: (name, severity, message, args) => console.log(`${name} ${message}`)
}
});
diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md
index 922a7df3bf..caea5998b6 100644
--- a/docs/src/api/class-page.md
+++ b/docs/src/api/class-page.md
@@ -608,7 +608,7 @@ page.add_init_script(path="./preload.js")
```
```csharp
-await page.AddInitScriptAsync(scriptPath: "./preload.js");
+await Page.AddInitScriptAsync(scriptPath: "./preload.js");
```
:::note
@@ -3146,32 +3146,38 @@ return value resolves to `[]`.
## async method: Page.addLocatorHandler
* since: v1.42
-Sometimes, the web page can show an overlay that obstructs elements behind it and prevents certain actions, like click, from completing. When such an overlay is shown predictably, we recommend dismissing it as a part of your test flow. However, sometimes such an overlay may appear non-deterministically, for example certain cookies consent dialogs behave this way. In this case, [`method: Page.addLocatorHandler`] allows handling an overlay during an action that it would block.
+:::warning Experimental
+This method is experimental and its behavior may change in the upcoming releases.
+:::
-This method registers a handler for an overlay that is executed once the locator is visible on the page. The handler should get rid of the overlay so that actions blocked by it can proceed. This is useful for nondeterministic interstitial pages or dialogs, like a cookie consent dialog.
+When testing a web page, sometimes unexpected overlays like a "Sign up" dialog appear and block actions you want to automate, e.g. clicking a button. These overlays don't always show up in the same way or at the same time, making them tricky to handle in automated tests.
-Note that execution time of the handler counts towards the timeout of the action/assertion that executed the handler.
+This method lets you set up a special function, called a handler, that activates when it detects that overlay is visible. The handler's job is to remove the overlay, allowing your test to continue as if the overlay wasn't there.
-You can register multiple handlers. However, only a single handler will be running at a time. Any actions inside a handler must not require another handler to run.
+Things to keep in mind:
+* When an overlay is shown predictably, we recommend explicitly waiting for it in your test and dismissing it as a part of your normal test flow, instead of using [`method: Page.addLocatorHandler`].
+* Playwright checks for the overlay every time before executing or retrying an action that requires an [actionability check](../actionability.md), or before performing an auto-waiting assertion check. When overlay is visible, Playwright calls the handler first, and then proceeds with the action/assertion. Note that the handler is only called when you perform an action/assertion - if the overlay becomes visible but you don't perform any actions, the handler will not be triggered.
+* The execution time of the handler counts towards the timeout of the action/assertion that executed the handler. If your handler takes too long, it might cause timeouts.
+* You can register multiple handlers. However, only a single handler will be running at a time. Make sure the actions within a handler don't depend on another handler.
:::warning
-Running the interceptor will alter your page state mid-test. For example it will change the currently focused element and move the mouse. Make sure that the actions that run after the interceptor are self-contained and do not rely on the focus and mouse state.
+Running the handler will alter your page state mid-test. For example it will change the currently focused element and move the mouse. Make sure that actions that run after the handler are self-contained and do not rely on the focus and mouse state being unchanged.
For example, consider a test that calls [`method: Locator.focus`] followed by [`method: Keyboard.press`]. If your handler clicks a button between these two actions, the focused element most likely will be wrong, and key press will happen on the unexpected element. Use [`method: Locator.press`] instead to avoid this problem.
-Another example is a series of mouse actions, where [`method: Mouse.move`] is followed by [`method: Mouse.down`]. Again, when the handler runs between these two actions, the mouse position will be wrong during the mouse down. Prefer methods like [`method: Locator.click`] that are self-contained.
+Another example is a series of mouse actions, where [`method: Mouse.move`] is followed by [`method: Mouse.down`]. Again, when the handler runs between these two actions, the mouse position will be wrong during the mouse down. Prefer self-contained actions like [`method: Locator.click`] that do not rely on the state being unchanged by a handler.
:::
**Usage**
-An example that closes a cookie dialog when it appears:
+An example that closes a "Sign up to the newsletter" dialog when it appears:
```js
// Setup the handler.
-await page.addLocatorHandler(page.getByRole('button', { name: 'Accept all cookies' }), async () => {
- await page.getByRole('button', { name: 'Reject all cookies' }).click();
+await page.addLocatorHandler(page.getByText('Sign up to the newsletter'), async () => {
+ await page.getByRole('button', { name: 'No thanks' }).click();
});
// Write the test as usual.
@@ -3181,8 +3187,8 @@ await page.getByRole('button', { name: 'Start here' }).click();
```java
// Setup the handler.
-page.addLocatorHandler(page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Accept all cookies")), () => {
- page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Reject all cookies")).click();
+page.addLocatorHandler(page.getByText("Sign up to the newsletter"), () => {
+ page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("No thanks")).click();
});
// Write the test as usual.
@@ -3193,8 +3199,8 @@ page.getByRole("button", Page.GetByRoleOptions().setName("Start here")).click();
```python sync
# Setup the handler.
def handler():
- page.get_by_role("button", name="Reject all cookies").click()
-page.add_locator_handler(page.get_by_role("button", name="Accept all cookies"), handler)
+ page.get_by_role("button", name="No thanks").click()
+page.add_locator_handler(page.get_by_text("Sign up to the newsletter"), handler)
# Write the test as usual.
page.goto("https://example.com")
@@ -3204,8 +3210,8 @@ page.get_by_role("button", name="Start here").click()
```python async
# Setup the handler.
def handler():
- await page.get_by_role("button", name="Reject all cookies").click()
-await page.add_locator_handler(page.get_by_role("button", name="Accept all cookies"), handler)
+ await page.get_by_role("button", name="No thanks").click()
+await page.add_locator_handler(page.get_by_text("Sign up to the newsletter"), handler)
# Write the test as usual.
await page.goto("https://example.com")
@@ -3214,8 +3220,8 @@ await page.get_by_role("button", name="Start here").click()
```csharp
// Setup the handler.
-await page.AddLocatorHandlerAsync(page.GetByRole(AriaRole.Button, new() { Name = "Accept all cookies" }), async () => {
- await page.GetByRole(AriaRole.Button, new() { Name = "Reject all cookies" }).ClickAsync();
+await page.AddLocatorHandlerAsync(page.GetByText("Sign up to the newsletter"), async () => {
+ await page.GetByRole(AriaRole.Button, new() { Name = "No thanks" }).ClickAsync();
});
// Write the test as usual.
@@ -3228,7 +3234,7 @@ An example that skips the "Confirm your security details" page when it is shown:
```js
// Setup the handler.
await page.addLocatorHandler(page.getByText('Confirm your security details'), async () => {
- await page.getByRole('button', 'Remind me later').click();
+ await page.getByRole('button', { name: 'Remind me later' }).click();
});
// Write the test as usual.
diff --git a/docs/src/api/params.md b/docs/src/api/params.md
index b28104814e..e3b2894c3c 100644
--- a/docs/src/api/params.md
+++ b/docs/src/api/params.md
@@ -999,6 +999,7 @@ disable timeout.
If specified, traces are saved into this directory.
## browser-option-devtools
+* deprecated: Use [debugging tools](../debug.md) instead.
- `devtools` <[boolean]>
**Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the
diff --git a/docs/src/auth.md b/docs/src/auth.md
index df03d89c60..ba7af6f4cd 100644
--- a/docs/src/auth.md
+++ b/docs/src/auth.md
@@ -113,6 +113,13 @@ test('test', async ({ page }) => {
});
```
+### Authenticating in UI mode
+* langs: js
+
+UI mode will not run the `setup` project by default to improve testing speed. We recommend to authenticate by manually running the `auth.setup.ts` from time to time, whenever existing authentication expires.
+
+First [enable the `setup` project in the filters](./test-ui-mode#filtering-tests), then click the triangle button next to `auth.setup.ts` file, and then disable the `setup` project in the filters again.
+
## Moderate: one account per parallel worker
* langs: js
@@ -256,7 +263,7 @@ existing authentication state instead.
Playwright provides a way to reuse the signed-in state in the tests. That way you can log
in only once and then skip the log in step for all of the tests.
-Web apps use cookie-based or token-based authentication, where authenticated state is stored as [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) or in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage). Playwright provides [browserContext.storageState([options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state) method that can be used to retrieve storage state from authenticated contexts and then create new contexts with pre-populated state.
+Web apps use cookie-based or token-based authentication, where authenticated state is stored as [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) or in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage). Playwright provides [`method: BrowserContext.storageState`] method that can be used to retrieve storage state from authenticated contexts and then create new contexts with pre-populated state.
Cookies and local storage state can be used across different browsers. They depend on your application's authentication model: some apps might require both cookies and local storage.
@@ -457,6 +464,8 @@ test.describe(() => {
});
```
+See also about [authenticating in the UI mode](#authenticating-in-ui-mode).
+
### Testing multiple roles together
* langs: js
diff --git a/docs/src/ci-intro.md b/docs/src/ci-intro.md
index da4f31522f..f5393cfc9e 100644
--- a/docs/src/ci-intro.md
+++ b/docs/src/ci-intro.md
@@ -8,7 +8,7 @@ title: "CI GitHub Actions"
Playwright tests can be run on any CI provider. In this section we will cover running tests on GitHub using GitHub actions. If you would like to see how to configure other CI providers check out our detailed [doc on Continuous Integration](./ci.md).
-When [installing Playwright](./intro.md) using the [VS Code extension](./getting-started-vscode.md) or with `npm init playwright@latest` you are given the option to add a [GitHub Actions](https://docs.github.com/en/actions). This creates a `playwright.yml` file inside a `.github/workflows` folder containing everything you need so that your tests run on each push and pull request into the main/master branch.
+When [installing Playwright](./intro.md) using the [VS Code extension](./getting-started-vscode.md) or with `npm init playwright@latest` you are given the option to add a [GitHub Actions](https://docs.github.com/en/actions) workflow. This creates a `playwright.yml` file inside a `.github/workflows` folder containing everything you need so that your tests run on each push and pull request into the main/master branch.
#### You will learn
* langs: js
diff --git a/docs/src/dialogs.md b/docs/src/dialogs.md
index c210ffdb3c..1d8938778c 100644
--- a/docs/src/dialogs.md
+++ b/docs/src/dialogs.md
@@ -32,8 +32,11 @@ page.get_by_role("button").click()
```
```csharp
-page.Dialog += (_, dialog) => dialog.AcceptAsync();
-await page.GetByRole(AriaRole.Button).ClickAsync();
+Page.Dialog += async (_, dialog) =>
+{
+ await dialog.AcceptAsync();
+};
+await Page.GetByRole(AriaRole.Button).ClickAsync();
```
:::note
@@ -116,10 +119,10 @@ page.close(run_before_unload=True)
```
```csharp
-page.Dialog += (_, dialog) =>
+Page.Dialog += async (_, dialog) =>
{
Assert.AreEqual("beforeunload", dialog.Type);
- dialog.DismissAsync();
+ await dialog.DismissAsync();
};
-await page.CloseAsync(runBeforeUnload: true);
+await Page.CloseAsync(new() { RunBeforeUnload = true });
```
diff --git a/docs/src/junit-java.md b/docs/src/junit-java.md
new file mode 100644
index 0000000000..7ea43399dd
--- /dev/null
+++ b/docs/src/junit-java.md
@@ -0,0 +1,180 @@
+---
+id: junit
+title: "JUnit (experimental)"
+---
+
+## Introduction
+
+With a few lines of code, you can hook up Playwright to your favorite Java test runner.
+
+In [JUnit](https://junit.org/junit5/), you can use Playwright [fixtures](./junit.md#fixtures) to automatically initialize [Playwright], [Browser], [BrowserContext] or [Page]. In the example below, all three test methods use the same
+[Browser]. Each test uses its own [BrowserContext] and [Page].
+
+
+
+```java
+package org.example;
+
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.junit.UsePlaywright;
+import org.junit.jupiter.api.Test;
+
+import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@UsePlaywright
+public class TestExample {
+ @Test
+ void shouldClickButton(Page page) {
+ page.navigate("data:text/html,");
+ page.locator("button").click();
+ assertEquals("Clicked", page.evaluate("result"));
+ }
+
+ @Test
+ void shouldCheckTheBox(Page page) {
+ page.setContent("");
+ page.locator("input").check();
+ assertEquals(true, page.evaluate("window['checkbox'].checked"));
+ }
+
+ @Test
+ void shouldSearchWiki(Page page) {
+ page.navigate("https://www.wikipedia.org/");
+ page.locator("input[name=\"search\"]").click();
+ page.locator("input[name=\"search\"]").fill("playwright");
+ page.locator("input[name=\"search\"]").press("Enter");
+ assertThat(page).hasURL("https://en.wikipedia.org/wiki/Playwright");
+ }
+}
+```
+
+## Fixtures
+
+Simply add JUnit annotation `@UsePlaywright` to your test classes to enable Playwright fixtures. Test fixtures are used to establish environment for each test, giving the test everything it needs and nothing else.
+
+```java
+@UsePlaywright
+public class TestExample {
+
+ @Test
+ void basicTest(Page page) {
+ page.navigate("https://playwright.dev/");
+
+ assertThat(page).hasTitle(Pattern.compile("Playwright"));
+ }
+}
+```
+
+The `Page page` argument tells JUnit to setup the `page` fixture and provide it to your test method.
+
+Here is a list of the pre-defined fixtures:
+
+|Fixture |Type |Description |
+|:-------------|:------------------|:--------------------------------|
+|page |[Page] |Isolated page for this test run.|
+|browserContext|[BrowserContext] |Isolated context for this test run. The `page` fixture belongs to this context as well.|
+|browser |[Browser] |Browsers are shared across tests to optimize resources.|
+|playwright |[Playwright] |Playwright instance is shared between tests running on the same thread.|
+|request |[APIRequestContext]|Isolated APIRequestContext for this test run. Learn how to do [API testing](./api-testing).|
+
+## Customizing options
+
+To customize fixture options, you should implement an `OptionsFactory` and specify the class in the `@UsePlaywright()` annotation.
+
+You can easily override launch options for [`method: BrowserType.launch`], or context options for [`method: Browser.newContext`] and [`method: APIRequest.newContext`]. See the following example:
+
+```java
+import com.microsoft.playwright.junit.Options;
+import com.microsoft.playwright.junit.OptionsFactory;
+import com.microsoft.playwright.junit.UsePlaywright;
+
+@UsePlaywright(MyTest.CustomOptions.class)
+public class MyTest {
+
+ public static class CustomOptions implements OptionsFactory {
+ @Override
+ public Options getOptions() {
+ return new Options()
+ .setHeadless(false)
+ .setContextOption(new Browser.NewContextOptions()
+ .setBaseURL("https://github.com"))
+ .setApiRequestOptions(new APIRequest.NewContextOptions()
+ .setBaseURL("https://playwright.dev"));
+ }
+ }
+
+ @Test
+ public void testWithCustomOptions(Page page, APIRequestContext request) {
+ page.navigate("/");
+ assertThat(page).hasURL(Pattern.compile("github"));
+
+ APIResponse response = request.get("/");
+ assertTrue(response.text().contains("Playwright"));
+ }
+}
+```
+
+## Running Tests in Parallel
+
+By default JUnit will run all tests sequentially on a single thread. Since JUnit 5.3 you can change this behavior to run tests in parallel
+to speed up execution (see [this page](https://junit.org/junit5/docs/snapshot/user-guide/index.html#writing-tests-parallel-execution)).
+Since it is not safe to use same Playwright objects from multiple threads without extra synchronization we recommend you create Playwright
+instance per thread and use it on that thread exclusively. Here is an example how to run multiple test classes in parallel.
+
+```java
+@UsePlaywright
+class Test1 {
+ @Test
+ void shouldClickButton(Page page) {
+ page.navigate("data:text/html,");
+ page.locator("button").click();
+ assertEquals("Clicked", page.evaluate("result"));
+ }
+
+ @Test
+ void shouldCheckTheBox(Page page) {
+ page.setContent("");
+ page.locator("input").check();
+ assertEquals(true, page.evaluate("window['checkbox'].checked"));
+ }
+
+ @Test
+ void shouldSearchWiki(Page page) {
+ page.navigate("https://www.wikipedia.org/");
+ page.locator("input[name=\"search\"]").click();
+ page.locator("input[name=\"search\"]").fill("playwright");
+ page.locator("input[name=\"search\"]").press("Enter");
+ assertThat(page).hasURL("https://en.wikipedia.org/wiki/Playwright");
+ }
+}
+
+@UsePlaywright
+class Test2 {
+ @Test
+ void shouldReturnInnerHTML(Page page) {
+ page.setContent("
hello
");
+ assertEquals("hello", page.innerHTML("css=div"));
+ }
+
+ @Test
+ void shouldClickButton(Page page) {
+ Page popup = page.waitForPopup(() -> {
+ page.evaluate("window.open('about:blank');");
+ });
+ assertEquals("about:blank", popup.url());
+ }
+}
+```
+
+
+Configure JUnit to run tests in each class sequentially and run multiple classes on parallel threads (with max
+number of thread equal to 1/2 of the number of CPU cores):
+
+```bash
+junit.jupiter.execution.parallel.enabled = true
+junit.jupiter.execution.parallel.mode.default = same_thread
+junit.jupiter.execution.parallel.mode.classes.default = concurrent
+junit.jupiter.execution.parallel.config.strategy=dynamic
+junit.jupiter.execution.parallel.config.dynamic.factor=0.5
+```
diff --git a/docs/src/network.md b/docs/src/network.md
index 438e929e0c..f1fbc620b3 100644
--- a/docs/src/network.md
+++ b/docs/src/network.md
@@ -550,7 +550,7 @@ await page.RouteAsync("**/*", async route => {
});
// Continue requests as POST.
-await page.RouteAsync("**/*", async route => await route.ContinueAsync(method: "POST"));
+await Page.RouteAsync("**/*", async route => await route.ContinueAsync(new() { Method = "POST" }));
```
You can continue requests with modifications. Example above removes an HTTP header from the outgoing requests.
diff --git a/docs/src/release-notes-csharp.md b/docs/src/release-notes-csharp.md
index b562b982bd..040b66e218 100644
--- a/docs/src/release-notes-csharp.md
+++ b/docs/src/release-notes-csharp.md
@@ -4,6 +4,45 @@ title: "Release notes"
toc_max_heading_level: 2
---
+## Version 1.42
+
+### New Locator Handler
+
+New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears.
+
+```csharp
+// Setup the handler.
+await Page.AddLocatorHandlerAsync(
+ Page.GetByRole(AriaRole.Heading, new() { Name = "Hej! You are in control of your cookies." }),
+ async () =>
+ {
+ await Page.GetByRole(AriaRole.Button, new() { Name = "Accept all" }).ClickAsync();
+ });
+// Write the test as usual.
+await Page.GotoAsync("https://www.ikea.com/");
+await Page.GetByRole(AriaRole.Link, new() { Name = "Collection of blue and white" }).ClickAsync();
+await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Light and easy" })).ToBeVisibleAsync();
+```
+
+### New APIs
+
+- [`method: Page.pdf`] accepts two new options [`option: tagged`] and [`option: outline`].
+
+### Announcements
+
+* ⚠️ Ubuntu 18 is not supported anymore.
+
+### Browser Versions
+
+* Chromium 123.0.6312.4
+* Mozilla Firefox 123.0
+* WebKit 17.4
+
+This version was also tested against the following stable channels:
+
+* Google Chrome 122
+* Microsoft Edge 123
+
## Version 1.41
### New APIs
@@ -511,7 +550,7 @@ This version was also tested against the following stable channels:
### New .runsettings file support
-`Microsoft.Playwright.NUnit` and `Microsoft.Playwright.MSTest` will now consider the `.runsettings` file and passed settings via the CLI when running end-to-end tests. See in the [documentation](https://playwright.dev/dotnet/docs/test-runners) for a full list of supported settings.
+`Microsoft.Playwright.NUnit` and `Microsoft.Playwright.MSTest` will now consider the `.runsettings` file and passed settings via the CLI when running end-to-end tests. See in the [documentation](./test-runners) for a full list of supported settings.
The following does now work:
@@ -574,7 +613,7 @@ Linux support looks like this:
### New introduction docs
-We rewrote our Getting Started docs to be more end-to-end testing focused. Check them out on [playwright.dev](https://playwright.dev/dotnet/docs/intro).
+We rewrote our Getting Started docs to be more end-to-end testing focused. Check them out on [playwright.dev](./intro).
## Version 1.23
@@ -952,31 +991,31 @@ This version of Playwright was also tested against the following stable channels
### 🖱️ Mouse Wheel
-By using [`Page.Mouse.WheelAsync`](https://playwright.dev/dotnet/docs/next/api/class-mouse#mouse-wheel) you are now able to scroll vertically or horizontally.
+By using [`method: Mouse.wheel`] you are now able to scroll vertically or horizontally.
### 📜 New Headers API
Previously it was not possible to get multiple header values of a response. This is now possible and additional helper functions are available:
-- [Request.AllHeadersAsync()](https://playwright.dev/dotnet/docs/next/api/class-request#request-all-headers)
-- [Request.HeadersArrayAsync()](https://playwright.dev/dotnet/docs/next/api/class-request#request-headers-array)
-- [Request.HeaderValueAsync(name: string)](https://playwright.dev/dotnet/docs/next/api/class-request#request-header-value)
-- [Response.AllHeadersAsync()](https://playwright.dev/dotnet/docs/next/api/class-response#response-all-headers)
-- [Response.HeadersArrayAsync()](https://playwright.dev/dotnet/docs/next/api/class-response#response-headers-array)
-- [Response.HeaderValueAsync(name: string)](https://playwright.dev/dotnet/docs/next/api/class-response#response-header-value)
-- [Response.HeaderValuesAsync(name: string)](https://playwright.dev/dotnet/docs/next/api/class-response#response-header-values)
+- [`method: Request.allHeaders`]
+- [`method: Request.headersArray`]
+- [`method: Request.headerValue`]
+- [`method: Response.allHeaders`]
+- [`method: Response.headersArray`]
+- [`method: Response.headerValue`]
+- [`method: Response.headerValues`]
### 🌈 Forced-Colors emulation
-Its now possible to emulate the `forced-colors` CSS media feature by passing it in the [context options](https://playwright.dev/dotnet/docs/next/api/class-browser#browser-new-context-option-forced-colors) or calling [Page.EmulateMediaAsync()](https://playwright.dev/dotnet/docs/next/api/class-page#page-emulate-media).
+Its now possible to emulate the `forced-colors` CSS media feature by passing it in the [`method: Browser.newContext`] or calling [`method: Page.emulateMedia`].
### New APIs
-- [Page.RouteAsync()](https://playwright.dev/dotnet/docs/next/api/class-page#page-route) accepts new `times` option to specify how many times this route should be matched.
-- [Page.SetCheckedAsync(selector: string, checked: Boolean)](https://playwright.dev/dotnet/docs/next/api/class-page#page-set-checked) and [Locator.SetCheckedAsync(selector: string, checked: Boolean)](https://playwright.dev/dotnet/docs/next/api/class-locator#locator-set-checked) was introduced to set the checked state of a checkbox.
-- [Request.SizesAsync()](https://playwright.dev/dotnet/docs/next/api/class-request#request-sizes) Returns resource size information for given http request.
-- [Tracing.StartChunkAsync()](https://playwright.dev/dotnet/docs/next/api/class-tracing#tracing-start-chunk) - Start a new trace chunk.
-- [Tracing.StopChunkAsync()](https://playwright.dev/dotnet/docs/next/api/class-tracing#tracing-stop-chunk) - Stops a new trace chunk.
+- [`method: Page.route`] accepts new `times` option to specify how many times this route should be matched.
+- [`method: Page.setChecked`] and [`method: Locator.setChecked`] were introduced to set the checked state of a checkbox.
+- [`method: Request.sizes`] Returns resource size information for given http request.
+- [`method: Tracing.startChunk`] - Start a new trace chunk.
+- [`method: Tracing.stopChunk`] - Stops a new trace chunk.
### Important ⚠
* ⬆ .NET Core Apps 2.1 are **no longer** supported for our CLI tooling. As of August 31st, 2021, .NET Core 2.1 is no [longer supported](https://devblogs.microsoft.com/dotnet/net-core-2-1-will-reach-end-of-support-on-august-21-2021/) and will not receive any security updates. We've decided to move the CLI forward and require .NET Core 3.1 as a minimum.
diff --git a/docs/src/release-notes-java.md b/docs/src/release-notes-java.md
index 387dda000a..3d4df2f980 100644
--- a/docs/src/release-notes-java.md
+++ b/docs/src/release-notes-java.md
@@ -4,6 +4,126 @@ title: "Release notes"
toc_max_heading_level: 2
---
+## Version 1.42
+
+### Experimental JUnit integration
+
+Add new [`@UsePlaywright`](./junit.md) annotation to your test classes to start using Playwright
+fixtures for [Page], [BrowserContext], [Browser], [APIRequestContext] and [Playwright] in the
+test methods.
+
+```java
+package org.example;
+
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.junit.UsePlaywright;
+import org.junit.jupiter.api.Test;
+
+import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@UsePlaywright
+public class TestExample {
+ void shouldNavigateToInstallationGuide(Page page) {
+ page.navigate("https://playwright.dev/java/");
+ page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("Docs")).click();
+ assertThat(page.getByRole(AriaRole.HEADING, new Page.GetByRoleOptions().setName("Installation"))).isVisible();
+ }
+
+ @Test
+ void shouldCheckTheBox(Page page) {
+ page.setContent("");
+ page.locator("input").check();
+ assertEquals(true, page.evaluate("window['checkbox'].checked"));
+ }
+
+ @Test
+ void shouldSearchWiki(Page page) {
+ page.navigate("https://www.wikipedia.org/");
+ page.locator("input[name=\"search\"]").click();
+ page.locator("input[name=\"search\"]").fill("playwright");
+ page.locator("input[name=\"search\"]").press("Enter");
+ assertThat(page).hasURL("https://en.wikipedia.org/wiki/Playwright");
+ }
+}
+```
+
+In the example above, all three test methods use the same [Browser]. Each test
+uses its own [BrowserContext] and [Page].
+
+**Custom options**
+
+Implement your own `OptionsFactory` to initialize the fixtures with custom configuration.
+
+```java
+import com.microsoft.playwright.junit.Options;
+import com.microsoft.playwright.junit.OptionsFactory;
+import com.microsoft.playwright.junit.UsePlaywright;
+
+@UsePlaywright(MyTest.CustomOptions.class)
+public class MyTest {
+
+ public static class CustomOptions implements OptionsFactory {
+ @Override
+ public Options getOptions() {
+ return new Options()
+ .setHeadless(false)
+ .setContextOption(new Browser.NewContextOptions()
+ .setBaseURL("https://github.com"))
+ .setApiRequestOptions(new APIRequest.NewContextOptions()
+ .setBaseURL("https://playwright.dev"));
+ }
+ }
+
+ @Test
+ public void testWithCustomOptions(Page page, APIRequestContext request) {
+ page.navigate("/");
+ assertThat(page).hasURL(Pattern.compile("github"));
+
+ APIResponse response = request.get("/");
+ assertTrue(response.text().contains("Playwright"));
+ }
+}
+```
+
+Learn more about the fixtures in our [JUnit guide](./junit.md).
+
+### New Locator Handler
+
+New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears.
+
+```java
+// Setup the handler.
+page.addLocatorHandler(
+ page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Hej! You are in control of your cookies.")),
+ () - > {
+ page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Accept all")).click();
+ });
+// Write the test as usual.
+page.navigate("https://www.ikea.com/");
+page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("Collection of blue and white")).click();
+assertThat(page.getByRole(AriaRole.HEADING, new Page.GetByRoleOptions().setName("Light and easy"))).isVisible();
+```
+
+### New APIs
+
+- [`method: Page.pdf`] accepts two new options [`option: tagged`] and [`option: outline`].
+
+### Announcements
+
+* ⚠️ Ubuntu 18 is not supported anymore.
+
+### Browser Versions
+
+* Chromium 123.0.6312.4
+* Mozilla Firefox 123.0
+* WebKit 17.4
+
+This version was also tested against the following stable channels:
+
+* Google Chrome 122
+* Microsoft Edge 123
+
## Version 1.41
### New APIs
@@ -931,31 +1051,31 @@ This version of Playwright was also tested against the following stable channels
### 🖱️ Mouse Wheel
-By using [`Mouse.wheel`](https://playwright.dev/java/docs/api/class-mouse#mouse-wheel) you are now able to scroll vertically or horizontally.
+By using [`method: Mouse.wheel`] you are now able to scroll vertically or horizontally.
### 📜 New Headers API
Previously it was not possible to get multiple header values of a response. This is now possible and additional helper functions are available:
-- [Request.allHeaders()](https://playwright.dev/java/docs/api/class-request#request-all-headers)
-- [Request.headersArray()](https://playwright.dev/java/docs/api/class-request#request-headers-array)
-- [Request.headerValue(name: string)](https://playwright.dev/java/docs/api/class-request#request-header-value)
-- [Response.allHeaders()](https://playwright.dev/java/docs/api/class-response#response-all-headers)
-- [Response.headersArray()](https://playwright.dev/java/docs/api/class-response#response-headers-array)
-- [Response.headerValue(name: string)](https://playwright.dev/java/docs/api/class-response#response-header-value)
-- [Response.headerValues(name: string)](https://playwright.dev/java/docs/api/class-response#response-header-values)
+- [`method: Request.allHeaders`]
+- [`method: Request.headersArray`]
+- [`method: Request.headerValue`]
+- [`method: Response.allHeaders`]
+- [`method: Response.headersArray`]
+- [`method: Response.headerValue`]
+- [`method: Response.headerValues`]
### 🌈 Forced-Colors emulation
-Its now possible to emulate the `forced-colors` CSS media feature by passing it in the [context options](https://playwright.dev/java/docs/api/class-browser#browser-new-context-option-color-scheme) or calling [Page.emulateMedia()](https://playwright.dev/java/docs/api/class-page#page-emulate-media).
+Its now possible to emulate the `forced-colors` CSS media feature by passing it in the [`method: Browser.newContext`] or calling [`method: Page.emulateMedia`].
### New APIs
-- [Page.route()](https://playwright.dev/java/docs/api/class-page#page-route) accepts new `times` option to specify how many times this route should be matched.
-- [Page.setChecked(selector: string, checked: boolean)](https://playwright.dev/java/docs/api/class-page#page-set-checked) and [Locator.setChecked(selector: string, checked: boolean)](https://playwright.dev/java/docs/api/class-locator#locator-set-checked) was introduced to set the checked state of a checkbox.
-- [Request.sizes()](https://playwright.dev/java/docs/api/class-request#request-sizes) Returns resource size information for given http request.
-- [Tracing.startChunk()](https://playwright.dev/java/docs/api/class-tracing#tracing-start-chunk) - Start a new trace chunk.
-- [Tracing.stopChunk()](https://playwright.dev/java/docs/api/class-tracing#tracing-stop-chunk) - Stops a new trace chunk.
+- [`method: Page.route`] accepts new `times` option to specify how many times this route should be matched.
+- [`method: Page.setChecked`] and [`method: Locator.setChecked`] were introduced to set the checked state of a checkbox.
+- [`method: Request.sizes`] Returns resource size information for given http request.
+- [`method: Tracing.startChunk`] - Start a new trace chunk.
+- [`method: Tracing.stopChunk`] - Stops a new trace chunk.
### Browser Versions
diff --git a/docs/src/release-notes-js.md b/docs/src/release-notes-js.md
index c496c20ffd..423dcc9b60 100644
--- a/docs/src/release-notes-js.md
+++ b/docs/src/release-notes-js.md
@@ -6,6 +6,89 @@ toc_max_heading_level: 2
import LiteYouTube from '@site/src/components/LiteYouTube';
+## Version 1.42
+
+
+
+### New APIs
+
+- New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears:
+```js
+// Setup the handler.
+await page.addLocatorHandler(
+ page.getByRole('heading', { name: 'Hej! You are in control of your cookies.' }),
+ async () => {
+ await page.getByRole('button', { name: 'Accept all' }).click();
+ });
+// Write the test as usual.
+await page.goto('https://www.ikea.com/');
+await page.getByRole('link', { name: 'Collection of blue and white' }).click();
+await expect(page.getByRole('heading', { name: 'Light and easy' })).toBeVisible();
+```
+
+- `expect(callback).toPass()` timeout can now be configured by `expect.toPass.timeout` option [globally](./api/class-testconfig#test-config-expect) or in [project config](./api/class-testproject#test-project-expect)
+
+- [`event: ElectronApplication.console`] event is emitted when Electron main process calls console API methods.
+```js
+electronApp.on('console', async msg => {
+ const values = [];
+ for (const arg of msg.args())
+ values.push(await arg.jsonValue());
+ console.log(...values);
+});
+await electronApp.evaluate(() => console.log('hello', 5, { foo: 'bar' }));
+```
+
+- [New syntax](./test-annotations#tag-tests) for adding tags to the tests (@-tokens in the test title are still supported):
+```js
+test('test customer login', {
+ tag: ['@fast', '@login'],
+}, async ({ page }) => {
+ // ...
+});
+```
+ Use `--grep` command line option to run only tests with certain tags.
+```sh
+npx playwright test --grep @fast
+```
+
+- `--project` command line [flag](./test-cli#reference) now supports '*' wildcard:
+```sh
+npx playwright test --project='*mobile*'
+```
+
+- [New syntax](./test-annotations#annotate-tests) for test annotations:
+```js
+test('test full report', {
+ annotation: [
+ { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/23180' },
+ { type: 'docs', description: 'https://playwright.dev/docs/test-annotations#tag-tests' },
+ ],
+}, async ({ page }) => {
+ // ...
+});
+```
+
+- [`method: Page.pdf`] accepts two new options [`tagged`](./api/class-page#page-pdf-option-tagged) and [`outline`](./api/class-page#page-pdf-option-outline).
+
+### Announcements
+
+* ⚠️ Ubuntu 18 is not supported anymore.
+
+### Browser Versions
+
+* Chromium 123.0.6312.4
+* Mozilla Firefox 123.0
+* WebKit 17.4
+
+This version was also tested against the following stable channels:
+
+* Google Chrome 122
+* Microsoft Edge 123
+
## Version 1.41
### New APIs
@@ -1900,31 +1983,31 @@ This version of Playwright was also tested against the following stable channels
#### 🖱️ Mouse Wheel
-By using [`Page.mouse.wheel`](https://playwright.dev/docs/api/class-mouse#mouse-wheel) you are now able to scroll vertically or horizontally.
+By using [`method: Mouse.wheel`] you are now able to scroll vertically or horizontally.
#### 📜 New Headers API
Previously it was not possible to get multiple header values of a response. This is now possible and additional helper functions are available:
-- [Request.allHeaders()](https://playwright.dev/docs/api/class-request#request-all-headers)
-- [Request.headersArray()](https://playwright.dev/docs/api/class-request#request-headers-array)
-- [Request.headerValue(name: string)](https://playwright.dev/docs/api/class-request#request-header-value)
-- [Response.allHeaders()](https://playwright.dev/docs/api/class-response#response-all-headers)
-- [Response.headersArray()](https://playwright.dev/docs/api/class-response#response-headers-array)
-- [Response.headerValue(name: string)](https://playwright.dev/docs/api/class-response#response-header-value)
-- [Response.headerValues(name: string)](https://playwright.dev/docs/api/class-response#response-header-values)
+- [`method: Request.allHeaders`]
+- [`method: Request.headersArray`]
+- [`method: Request.headerValue`]
+- [`method: Response.allHeaders`]
+- [`method: Response.headersArray`]
+- [`method: Response.headerValue`]
+- [`method: Response.headerValues`]
#### 🌈 Forced-Colors emulation
-Its now possible to emulate the `forced-colors` CSS media feature by passing it in the [context options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-forced-colors) or calling [Page.emulateMedia()](https://playwright.dev/docs/api/class-page#page-emulate-media).
+Its now possible to emulate the `forced-colors` CSS media feature by passing it in the [`method: Browser.newContext`] or calling [`method: Page.emulateMedia`].
#### New APIs
-- [Page.route()](https://playwright.dev/docs/api/class-page#page-route) accepts new `times` option to specify how many times this route should be matched.
-- [Page.setChecked(selector: string, checked: boolean)](https://playwright.dev/docs/api/class-page#page-set-checked) and [Locator.setChecked(selector: string, checked: boolean)](https://playwright.dev/docs/api/class-locator#locator-set-checked) was introduced to set the checked state of a checkbox.
-- [Request.sizes()](https://playwright.dev/docs/api/class-request#request-sizes) Returns resource size information for given http request.
-- [BrowserContext.tracing.startChunk()](https://playwright.dev/docs/api/class-tracing#tracing-start-chunk) - Start a new trace chunk.
-- [BrowserContext.tracing.stopChunk()](https://playwright.dev/docs/api/class-tracing#tracing-stop-chunk) - Stops a new trace chunk.
+- [`method: Page.route`] accepts new `times` option to specify how many times this route should be matched.
+- [`method: Page.setChecked`] and [`method: Locator.setChecked`] were introduced to set the checked state of a checkbox.
+- [`method: Request.sizes`] Returns resource size information for given http request.
+- [`method: Tracing.startChunk`] - Start a new trace chunk.
+- [`method: Tracing.stopChunk`] - Stops a new trace chunk.
### 🎭 Playwright Test
@@ -1939,11 +2022,11 @@ test.describe.parallel('group', () => {
});
```
-By default, tests in a single file are run in order. If you have many independent tests in a single file, you can now run them in parallel with [test.describe.parallel(title, callback)](https://playwright.dev/docs/api/class-test#test-describe-parallel).
+By default, tests in a single file are run in order. If you have many independent tests in a single file, you can now run them in parallel with [test.describe.parallel(title, callback)](./api/class-test#test-describe-parallel).
#### 🛠 Add `--debug` CLI flag
-By using `npx playwright test --debug` it will enable the [Playwright Inspector](https://playwright.dev/docs/debug#playwright-inspector) for you to debug your tests.
+By using `npx playwright test --debug` it will enable the [Playwright Inspector](./debug#playwright-inspector) for you to debug your tests.
### Browser Versions
diff --git a/docs/src/release-notes-python.md b/docs/src/release-notes-python.md
index cc92290af1..72481e0284 100644
--- a/docs/src/release-notes-python.md
+++ b/docs/src/release-notes-python.md
@@ -4,6 +4,43 @@ title: "Release notes"
toc_max_heading_level: 2
---
+## Version 1.42
+
+### New Locator Handler
+
+New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears.
+
+```python
+# Setup the handler.
+page.add_locator_handler(
+ page.get_by_role("heading", name="Hej! You are in control of your cookies."),
+ lambda: page.get_by_role("button", name="Accept all").click(),
+)
+# Write the test as usual.
+page.goto("https://www.ikea.com/")
+page.get_by_role("link", name="Collection of blue and white").click()
+expect(page.get_by_role("heading", name="Light and easy")).to_be_visible()
+```
+
+### New APIs
+
+- [`method: Page.pdf`] accepts two new options [`option: tagged`] and [`option: outline`].
+
+### Announcements
+
+* ⚠️ Ubuntu 18 is not supported anymore.
+
+### Browser Versions
+
+* Chromium 123.0.6312.4
+* Mozilla Firefox 123.0
+* WebKit 17.4
+
+This version was also tested against the following stable channels:
+
+* Google Chrome 122
+* Microsoft Edge 123
+
## Version 1.41
### New APIs
@@ -1003,31 +1040,31 @@ This version of Playwright was also tested against the following stable channels
### 🖱️ Mouse Wheel
-By using [`Page.mouse.wheel`](https://playwright.dev/python/docs/api/class-mouse#mouse-wheel) you are now able to scroll vertically or horizontally.
+By using [`method: Mouse.wheel`] you are now able to scroll vertically or horizontally.
### 📜 New Headers API
Previously it was not possible to get multiple header values of a response. This is now possible and additional helper functions are available:
-- [Request.all_headers()](https://playwright.dev/python/docs/api/class-request#request-all-headers)
-- [Request.headers_array()](https://playwright.dev/python/docs/api/class-request#request-headers-array)
-- [Request.header_value(name: str)](https://playwright.dev/python/docs/api/class-request#request-header-value)
-- [Response.all_headers()](https://playwright.dev/python/docs/api/class-response#response-all-headers)
-- [Response.headers_array()](https://playwright.dev/python/docs/api/class-response#response-headers-array)
-- [Response.header_value(name: str)](https://playwright.dev/python/docs/api/class-response#response-header-value)
-- [Response.header_values(name: str)](https://playwright.dev/python/docs/api/class-response#response-header-values)
+- [`method: Request.allHeaders`]
+- [`method: Request.headersArray`]
+- [`method: Request.headerValue`]
+- [`method: Response.allHeaders`]
+- [`method: Response.headersArray`]
+- [`method: Response.headerValue`]
+- [`method: Response.headerValues`]
### 🌈 Forced-Colors emulation
-Its now possible to emulate the `forced-colors` CSS media feature by passing it in the [context options](https://playwright.dev/python/docs/api/class-browser#browser-new-context-option-forced-colors) or calling [Page.emulate_media()](https://playwright.dev/python/docs/api/class-page#page-emulate-media).
+Its now possible to emulate the `forced-colors` CSS media feature by passing it in the [`method: Browser.newContext`] or calling [`method: Page.emulateMedia`].
### New APIs
-- [Page.route()](https://playwright.dev/python/docs/api/class-page#page-route) accepts new `times` option to specify how many times this route should be matched.
-- [Page.set_checked(selector: str, checked: bool)](https://playwright.dev/python/docs/api/class-page#page-set-checked) and [Locator.set_checked(selector: str, checked: bool)](https://playwright.dev/python/docs/api/class-locator#locator-set-checked) was introduced to set the checked state of a checkbox.
-- [Request.sizes()](https://playwright.dev/python/docs/api/class-request#request-sizes) Returns resource size information for given http request.
-- [BrowserContext.tracing.start_chunk()](https://playwright.dev/python/docs/api/class-tracing#tracing-start-chunk) - Start a new trace chunk.
-- [BrowserContext.tracing.stop_chunk()](https://playwright.dev/python/docs/api/class-tracing#tracing-stop-chunk) - Stops a new trace chunk.
+- [`method: Page.route`] accepts new `times` option to specify how many times this route should be matched.
+- [`method: Page.setChecked`] and [`method: Locator.setChecked`] were introduced to set the checked state of a checkbox.
+- [`method: Request.sizes`] Returns resource size information for given http request.
+- [`method: Tracing.startChunk`] - Start a new trace chunk.
+- [`method: Tracing.stopChunk`] - Stops a new trace chunk.
### Browser Versions
diff --git a/docs/src/running-tests-java.md b/docs/src/running-tests-java.md
index d769a73e51..69943f5470 100644
--- a/docs/src/running-tests-java.md
+++ b/docs/src/running-tests-java.md
@@ -83,6 +83,8 @@ public class TestExample {
See [here](./test-runners.md) for further details on how to run tests in parallel, etc.
+See experimental [JUnit integration](./junit.md) to automatically initialize Playwright objects and more.
+
## What's Next
- [Debugging tests](./debug.md)
diff --git a/docs/src/service-workers-experimental-network-events-js.md b/docs/src/service-workers-experimental-network-events-js.md
index 96b8ead1e5..0928460b54 100644
--- a/docs/src/service-workers-experimental-network-events-js.md
+++ b/docs/src/service-workers-experimental-network-events-js.md
@@ -137,10 +137,12 @@ self.addEventListener('fetch', event => {
(async () => {
// 1. Try to first serve directly from caches
const response = await caches.match(event.request);
- if (response) return response;
+ if (response)
+ return response;
// 2. Re-write request for /foo to /bar
- if (event.request.url.endsWith('foo')) return fetch('./bar');
+ if (event.request.url.endsWith('foo'))
+ return fetch('./bar');
// 3. Prevent tracker.js from being retrieved, and returns a placeholder response
if (event.request.url.endsWith('tracker.js')) {
diff --git a/docs/src/test-api/class-testinfo.md b/docs/src/test-api/class-testinfo.md
index d766920b06..97e9c16ebf 100644
--- a/docs/src/test-api/class-testinfo.md
+++ b/docs/src/test-api/class-testinfo.md
@@ -210,6 +210,14 @@ Optional description that will be reflected in a test report.
Test function as passed to `test(title, testFunction)`.
+## property: TestInfo.tags
+* since: v1.43
+- type: <[Array]<[string]>>
+
+Tags that apply to the test. Learn more about [tags](../test-annotations.md#tag-tests).
+
+Note that any changes made to this list while the test is running will not be visible to test reporters.
+
## property: TestInfo.testId
* since: v1.32
- type: <[string]>
diff --git a/docs/src/test-api/class-testoptions.md b/docs/src/test-api/class-testoptions.md
index 946171a935..0fba476d73 100644
--- a/docs/src/test-api/class-testoptions.md
+++ b/docs/src/test-api/class-testoptions.md
@@ -546,8 +546,8 @@ export default defineConfig({
## property: TestOptions.trace
* since: v1.10
-- type: <[Object]|[TraceMode]<"off"|"on"|"retain-on-failure"|"on-first-retry">>
- - `mode` <[TraceMode]<"off"|"on"|"retain-on-failure"|"on-first-retry"|"on-all-retries">> Trace recording mode.
+- type: <[Object]|[TraceMode]<"off"|"on"|"retain-on-failure"|"on-first-retry"|"retain-on-first-failure">>
+ - `mode` <[TraceMode]<"off"|"on"|"retain-on-failure"|"on-first-retry"|"on-all-retries"|"retain-on-first-failure">> Trace recording mode.
- `attachments` ?<[boolean]> Whether to include test attachments. Defaults to true. Optional.
- `screenshots` ?<[boolean]> Whether to capture screenshots during tracing. Screenshots are used to build a timeline preview. Defaults to true. Optional.
- `snapshots` ?<[boolean]> Whether to capture DOM snapshot on every action. Defaults to true. Optional.
@@ -559,6 +559,7 @@ Whether to record trace for each test. Defaults to `'off'`.
* `'retain-on-failure'`: Record trace for each test, but remove all traces from successful test runs.
* `'on-first-retry'`: Record trace only when retrying a test for the first time.
* `'on-all-retries'`: Record traces only when retrying for all retries.
+* `'retain-on-first-failure'`: Record traces only when the test fails for the first time.
For more control, pass an object that specifies `mode` and trace features to enable.
diff --git a/docs/src/test-configuration-js.md b/docs/src/test-configuration-js.md
index ea598c8c8a..99fd492816 100644
--- a/docs/src/test-configuration-js.md
+++ b/docs/src/test-configuration-js.md
@@ -147,6 +147,6 @@ export default defineConfig({
| Option | Description |
| :- | :- |
| [`property: TestConfig.expect`] | [Web first assertions](./test-assertions.md) like `expect(locator).toHaveText()` have a separate timeout of 5 seconds by default. This is the maximum time the `expect()` should wait for the condition to be met. Learn more about [test and expect timeouts](./test-timeouts.md) and how to set them for a single test. |
-| [`method: PageAssertions.toHaveScreenshot#1`] | Configuration for the `expect(locator).toHaveScreeshot()` method. |
+| [`method: PageAssertions.toHaveScreenshot#1`] | Configuration for the `expect(locator).toHaveScreenshot()` method. |
| [`method: SnapshotAssertions.toMatchSnapshot#1`]| Configuration for the `expect(locator).toMatchSnapshot()` method.|
diff --git a/docs/src/test-reporters-js.md b/docs/src/test-reporters-js.md
index 2673739c9c..ecece58dcd 100644
--- a/docs/src/test-reporters-js.md
+++ b/docs/src/test-reporters-js.md
@@ -354,6 +354,7 @@ npx playwright test --reporter="./myreporter/my-awesome-reporter.ts"
* [Allure](https://www.npmjs.com/package/allure-playwright)
* [Argos Visual Testing](https://argos-ci.com/docs/playwright)
* [Currents](https://www.npmjs.com/package/@currents/playwright)
+* [GitHub Actions Reporter](https://www.npmjs.com/package/@estruyf/github-actions-reporter)
* [Monocart](https://github.com/cenfun/monocart-reporter)
* [ReportPortal](https://github.com/reportportal/agent-js-playwright)
* [Serenity/JS](https://serenity-js.org/handbook/test-runners/playwright-test)
diff --git a/docs/src/test-runners-java.md b/docs/src/test-runners-java.md
index f9c808b5b7..d29da2e8ba 100644
--- a/docs/src/test-runners-java.md
+++ b/docs/src/test-runners-java.md
@@ -87,6 +87,8 @@ public class TestExample {
}
```
+See experimental [JUnit integration](./junit.md) to automatically initialize Playwright objects and more.
+
### Running Tests in Parallel
By default JUnit will run all tests sequentially on a single thread. Since JUnit 5.3 you can change this behavior to run tests in parallel
diff --git a/docs/src/test-runners-python.md b/docs/src/test-runners-python.md
index 402d33987f..0e3ffde0dd 100644
--- a/docs/src/test-runners-python.md
+++ b/docs/src/test-runners-python.md
@@ -53,14 +53,14 @@ def test_my_app_is_working(fixture_name):
**Function scope**: These fixtures are created when requested in a test function and destroyed when the test ends.
-- `context`: New [browser context](https://playwright.dev/python/docs/browser-contexts) for a test.
-- `page`: New [browser page](https://playwright.dev/python/docs/pages) for a test.
+- `context`: New [browser context](./browser-contexts) for a test.
+- `page`: New [browser page](./pages) for a test.
**Session scope**: These fixtures are created when requested in a test function and destroyed when all tests end.
-- `playwright`: [Playwright](https://playwright.dev/python/docs/api/class-playwright) instance.
-- `browser_type`: [BrowserType](https://playwright.dev/python/docs/api/class-browsertype) instance of the current browser.
-- `browser`: [Browser](https://playwright.dev/python/docs/api/class-browser) instance launched by Playwright.
+- `playwright`: [Playwright](./api/class-playwright) instance.
+- `browser_type`: [BrowserType](./api/class-browsertype) instance of the current browser.
+- `browser`: [Browser](./api/class-browser) instance launched by Playwright.
- `browser_name`: Browser name as string.
- `browser_channel`: Browser channel as string.
- `is_chromium`, `is_webkit`, `is_firefox`: Booleans for the respective browser types.
diff --git a/docs/src/test-snapshots-js.md b/docs/src/test-snapshots-js.md
index a6e5b98636..d2c7606adf 100644
--- a/docs/src/test-snapshots-js.md
+++ b/docs/src/test-snapshots-js.md
@@ -44,6 +44,8 @@ The snapshot name `example-test-1-chromium-darwin.png` consists of a few parts:
- `chromium-darwin` - the browser name and the platform. Screenshots differ between browsers and platforms due to different rendering, fonts and more, so you will need different snapshots for them. If you use multiple projects in your [configuration file](./test-configuration.md), project name will be used instead of `chromium`.
+The snapshot name and path can be configured with [`snapshotPathTemplate`](./api/class-testproject#test-project-snapshot-path-template) in the playwright config.
+
## Updating screenshots
Sometimes you need to update the reference screenshot, for example when the page has changed. Do this with the `--update-snapshots` flag.
diff --git a/docs/src/test-typescript-js.md b/docs/src/test-typescript-js.md
index 465f2c2d93..12a1173ea4 100644
--- a/docs/src/test-typescript-js.md
+++ b/docs/src/test-typescript-js.md
@@ -5,7 +5,26 @@ title: "TypeScript"
## Introduction
-Playwright supports TypeScript out of the box. You just write tests in TypeScript, and Playwright will read them, transform to JavaScript and run.
+Playwright supports TypeScript out of the box. You just write tests in TypeScript, and Playwright will read them, transform to JavaScript and run. Note that Playwright does not check the types and will run tests even if there are non-critical TypeScript compilation errors.
+
+We recommend you run TypeScript compiler alongside Playwright. For example on GitHub actions:
+
+```yaml
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ ...
+ - name: Run type checks
+ run: npx tsc -p tsconfig.json --noEmit
+ - name: Run Playwright tests
+ run: npx playwright test
+```
+
+For local development, you can run `tsc` in [watch](https://www.typescriptlang.org/docs/handbook/configuring-watch.html) mode like this:
+```sh
+npx tsc -p tsconfig.json --noEmit -w
+```
## tsconfig.json
diff --git a/package-lock.json b/package-lock.json
index 15febb2051..d225f8eafc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "playwright-internal",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "playwright-internal",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"license": "Apache-2.0",
"workspaces": [
"packages/*"
@@ -8360,10 +8360,10 @@
}
},
"packages/playwright": {
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.42.0-next"
+ "playwright-core": "1.43.0-next"
},
"bin": {
"playwright": "cli.js"
@@ -8377,11 +8377,11 @@
},
"packages/playwright-browser-chromium": {
"name": "@playwright/browser-chromium",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.42.0-next"
+ "playwright-core": "1.43.0-next"
},
"engines": {
"node": ">=16"
@@ -8389,11 +8389,11 @@
},
"packages/playwright-browser-firefox": {
"name": "@playwright/browser-firefox",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.42.0-next"
+ "playwright-core": "1.43.0-next"
},
"engines": {
"node": ">=16"
@@ -8401,22 +8401,22 @@
},
"packages/playwright-browser-webkit": {
"name": "@playwright/browser-webkit",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.42.0-next"
+ "playwright-core": "1.43.0-next"
},
"engines": {
"node": ">=16"
}
},
"packages/playwright-chromium": {
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.42.0-next"
+ "playwright-core": "1.43.0-next"
},
"bin": {
"playwright": "cli.js"
@@ -8426,7 +8426,7 @@
}
},
"packages/playwright-core": {
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
@@ -8614,11 +8614,11 @@
},
"packages/playwright-ct-core": {
"name": "@playwright/experimental-ct-core",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"license": "Apache-2.0",
"dependencies": {
- "playwright": "1.42.0-next",
- "playwright-core": "1.42.0-next",
+ "playwright": "1.43.0-next",
+ "playwright-core": "1.43.0-next",
"vite": "^5.0.12"
},
"bin": {
@@ -8630,15 +8630,14 @@
},
"packages/playwright-ct-react": {
"name": "@playwright/experimental-ct-react",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"license": "Apache-2.0",
"dependencies": {
- "@playwright/experimental-ct-core": "1.42.0-next",
+ "@playwright/experimental-ct-core": "1.43.0-next",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {
- "playwright": "cli.js",
- "pw-react": "cli.js"
+ "playwright": "cli.js"
},
"engines": {
"node": ">=16"
@@ -8646,15 +8645,14 @@
},
"packages/playwright-ct-react17": {
"name": "@playwright/experimental-ct-react17",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"license": "Apache-2.0",
"dependencies": {
- "@playwright/experimental-ct-core": "1.42.0-next",
+ "@playwright/experimental-ct-core": "1.43.0-next",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {
- "playwright": "cli.js",
- "pw-react17": "cli.js"
+ "playwright": "cli.js"
},
"engines": {
"node": ">=16"
@@ -8662,15 +8660,14 @@
},
"packages/playwright-ct-solid": {
"name": "@playwright/experimental-ct-solid",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"license": "Apache-2.0",
"dependencies": {
- "@playwright/experimental-ct-core": "1.42.0-next",
+ "@playwright/experimental-ct-core": "1.43.0-next",
"vite-plugin-solid": "^2.7.0"
},
"bin": {
- "playwright": "cli.js",
- "pw-solid": "cli.js"
+ "playwright": "cli.js"
},
"devDependencies": {
"solid-js": "^1.7.0"
@@ -8681,15 +8678,14 @@
},
"packages/playwright-ct-svelte": {
"name": "@playwright/experimental-ct-svelte",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"license": "Apache-2.0",
"dependencies": {
- "@playwright/experimental-ct-core": "1.42.0-next",
+ "@playwright/experimental-ct-core": "1.43.0-next",
"@sveltejs/vite-plugin-svelte": "^3.0.1"
},
"bin": {
- "playwright": "cli.js",
- "pw-svelte": "cli.js"
+ "playwright": "cli.js"
},
"devDependencies": {
"svelte": "^4.2.8"
@@ -8700,15 +8696,14 @@
},
"packages/playwright-ct-vue": {
"name": "@playwright/experimental-ct-vue",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"license": "Apache-2.0",
"dependencies": {
- "@playwright/experimental-ct-core": "1.42.0-next",
+ "@playwright/experimental-ct-core": "1.43.0-next",
"@vitejs/plugin-vue": "^4.2.1"
},
"bin": {
- "playwright": "cli.js",
- "pw-vue": "cli.js"
+ "playwright": "cli.js"
},
"engines": {
"node": ">=16"
@@ -8716,15 +8711,14 @@
},
"packages/playwright-ct-vue2": {
"name": "@playwright/experimental-ct-vue2",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"license": "Apache-2.0",
"dependencies": {
- "@playwright/experimental-ct-core": "1.42.0-next",
+ "@playwright/experimental-ct-core": "1.43.0-next",
"@vitejs/plugin-vue2": "^2.2.0"
},
"bin": {
- "playwright": "cli.js",
- "pw-vue2": "cli.js"
+ "playwright": "cli.js"
},
"devDependencies": {
"vue": "^2.7.14"
@@ -8769,11 +8763,11 @@
}
},
"packages/playwright-firefox": {
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.42.0-next"
+ "playwright-core": "1.43.0-next"
},
"bin": {
"playwright": "cli.js"
@@ -8784,10 +8778,10 @@
},
"packages/playwright-test": {
"name": "@playwright/test",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"license": "Apache-2.0",
"dependencies": {
- "playwright": "1.42.0-next"
+ "playwright": "1.43.0-next"
},
"bin": {
"playwright": "cli.js"
@@ -8797,11 +8791,11 @@
}
},
"packages/playwright-webkit": {
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.42.0-next"
+ "playwright-core": "1.43.0-next"
},
"bin": {
"playwright": "cli.js"
diff --git a/package.json b/package.json
index 89550ec4e4..253206f667 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "playwright-internal",
"private": true,
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"description": "A high-level API to automate web browsers",
"repository": {
"type": "git",
@@ -16,9 +16,9 @@
},
"license": "Apache-2.0",
"scripts": {
- "ctest": "playwright test --config=tests/library/playwright.config.ts --project=chromium",
- "ftest": "playwright test --config=tests/library/playwright.config.ts --project=firefox",
- "wtest": "playwright test --config=tests/library/playwright.config.ts --project=webkit",
+ "ctest": "playwright test --config=tests/library/playwright.config.ts --project=chromium-*",
+ "ftest": "playwright test --config=tests/library/playwright.config.ts --project=firefox-*",
+ "wtest": "playwright test --config=tests/library/playwright.config.ts --project=webkit-*",
"atest": "playwright test --config=tests/android/playwright.config.ts",
"etest": "playwright test --config=tests/electron/playwright.config.ts",
"webview2test": "playwright test --config=tests/webview2/playwright.config.ts",
@@ -29,7 +29,7 @@
"ttest": "node ./tests/playwright-test/stable-test-runner/node_modules/@playwright/test/cli test --config=tests/playwright-test/playwright.config.ts",
"ct": "playwright test tests/components/test-all.spec.js --reporter=list",
"test": "playwright test --config=tests/library/playwright.config.ts",
- "eslint": "eslint --cache --report-unused-disable-directives --ext ts,tsx .",
+ "eslint": "eslint --cache --report-unused-disable-directives --ext ts,tsx,js,jsx,mjs .",
"tsc": "tsc -p .",
"build-installer": "babel -s --extensions \".ts\" --out-dir packages/playwright-core/lib/utils/ packages/playwright-core/src/utils",
"doc": "node utils/doclint/cli.js",
diff --git a/packages/.eslintrc-with-ts-config.js b/packages/.eslintrc-with-ts-config.js
deleted file mode 100644
index dea9d4ef41..0000000000
--- a/packages/.eslintrc-with-ts-config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-module.exports = {
- extends: ".eslintrc.js",
- rules: {
- "@typescript-eslint/no-base-to-string": "error",
- },
- parserOptions: {
- project: "./tsconfig.json"
- },
-};
diff --git a/packages/html-reporter/src/filter.ts b/packages/html-reporter/src/filter.ts
index 6f7ef14525..97cf675936 100644
--- a/packages/html-reporter/src/filter.ts
+++ b/packages/html-reporter/src/filter.ts
@@ -14,7 +14,6 @@
limitations under the License.
*/
-import { testCaseLabels } from './labelUtils';
import type { TestCaseSummary } from './types';
export class Filter {
@@ -108,13 +107,13 @@ export class Filter {
if (test.outcome === 'skipped')
status = 'skipped';
const searchValues: SearchValues = {
- text: (status + ' ' + test.projectName + ' ' + (test.botName || '') + ' ' + test.location.file + ' ' + test.path.join(' ') + ' ' + test.title).toLowerCase(),
+ text: (status + ' ' + test.projectName + ' ' + test.tags.join(' ') + ' ' + test.location.file + ' ' + test.path.join(' ') + ' ' + test.title).toLowerCase(),
project: test.projectName.toLowerCase(),
status: status as any,
file: test.location.file,
line: String(test.location.line),
column: String(test.location.column),
- labels: testCaseLabels(test).map(label => label.toLowerCase()),
+ labels: test.tags.map(tag => tag.toLowerCase()),
};
(test as any).searchValues = searchValues;
}
diff --git a/packages/html-reporter/src/labelUtils.tsx b/packages/html-reporter/src/labelUtils.tsx
index 57d9c43c9a..014ec77d59 100644
--- a/packages/html-reporter/src/labelUtils.tsx
+++ b/packages/html-reporter/src/labelUtils.tsx
@@ -14,22 +14,6 @@
* limitations under the License.
*/
-import type { TestCaseSummary } from './types';
-
-const labelsSymbol = Symbol('labels');
-
-// Note: all labels start with "@"
-export function testCaseLabels(test: TestCaseSummary): string[] {
- if (!(test as any)[labelsSymbol]) {
- const labels: string[] = [];
- if (test.botName)
- labels.push('@' + test.botName);
- labels.push(...test.tags);
- (test as any)[labelsSymbol] = labels;
- }
- return (test as any)[labelsSymbol];
-}
-
// hash string to integer in range [0, 6] for color index, to get same color for same tag
export function hashStringToInt(str: string) {
let hash = 0;
diff --git a/packages/html-reporter/src/testCaseView.tsx b/packages/html-reporter/src/testCaseView.tsx
index 4b79908edf..eecbdac9f5 100644
--- a/packages/html-reporter/src/testCaseView.tsx
+++ b/packages/html-reporter/src/testCaseView.tsx
@@ -23,7 +23,7 @@ import { ProjectLink } from './links';
import { statusIcon } from './statusIcon';
import './testCaseView.css';
import { TestResultView } from './testResultView';
-import { hashStringToInt, testCaseLabels } from './labelUtils';
+import { hashStringToInt } from './labelUtils';
import { msToString } from './uiUtils';
export const TestCaseView: React.FC<{
@@ -37,7 +37,7 @@ export const TestCaseView: React.FC<{
const labels = React.useMemo(() => {
if (!test)
return undefined;
- return testCaseLabels(test);
+ return test.tags;
}, [test]);
return
{msToString(test.duration)}
diff --git a/packages/playwright-browser-chromium/package.json b/packages/playwright-browser-chromium/package.json
index 4c16717df4..e4ce3d992f 100644
--- a/packages/playwright-browser-chromium/package.json
+++ b/packages/playwright-browser-chromium/package.json
@@ -1,6 +1,6 @@
{
"name": "@playwright/browser-chromium",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"description": "Playwright package that automatically installs Chromium",
"repository": {
"type": "git",
@@ -27,6 +27,6 @@
"install": "node install.js"
},
"dependencies": {
- "playwright-core": "1.42.0-next"
+ "playwright-core": "1.43.0-next"
}
}
diff --git a/packages/playwright-browser-firefox/package.json b/packages/playwright-browser-firefox/package.json
index 1c100b2fce..866216d956 100644
--- a/packages/playwright-browser-firefox/package.json
+++ b/packages/playwright-browser-firefox/package.json
@@ -1,6 +1,6 @@
{
"name": "@playwright/browser-firefox",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"description": "Playwright package that automatically installs Firefox",
"repository": {
"type": "git",
@@ -27,6 +27,6 @@
"install": "node install.js"
},
"dependencies": {
- "playwright-core": "1.42.0-next"
+ "playwright-core": "1.43.0-next"
}
}
diff --git a/packages/playwright-browser-webkit/package.json b/packages/playwright-browser-webkit/package.json
index 270c601bd9..4e64a3aa48 100644
--- a/packages/playwright-browser-webkit/package.json
+++ b/packages/playwright-browser-webkit/package.json
@@ -1,6 +1,6 @@
{
"name": "@playwright/browser-webkit",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"description": "Playwright package that automatically installs WebKit",
"repository": {
"type": "git",
@@ -27,6 +27,6 @@
"install": "node install.js"
},
"dependencies": {
- "playwright-core": "1.42.0-next"
+ "playwright-core": "1.43.0-next"
}
}
diff --git a/packages/playwright-chromium/cli.js b/packages/playwright-chromium/cli.js
index 86adb86a84..f46d8b409e 100755
--- a/packages/playwright-chromium/cli.js
+++ b/packages/playwright-chromium/cli.js
@@ -15,5 +15,5 @@
* limitations under the License.
*/
-const { program } = require('playwright-core/lib/program');
+const { program } = require('playwright-core/lib/cli/program');
program.parse(process.argv);
diff --git a/packages/playwright-chromium/package.json b/packages/playwright-chromium/package.json
index 5f7431d84a..c39bc5031b 100644
--- a/packages/playwright-chromium/package.json
+++ b/packages/playwright-chromium/package.json
@@ -1,6 +1,6 @@
{
"name": "playwright-chromium",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"description": "A high-level API to automate Chromium",
"repository": {
"type": "git",
@@ -30,6 +30,6 @@
"install": "node install.js"
},
"dependencies": {
- "playwright-core": "1.42.0-next"
+ "playwright-core": "1.43.0-next"
}
}
diff --git a/packages/playwright-core/.eslintrc.js b/packages/playwright-core/.eslintrc.js
index ae8768db65..84888f1ae3 100644
--- a/packages/playwright-core/.eslintrc.js
+++ b/packages/playwright-core/.eslintrc.js
@@ -1,3 +1,3 @@
module.exports = {
- extends: "../.eslintrc-with-ts-config.js",
+ extends: "../../.eslintrc-with-ts-config.js",
};
diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json
index bf862ae74c..beaf0e4901 100644
--- a/packages/playwright-core/browsers.json
+++ b/packages/playwright-core/browsers.json
@@ -3,43 +3,37 @@
"browsers": [
{
"name": "chromium",
- "revision": "1105",
+ "revision": "1110",
"installByDefault": true,
- "browserVersion": "123.0.6312.4"
- },
- {
- "name": "chromium-with-symbols",
- "revision": "1105",
- "installByDefault": false,
- "browserVersion": "123.0.6312.4"
+ "browserVersion": "124.0.6367.8"
},
{
"name": "chromium-tip-of-tree",
- "revision": "1195",
+ "revision": "1204",
"installByDefault": false,
- "browserVersion": "123.0.6312.0"
+ "browserVersion": "125.0.6370.0"
},
{
"name": "firefox",
- "revision": "1440",
+ "revision": "1445",
"installByDefault": true,
"browserVersion": "123.0"
},
{
"name": "firefox-asan",
- "revision": "1440",
+ "revision": "1445",
"installByDefault": false,
"browserVersion": "123.0"
},
{
"name": "firefox-beta",
- "revision": "1440",
+ "revision": "1444",
"installByDefault": false,
"browserVersion": "124.0b3"
},
{
"name": "webkit",
- "revision": "1983",
+ "revision": "1990",
"installByDefault": true,
"revisionOverrides": {
"mac10.14": "1446",
diff --git a/packages/playwright-core/package.json b/packages/playwright-core/package.json
index c846a750e7..769dd27737 100644
--- a/packages/playwright-core/package.json
+++ b/packages/playwright-core/package.json
@@ -1,6 +1,6 @@
{
"name": "playwright-core",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"description": "A high-level API to automate web browsers",
"repository": {
"type": "git",
diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts
index c38683a832..ad943c049e 100644
--- a/packages/playwright-core/src/cli/program.ts
+++ b/packages/playwright-core/src/cli/program.ts
@@ -23,8 +23,8 @@ import type { Command } from '../utilsBundle';
import { program } from '../utilsBundle';
export { program } from '../utilsBundle';
import { runDriver, runServer, printApiJson, launchBrowserServer } from './driver';
-import type { OpenTraceViewerOptions } from '../server/trace/viewer/traceViewer';
-import { openTraceInBrowser, openTraceViewerApp } from '../server/trace/viewer/traceViewer';
+import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer';
+import type { TraceViewerServerOptions } from '../server/trace/viewer/traceViewer';
import * as playwright from '../..';
import type { BrowserContext } from '../client/browserContext';
import type { Browser } from '../client/browser';
@@ -305,19 +305,16 @@ program
if (options.browser === 'wk')
options.browser = 'webkit';
- const openOptions: OpenTraceViewerOptions = {
- headless: false,
+ const openOptions: TraceViewerServerOptions = {
host: options.host,
port: +options.port,
isServer: !!options.stdin,
};
- if (options.port !== undefined || options.host !== undefined) {
- openTraceInBrowser(traces, openOptions).catch(logErrorAndExit);
- } else {
- openTraceViewerApp(traces, options.browser, openOptions).then(page => {
- page.on('close', () => gracefullyProcessExitDoNotHang(0));
- }).catch(logErrorAndExit);
- }
+
+ if (options.port !== undefined || options.host !== undefined)
+ runTraceInBrowser(traces, openOptions).catch(logErrorAndExit);
+ else
+ runTraceViewerApp(traces, options.browser, openOptions, true).catch(logErrorAndExit);
}).addHelpText('afterAll', `
Examples:
diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts
index 6f32385ee8..37d6b6744f 100644
--- a/packages/playwright-core/src/client/browserContext.ts
+++ b/packages/playwright-core/src/client/browserContext.ts
@@ -269,6 +269,10 @@ export class BrowserContext extends ChannelOwner
await this._channel.clearCookies();
}
+ async removeCookies(filter: network.RemoveNetworkCookieParam): Promise {
+ await this._channel.removeCookies({ filter });
+ }
+
async grantPermissions(permissions: string[], options?: { origin?: string }): Promise {
await this._channel.grantPermissions({ permissions, ...options });
}
@@ -526,7 +530,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
function toAcceptDownloadsProtocol(acceptDownloads?: boolean) {
if (acceptDownloads === undefined)
return undefined;
- if (acceptDownloads === true)
+ if (acceptDownloads)
return 'accept';
return 'deny';
}
diff --git a/packages/playwright-core/src/client/jsHandle.ts b/packages/playwright-core/src/client/jsHandle.ts
index ad03609e40..bb85894870 100644
--- a/packages/playwright-core/src/client/jsHandle.ts
+++ b/packages/playwright-core/src/client/jsHandle.ts
@@ -19,6 +19,7 @@ import { ChannelOwner } from './channelOwner';
import { parseSerializedValue, serializeValue } from '../protocol/serializers';
import type * as api from '../../types/types';
import type * as structs from '../../types/structs';
+import { isTargetClosedError } from './errors';
export class JSHandle extends ChannelOwner implements api.JSHandle {
private _preview: string;
@@ -68,7 +69,13 @@ export class JSHandle extends ChannelOwner im
}
async dispose() {
- return await this._channel.dispose();
+ try {
+ await this._channel.dispose();
+ } catch (e) {
+ if (isTargetClosedError(e))
+ return;
+ throw e;
+ }
}
async _objectCount() {
diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts
index 36ded11194..a24286e71e 100644
--- a/packages/playwright-core/src/client/locator.ts
+++ b/packages/playwright-core/src/client/locator.ts
@@ -192,6 +192,10 @@ export class Locator implements api.Locator {
return await this._frame.$$(this._selector);
}
+ enterFrame() {
+ return new FrameLocator(this._frame, this._selector);
+ }
+
first(): Locator {
return new Locator(this._frame, this._selector + ' >> nth=0');
}
@@ -404,6 +408,10 @@ export class FrameLocator implements api.FrameLocator {
return this.locator(getByRoleSelector(role, options));
}
+ exitFrame() {
+ return new Locator(this._frame, this._frameSelector);
+ }
+
frameLocator(selector: string): FrameLocator {
return new FrameLocator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selector);
}
diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts
index 6f35fdbc70..f58048655e 100644
--- a/packages/playwright-core/src/client/network.ts
+++ b/packages/playwright-core/src/client/network.ts
@@ -58,6 +58,12 @@ export type SetNetworkCookieParam = {
sameSite?: 'Strict' | 'Lax' | 'None'
};
+export type RemoveNetworkCookieParam = {
+ name?: string,
+ domain?: string,
+ path?: string,
+};
+
type SerializedFallbackOverrides = {
url?: string;
method?: string;
@@ -135,7 +141,7 @@ export class Request extends ChannelOwner implements ap
return null;
const contentType = this.headers()['content-type'];
- if (contentType === 'application/x-www-form-urlencoded') {
+ if (contentType?.includes('application/x-www-form-urlencoded')) {
const entries: Record = {};
const parsed = new URLSearchParams(postData);
for (const [k, v] of parsed.entries())
diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts
index f33a3891b6..4a8f7fb3d1 100644
--- a/packages/playwright-core/src/protocol/validator.ts
+++ b/packages/playwright-core/src/protocol/validator.ts
@@ -828,6 +828,14 @@ scheme.BrowserContextAddInitScriptParams = tObject({
scheme.BrowserContextAddInitScriptResult = tOptional(tObject({}));
scheme.BrowserContextClearCookiesParams = tOptional(tObject({}));
scheme.BrowserContextClearCookiesResult = tOptional(tObject({}));
+scheme.BrowserContextRemoveCookiesParams = tObject({
+ filter: tObject({
+ name: tOptional(tString),
+ domain: tOptional(tString),
+ path: tOptional(tString),
+ }),
+});
+scheme.BrowserContextRemoveCookiesResult = tOptional(tObject({}));
scheme.BrowserContextClearPermissionsParams = tOptional(tObject({}));
scheme.BrowserContextClearPermissionsResult = tOptional(tObject({}));
scheme.BrowserContextCloseParams = tObject({
diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts
index 0bc14f45e1..2a5dfe765f 100644
--- a/packages/playwright-core/src/server/browserContext.ts
+++ b/packages/playwright-core/src/server/browserContext.ts
@@ -276,6 +276,22 @@ export abstract class BrowserContext extends SdkObject {
return await this.doGetCookies(urls as string[]);
}
+ async removeCookies(filter: {name?: string, domain?: string, path?: string}): Promise {
+ if (!filter.name && !filter.domain && !filter.path)
+ throw new Error(`Either name, domain or path are required`);
+
+ const currentCookies = await this.cookies();
+
+ const cookiesToKeep = currentCookies.filter(cookie => {
+ return !((!filter.name || filter.name === cookie.name) &&
+ (!filter.domain || filter.domain === cookie.domain) &&
+ (!filter.path || filter.path === cookie.path));
+ });
+
+ await this.clearCookies();
+ await this.addCookies(cookiesToKeep);
+ }
+
setHTTPCredentials(httpCredentials?: types.Credentials): Promise {
return this.doSetHTTPCredentials(httpCredentials);
}
@@ -468,14 +484,34 @@ export abstract class BrowserContext extends SdkObject {
cookies: await this.cookies(),
origins: []
};
- if (this._origins.size) {
+ const originsToSave = new Set(this._origins);
+
+ // First try collecting storage stage from existing pages.
+ for (const page of this.pages()) {
+ const origin = page.mainFrame().origin();
+ if (!origin || !originsToSave.has(origin))
+ continue;
+ try {
+ const storage = await page.mainFrame().nonStallingEvaluateInExistingContext(`({
+ localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })),
+ })`, false, 'utility');
+ if (storage.localStorage.length)
+ result.origins.push({ origin, localStorage: storage.localStorage } as channels.OriginStorage);
+ originsToSave.delete(origin);
+ } catch {
+ // When failed on the live page, we'll retry on the blank page below.
+ }
+ }
+
+ // If there are still origins to save, create a blank page to iterate over origins.
+ if (originsToSave.size) {
const internalMetadata = serverSideCallMetadata();
const page = await this.newPage(internalMetadata);
await page._setServerRequestInterceptor(handler => {
handler.fulfill({ body: '', requestUrl: handler.request().url() }).catch(() => {});
return true;
});
- for (const origin of this._origins) {
+ for (const origin of originsToSave) {
const originStorage: channels.OriginStorage = { origin, localStorage: [] };
const frame = page.mainFrame();
await frame.goto(internalMetadata, origin);
diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts
index 0217005427..9af7e6ba9d 100644
--- a/packages/playwright-core/src/server/chromium/crBrowser.ts
+++ b/packages/playwright-core/src/server/chromium/crBrowser.ts
@@ -562,7 +562,7 @@ export class CRBrowserContext extends BrowserContext {
override async clearCache(): Promise {
for (const page of this._crPages())
- await page._mainFrameSession._networkManager.clearCache();
+ await page._networkManager.clearCache();
}
async cancelDownload(guid: string) {
@@ -594,7 +594,8 @@ export class CRBrowserContext extends BrowserContext {
targetId = (page._delegate as CRPage)._targetId;
} else if (page instanceof Frame) {
const session = (page._page._delegate as CRPage)._sessions.get(page._id);
- if (!session) throw new Error(`This frame does not have a separate CDP session, it is a part of the parent frame's session`);
+ if (!session)
+ throw new Error(`This frame does not have a separate CDP session, it is a part of the parent frame's session`);
targetId = session._targetId;
} else {
throw new Error('page: expected Page or Frame');
diff --git a/packages/playwright-core/src/server/chromium/crNetworkManager.ts b/packages/playwright-core/src/server/chromium/crNetworkManager.ts
index f3952b82fd..afe753046a 100644
--- a/packages/playwright-core/src/server/chromium/crNetworkManager.ts
+++ b/packages/playwright-core/src/server/chromium/crNetworkManager.ts
@@ -26,70 +26,88 @@ import type * as contexts from '../browserContext';
import type * as frames from '../frames';
import type * as types from '../types';
import type { CRPage } from './crPage';
-import { assert, headersObjectToArray } from '../../utils';
+import { assert, headersArrayToObject, headersObjectToArray } from '../../utils';
import type { CRServiceWorker } from './crServiceWorker';
-import { isProtocolError } from '../protocolError';
+import { isProtocolError, isSessionClosedError } from '../protocolError';
type SessionInfo = {
session: CRSession;
+ isMain?: boolean;
workerFrame?: frames.Frame;
+ eventListeners: RegisteredListener[];
};
export class CRNetworkManager {
- private _session: CRSession;
private _page: Page | null;
private _serviceWorker: CRServiceWorker | null;
- private _parentManager: CRNetworkManager | null;
private _requestIdToRequest = new Map();
private _requestIdToRequestWillBeSentEvent = new Map();
private _credentials: {origin?: string, username: string, password: string} | null = null;
private _attemptedAuthentications = new Set();
private _userRequestInterceptionEnabled = false;
private _protocolRequestInterceptionEnabled = false;
+ private _offline = false;
+ private _extraHTTPHeaders: types.HeadersArray = [];
private _requestIdToRequestPausedEvent = new Map();
- private _eventListeners: RegisteredListener[];
private _responseExtraInfoTracker = new ResponseExtraInfoTracker();
+ private _sessions = new Map();
- constructor(session: CRSession, page: Page | null, serviceWorker: CRServiceWorker | null, parentManager: CRNetworkManager | null) {
- this._session = session;
+ constructor(page: Page | null, serviceWorker: CRServiceWorker | null) {
this._page = page;
this._serviceWorker = serviceWorker;
- this._parentManager = parentManager;
- this._eventListeners = this.instrumentNetworkEvents({ session });
}
- instrumentNetworkEvents(sessionInfo: SessionInfo): RegisteredListener[] {
- const listeners = [
- eventsHelper.addEventListener(sessionInfo.session, 'Fetch.requestPaused', this._onRequestPaused.bind(this, sessionInfo)),
- eventsHelper.addEventListener(sessionInfo.session, 'Fetch.authRequired', this._onAuthRequired.bind(this)),
- eventsHelper.addEventListener(sessionInfo.session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this, sessionInfo)),
- eventsHelper.addEventListener(sessionInfo.session, 'Network.requestWillBeSentExtraInfo', this._onRequestWillBeSentExtraInfo.bind(this)),
- eventsHelper.addEventListener(sessionInfo.session, 'Network.requestServedFromCache', this._onRequestServedFromCache.bind(this)),
- eventsHelper.addEventListener(sessionInfo.session, 'Network.responseReceived', this._onResponseReceived.bind(this, sessionInfo)),
- eventsHelper.addEventListener(sessionInfo.session, 'Network.responseReceivedExtraInfo', this._onResponseReceivedExtraInfo.bind(this)),
- eventsHelper.addEventListener(sessionInfo.session, 'Network.loadingFinished', this._onLoadingFinished.bind(this)),
- eventsHelper.addEventListener(sessionInfo.session, 'Network.loadingFailed', this._onLoadingFailed.bind(this, sessionInfo)),
+ async addSession(session: CRSession, workerFrame?: frames.Frame, isMain?: boolean) {
+ const sessionInfo: SessionInfo = { session, isMain, workerFrame, eventListeners: [] };
+ sessionInfo.eventListeners = [
+ eventsHelper.addEventListener(session, 'Fetch.requestPaused', this._onRequestPaused.bind(this, sessionInfo)),
+ eventsHelper.addEventListener(session, 'Fetch.authRequired', this._onAuthRequired.bind(this, sessionInfo)),
+ eventsHelper.addEventListener(session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this, sessionInfo)),
+ eventsHelper.addEventListener(session, 'Network.requestWillBeSentExtraInfo', this._onRequestWillBeSentExtraInfo.bind(this)),
+ eventsHelper.addEventListener(session, 'Network.requestServedFromCache', this._onRequestServedFromCache.bind(this)),
+ eventsHelper.addEventListener(session, 'Network.responseReceived', this._onResponseReceived.bind(this, sessionInfo)),
+ eventsHelper.addEventListener(session, 'Network.responseReceivedExtraInfo', this._onResponseReceivedExtraInfo.bind(this)),
+ eventsHelper.addEventListener(session, 'Network.loadingFinished', this._onLoadingFinished.bind(this, sessionInfo)),
+ eventsHelper.addEventListener(session, 'Network.loadingFailed', this._onLoadingFailed.bind(this, sessionInfo)),
];
if (this._page) {
- listeners.push(...[
- eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketCreated', e => this._page!._frameManager.onWebSocketCreated(e.requestId, e.url)),
- eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketWillSendHandshakeRequest', e => this._page!._frameManager.onWebSocketRequest(e.requestId)),
- eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketHandshakeResponseReceived', e => this._page!._frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)),
- eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page!._frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)),
- eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page!._frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)),
- eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketClosed', e => this._page!._frameManager.webSocketClosed(e.requestId)),
- eventsHelper.addEventListener(sessionInfo.session, 'Network.webSocketFrameError', e => this._page!._frameManager.webSocketError(e.requestId, e.errorMessage)),
+ sessionInfo.eventListeners.push(...[
+ eventsHelper.addEventListener(session, 'Network.webSocketCreated', e => this._page!._frameManager.onWebSocketCreated(e.requestId, e.url)),
+ eventsHelper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', e => this._page!._frameManager.onWebSocketRequest(e.requestId)),
+ eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page!._frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)),
+ eventsHelper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page!._frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)),
+ eventsHelper.addEventListener(session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page!._frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)),
+ eventsHelper.addEventListener(session, 'Network.webSocketClosed', e => this._page!._frameManager.webSocketClosed(e.requestId)),
+ eventsHelper.addEventListener(session, 'Network.webSocketFrameError', e => this._page!._frameManager.webSocketError(e.requestId, e.errorMessage)),
]);
}
- return listeners;
+ this._sessions.set(session, sessionInfo);
+ await Promise.all([
+ session.send('Network.enable'),
+ this._updateProtocolRequestInterceptionForSession(sessionInfo, true /* initial */),
+ this._setOfflineForSession(sessionInfo, true /* initial */),
+ this._setExtraHTTPHeadersForSession(sessionInfo, true /* initial */),
+ ]);
}
- async initialize() {
- await this._session.send('Network.enable');
+ removeSession(session: CRSession) {
+ const info = this._sessions.get(session);
+ if (info)
+ eventsHelper.removeEventListeners(info.eventListeners);
+ this._sessions.delete(session);
}
- dispose() {
- eventsHelper.removeEventListeners(this._eventListeners);
+ private async _forEachSession(cb: (sessionInfo: SessionInfo) => Promise) {
+ await Promise.all([...this._sessions.values()].map(info => {
+ if (info.isMain)
+ return cb(info);
+ return cb(info).catch(e => {
+ // Broadcasting a message to the closed target should be a noop.
+ if (isSessionClosedError(e))
+ return;
+ throw e;
+ });
+ }));
}
async authenticate(credentials: types.Credentials | null) {
@@ -98,8 +116,20 @@ export class CRNetworkManager {
}
async setOffline(offline: boolean) {
- await this._session.send('Network.emulateNetworkConditions', {
- offline,
+ if (offline === this._offline)
+ return;
+ this._offline = offline;
+ await this._forEachSession(info => this._setOfflineForSession(info));
+ }
+
+ private async _setOfflineForSession(info: SessionInfo, initial?: boolean) {
+ if (initial && !this._offline)
+ return;
+ // Workers are affected by the owner frame's Network.emulateNetworkConditions.
+ if (info.workerFrame)
+ return;
+ await info.session.send('Network.emulateNetworkConditions', {
+ offline: this._offline,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
@@ -117,28 +147,46 @@ export class CRNetworkManager {
if (enabled === this._protocolRequestInterceptionEnabled)
return;
this._protocolRequestInterceptionEnabled = enabled;
- if (enabled) {
- await Promise.all([
- this._session.send('Network.setCacheDisabled', { cacheDisabled: true }),
- this._session.send('Fetch.enable', {
- handleAuthRequests: true,
- patterns: [{ urlPattern: '*', requestStage: 'Request' }],
- }),
- ]);
- } else {
- await Promise.all([
- this._session.send('Network.setCacheDisabled', { cacheDisabled: false }),
- this._session.send('Fetch.disable')
- ]);
+ await this._forEachSession(info => this._updateProtocolRequestInterceptionForSession(info));
+ }
+
+ private async _updateProtocolRequestInterceptionForSession(info: SessionInfo, initial?: boolean) {
+ const enabled = this._protocolRequestInterceptionEnabled;
+ if (initial && !enabled)
+ return;
+ const cachePromise = info.session.send('Network.setCacheDisabled', { cacheDisabled: enabled });
+ let fetchPromise = Promise.resolve(undefined);
+ if (!info.workerFrame) {
+ if (enabled)
+ fetchPromise = info.session.send('Fetch.enable', { handleAuthRequests: true, patterns: [{ urlPattern: '*', requestStage: 'Request' }] });
+ else
+ fetchPromise = info.session.send('Fetch.disable');
}
+ await Promise.all([cachePromise, fetchPromise]);
+ }
+
+ async setExtraHTTPHeaders(extraHTTPHeaders: types.HeadersArray) {
+ if (!this._extraHTTPHeaders.length && !extraHTTPHeaders.length)
+ return;
+ this._extraHTTPHeaders = extraHTTPHeaders;
+ await this._forEachSession(info => this._setExtraHTTPHeadersForSession(info));
+ }
+
+ private async _setExtraHTTPHeadersForSession(info: SessionInfo, initial?: boolean) {
+ if (initial && !this._extraHTTPHeaders.length)
+ return;
+ await info.session.send('Network.setExtraHTTPHeaders', { headers: headersArrayToObject(this._extraHTTPHeaders, false /* lowerCase */) });
}
async clearCache() {
- // Sending 'Network.setCacheDisabled' with 'cacheDisabled = true' will clear the MemoryCache.
- await this._session.send('Network.setCacheDisabled', { cacheDisabled: true });
- if (!this._protocolRequestInterceptionEnabled)
- await this._session.send('Network.setCacheDisabled', { cacheDisabled: false });
- await this._session.send('Network.clearBrowserCache');
+ await this._forEachSession(async info => {
+ // Sending 'Network.setCacheDisabled' with 'cacheDisabled = true' will clear the MemoryCache.
+ await info.session.send('Network.setCacheDisabled', { cacheDisabled: true });
+ if (!this._protocolRequestInterceptionEnabled)
+ await info.session.send('Network.setCacheDisabled', { cacheDisabled: false });
+ if (!info.workerFrame)
+ await info.session.send('Network.clearBrowserCache');
+ });
}
_onRequestWillBeSent(sessionInfo: SessionInfo, event: Protocol.Network.requestWillBeSentPayload) {
@@ -165,7 +213,7 @@ export class CRNetworkManager {
this._responseExtraInfoTracker.requestWillBeSentExtraInfo(event);
}
- _onAuthRequired(event: Protocol.Fetch.authRequiredPayload) {
+ _onAuthRequired(sessionInfo: SessionInfo, event: Protocol.Fetch.authRequiredPayload) {
let response: 'Default' | 'CancelAuth' | 'ProvideCredentials' = 'Default';
const shouldProvideCredentials = this._shouldProvideCredentials(event.request.url);
if (this._attemptedAuthentications.has(event.requestId)) {
@@ -175,7 +223,7 @@ export class CRNetworkManager {
this._attemptedAuthentications.add(event.requestId);
}
const { username, password } = shouldProvideCredentials && this._credentials ? this._credentials : { username: undefined, password: undefined };
- this._session._sendMayFail('Fetch.continueWithAuth', {
+ sessionInfo.session._sendMayFail('Fetch.continueWithAuth', {
requestId: event.requestId,
authChallengeResponse: { response, username, password },
});
@@ -191,7 +239,7 @@ export class CRNetworkManager {
if (!event.networkId) {
// Fetch without networkId means that request was not recognized by inspector, and
// it will never receive Network.requestWillBeSent. Continue the request to not affect it.
- this._session._sendMayFail('Fetch.continueRequest', { requestId: event.requestId });
+ sessionInfo.session._sendMayFail('Fetch.continueRequest', { requestId: event.requestId });
return;
}
if (event.request.url.startsWith('data:'))
@@ -215,7 +263,7 @@ export class CRNetworkManager {
//
// Note: make sure not to prematurely continue the redirect, which shares the
// `networkId` between the original request and the redirect.
- this._session._sendMayFail('Fetch.continueRequest', {
+ sessionInfo.session._sendMayFail('Fetch.continueRequest', {
...alreadyContinuedParams,
requestId: event.requestId,
});
@@ -266,7 +314,7 @@ export class CRNetworkManager {
];
if (requestHeaders['Access-Control-Request-Headers'])
responseHeaders.push({ name: 'Access-Control-Allow-Headers', value: requestHeaders['Access-Control-Request-Headers'] });
- this._session._sendMayFail('Fetch.fulfillRequest', {
+ sessionInfo.session._sendMayFail('Fetch.fulfillRequest', {
requestId: requestPausedEvent.requestId,
responseCode: 204,
responsePhrase: network.STATUS_TEXTS['204'],
@@ -279,7 +327,7 @@ export class CRNetworkManager {
// Non-service-worker requests MUST have a frame—if they don't, we pretend there was no request
if (!frame && !this._serviceWorker) {
if (requestPausedEvent)
- this._session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId });
+ sessionInfo.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId });
return;
}
@@ -289,9 +337,9 @@ export class CRNetworkManager {
if (redirectedFrom || (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled)) {
// Chromium does not preserve header overrides between redirects, so we have to do it ourselves.
const headers = redirectedFrom?._originalRequestRoute?._alreadyContinuedParams?.headers;
- this._session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId, headers });
+ sessionInfo.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId, headers });
} else {
- route = new RouteImpl(this._session, requestPausedEvent.requestId);
+ route = new RouteImpl(sessionInfo.session, requestPausedEvent.requestId);
}
}
const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document';
@@ -426,16 +474,15 @@ export class CRNetworkManager {
(this._page?._frameManager || this._serviceWorker)!.requestReceivedResponse(response);
}
- _onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) {
+ _onLoadingFinished(sessionInfo: SessionInfo, event: Protocol.Network.loadingFinishedPayload) {
this._responseExtraInfoTracker.loadingFinished(event);
- let request = this._requestIdToRequest.get(event.requestId);
- if (!request)
- request = this._maybeAdoptMainRequest(event.requestId);
+ const request = this._requestIdToRequest.get(event.requestId);
// For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469
if (!request)
return;
+ this._maybeUpdateOOPIFMainRequest(sessionInfo, request);
// Under certain conditions we never get the Network.responseReceived
// event from protocol. @see https://crbug.com/883475
@@ -453,8 +500,6 @@ export class CRNetworkManager {
this._responseExtraInfoTracker.loadingFailed(event);
let request = this._requestIdToRequest.get(event.requestId);
- if (!request)
- request = this._maybeAdoptMainRequest(event.requestId);
if (!request) {
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(event.requestId);
@@ -472,33 +517,27 @@ export class CRNetworkManager {
// @see https://crbug.com/750469
if (!request)
return;
+ this._maybeUpdateOOPIFMainRequest(sessionInfo, request);
const response = request.request._existingResponse();
if (response) {
response.setTransferSize(null);
response.setEncodedBodySize(null);
response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp));
+ } else {
+ // Loading failed before response has arrived - there will be no extra info events.
+ request.request.setRawRequestHeaders(null);
}
this._deleteRequest(request);
- request.request._setFailureText(event.errorText);
+ request.request._setFailureText(event.errorText || event.blockedReason || '');
(this._page?._frameManager || this._serviceWorker)!.requestFailed(request.request, !!event.canceled);
}
- private _maybeAdoptMainRequest(requestId: Protocol.Network.RequestId): InterceptableRequest | undefined {
+ private _maybeUpdateOOPIFMainRequest(sessionInfo: SessionInfo, request: InterceptableRequest) {
// OOPIF has a main request that starts in the parent session but finishes in the child session.
- if (!this._parentManager)
- return;
- const request = this._parentManager._requestIdToRequest.get(requestId);
- // Main requests have matching loaderId and requestId.
- if (!request || request._documentId !== requestId)
- return;
- this._requestIdToRequest.set(requestId, request);
- request.session = this._session;
- this._parentManager._requestIdToRequest.delete(requestId);
- if (request._interceptionId && this._parentManager._attemptedAuthentications.has(request._interceptionId)) {
- this._parentManager._attemptedAuthentications.delete(request._interceptionId);
- this._attemptedAuthentications.add(request._interceptionId);
- }
- return request;
+ // We check for the main request by matching loaderId and requestId, and if it now belongs to
+ // a child session, migrate it there.
+ if (request.session !== sessionInfo.session && !sessionInfo.isMain && request._documentId === request._requestId)
+ request.session = sessionInfo.session;
}
}
diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts
index 2c0d3f94b4..2828991e11 100644
--- a/packages/playwright-core/src/server/chromium/crPage.ts
+++ b/packages/playwright-core/src/server/chromium/crPage.ts
@@ -20,7 +20,7 @@ import type { RegisteredListener } from '../../utils/eventsHelper';
import { eventsHelper } from '../../utils/eventsHelper';
import { registry } from '../registry';
import { rewriteErrorMessage } from '../../utils/stackTrace';
-import { assert, createGuid, headersArrayToObject } from '../../utils';
+import { assert, createGuid } from '../../utils';
import * as dialog from '../dialog';
import * as dom from '../dom';
import * as frames from '../frames';
@@ -61,6 +61,7 @@ export class CRPage implements PageDelegate {
readonly rawTouchscreen: RawTouchscreenImpl;
readonly _targetId: string;
readonly _opener: CRPage | null;
+ readonly _networkManager: CRNetworkManager;
private readonly _pdf: CRPDF;
private readonly _coverage: CRCoverage;
readonly _browserContext: CRBrowserContext;
@@ -92,6 +93,13 @@ export class CRPage implements PageDelegate {
this._coverage = new CRCoverage(client);
this._browserContext = browserContext;
this._page = new Page(this, browserContext);
+ this._networkManager = new CRNetworkManager(this._page, null);
+ // Sync any browser context state to the network manager. This does not talk over CDP because
+ // we have not connected any sessions to the network manager yet.
+ this.updateOffline();
+ this.updateExtraHTTPHeaders();
+ this.updateHttpCredentials();
+ this.updateRequestInterception();
this._mainFrameSession = new FrameSession(this, client, targetId, null);
this._sessions.set(targetId, this._mainFrameSession);
if (opener && !browserContext._options.noDefaultViewport) {
@@ -184,7 +192,11 @@ export class CRPage implements PageDelegate {
}
async updateExtraHTTPHeaders(): Promise {
- await this._forAllFrameSessions(frame => frame._updateExtraHTTPHeaders(false));
+ const headers = network.mergeHeaders([
+ this._browserContext._options.extraHTTPHeaders,
+ this._page.extraHTTPHeaders()
+ ]);
+ await this._networkManager.setExtraHTTPHeaders(headers);
}
async updateGeolocation(): Promise {
@@ -192,11 +204,11 @@ export class CRPage implements PageDelegate {
}
async updateOffline(): Promise {
- await this._forAllFrameSessions(frame => frame._updateOffline(false));
+ await this._networkManager.setOffline(!!this._browserContext._options.offline);
}
async updateHttpCredentials(): Promise {
- await this._forAllFrameSessions(frame => frame._updateHttpCredentials(false));
+ await this._networkManager.authenticate(this._browserContext._options.httpCredentials || null);
}
async updateEmulatedViewportSize(preserveWindowBoundaries?: boolean): Promise {
@@ -216,7 +228,7 @@ export class CRPage implements PageDelegate {
}
async updateRequestInterception(): Promise {
- await this._forAllFrameSessions(frame => frame._updateRequestInterception());
+ await this._networkManager.setRequestInterception(this._page.needsRequestInterception());
}
async updateFileChooserInterception() {
@@ -333,7 +345,7 @@ export class CRPage implements PageDelegate {
injected.setInputFiles(node, files), files);
}
- async setInputFilePaths(progress: Progress, handle: dom.ElementHandle, files: string[]): Promise {
+ async setInputFilePaths(handle: dom.ElementHandle, files: string[]): Promise {
const frame = await handle.ownerFrame();
if (!frame)
throw new Error('Cannot set input files to detached input element');
@@ -392,7 +404,6 @@ class FrameSession {
readonly _client: CRSession;
readonly _crPage: CRPage;
readonly _page: Page;
- readonly _networkManager: CRNetworkManager;
private readonly _parentSession: FrameSession | null;
private readonly _childSessions = new Set();
private readonly _contextIdToContext = new Map();
@@ -418,7 +429,6 @@ class FrameSession {
this._crPage = crPage;
this._page = crPage._page;
this._targetId = targetId;
- this._networkManager = new CRNetworkManager(client, this._page, null, parentSession ? parentSession._networkManager : null);
this._parentSession = parentSession;
if (parentSession)
parentSession._childSessions.add(this);
@@ -533,7 +543,7 @@ class FrameSession {
source: '',
worldName: UTILITY_WORLD_NAME,
}),
- this._networkManager.initialize(),
+ this._crPage._networkManager.addSession(this._client, undefined, this._isMainFrame()),
this._client.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }),
];
if (!isSettingStorageState) {
@@ -559,10 +569,6 @@ class FrameSession {
if (!this._crPage._browserContext._browser.options.headful)
promises.push(this._setDefaultFontFamilies(this._client));
promises.push(this._updateGeolocation(true));
- promises.push(this._updateExtraHTTPHeaders(true));
- promises.push(this._updateRequestInterception());
- promises.push(this._updateOffline(true));
- promises.push(this._updateHttpCredentials(true));
promises.push(this._updateEmulateMedia());
promises.push(this._updateFileChooserInterception(true));
for (const binding of this._crPage._page.allBindings())
@@ -586,7 +592,7 @@ class FrameSession {
if (this._parentSession)
this._parentSession._childSessions.delete(this);
eventsHelper.removeEventListeners(this._eventListeners);
- this._networkManager.dispose();
+ this._crPage._networkManager.removeSession(this._client);
this._crPage._sessions.delete(this._targetId);
this._client.dispose();
}
@@ -752,7 +758,8 @@ class FrameSession {
});
// This might fail if the target is closed before we initialize.
session._sendMayFail('Runtime.enable');
- session._sendMayFail('Network.enable');
+ // TODO: attribute workers to the right frame.
+ this._crPage._networkManager.addSession(session, this._page._frameManager.frame(this._targetId) ?? undefined).catch(() => {});
session._sendMayFail('Runtime.runIfWaitingForDebugger');
session._sendMayFail('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true });
session.on('Target.attachedToTarget', event => this._onAttachedToTarget(event));
@@ -762,8 +769,6 @@ class FrameSession {
this._page._addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace));
});
session.on('Runtime.exceptionThrown', exception => this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, exceptionToError(exception.exceptionDetails), this._page));
- // TODO: attribute workers to the right frame.
- this._networkManager.instrumentNetworkEvents({ session, workerFrame: this._page._frameManager.frame(this._targetId) ?? undefined });
}
_onDetachedFromTarget(event: Protocol.Target.detachedFromTargetPayload) {
@@ -981,33 +986,12 @@ class FrameSession {
await this._client._sendMayFail('Page.stopScreencast');
}
- async _updateExtraHTTPHeaders(initial: boolean): Promise {
- const headers = network.mergeHeaders([
- this._crPage._browserContext._options.extraHTTPHeaders,
- this._page.extraHTTPHeaders()
- ]);
- if (!initial || headers.length)
- await this._client.send('Network.setExtraHTTPHeaders', { headers: headersArrayToObject(headers, false /* lowerCase */) });
- }
-
async _updateGeolocation(initial: boolean): Promise {
const geolocation = this._crPage._browserContext._options.geolocation;
if (!initial || geolocation)
await this._client.send('Emulation.setGeolocationOverride', geolocation || {});
}
- async _updateOffline(initial: boolean): Promise {
- const offline = !!this._crPage._browserContext._options.offline;
- if (!initial || offline)
- await this._networkManager.setOffline(offline);
- }
-
- async _updateHttpCredentials(initial: boolean): Promise {
- const credentials = this._crPage._browserContext._options.httpCredentials || null;
- if (!initial || credentials)
- await this._networkManager.authenticate(credentials);
- }
-
async _updateViewport(preserveWindowBoundaries?: boolean): Promise {
if (this._crPage._browserContext._browser.isClank())
return;
@@ -1106,10 +1090,6 @@ class FrameSession {
await session.send('Page.setFontFamilies', fontFamilies);
}
- async _updateRequestInterception(): Promise {
- await this._networkManager.setRequestInterception(this._page.needsRequestInterception());
- }
-
async _updateFileChooserInterception(initial: boolean) {
const enabled = this._page.fileChooserIntercepted();
if (initial && !enabled)
diff --git a/packages/playwright-core/src/server/chromium/crServiceWorker.ts b/packages/playwright-core/src/server/chromium/crServiceWorker.ts
index 11d6385356..4de63fac55 100644
--- a/packages/playwright-core/src/server/chromium/crServiceWorker.ts
+++ b/packages/playwright-core/src/server/chromium/crServiceWorker.ts
@@ -34,13 +34,13 @@ export class CRServiceWorker extends Worker {
this._session = session;
this._browserContext = browserContext;
if (!!process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS)
- this._networkManager = new CRNetworkManager(session, null, this, null);
+ this._networkManager = new CRNetworkManager(null, this);
session.once('Runtime.executionContextCreated', event => {
this._createExecutionContext(new CRExecutionContext(session, event.context));
});
if (this._networkManager && this._isNetworkInspectionEnabled()) {
- this._networkManager.initialize().catch(() => {});
+ this._networkManager.addSession(session, undefined, true /* isMain */).catch(() => {});
this.updateRequestInterception();
this.updateExtraHTTPHeaders(true);
this.updateHttpCredentials(true);
@@ -56,6 +56,7 @@ export class CRServiceWorker extends Worker {
}
override didClose() {
+ this._networkManager?.removeSession(this._session);
this._session.dispose();
super.didClose();
}
diff --git a/packages/playwright-core/src/server/chromium/protocol.d.ts b/packages/playwright-core/src/server/chromium/protocol.d.ts
index 83b8d235ce..0ea475d12e 100644
--- a/packages/playwright-core/src/server/chromium/protocol.d.ts
+++ b/packages/playwright-core/src/server/chromium/protocol.d.ts
@@ -831,7 +831,7 @@ CORS RFC1918 enforcement.
resourceIPAddressSpace?: Network.IPAddressSpace;
clientSecurityState?: Network.ClientSecurityState;
}
- export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation";
+ export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation"|"InvalidInfoHeader"|"NoRegisterSourceHeader"|"NoRegisterTriggerHeader"|"NoRegisterOsSourceHeader"|"NoRegisterOsTriggerHeader";
/**
* Details for issues around "Attribution Reporting API" usage.
Explainer: https://github.com/WICG/attribution-reporting-api
@@ -2493,6 +2493,28 @@ stylesheet rules) this rule came from.
*/
tryRules: CSSTryRule[];
}
+ /**
+ * CSS @position-try rule representation.
+ */
+ export interface CSSPositionTryRule {
+ /**
+ * The prelude dashed-ident name
+ */
+ name: Value;
+ /**
+ * The css style sheet identifier (absent for user agent stylesheet and user-specified
+stylesheet rules) this rule came from.
+ */
+ styleSheetId?: StyleSheetId;
+ /**
+ * Parent stylesheet's origin.
+ */
+ origin: StyleSheetOrigin;
+ /**
+ * Associated style declaration.
+ */
+ style: CSSStyle;
+ }
/**
* CSS keyframes rule representation.
*/
@@ -2820,6 +2842,10 @@ attributes) for a DOM node identified by `nodeId`.
* A list of CSS position fallbacks matching this node.
*/
cssPositionFallbackRules?: CSSPositionFallbackRule[];
+ /**
+ * A list of CSS @position-try rules matching this node, based on the position-try-options property.
+ */
+ cssPositionTryRules?: CSSPositionTryRule[];
/**
* A list of CSS at-property rules matching this node.
*/
@@ -2882,6 +2908,17 @@ the full layer tree for the tree scope and their ordering.
export type getLayersForNodeReturnValue = {
rootLayer: CSSLayerData;
}
+ /**
+ * Given a CSS selector text and a style sheet ID, getLocationForSelector
+returns an array of locations of the CSS selector in the style sheet.
+ */
+ export type getLocationForSelectorParameters = {
+ styleSheetId: StyleSheetId;
+ selectorText: string;
+ }
+ export type getLocationForSelectorReturnValue = {
+ ranges: SourceRange[];
+ }
/**
* Starts tracking the given computed styles for updates. The specified array of properties
replaces the one previously specified. Pass empty array to disable tracking.
@@ -8052,6 +8089,7 @@ milliseconds relatively to this requestTime.
headers: Headers;
/**
* HTTP POST request data.
+Use postDataEntries instead.
*/
postData?: string;
/**
@@ -8059,7 +8097,7 @@ milliseconds relatively to this requestTime.
*/
hasPostData?: boolean;
/**
- * Request body elements. This will be converted from base64 to binary
+ * Request body elements (post data broken into individual entries).
*/
postDataEntries?: PostDataEntry[];
/**
@@ -9787,6 +9825,18 @@ matches provided URL.
* Connection type if known.
*/
connectionType?: ConnectionType;
+ /**
+ * WebRTC packet loss (percent, 0-100). 0 disables packet loss emulation, 100 drops all the packets.
+ */
+ packetLoss?: number;
+ /**
+ * WebRTC packet queue length (packet). 0 removes any queue length limitations.
+ */
+ packetQueueLength?: number;
+ /**
+ * WebRTC packetReordering feature.
+ */
+ packetReordering?: boolean;
}
export type emulateNetworkConditionsReturnValue = {
}
@@ -11065,7 +11115,7 @@ as an ad.
* All Permissions Policy features. This enum should match the one defined
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.
*/
- export type PermissionsPolicyFeature = "accelerometer"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factor"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"window-placement"|"xr-spatial-tracking";
+ export type PermissionsPolicyFeature = "accelerometer"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"window-placement"|"xr-spatial-tracking";
/**
* Reason for a permissions policy feature to be disabled.
*/
@@ -11534,7 +11584,7 @@ Example URLs: http://www.google.com/file.html -> "google.com"
/**
* List of not restored reasons for back-forward cache.
*/
- export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"Portal"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame";
+ export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"Portal"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame";
/**
* Types of not restored reasons for back-forward cache.
*/
@@ -11811,7 +11861,7 @@ open.
*/
type: DialogType;
/**
- * True if browser is capable showing or acting on the given dialog. When browser has no
+ * True iff browser is capable showing or acting on the given dialog. When browser has no
dialog handler for given target, calling alert while Page domain is engaged will stall
the page execution. Execution can be resumed via calling Page.handleJavaScriptDialog.
*/
@@ -13533,34 +13583,10 @@ Tokens from that issuer.
* Enum of network fetches auctions can do.
*/
export type InterestGroupAuctionFetchType = "bidderJs"|"bidderWasm"|"sellerJs"|"bidderTrustedSignals"|"sellerTrustedSignals";
- /**
- * Ad advertising element inside an interest group.
- */
- export interface InterestGroupAd {
- renderURL: string;
- metadata?: string;
- }
- /**
- * The full details of an interest group.
- */
- export interface InterestGroupDetails {
- ownerOrigin: string;
- name: string;
- expirationTime: Network.TimeSinceEpoch;
- joiningOrigin: string;
- biddingLogicURL?: string;
- biddingWasmHelperURL?: string;
- updateURL?: string;
- trustedBiddingSignalsURL?: string;
- trustedBiddingSignalsKeys: string[];
- userBiddingSignals?: string;
- ads: InterestGroupAd[];
- adComponents: InterestGroupAd[];
- }
/**
* Enum of shared storage access types.
*/
- export type SharedStorageAccessType = "documentAddModule"|"documentSelectURL"|"documentRun"|"documentSet"|"documentAppend"|"documentDelete"|"documentClear"|"workletSet"|"workletAppend"|"workletDelete"|"workletClear"|"workletGet"|"workletKeys"|"workletEntries"|"workletLength"|"workletRemainingBudget";
+ export type SharedStorageAccessType = "documentAddModule"|"documentSelectURL"|"documentRun"|"documentSet"|"documentAppend"|"documentDelete"|"documentClear"|"documentGet"|"workletSet"|"workletAppend"|"workletDelete"|"workletClear"|"workletGet"|"workletKeys"|"workletEntries"|"workletLength"|"workletRemainingBudget"|"headerSet"|"headerAppend"|"headerDelete"|"headerClear";
/**
* Struct for a single key-value pair in an origin's shared storage.
*/
@@ -13644,22 +13670,28 @@ SharedStorageAccessType.documentAppend,
SharedStorageAccessType.documentDelete,
SharedStorageAccessType.workletSet,
SharedStorageAccessType.workletAppend,
-SharedStorageAccessType.workletDelete, and
-SharedStorageAccessType.workletGet.
+SharedStorageAccessType.workletDelete,
+SharedStorageAccessType.workletGet,
+SharedStorageAccessType.headerSet,
+SharedStorageAccessType.headerAppend, and
+SharedStorageAccessType.headerDelete.
*/
key?: string;
/**
* Value for a specific entry in an origin's shared storage.
Present only for SharedStorageAccessType.documentSet,
SharedStorageAccessType.documentAppend,
-SharedStorageAccessType.workletSet, and
-SharedStorageAccessType.workletAppend.
+SharedStorageAccessType.workletSet,
+SharedStorageAccessType.workletAppend,
+SharedStorageAccessType.headerSet, and
+SharedStorageAccessType.headerAppend.
*/
value?: string;
/**
* Whether or not to set an entry for a key if that key is already present.
-Present only for SharedStorageAccessType.documentSet and
-SharedStorageAccessType.workletSet.
+Present only for SharedStorageAccessType.documentSet,
+SharedStorageAccessType.workletSet, and
+SharedStorageAccessType.headerSet.
*/
ignoreIfPresent?: boolean;
}
@@ -13789,6 +13821,23 @@ int
}
export type AttributionReportingEventLevelResult = "success"|"successDroppedLowerPriority"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"deduplicated"|"excessiveAttributions"|"priorityTooLow"|"neverAttributedSource"|"excessiveReportingOrigins"|"noMatchingSourceFilterData"|"prohibitedByBrowserPolicy"|"noMatchingConfigurations"|"excessiveReports"|"falselyAttributedSource"|"reportWindowPassed"|"notRegistered"|"reportWindowNotStarted"|"noMatchingTriggerData";
export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports";
+ /**
+ * A single Related Website Set object.
+ */
+ export interface RelatedWebsiteSet {
+ /**
+ * The primary site of this set, along with the ccTLDs if there is any.
+ */
+ primarySites: string[];
+ /**
+ * The associated sites of this set, along with the ccTLDs if there is any.
+ */
+ associatedSites: string[];
+ /**
+ * The service sites of this set, along with the ccTLDs if there is any.
+ */
+ serviceSites: string[];
+ }
/**
* A cache's contents have been modified.
@@ -14216,7 +14265,13 @@ Leaves other stored data, including the issuer's Redemption Records, intact.
name: string;
}
export type getInterestGroupDetailsReturnValue = {
- details: InterestGroupDetails;
+ /**
+ * This largely corresponds to:
+https://wicg.github.io/turtledove/#dictdef-generatebidinterestgroup
+but has absolute expirationTime instead of relative lifetimeMs and
+also adds joiningOrigin.
+ */
+ details: { [key: string]: string };
}
/**
* Enables/Disables issuing of interestGroupAccessed events.
@@ -14345,6 +14400,15 @@ interestGroupAuctionNetworkRequestCreated.
}
export type setAttributionReportingTrackingReturnValue = {
}
+ /**
+ * Returns the effective Related Website Sets in use by this profile for the browser
+session. The effective Related Website Sets will not change during a browser session.
+ */
+ export type getRelatedWebsiteSetsParameters = {
+ }
+ export type getRelatedWebsiteSetsReturnValue = {
+ sets: RelatedWebsiteSet[];
+ }
}
/**
@@ -14582,10 +14646,10 @@ supported.
export type SessionID = string;
export interface TargetInfo {
targetId: TargetID;
- type: string;
/**
* List of types: https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/devtools_agent_host_impl.cc?ss=chromium&q=f:devtools%20-f:out%20%22::kTypeTab%5B%5D%22
*/
+ type: string;
title: string;
url: string;
/**
@@ -16385,7 +16449,7 @@ See also:
requestId?: Network.RequestId;
/**
* Error information
-`errorMessage` is null if `errorType` is null.
+`errorMessage` is null iff `errorType` is null.
*/
errorType?: RuleSetErrorType;
/**
@@ -19515,6 +19579,7 @@ Error was thrown.
"CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeParameters;
"CSS.getStyleSheetText": CSS.getStyleSheetTextParameters;
"CSS.getLayersForNode": CSS.getLayersForNodeParameters;
+ "CSS.getLocationForSelector": CSS.getLocationForSelectorParameters;
"CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesParameters;
"CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesParameters;
"CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeParameters;
@@ -19885,6 +19950,7 @@ Error was thrown.
"Storage.runBounceTrackingMitigations": Storage.runBounceTrackingMitigationsParameters;
"Storage.setAttributionReportingLocalTestingMode": Storage.setAttributionReportingLocalTestingModeParameters;
"Storage.setAttributionReportingTracking": Storage.setAttributionReportingTrackingParameters;
+ "Storage.getRelatedWebsiteSets": Storage.getRelatedWebsiteSetsParameters;
"SystemInfo.getInfo": SystemInfo.getInfoParameters;
"SystemInfo.getFeatureState": SystemInfo.getFeatureStateParameters;
"SystemInfo.getProcessInfo": SystemInfo.getProcessInfoParameters;
@@ -20097,6 +20163,7 @@ Error was thrown.
"CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeReturnValue;
"CSS.getStyleSheetText": CSS.getStyleSheetTextReturnValue;
"CSS.getLayersForNode": CSS.getLayersForNodeReturnValue;
+ "CSS.getLocationForSelector": CSS.getLocationForSelectorReturnValue;
"CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesReturnValue;
"CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesReturnValue;
"CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeReturnValue;
@@ -20467,6 +20534,7 @@ Error was thrown.
"Storage.runBounceTrackingMitigations": Storage.runBounceTrackingMitigationsReturnValue;
"Storage.setAttributionReportingLocalTestingMode": Storage.setAttributionReportingLocalTestingModeReturnValue;
"Storage.setAttributionReportingTracking": Storage.setAttributionReportingTrackingReturnValue;
+ "Storage.getRelatedWebsiteSets": Storage.getRelatedWebsiteSetsReturnValue;
"SystemInfo.getInfo": SystemInfo.getInfoReturnValue;
"SystemInfo.getFeatureState": SystemInfo.getFeatureStateReturnValue;
"SystemInfo.getProcessInfo": SystemInfo.getProcessInfoReturnValue;
diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json
index 6191ef5d2e..49a3c9446e 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/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@@ -121,7 +121,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S5 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@@ -132,7 +132,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 740
@@ -143,7 +143,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 740,
"height": 360
@@ -154,7 +154,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 320,
"height": 658
@@ -165,7 +165,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+ landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 658,
"height": 320
@@ -176,7 +176,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36",
"viewport": {
"width": 712,
"height": 1138
@@ -187,7 +187,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36",
"viewport": {
"width": 1138,
"height": 712
@@ -978,7 +978,7 @@
"defaultBrowserType": "webkit"
},
"LG Optimus L70": {
- "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 384,
"height": 640
@@ -989,7 +989,7 @@
"defaultBrowserType": "chromium"
},
"LG Optimus L70 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 384
@@ -1000,7 +1000,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 550": {
- "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36 Edge/14.14263",
+ "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 640,
"height": 360
@@ -1011,7 +1011,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 550 landscape": {
- "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36 Edge/14.14263",
+ "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 360,
"height": 640
@@ -1022,7 +1022,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 950": {
- "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36 Edge/14.14263",
+ "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 360,
"height": 640
@@ -1033,7 +1033,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 950 landscape": {
- "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36 Edge/14.14263",
+ "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 640,
"height": 360
@@ -1044,7 +1044,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36",
"viewport": {
"width": 800,
"height": 1280
@@ -1055,7 +1055,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36",
"viewport": {
"width": 1280,
"height": 800
@@ -1066,7 +1066,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4": {
- "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 384,
"height": 640
@@ -1077,7 +1077,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 384
@@ -1088,7 +1088,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@@ -1099,7 +1099,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@@ -1110,7 +1110,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@@ -1121,7 +1121,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@@ -1132,7 +1132,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@@ -1143,7 +1143,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@@ -1154,7 +1154,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@@ -1165,7 +1165,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@@ -1176,7 +1176,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36",
"viewport": {
"width": 600,
"height": 960
@@ -1187,7 +1187,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36",
"viewport": {
"width": 960,
"height": 600
@@ -1242,7 +1242,7 @@
"defaultBrowserType": "webkit"
},
"Pixel 2": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 411,
"height": 731
@@ -1253,7 +1253,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 731,
"height": 411
@@ -1264,7 +1264,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 XL": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 411,
"height": 823
@@ -1275,7 +1275,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 XL landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 823,
"height": 411
@@ -1286,7 +1286,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3": {
- "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 393,
"height": 786
@@ -1297,7 +1297,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 786,
"height": 393
@@ -1308,7 +1308,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4": {
- "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 353,
"height": 745
@@ -1319,7 +1319,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 745,
"height": 353
@@ -1330,7 +1330,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G)": {
- "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"screen": {
"width": 412,
"height": 892
@@ -1345,7 +1345,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G) landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"screen": {
"height": 892,
"width": 412
@@ -1360,7 +1360,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5": {
- "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"screen": {
"width": 393,
"height": 851
@@ -1375,7 +1375,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"screen": {
"width": 851,
"height": 393
@@ -1390,7 +1390,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7": {
- "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"screen": {
"width": 412,
"height": 915
@@ -1405,7 +1405,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"screen": {
"width": 915,
"height": 412
@@ -1420,7 +1420,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@@ -1431,7 +1431,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@@ -1442,7 +1442,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Chrome HiDPI": {
- "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36",
"screen": {
"width": 1792,
"height": 1120
@@ -1457,7 +1457,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge HiDPI": {
- "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36 Edg/123.0.6312.4",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36 Edg/124.0.6367.8",
"screen": {
"width": 1792,
"height": 1120
@@ -1502,7 +1502,7 @@
"defaultBrowserType": "webkit"
},
"Desktop Chrome": {
- "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36",
"screen": {
"width": 1920,
"height": 1080
@@ -1517,7 +1517,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge": {
- "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.4 Safari/537.36 Edg/123.0.6312.4",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.8 Safari/537.36 Edg/124.0.6367.8",
"screen": {
"width": 1920,
"height": 1080
diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts
index b4a06a67b5..d04418866a 100644
--- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts
@@ -224,6 +224,10 @@ export class BrowserContextDispatcher extends Dispatcher {
+ await this._context.removeCookies(params.filter);
+ }
+
async grantPermissions(params: channels.BrowserContextGrantPermissionsParams): Promise {
await this._context.grantPermissions(params.permissions, params.origin);
}
diff --git a/packages/playwright-core/src/server/dispatchers/dispatcher.ts b/packages/playwright-core/src/server/dispatchers/dispatcher.ts
index 0b250fe76e..44d68c73a2 100644
--- a/packages/playwright-core/src/server/dispatchers/dispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/dispatcher.ts
@@ -24,7 +24,6 @@ import { SdkObject } from '../instrumentation';
import type { PlaywrightDispatcher } from './playwrightDispatcher';
import { eventsHelper } from '../..//utils/eventsHelper';
import type { RegisteredListener } from '../..//utils/eventsHelper';
-import type * as trace from '@trace/trace';
import { isProtocolError } from '../protocolError';
export const dispatcherSymbol = Symbol('dispatcher');
@@ -81,7 +80,7 @@ export class Dispatcher extends js.JSHandle {
await progress.beforeInputAction(this);
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
progress.throwIfAborted(); // Avoid action that has side-effects.
- if (localPaths)
- await this._page._delegate.setInputFilePaths(progress, retargeted, localPaths);
- else
+ if (localPaths) {
+ await Promise.all(localPaths.map(localPath => (
+ fs.promises.access(localPath, fs.constants.F_OK)
+ )));
+ await this._page._delegate.setInputFilePaths(retargeted, localPaths);
+ } else {
await this._page._delegate.setInputFiles(retargeted, filePayloads!);
+ }
});
return 'done';
}
diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts
index d2d2a03412..49435823c2 100644
--- a/packages/playwright-core/src/server/firefox/ffPage.ts
+++ b/packages/playwright-core/src/server/firefox/ffPage.ts
@@ -40,7 +40,7 @@ import { TargetClosedError } from '../errors';
export const UTILITY_WORLD_NAME = '__playwright_utility_world__';
export class FFPage implements PageDelegate {
- readonly cspErrorsAsynchronousForInlineScipts = true;
+ readonly cspErrorsAsynchronousForInlineScripts = true;
readonly rawMouse: RawMouseImpl;
readonly rawKeyboard: RawKeyboardImpl;
readonly rawTouchscreen: RawTouchscreenImpl;
@@ -543,16 +543,12 @@ export class FFPage implements PageDelegate {
injected.setInputFiles(node, files), files);
}
- async setInputFilePaths(progress: Progress, handle: dom.ElementHandle, files: string[]): Promise {
- await Promise.all([
- this._session.send('Page.setFileInputFiles', {
- frameId: handle._context.frame._id,
- objectId: handle._objectId,
- files
- }),
- handle.dispatchEvent(progress.metadata, 'input'),
- handle.dispatchEvent(progress.metadata, 'change')
- ]);
+ async setInputFilePaths(handle: dom.ElementHandle, files: string[]): Promise {
+ await this._session.send('Page.setFileInputFiles', {
+ frameId: handle._context.frame._id,
+ objectId: handle._objectId,
+ files
+ });
}
async adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise> {
diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts
index dab9a1ebcc..fb4dcee705 100644
--- a/packages/playwright-core/src/server/frames.ts
+++ b/packages/playwright-core/src/server/frames.ts
@@ -914,6 +914,12 @@ export class Frame extends SdkObject {
return this._url;
}
+ origin(): string | undefined {
+ if (!this._url.startsWith('http'))
+ return;
+ return network.parsedURL(this._url)?.origin;
+ }
+
parentFrame(): Frame | null {
return this._parentFrame;
}
@@ -942,7 +948,7 @@ export class Frame extends SdkObject {
const result = (await context.evaluateHandle(addScriptContent, { content: content!, type })).asElement()!;
// Another round trip to the browser to ensure that we receive CSP error messages
// (if any) logged asynchronously in a separate task on the content main thread.
- if (this._page._delegate.cspErrorsAsynchronousForInlineScipts)
+ if (this._page._delegate.cspErrorsAsynchronousForInlineScripts)
await context.evaluate(() => true);
return result;
});
@@ -1686,6 +1692,17 @@ export class Frame extends SdkObject {
if (db.name)
indexedDB.deleteDatabase(db.name!);
}
+
+ // Clean StorageManager
+ const root = await navigator.storage.getDirectory();
+ const entries = await (root as any).entries();
+ // Manual loop instead of for await because in Firefox's utility context instanceof AsyncIterable is not working.
+ let entry = await entries.next();
+ while (!entry.done) {
+ const [name] = entry.value;
+ await root.removeEntry(name, { recursive: true });
+ entry = await entries.next();
+ }
}, { ls: newStorage?.localStorage }).catch(() => {});
}
diff --git a/packages/playwright-core/src/server/index.ts b/packages/playwright-core/src/server/index.ts
index 9d619c205a..439febfe18 100644
--- a/packages/playwright-core/src/server/index.ts
+++ b/packages/playwright-core/src/server/index.ts
@@ -29,6 +29,6 @@ export { createPlaywright } from './playwright';
export type { DispatcherScope } from './dispatchers/dispatcher';
export type { Playwright } from './playwright';
-export { openTraceInBrowser, openTraceViewerApp } from './trace/viewer/traceViewer';
+export { openTraceInBrowser, openTraceViewerApp, runTraceViewerApp, startTraceViewerServer, installRootRedirect } from './trace/viewer/traceViewer';
export { serverSideCallMetadata } from './instrumentation';
export { SocksProxy } from '../common/socksProxy';
diff --git a/packages/playwright-core/src/server/injected/highlight.css b/packages/playwright-core/src/server/injected/highlight.css
index 9ee0370720..4efa63de0a 100644
--- a/packages/playwright-core/src/server/injected/highlight.css
+++ b/packages/playwright-core/src/server/injected/highlight.css
@@ -231,6 +231,12 @@ x-pw-tool-item.value > x-div {
mask-image: url("data:image/svg+xml;utf8,");
}
+x-pw-tool-item.screenshot > x-div {
+ /* codicon: device-camera */
+ -webkit-mask-image: url("data:image/svg+xml;utf8,");
+ mask-image: url("data:image/svg+xml;utf8,");
+}
+
x-pw-tool-item.accept > x-div {
-webkit-mask-image: url("data:image/svg+xml;utf8,");
mask-image: url("data:image/svg+xml;utf8,");
diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts
index 2074ffa67f..e85f91c44c 100644
--- a/packages/playwright-core/src/server/injected/recorder/recorder.ts
+++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts
@@ -761,6 +761,7 @@ class Overlay {
private _assertVisibilityToggle: HTMLElement;
private _assertTextToggle: HTMLElement;
private _assertValuesToggle: HTMLElement;
+ private _assertScreenshotButton: HTMLElement;
private _offsetX = 0;
private _dragState: { offsetX: number, dragStart: { x: number, y: number } } | undefined;
private _measure: { width: number, height: number } = { width: 0, height: 0 };
@@ -807,6 +808,12 @@ class Overlay {
this._assertValuesToggle.appendChild(this._recorder.injectedScript.document.createElement('x-div'));
toolsListElement.appendChild(this._assertValuesToggle);
+ this._assertScreenshotButton = this._recorder.injectedScript.document.createElement('x-pw-tool-item');
+ this._assertScreenshotButton.title = 'Assert screenshot';
+ this._assertScreenshotButton.classList.add('screenshot');
+ this._assertScreenshotButton.appendChild(this._recorder.injectedScript.document.createElement('x-div'));
+ toolsListElement.appendChild(this._assertScreenshotButton);
+
this._updateVisualPosition();
this._refreshListeners();
}
@@ -845,6 +852,15 @@ class Overlay {
if (!this._assertValuesToggle.classList.contains('disabled'))
this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingValue' ? 'recording' : 'assertingValue');
}),
+ addEventListener(this._assertScreenshotButton, 'click', () => {
+ if (!this._assertScreenshotButton.classList.contains('disabled')) {
+ this._recorder.delegate.recordAction?.({
+ name: 'assertScreenshot',
+ signals: [],
+ });
+ this.flashToolSucceeded('assertScreenshot');
+ }
+ }),
];
}
@@ -867,6 +883,7 @@ class Overlay {
this._assertTextToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting');
this._assertValuesToggle.classList.toggle('active', state.mode === 'assertingValue');
this._assertValuesToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting');
+ this._assertScreenshotButton.classList.toggle('disabled', state.mode !== 'recording');
if (this._offsetX !== state.overlay.offsetX) {
this._offsetX = state.overlay.offsetX;
this._updateVisualPosition();
@@ -877,8 +894,12 @@ class Overlay {
this._showOverlay();
}
- flashToolSucceeded(tool: 'assertingVisibility' | 'assertingValue') {
- const element = tool === 'assertingVisibility' ? this._assertVisibilityToggle : this._assertValuesToggle;
+ flashToolSucceeded(tool: 'assertingVisibility' | 'assertingValue' | 'assertScreenshot') {
+ const element = {
+ 'assertingVisibility': this._assertVisibilityToggle,
+ 'assertingValue': this._assertValuesToggle,
+ 'assertScreenshot': this._assertScreenshotButton,
+ }[tool];
element.classList.add('succeeded');
setTimeout(() => element.classList.remove('succeeded'), 2000);
}
diff --git a/packages/playwright-core/src/server/injected/roleSelectorEngine.ts b/packages/playwright-core/src/server/injected/roleSelectorEngine.ts
index 56643199e5..b647f81b8e 100644
--- a/packages/playwright-core/src/server/injected/roleSelectorEngine.ts
+++ b/packages/playwright-core/src/server/injected/roleSelectorEngine.ts
@@ -16,10 +16,9 @@
import type { SelectorEngine, SelectorRoot } from './selectorEngine';
import { matchesAttributePart } from './selectorUtils';
-import { beginAriaCaches, endAriaCaches, getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaLevel, getAriaPressed, getAriaSelected, getElementAccessibleName, getElementsByRole, isElementHiddenForAria, kAriaCheckedRoles, kAriaExpandedRoles, kAriaLevelRoles, kAriaPressedRoles, kAriaSelectedRoles } from './roleUtils';
+import { beginAriaCaches, endAriaCaches, getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaLevel, getAriaPressed, getAriaRole, getAriaSelected, getElementAccessibleName, isElementHiddenForAria, kAriaCheckedRoles, kAriaExpandedRoles, kAriaLevelRoles, kAriaPressedRoles, kAriaSelectedRoles } from './roleUtils';
import { parseAttributeSelector, type AttributeSelectorPart, type AttributeSelectorOperator } from '../../utils/isomorphic/selectorParser';
import { normalizeWhiteSpace } from '../../utils/isomorphic/stringUtils';
-import { isInsideScope } from './domUtils';
type RoleEngineOptions = {
role: string;
@@ -126,27 +125,26 @@ function validateAttributes(attrs: AttributeSelectorPart[], role: string): RoleE
}
function queryRole(scope: SelectorRoot, options: RoleEngineOptions, internal: boolean): Element[] {
- const doc = scope.nodeType === 9 /* Node.DOCUMENT_NODE */ ? scope as Document : scope.ownerDocument;
- const elements = doc ? getElementsByRole(doc, options.role) : [];
- return elements.filter(element => {
- if (!isInsideScope(scope, element))
- return false;
+ const result: Element[] = [];
+ const match = (element: Element) => {
+ if (getAriaRole(element) !== options.role)
+ return;
if (options.selected !== undefined && getAriaSelected(element) !== options.selected)
- return false;
+ return;
if (options.checked !== undefined && getAriaChecked(element) !== options.checked)
- return false;
+ return;
if (options.pressed !== undefined && getAriaPressed(element) !== options.pressed)
- return false;
+ return;
if (options.expanded !== undefined && getAriaExpanded(element) !== options.expanded)
- return false;
+ return;
if (options.level !== undefined && getAriaLevel(element) !== options.level)
- return false;
+ return;
if (options.disabled !== undefined && getAriaDisabled(element) !== options.disabled)
- return false;
+ return;
if (!options.includeHidden) {
const isHidden = isElementHiddenForAria(element);
if (isHidden)
- return false;
+ return;
}
if (options.name !== undefined) {
// Always normalize whitespace in the accessible name.
@@ -157,10 +155,25 @@ function queryRole(scope: SelectorRoot, options: RoleEngineOptions, internal: bo
if (internal && !options.exact && options.nameOp === '=')
options.nameOp = '*=';
if (!matchesAttributePart(accessibleName, { name: '', jsonPath: [], op: options.nameOp || '=', value: options.name, caseSensitive: !!options.exact }))
- return false;
+ return;
}
- return true;
- });
+ result.push(element);
+ };
+
+ const query = (root: Element | ShadowRoot | Document) => {
+ const shadows: ShadowRoot[] = [];
+ if ((root as Element).shadowRoot)
+ shadows.push((root as Element).shadowRoot!);
+ for (const element of root.querySelectorAll('*')) {
+ match(element);
+ if (element.shadowRoot)
+ shadows.push(element.shadowRoot);
+ }
+ shadows.forEach(query);
+ };
+
+ query(scope);
+ return result;
}
export function createRoleEngine(internal: boolean): SelectorEngine {
diff --git a/packages/playwright-core/src/server/injected/roleUtils.ts b/packages/playwright-core/src/server/injected/roleUtils.ts
index a42b60233e..6cecb1d5a5 100644
--- a/packages/playwright-core/src/server/injected/roleUtils.ts
+++ b/packages/playwright-core/src/server/injected/roleUtils.ts
@@ -845,51 +845,11 @@ function getAccessibleNameFromAssociatedLabels(labels: Iterable !!accessibleName).join(' ');
}
-export function getElementsByRole(document: Document, role: string): Element[] {
- if (document === cacheElementsByRoleDocument)
- return cacheElementsByRole!.get(role) || [];
- const map = calculateElementsByRoleMap(document);
- if (cachesCounter) {
- cacheElementsByRoleDocument = document;
- cacheElementsByRole = map;
- }
- return map.get(role) || [];
-}
-
-function calculateElementsByRoleMap(document: Document) {
- const result = new Map();
-
- const visit = (root: Element | ShadowRoot | Document) => {
- const shadows: ShadowRoot[] = [];
- if ((root as Element).shadowRoot)
- shadows.push((root as Element).shadowRoot!);
- for (const element of root.querySelectorAll('*')) {
- const role = getAriaRole(element);
- if (role) {
- let list = result.get(role);
- if (!list) {
- list = [];
- result.set(role, list);
- }
- list.push(element);
- }
- if (element.shadowRoot)
- shadows.push(element.shadowRoot);
- }
- shadows.forEach(visit);
- };
- visit(document);
-
- return result;
-}
-
let cacheAccessibleName: Map | undefined;
let cacheAccessibleNameHidden: Map | undefined;
let cacheIsHidden: Map | undefined;
let cachePseudoContentBefore: Map | undefined;
let cachePseudoContentAfter: Map | undefined;
-let cacheElementsByRole: Map | undefined;
-let cacheElementsByRoleDocument: Document | undefined;
let cachesCounter = 0;
export function beginAriaCaches() {
@@ -908,7 +868,5 @@ export function endAriaCaches() {
cacheIsHidden = undefined;
cachePseudoContentBefore = undefined;
cachePseudoContentAfter = undefined;
- cacheElementsByRole = undefined;
- cacheElementsByRoleDocument = undefined;
}
}
diff --git a/packages/playwright-core/src/server/injected/vueSelectorEngine.ts b/packages/playwright-core/src/server/injected/vueSelectorEngine.ts
index 0ba552d558..154f36b983 100644
--- a/packages/playwright-core/src/server/injected/vueSelectorEngine.ts
+++ b/packages/playwright-core/src/server/injected/vueSelectorEngine.ts
@@ -86,12 +86,18 @@ function buildComponentsTreeVue3(instance: VueVNode): ComponentNode {
// @see https://github.com/vuejs/devtools/blob/e7132f3392b975e39e1d9a23cf30456c270099c2/packages/app-backend-vue3/src/components/util.ts#L29
function getInstanceName(instance: VueVNode): string {
const name = getComponentTypeName(instance.type || {});
- if (name) return name;
- if (instance.root === instance) return 'Root';
- for (const key in instance.parent?.type?.components)
- if (instance.parent?.type.components[key] === instance.type) return saveComponentName(instance, key);
- for (const key in instance.appContext?.components)
- if (instance.appContext.components[key] === instance.type) return saveComponentName(instance, key);
+ if (name)
+ return name;
+ if (instance.root === instance)
+ return 'Root';
+ for (const key in instance.parent?.type?.components) {
+ if (instance.parent?.type.components[key] === instance.type)
+ return saveComponentName(instance, key);
+ }
+ for (const key in instance.appContext?.components) {
+ if (instance.appContext.components[key] === instance.type)
+ return saveComponentName(instance, key);
+ }
return 'Anonymous Component';
}
@@ -132,7 +138,8 @@ function buildComponentsTreeVue3(instance: VueVNode): ComponentNode {
// @see https://github.com/vuejs/devtools/blob/e7132f3392b975e39e1d9a23cf30456c270099c2/packages/app-backend-vue3/src/components/el.ts#L15
function getFragmentRootElements(vnode: any): Element[] {
- if (!vnode.children) return [];
+ if (!vnode.children)
+ return [];
const list = [];
diff --git a/packages/playwright-core/src/server/instrumentation.ts b/packages/playwright-core/src/server/instrumentation.ts
index 7ff19202d9..5d212279e3 100644
--- a/packages/playwright-core/src/server/instrumentation.ts
+++ b/packages/playwright-core/src/server/instrumentation.ts
@@ -36,7 +36,6 @@ export type Attribution = {
import type { CallMetadata } from '@protocol/callMetadata';
export type { CallMetadata } from '@protocol/callMetadata';
-import type * as trace from '@trace/trace';
export const kTestSdkObjects = new WeakSet();
@@ -63,7 +62,6 @@ export interface Instrumentation {
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise;
onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void;
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise;
- onEvent(sdkObject: SdkObject, event: trace.EventTraceEvent): void;
onPageOpen(page: Page): void;
onPageClose(page: Page): void;
onBrowserOpen(browser: Browser): void;
@@ -75,7 +73,6 @@ export interface InstrumentationListener {
onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise;
onCallLog?(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void;
onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise;
- onEvent?(sdkObject: SdkObject, event: trace.EventTraceEvent): void;
onPageOpen?(page: Page): void;
onPageClose?(page: Page): void;
onBrowserOpen?(browser: Browser): void;
diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts
index 359fed8360..2c3fd8f6dd 100644
--- a/packages/playwright-core/src/server/page.ts
+++ b/packages/playwright-core/src/server/page.ts
@@ -19,7 +19,7 @@ import type * as dom from './dom';
import * as frames from './frames';
import * as input from './input';
import * as js from './javascript';
-import * as network from './network';
+import type * as network from './network';
import type * as channels from '@protocol/channels';
import type { ScreenshotOptions } from './screenshotter';
import { Screenshotter, validateScreenshotOptions } from './screenshotter';
@@ -80,7 +80,7 @@ export interface PageDelegate {
getOwnerFrame(handle: dom.ElementHandle): Promise; // Returns frameId.
getContentQuads(handle: dom.ElementHandle): Promise;
setInputFiles(handle: dom.ElementHandle, files: types.FilePayload[]): Promise;
- setInputFilePaths(progress: Progress, handle: dom.ElementHandle, files: string[]): Promise;
+ setInputFilePaths(handle: dom.ElementHandle, files: string[]): Promise;
getBoundingBox(handle: dom.ElementHandle): Promise;
getFrameElement(frame: frames.Frame): Promise;
scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'error:notvisible' | 'error:notconnected' | 'done'>;
@@ -95,7 +95,7 @@ export interface PageDelegate {
// Work around Chrome's non-associated input and protocol.
inputActionEpilogue(): Promise;
// Work around for asynchronously dispatched CSP errors in Firefox.
- readonly cspErrorsAsynchronousForInlineScipts?: boolean;
+ readonly cspErrorsAsynchronousForInlineScripts?: boolean;
// Work around for mouse position in Firefox.
resetForReuse(): Promise;
// WebKit hack.
@@ -706,12 +706,9 @@ export class Page extends SdkObject {
frameNavigatedToNewDocument(frame: frames.Frame) {
this.emit(Page.Events.InternalFrameNavigatedToNewDocument, frame);
- const url = frame.url();
- if (!url.startsWith('http'))
- return;
- const purl = network.parsedURL(url);
- if (purl)
- this._browserContext.addVisitedOrigin(purl.origin);
+ const origin = frame.origin();
+ if (origin)
+ this._browserContext.addVisitedOrigin(origin);
}
allBindings() {
diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts
index 997e1244d2..d5f84ba625 100644
--- a/packages/playwright-core/src/server/recorder.ts
+++ b/packages/playwright-core/src/server/recorder.ts
@@ -626,13 +626,8 @@ class ContextRecorder extends EventEmitter {
callMetadata.endTime = monotonicTime();
await frame.instrumentation.onAfterCall(frame, callMetadata);
- const timer = setTimeout(() => {
- // Commit the action after 5 seconds so that no further signals are added to it.
- actionInContext.committed = true;
- this._timers.delete(timer);
- }, 5000);
+ this._setCommittedAfterTimeout(actionInContext);
this._generator.didPerformAction(actionInContext);
- this._timers.add(timer);
};
const kActionTimeout = 5000;
@@ -664,9 +659,19 @@ class ContextRecorder extends EventEmitter {
frame: frameDescription,
action
};
+ this._setCommittedAfterTimeout(actionInContext);
this._generator.addAction(actionInContext);
}
+ private _setCommittedAfterTimeout(actionInContext: ActionInContext) {
+ const timer = setTimeout(() => {
+ // Commit the action after 5 seconds so that no further signals are added to it.
+ actionInContext.committed = true;
+ this._timers.delete(timer);
+ }, isUnderTest() ? 500 : 5000);
+ this._timers.add(timer);
+ }
+
private _onFrameNavigated(frame: Frame, page: Page) {
const pageAlias = this._pageAliases.get(page);
this._generator.signal(pageAlias!, frame, { name: 'navigation', url: frame.url() });
diff --git a/packages/playwright-core/src/server/recorder/csharp.ts b/packages/playwright-core/src/server/recorder/csharp.ts
index 46fadc244a..2b42ae4e2c 100644
--- a/packages/playwright-core/src/server/recorder/csharp.ts
+++ b/packages/playwright-core/src/server/recorder/csharp.ts
@@ -164,6 +164,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
const assertion = action.value ? `ToHaveValueAsync(${quote(action.value)})` : `ToBeEmptyAsync()`;
return `await Expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
}
+ case 'assertScreenshot':
+ return `// AssertScreenshot(await ${subject}.ScreenshotAsync());`;
}
}
diff --git a/packages/playwright-core/src/server/recorder/java.ts b/packages/playwright-core/src/server/recorder/java.ts
index d4ebfdeea4..384584d272 100644
--- a/packages/playwright-core/src/server/recorder/java.ts
+++ b/packages/playwright-core/src/server/recorder/java.ts
@@ -152,6 +152,8 @@ export class JavaLanguageGenerator implements LanguageGenerator {
const assertion = action.value ? `hasValue(${quote(action.value)})` : `isEmpty()`;
return `assertThat(${subject}.${this._asLocator(action.selector, inFrameLocator)}).${assertion};`;
}
+ case 'assertScreenshot':
+ return `// assertScreenshot(${subject}.screenshot());`;
}
}
diff --git a/packages/playwright-core/src/server/recorder/javascript.ts b/packages/playwright-core/src/server/recorder/javascript.ts
index 548e0f6071..e30c6c6e59 100644
--- a/packages/playwright-core/src/server/recorder/javascript.ts
+++ b/packages/playwright-core/src/server/recorder/javascript.ts
@@ -135,6 +135,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
const assertion = action.value ? `toHaveValue(${quote(action.value)})` : `toBeEmpty()`;
return `${this._isTest ? '' : '// '}await expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
}
+ case 'assertScreenshot':
+ return `${this._isTest ? '' : '// '}await expect(${subject}).toHaveScreenshot();`;
}
}
diff --git a/packages/playwright-core/src/server/recorder/python.ts b/packages/playwright-core/src/server/recorder/python.ts
index b00e02178c..d9ddc0fd9c 100644
--- a/packages/playwright-core/src/server/recorder/python.ts
+++ b/packages/playwright-core/src/server/recorder/python.ts
@@ -73,7 +73,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
if (signals.dialog)
formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`);
- let code = `${this._awaitPrefix}${this._generateActionCall(subject, action)}`;
+ let code = this._generateActionCall(subject, action);
if (signals.popup) {
code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as ${signals.popup.popupAlias}_info {
@@ -99,7 +99,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
case 'openPage':
throw Error('Not reached');
case 'closePage':
- return `${subject}.close()`;
+ return `${this._awaitPrefix}${subject}.close()`;
case 'click': {
let method = 'click';
if (action.clickCount === 2)
@@ -115,35 +115,37 @@ export class PythonLanguageGenerator implements LanguageGenerator {
if (action.position)
options.position = action.position;
const optionsString = formatOptions(options, false);
- return `${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`;
+ return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`;
}
case 'check':
- return `${subject}.${this._asLocator(action.selector)}.check()`;
+ return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.check()`;
case 'uncheck':
- return `${subject}.${this._asLocator(action.selector)}.uncheck()`;
+ return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.uncheck()`;
case 'fill':
- return `${subject}.${this._asLocator(action.selector)}.fill(${quote(action.text)})`;
+ return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.fill(${quote(action.text)})`;
case 'setInputFiles':
- return `${subject}.${this._asLocator(action.selector)}.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`;
+ return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`;
case 'press': {
const modifiers = toModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+');
- return `${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)})`;
+ return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)})`;
}
case 'navigate':
- return `${subject}.goto(${quote(action.url)})`;
+ return `${this._awaitPrefix}${subject}.goto(${quote(action.url)})`;
case 'select':
- return `${subject}.${this._asLocator(action.selector)}.select_option(${formatValue(action.options.length === 1 ? action.options[0] : action.options)})`;
+ return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.select_option(${formatValue(action.options.length === 1 ? action.options[0] : action.options)})`;
case 'assertText':
- return `expect(${subject}.${this._asLocator(action.selector)}).${action.substring ? 'to_contain_text' : 'to_have_text'}(${quote(action.text)})`;
+ return `${this._awaitPrefix}expect(${subject}.${this._asLocator(action.selector)}).${action.substring ? 'to_contain_text' : 'to_have_text'}(${quote(action.text)})`;
case 'assertChecked':
- return `expect(${subject}.${this._asLocator(action.selector)}).${action.checked ? 'to_be_checked()' : 'not_to_be_checked()'}`;
+ return `${this._awaitPrefix}expect(${subject}.${this._asLocator(action.selector)}).${action.checked ? 'to_be_checked()' : 'not_to_be_checked()'}`;
case 'assertVisible':
- return `expect(${subject}.${this._asLocator(action.selector)}).to_be_visible()`;
+ return `${this._awaitPrefix}expect(${subject}.${this._asLocator(action.selector)}).to_be_visible()`;
case 'assertValue': {
const assertion = action.value ? `to_have_value(${quote(action.value)})` : `to_be_empty()`;
- return `expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
+ return `${this._awaitPrefix}expect(${subject}.${this._asLocator(action.selector)}).${assertion}`;
}
+ case 'assertScreenshot':
+ return `# assert_screenshot(${this._awaitPrefix}${subject}.screenshot())`;
}
}
@@ -162,7 +164,7 @@ def browser_context_args(browser_context_args, playwright) {
return {${contextOptions}}
}
` : '';
- formatter.add(`${options.deviceName ? 'import pytest\n' : ''}
+ formatter.add(`${options.deviceName ? 'import pytest\n' : ''}import re
from playwright.sync_api import Page, expect
${fixture}
@@ -170,7 +172,7 @@ def test_example(page: Page) -> None {`);
} else if (this._isAsync) {
formatter.add(`
import asyncio
-
+import re
from playwright.async_api import Playwright, async_playwright, expect
@@ -179,6 +181,7 @@ async def run(playwright: Playwright) -> None {
context = await browser.new_context(${formatContextOptions(options.contextOptions, options.deviceName)})`);
} else {
formatter.add(`
+import re
from playwright.sync_api import Playwright, sync_playwright, expect
diff --git a/packages/playwright-core/src/server/recorder/recorderActions.ts b/packages/playwright-core/src/server/recorder/recorderActions.ts
index 3c9720cbc4..ff8c168dbc 100644
--- a/packages/playwright-core/src/server/recorder/recorderActions.ts
+++ b/packages/playwright-core/src/server/recorder/recorderActions.ts
@@ -30,6 +30,7 @@ export type ActionName =
'assertText' |
'assertValue' |
'assertChecked' |
+ 'assertScreenshot' |
'assertVisible';
export type ActionBase = {
@@ -119,7 +120,11 @@ export type AssertVisibleAction = ActionBase & {
selector: string,
};
-export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction;
+export type AssertScreenshotAction = ActionBase & {
+ name: 'assertScreenshot',
+};
+
+export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction | AssertScreenshotAction;
export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction | AssertVisibleAction;
// Signals.
diff --git a/packages/playwright-core/src/server/registry/index.ts b/packages/playwright-core/src/server/registry/index.ts
index fa3a1bbf78..c3547c1118 100644
--- a/packages/playwright-core/src/server/registry/index.ts
+++ b/packages/playwright-core/src/server/registry/index.ts
@@ -121,29 +121,6 @@ const DOWNLOAD_PATHS: Record = {
'mac13-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac-arm64.zip',
'win64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-win64.zip',
},
- 'chromium-with-symbols': {
- '': undefined,
- 'ubuntu18.04-x64': undefined,
- 'ubuntu20.04-x64': 'builds/chromium/%s/chromium-with-symbols-linux.zip',
- 'ubuntu22.04-x64': 'builds/chromium/%s/chromium-with-symbols-linux.zip',
- 'ubuntu18.04-arm64': undefined,
- 'ubuntu20.04-arm64': 'builds/chromium/%s/chromium-with-symbols-linux-arm64.zip',
- 'ubuntu22.04-arm64': 'builds/chromium/%s/chromium-with-symbols-linux-arm64.zip',
- 'debian11-x64': 'builds/chromium/%s/chromium-with-symbols-linux.zip',
- 'debian11-arm64': 'builds/chromium/%s/chromium-with-symbols-linux-arm64.zip',
- 'debian12-x64': 'builds/chromium/%s/chromium-with-symbols-linux.zip',
- 'debian12-arm64': 'builds/chromium/%s/chromium-with-symbols-linux-arm64.zip',
- 'mac10.13': 'builds/chromium/%s/chromium-with-symbols-mac.zip',
- 'mac10.14': 'builds/chromium/%s/chromium-with-symbols-mac.zip',
- 'mac10.15': 'builds/chromium/%s/chromium-with-symbols-mac.zip',
- 'mac11': 'builds/chromium/%s/chromium-with-symbols-mac.zip',
- 'mac11-arm64': 'builds/chromium/%s/chromium-with-symbols-mac-arm64.zip',
- 'mac12': 'builds/chromium/%s/chromium-with-symbols-mac.zip',
- 'mac12-arm64': 'builds/chromium/%s/chromium-with-symbols-mac-arm64.zip',
- 'mac13': 'builds/chromium/%s/chromium-with-symbols-mac.zip',
- 'mac13-arm64': 'builds/chromium/%s/chromium-with-symbols-mac-arm64.zip',
- 'win64': 'builds/chromium/%s/chromium-with-symbols-win64.zip',
- },
'firefox': {
'': undefined,
'ubuntu18.04-x64': undefined,
@@ -368,9 +345,9 @@ function readDescriptors(browsersJSON: BrowsersJSON) {
}
export type BrowserName = 'chromium' | 'firefox' | 'webkit';
-type InternalTool = 'ffmpeg' | 'firefox-beta' | 'firefox-asan' | 'chromium-with-symbols' | 'chromium-tip-of-tree' | 'android';
+type InternalTool = 'ffmpeg' | 'firefox-beta' | 'firefox-asan' | 'chromium-tip-of-tree' | 'android';
type ChromiumChannel = 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary' | 'msedge' | 'msedge-beta' | 'msedge-dev' | 'msedge-canary';
-const allDownloadable = ['chromium', 'firefox', 'webkit', 'ffmpeg', 'firefox-beta', 'chromium-with-symbols', 'chromium-tip-of-tree'];
+const allDownloadable = ['chromium', 'firefox', 'webkit', 'ffmpeg', 'firefox-beta', 'chromium-tip-of-tree'];
export interface Executable {
type: 'browser' | 'tool' | 'channel';
@@ -453,24 +430,6 @@ export class Registry {
_isHermeticInstallation: true,
});
- const chromiumWithSymbols = descriptors.find(d => d.name === 'chromium-with-symbols')!;
- const chromiumWithSymbolsExecutable = findExecutablePath(chromiumWithSymbols.dir, 'chromium');
- this._executables.push({
- type: 'tool',
- name: 'chromium-with-symbols',
- browserName: 'chromium',
- directory: chromiumWithSymbols.dir,
- executablePath: () => chromiumWithSymbolsExecutable,
- executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('chromium-with-symbols', chromiumWithSymbolsExecutable, chromiumWithSymbols.installByDefault, sdkLanguage),
- installType: chromiumWithSymbols.installByDefault ? 'download-by-default' : 'download-on-demand',
- _validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, 'chromium', chromiumWithSymbols.dir, ['chrome-linux'], [], ['chrome-win']),
- downloadURLs: this._downloadURLs(chromiumWithSymbols),
- browserVersion: chromiumWithSymbols.browserVersion,
- _install: () => this._downloadExecutable(chromiumWithSymbols, chromiumWithSymbolsExecutable),
- _dependencyGroup: 'chromium',
- _isHermeticInstallation: true,
- });
-
const chromiumTipOfTree = descriptors.find(d => d.name === 'chromium-tip-of-tree')!;
const chromiumTipOfTreeExecutable = findExecutablePath(chromiumTipOfTree.dir, 'chromium');
this._executables.push({
diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts
index 52fe777134..774cd24163 100644
--- a/packages/playwright-core/src/server/trace/recorder/tracing.ts
+++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts
@@ -43,6 +43,7 @@ import { Snapshotter } from './snapshotter';
import { yazl } from '../../../zipBundle';
import type { ConsoleMessage } from '../../console';
import { Dispatcher } from '../../dispatchers/dispatcher';
+import { serializeError } from '../../errors';
const version: trace.VERSION = 6;
@@ -182,6 +183,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
this._context.instrumentation.addListener(this, this._context);
this._eventListeners.push(
eventsHelper.addEventListener(this._context, BrowserContext.Events.Console, this._onConsoleMessage.bind(this)),
+ eventsHelper.addEventListener(this._context, BrowserContext.Events.PageError, this._onPageError.bind(this)),
);
if (this._state.options.screenshots)
this._startScreencast();
@@ -396,18 +398,6 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
return this._captureSnapshot(event.afterSnapshot, sdkObject, metadata);
}
- onEvent(sdkObject: SdkObject, event: trace.EventTraceEvent) {
- if (!sdkObject.attribution.context)
- return;
- if (event.method === 'console' ||
- (event.method === '__create__' && event.class === 'ConsoleMessage') ||
- (event.method === '__create__' && event.class === 'JSHandle')) {
- // Console messages are handled separately.
- return;
- }
- this._appendTraceEvent(event);
- }
-
onEntryStarted(entry: har.Entry) {
this._pendingHarEntries.add(entry);
}
@@ -456,6 +446,18 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
this._appendTraceEvent(event);
}
+ private _onPageError(error: Error, page: Page) {
+ const event: trace.EventTraceEvent = {
+ type: 'event',
+ time: monotonicTime(),
+ class: 'BrowserContext',
+ method: 'pageError',
+ params: { error: serializeError(error) },
+ pageId: page.guid,
+ };
+ this._appendTraceEvent(event);
+ }
+
private _startScreencastInPage(page: Page) {
page.setScreencastOptions(kScreencastOptions);
const prefix = page.guid;
diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts
index f328ec6f05..aa6ef60271 100644
--- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts
+++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts
@@ -17,34 +17,35 @@
import path from 'path';
import fs from 'fs';
import { HttpServer } from '../../../utils/httpServer';
-import { createGuid, gracefullyProcessExitDoNotHang, isUnderTest } from '../../../utils';
+import type { Transport } from '../../../utils/httpServer';
+import { gracefullyProcessExitDoNotHang, isUnderTest } from '../../../utils';
import { syncLocalStorageWithSettings } from '../../launchApp';
import { serverSideCallMetadata } from '../../instrumentation';
import { createPlaywright } from '../../playwright';
import { ProgressController } from '../../progress';
-import { open, wsServer } from '../../../utilsBundle';
+import { open } from '../../../utilsBundle';
import type { Page } from '../../page';
import type { BrowserType } from '../../browserType';
import { launchApp } from '../../launchApp';
-export type Transport = {
- sendEvent?: (method: string, params: any) => void;
- dispatch: (method: string, params: any) => Promise;
- close?: () => void;
- onclose: () => void;
-};
-
-export type OpenTraceViewerOptions = {
- app?: string;
- headless?: boolean;
+export type TraceViewerServerOptions = {
host?: string;
port?: number;
isServer?: boolean;
transport?: Transport;
+};
+
+export type TraceViewerRedirectOptions = {
+ webApp?: string;
+ isServer?: boolean;
+};
+
+export type TraceViewerAppOptions = {
+ headless?: boolean;
persistentContextOptions?: Parameters[2];
};
-async function startTraceViewerServer(traceUrls: string[], options?: OpenTraceViewerOptions): Promise<{ server: HttpServer, url: string }> {
+async function validateTraceUrls(traceUrls: string[]) {
for (const traceUrl of traceUrls) {
let traceFile = traceUrl;
// If .json is requested, we'll synthesize it.
@@ -54,10 +55,13 @@ async function startTraceViewerServer(traceUrls: string[], options?: OpenTraceVi
if (!traceUrl.startsWith('http://') && !traceUrl.startsWith('https://') && !fs.existsSync(traceFile) && !fs.existsSync(traceFile + '.trace')) {
// eslint-disable-next-line no-console
console.error(`Trace file ${traceUrl} does not exist!`);
- gracefullyProcessExitDoNotHang(1);
+ return false;
}
}
+ return true;
+}
+export async function startTraceViewerServer(options?: TraceViewerServerOptions): Promise {
const server = new HttpServer();
server.routePrefix('/trace', (request, response) => {
const url = new URL('http://localhost' + request.url!);
@@ -88,36 +92,25 @@ async function startTraceViewerServer(traceUrls: string[], options?: OpenTraceVi
return server.serveFile(request, response, absolutePath);
});
- const params = traceUrls.map(t => `trace=${encodeURIComponent(t)}`);
const transport = options?.transport || (options?.isServer ? new StdinServer() : undefined);
+ if (transport)
+ server.createWebSocket(transport);
- if (transport) {
- const guid = createGuid();
- params.push('ws=' + guid);
- const wss = new wsServer({ server: server.server(), path: '/' + guid });
- wss.on('connection', ws => {
- transport.sendEvent = (method, params) => ws.send(JSON.stringify({ method, params }));
- transport.close = () => ws.close();
- ws.on('message', async (message: string) => {
- const { id, method, params } = JSON.parse(message);
- const result = await transport.dispatch(method, params);
- ws.send(JSON.stringify({ id, result }));
- });
- ws.on('close', () => transport.onclose());
- ws.on('error', () => transport.onclose());
- });
- }
+ const { host, port } = options || {};
+ await server.start({ preferredPort: port, host });
+ return server;
+}
+export async function installRootRedirect(server: HttpServer, traceUrls: string[], options: TraceViewerRedirectOptions) {
+ const params = (traceUrls || []).map(t => `trace=${encodeURIComponent(t)}`);
+ if (server.wsGuid())
+ params.push('ws=' + server.wsGuid());
if (options?.isServer)
params.push('isServer');
if (isUnderTest())
params.push('isUnderTest=true');
-
- const { host, port } = options || {};
- const url = await server.start({ preferredPort: port, host });
- const { app } = options || {};
const searchQuery = params.length ? '?' + params.join('&') : '';
- const urlPath = `/trace/${app || 'index.html'}${searchQuery}`;
+ const urlPath = `/trace/${options.webApp || 'index.html'}${searchQuery}`;
server.routePath('/', (request, response) => {
response.statusCode = 302;
@@ -125,12 +118,28 @@ async function startTraceViewerServer(traceUrls: string[], options?: OpenTraceVi
response.end();
return true;
});
-
- return { server, url };
}
-export async function openTraceViewerApp(traceUrls: string[], browserName: string, options?: OpenTraceViewerOptions): Promise {
- const { url } = await startTraceViewerServer(traceUrls, options);
+export async function runTraceViewerApp(traceUrls: string[], browserName: string, options: TraceViewerServerOptions & { headless?: boolean }, exitOnClose?: boolean) {
+ if (!validateTraceUrls(traceUrls))
+ return;
+ const server = await startTraceViewerServer(options);
+ await installRootRedirect(server, traceUrls, options);
+ const page = await openTraceViewerApp(server.urlPrefix(), browserName, options);
+ if (exitOnClose)
+ page.on('close', () => gracefullyProcessExitDoNotHang(0));
+ return page;
+}
+
+export async function runTraceInBrowser(traceUrls: string[], options: TraceViewerServerOptions) {
+ if (!validateTraceUrls(traceUrls))
+ return;
+ const server = await startTraceViewerServer(options);
+ await installRootRedirect(server, traceUrls, options);
+ await openTraceInBrowser(server.urlPrefix());
+}
+
+export async function openTraceViewerApp(url: string, browserName: string, options?: TraceViewerAppOptions): Promise {
const traceViewerPlaywright = createPlaywright({ sdkLanguage: 'javascript', isInternalPlaywright: true });
const traceViewerBrowser = isUnderTest() ? 'chromium' : browserName;
@@ -141,7 +150,7 @@ export async function openTraceViewerApp(traceUrls: string[], browserName: strin
persistentContextOptions: {
...options?.persistentContextOptions,
useWebSocket: isUnderTest(),
- headless: options?.headless,
+ headless: !!options?.headless,
},
});
@@ -163,8 +172,7 @@ export async function openTraceViewerApp(traceUrls: string[], browserName: strin
return page;
}
-export async function openTraceInBrowser(traceUrls: string[], options?: OpenTraceViewerOptions) {
- const { url } = await startTraceViewerServer(traceUrls, options);
+export async function openTraceInBrowser(url: string) {
// eslint-disable-next-line no-console
console.log('\nListening on ' + url);
if (!isUnderTest())
@@ -202,10 +210,10 @@ class StdinServer implements Transport {
sendEvent?: (method: string, params: any) => void;
close?: () => void;
- private _loadTrace(url: string) {
- this._traceUrl = url;
+ private _loadTrace(traceUrl: string) {
+ this._traceUrl = traceUrl;
clearTimeout(this._pollTimer);
- this.sendEvent?.('loadTrace', { url });
+ this.sendEvent?.('loadTraceRequested', { traceUrl });
}
private _pollLoadTrace(url: string) {
diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts
index 038010f456..e0326072f7 100644
--- a/packages/playwright-core/src/server/webkit/wkPage.ts
+++ b/packages/playwright-core/src/server/webkit/wkPage.ts
@@ -966,7 +966,7 @@ export class WKPage implements PageDelegate {
await this._session.send('DOM.setInputFiles', { objectId, files: protocolFiles });
}
- async setInputFilePaths(progress: Progress, handle: dom.ElementHandle, paths: string[]): Promise {
+ async setInputFilePaths(handle: dom.ElementHandle, paths: string[]): Promise {
const pageProxyId = this._pageProxySession.sessionId;
const objectId = handle._objectId;
await Promise.all([
diff --git a/packages/playwright-core/src/utils/httpServer.ts b/packages/playwright-core/src/utils/httpServer.ts
index 32901eb3d4..32012e6f96 100644
--- a/packages/playwright-core/src/utils/httpServer.ts
+++ b/packages/playwright-core/src/utils/httpServer.ts
@@ -17,19 +17,28 @@
import type http from 'http';
import fs from 'fs';
import path from 'path';
-import { mime } from '../utilsBundle';
+import { mime, wsServer } from '../utilsBundle';
import { assert } from './debug';
import { createHttpServer } from './network';
import { ManualPromise } from './manualPromise';
+import { createGuid } from './crypto';
export type ServerRouteHandler = (request: http.IncomingMessage, response: http.ServerResponse) => boolean;
+export type Transport = {
+ sendEvent?: (method: string, params: any) => void;
+ dispatch: (method: string, params: any) => Promise;
+ close?: () => void;
+ onclose: () => void;
+};
+
export class HttpServer {
private _server: http.Server;
private _urlPrefix: string;
private _port: number = 0;
private _started = false;
private _routes: { prefix?: string, exact?: string, handler: ServerRouteHandler }[] = [];
+ private _wsGuid: string | undefined;
constructor(address: string = '') {
this._urlPrefix = address;
@@ -68,6 +77,31 @@ export class HttpServer {
}
}
+ createWebSocket(transport: Transport, guid?: string) {
+ assert(!this._wsGuid, 'can only create one main websocket transport per server');
+ this._wsGuid = guid || createGuid();
+ const wss = new wsServer({ server: this._server, path: '/' + this._wsGuid });
+ wss.on('connection', ws => {
+ transport.sendEvent = (method, params) => ws.send(JSON.stringify({ method, params }));
+ transport.close = () => ws.close();
+ ws.on('message', async message => {
+ const { id, method, params } = JSON.parse(String(message));
+ try {
+ const result = await transport.dispatch(method, params);
+ ws.send(JSON.stringify({ id, result }));
+ } catch (e) {
+ ws.send(JSON.stringify({ id, error: String(e) }));
+ }
+ });
+ ws.on('close', () => transport.onclose());
+ ws.on('error', () => transport.onclose());
+ });
+ }
+
+ wsGuid(): string | undefined {
+ return this._wsGuid;
+ }
+
async start(options: { port?: number, preferredPort?: number, host?: string } = {}): Promise {
assert(!this._started, 'server already started');
this._started = true;
diff --git a/packages/playwright-core/src/utils/isomorphic/cssTokenizer.ts b/packages/playwright-core/src/utils/isomorphic/cssTokenizer.ts
index 12fa08e80d..f72ef27eb4 100644
--- a/packages/playwright-core/src/utils/isomorphic/cssTokenizer.ts
+++ b/packages/playwright-core/src/utils/isomorphic/cssTokenizer.ts
@@ -48,8 +48,10 @@ function preprocess(str: string): number[] {
if (code === 0xd && str.charCodeAt(i + 1) === 0xa) {
code = 0xa; i++;
}
- if (code === 0xd || code === 0xc) code = 0xa;
- if (code === 0x0) code = 0xfffd;
+ if (code === 0xd || code === 0xc)
+ code = 0xa;
+ if (code === 0x0)
+ code = 0xfffd;
if (between(code, 0xd800, 0xdbff) && between(str.charCodeAt(i + 1), 0xdc00, 0xdfff)) {
// Decode a surrogate pair into an astral codepoint.
const lead = code - 0xd800;
@@ -63,7 +65,8 @@ function preprocess(str: string): number[] {
}
function stringFromCode(code: number) {
- if (code <= 0xffff) return String.fromCharCode(code);
+ if (code <= 0xffff)
+ return String.fromCharCode(code);
// Otherwise, encode astral char as surrogate pair.
code -= Math.pow(2, 16);
const lead = Math.floor(code / Math.pow(2, 10)) + 0xd800;
@@ -107,8 +110,10 @@ export function tokenize(str1: string): CSSTokenInterface[] {
num = 1;
i += num;
code = codepoint(i);
- if (newline(code)) incrLineno();
- else column += num;
+ if (newline(code))
+ incrLineno();
+ else
+ column += num;
// console.log('Consume '+i+' '+String.fromCharCode(code) + ' 0x' + code.toString(16));
return true;
};
@@ -125,7 +130,8 @@ export function tokenize(str1: string): CSSTokenInterface[] {
return true;
};
const eof = function(codepoint?: number): boolean {
- if (codepoint === undefined) codepoint = code;
+ if (codepoint === undefined)
+ codepoint = code;
return codepoint === -1;
};
const donothing = function() { };
@@ -138,12 +144,14 @@ export function tokenize(str1: string): CSSTokenInterface[] {
consumeComments();
consume();
if (whitespace(code)) {
- while (whitespace(next())) consume();
+ while (whitespace(next()))
+ consume();
return new WhitespaceToken();
} else if (code === 0x22) {return consumeAStringToken();} else if (code === 0x23) {
if (namechar(next()) || areAValidEscape(next(1), next(2))) {
const token = new HashToken('');
- if (wouldStartAnIdentifier(next(1), next(2), next(3))) token.type = 'id';
+ if (wouldStartAnIdentifier(next(1), next(2), next(3)))
+ token.type = 'id';
token.value = consumeAName();
return token;
} else {
@@ -288,7 +296,8 @@ export function tokenize(str1: string): CSSTokenInterface[] {
const str = consumeAName();
if (str.toLowerCase() === 'url' && next() === 0x28) {
consume();
- while (whitespace(next(1)) && whitespace(next(2))) consume();
+ while (whitespace(next(1)) && whitespace(next(2)))
+ consume();
if (next() === 0x22 || next() === 0x27)
return new FunctionToken(str);
else if (whitespace(next()) && (next(2) === 0x22 || next(2) === 0x27))
@@ -305,7 +314,8 @@ export function tokenize(str1: string): CSSTokenInterface[] {
};
const consumeAStringToken = function(endingCodePoint?: number): CSSParserToken {
- if (endingCodePoint === undefined) endingCodePoint = code;
+ if (endingCodePoint === undefined)
+ endingCodePoint = code;
let string = '';
while (consume()) {
if (code === endingCodePoint || eof()) {
@@ -331,13 +341,16 @@ export function tokenize(str1: string): CSSTokenInterface[] {
const consumeAURLToken = function(): CSSTokenInterface {
const token = new URLToken('');
- while (whitespace(next())) consume();
- if (eof(next())) return token;
+ while (whitespace(next()))
+ consume();
+ if (eof(next()))
+ return token;
while (consume()) {
if (code === 0x29 || eof()) {
return token;
} else if (whitespace(code)) {
- while (whitespace(next())) consume();
+ while (whitespace(next()))
+ consume();
if (next() === 0x29 || eof(next())) {
consume();
return token;
@@ -379,9 +392,11 @@ export function tokenize(str1: string): CSSTokenInterface[] {
break;
}
}
- if (whitespace(next())) consume();
+ if (whitespace(next()))
+ consume();
let value = parseInt(digits.map(function(x) { return String.fromCharCode(x); }).join(''), 16);
- if (value > maximumallowedcodepoint) value = 0xfffd;
+ if (value > maximumallowedcodepoint)
+ value = 0xfffd;
return value;
} else if (eof()) {
return 0xfffd;
@@ -391,8 +406,10 @@ export function tokenize(str1: string): CSSTokenInterface[] {
};
const areAValidEscape = function(c1: number, c2: number) {
- if (c1 !== 0x5c) return false;
- if (newline(c2)) return false;
+ if (c1 !== 0x5c)
+ return false;
+ if (newline(c2))
+ return false;
return true;
};
const startsWithAValidEscape = function() {
@@ -416,11 +433,14 @@ export function tokenize(str1: string): CSSTokenInterface[] {
const wouldStartANumber = function(c1: number, c2: number, c3: number) {
if (c1 === 0x2b || c1 === 0x2d) {
- if (digit(c2)) return true;
- if (c2 === 0x2e && digit(c3)) return true;
+ if (digit(c2))
+ return true;
+ if (c2 === 0x2e && digit(c3))
+ return true;
return false;
} else if (c1 === 0x2e) {
- if (digit(c2)) return true;
+ if (digit(c2))
+ return true;
return false;
} else if (digit(c1)) {
return true;
@@ -519,7 +539,8 @@ export function tokenize(str1: string): CSSTokenInterface[] {
while (!eof(next())) {
tokens.push(consumeAToken());
iterationCount++;
- if (iterationCount > str.length * 2) throw new Error("I'm infinite-looping!");
+ if (iterationCount > str.length * 2)
+ throw new Error("I'm infinite-looping!");
}
return tokens;
}
diff --git a/packages/playwright-core/src/utils/network.ts b/packages/playwright-core/src/utils/network.ts
index bf834abaa0..8cacb8aeb6 100644
--- a/packages/playwright-core/src/utils/network.ts
+++ b/packages/playwright-core/src/utils/network.ts
@@ -181,9 +181,8 @@ export async function isURLAvailable(url: URL, ignoreHTTPSErrors: boolean, onLog
async function httpStatusCode(url: URL, ignoreHTTPSErrors: boolean, onLog?: (data: string) => void, onStdErr?: (data: string) => void): Promise {
return new Promise(resolve => {
- onLog?.(`HTTP HEAD: ${url}`);
+ onLog?.(`HTTP GET: ${url}`);
httpRequest({
- method: 'HEAD',
url: url.toString(),
headers: { Accept: '*/*' },
rejectUnauthorized: !ignoreHTTPSErrors
diff --git a/packages/playwright-core/src/utils/timeoutRunner.ts b/packages/playwright-core/src/utils/timeoutRunner.ts
index fc4db8aed9..622019565a 100644
--- a/packages/playwright-core/src/utils/timeoutRunner.ts
+++ b/packages/playwright-core/src/utils/timeoutRunner.ts
@@ -14,108 +14,22 @@
* limitations under the License.
*/
-import { ManualPromise } from './manualPromise';
import { monotonicTime } from './';
-export class TimeoutRunnerError extends Error {}
-
-type TimeoutRunnerData = {
- lastElapsedSync: number,
- timer: NodeJS.Timeout | undefined,
- timeoutPromise: ManualPromise,
-};
-
-export const MaxTime = 2147483647; // 2^31-1
-
-export class TimeoutRunner {
- private _running: TimeoutRunnerData | undefined;
- private _timeout: number;
- private _elapsed: number;
- private _deadline = MaxTime;
-
- constructor(timeout: number) {
- this._timeout = timeout;
- this._elapsed = 0;
- }
-
- async run(cb: () => Promise): Promise {
- const running = this._running = {
- lastElapsedSync: monotonicTime(),
- timer: undefined,
- timeoutPromise: new ManualPromise(),
- };
- try {
- const resultPromise = Promise.race([
- cb(),
- running.timeoutPromise
- ]);
- this._updateTimeout(running, this._timeout);
- return await resultPromise;
- } finally {
- this._updateTimeout(running, 0);
- if (this._running === running)
- this._running = undefined;
- }
- }
-
- interrupt() {
- if (this._running)
- this._updateTimeout(this._running, -1);
- }
-
- elapsed() {
- this._syncElapsedAndStart();
- return this._elapsed;
- }
-
- deadline(): number {
- return this._deadline;
- }
-
- updateTimeout(timeout: number, elapsed?: number) {
- this._timeout = timeout;
- if (elapsed !== undefined) {
- this._syncElapsedAndStart();
- this._elapsed = elapsed;
- }
- if (this._running)
- this._updateTimeout(this._running, timeout);
- }
-
- private _syncElapsedAndStart() {
- if (this._running) {
- const now = monotonicTime();
- this._elapsed += now - this._running.lastElapsedSync;
- this._running.lastElapsedSync = now;
- }
- }
-
- private _updateTimeout(running: TimeoutRunnerData, timeout: number) {
- if (running.timer) {
- clearTimeout(running.timer);
- running.timer = undefined;
- }
- this._syncElapsedAndStart();
- this._deadline = timeout ? monotonicTime() + timeout : MaxTime;
- if (timeout === 0)
- return;
- timeout = timeout - this._elapsed;
- if (timeout <= 0)
- running.timeoutPromise.reject(new TimeoutRunnerError());
- else
- running.timer = setTimeout(() => running.timeoutPromise.reject(new TimeoutRunnerError()), timeout);
- }
-}
-
export async function raceAgainstDeadline(cb: () => Promise, deadline: number): Promise<{ result: T, timedOut: false } | { timedOut: true }> {
- const runner = new TimeoutRunner((deadline || MaxTime) - monotonicTime());
- try {
- return { result: await runner.run(cb), timedOut: false };
- } catch (e) {
- if (e instanceof TimeoutRunnerError)
- return { timedOut: true };
- throw e;
- }
+ let timer: NodeJS.Timeout | undefined;
+ return Promise.race([
+ cb().then(result => {
+ return { result, timedOut: false };
+ }),
+ new Promise<{ timedOut: true }>(resolve => {
+ const kMaxDeadline = 2147483647; // 2^31-1
+ const timeout = (deadline || kMaxDeadline) - monotonicTime();
+ timer = setTimeout(() => resolve({ timedOut: true }), timeout);
+ }),
+ ]).finally(() => {
+ clearTimeout(timer);
+ });
}
export async function pollAgainstDeadline(callback: () => Promise<{ continuePolling: boolean, result: T }>, deadline: number, pollIntervals: number[] = [100, 250, 500, 1000]): Promise<{ result?: T, timedOut: boolean }> {
diff --git a/packages/playwright-core/types/protocol.d.ts b/packages/playwright-core/types/protocol.d.ts
index 83b8d235ce..0ea475d12e 100644
--- a/packages/playwright-core/types/protocol.d.ts
+++ b/packages/playwright-core/types/protocol.d.ts
@@ -831,7 +831,7 @@ CORS RFC1918 enforcement.
resourceIPAddressSpace?: Network.IPAddressSpace;
clientSecurityState?: Network.ClientSecurityState;
}
- export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation";
+ export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation"|"InvalidInfoHeader"|"NoRegisterSourceHeader"|"NoRegisterTriggerHeader"|"NoRegisterOsSourceHeader"|"NoRegisterOsTriggerHeader";
/**
* Details for issues around "Attribution Reporting API" usage.
Explainer: https://github.com/WICG/attribution-reporting-api
@@ -2493,6 +2493,28 @@ stylesheet rules) this rule came from.
*/
tryRules: CSSTryRule[];
}
+ /**
+ * CSS @position-try rule representation.
+ */
+ export interface CSSPositionTryRule {
+ /**
+ * The prelude dashed-ident name
+ */
+ name: Value;
+ /**
+ * The css style sheet identifier (absent for user agent stylesheet and user-specified
+stylesheet rules) this rule came from.
+ */
+ styleSheetId?: StyleSheetId;
+ /**
+ * Parent stylesheet's origin.
+ */
+ origin: StyleSheetOrigin;
+ /**
+ * Associated style declaration.
+ */
+ style: CSSStyle;
+ }
/**
* CSS keyframes rule representation.
*/
@@ -2820,6 +2842,10 @@ attributes) for a DOM node identified by `nodeId`.
* A list of CSS position fallbacks matching this node.
*/
cssPositionFallbackRules?: CSSPositionFallbackRule[];
+ /**
+ * A list of CSS @position-try rules matching this node, based on the position-try-options property.
+ */
+ cssPositionTryRules?: CSSPositionTryRule[];
/**
* A list of CSS at-property rules matching this node.
*/
@@ -2882,6 +2908,17 @@ the full layer tree for the tree scope and their ordering.
export type getLayersForNodeReturnValue = {
rootLayer: CSSLayerData;
}
+ /**
+ * Given a CSS selector text and a style sheet ID, getLocationForSelector
+returns an array of locations of the CSS selector in the style sheet.
+ */
+ export type getLocationForSelectorParameters = {
+ styleSheetId: StyleSheetId;
+ selectorText: string;
+ }
+ export type getLocationForSelectorReturnValue = {
+ ranges: SourceRange[];
+ }
/**
* Starts tracking the given computed styles for updates. The specified array of properties
replaces the one previously specified. Pass empty array to disable tracking.
@@ -8052,6 +8089,7 @@ milliseconds relatively to this requestTime.
headers: Headers;
/**
* HTTP POST request data.
+Use postDataEntries instead.
*/
postData?: string;
/**
@@ -8059,7 +8097,7 @@ milliseconds relatively to this requestTime.
*/
hasPostData?: boolean;
/**
- * Request body elements. This will be converted from base64 to binary
+ * Request body elements (post data broken into individual entries).
*/
postDataEntries?: PostDataEntry[];
/**
@@ -9787,6 +9825,18 @@ matches provided URL.
* Connection type if known.
*/
connectionType?: ConnectionType;
+ /**
+ * WebRTC packet loss (percent, 0-100). 0 disables packet loss emulation, 100 drops all the packets.
+ */
+ packetLoss?: number;
+ /**
+ * WebRTC packet queue length (packet). 0 removes any queue length limitations.
+ */
+ packetQueueLength?: number;
+ /**
+ * WebRTC packetReordering feature.
+ */
+ packetReordering?: boolean;
}
export type emulateNetworkConditionsReturnValue = {
}
@@ -11065,7 +11115,7 @@ as an ad.
* All Permissions Policy features. This enum should match the one defined
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.
*/
- export type PermissionsPolicyFeature = "accelerometer"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factor"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"window-placement"|"xr-spatial-tracking";
+ export type PermissionsPolicyFeature = "accelerometer"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"window-placement"|"xr-spatial-tracking";
/**
* Reason for a permissions policy feature to be disabled.
*/
@@ -11534,7 +11584,7 @@ Example URLs: http://www.google.com/file.html -> "google.com"
/**
* List of not restored reasons for back-forward cache.
*/
- export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"Portal"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame";
+ export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"Portal"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame";
/**
* Types of not restored reasons for back-forward cache.
*/
@@ -11811,7 +11861,7 @@ open.
*/
type: DialogType;
/**
- * True if browser is capable showing or acting on the given dialog. When browser has no
+ * True iff browser is capable showing or acting on the given dialog. When browser has no
dialog handler for given target, calling alert while Page domain is engaged will stall
the page execution. Execution can be resumed via calling Page.handleJavaScriptDialog.
*/
@@ -13533,34 +13583,10 @@ Tokens from that issuer.
* Enum of network fetches auctions can do.
*/
export type InterestGroupAuctionFetchType = "bidderJs"|"bidderWasm"|"sellerJs"|"bidderTrustedSignals"|"sellerTrustedSignals";
- /**
- * Ad advertising element inside an interest group.
- */
- export interface InterestGroupAd {
- renderURL: string;
- metadata?: string;
- }
- /**
- * The full details of an interest group.
- */
- export interface InterestGroupDetails {
- ownerOrigin: string;
- name: string;
- expirationTime: Network.TimeSinceEpoch;
- joiningOrigin: string;
- biddingLogicURL?: string;
- biddingWasmHelperURL?: string;
- updateURL?: string;
- trustedBiddingSignalsURL?: string;
- trustedBiddingSignalsKeys: string[];
- userBiddingSignals?: string;
- ads: InterestGroupAd[];
- adComponents: InterestGroupAd[];
- }
/**
* Enum of shared storage access types.
*/
- export type SharedStorageAccessType = "documentAddModule"|"documentSelectURL"|"documentRun"|"documentSet"|"documentAppend"|"documentDelete"|"documentClear"|"workletSet"|"workletAppend"|"workletDelete"|"workletClear"|"workletGet"|"workletKeys"|"workletEntries"|"workletLength"|"workletRemainingBudget";
+ export type SharedStorageAccessType = "documentAddModule"|"documentSelectURL"|"documentRun"|"documentSet"|"documentAppend"|"documentDelete"|"documentClear"|"documentGet"|"workletSet"|"workletAppend"|"workletDelete"|"workletClear"|"workletGet"|"workletKeys"|"workletEntries"|"workletLength"|"workletRemainingBudget"|"headerSet"|"headerAppend"|"headerDelete"|"headerClear";
/**
* Struct for a single key-value pair in an origin's shared storage.
*/
@@ -13644,22 +13670,28 @@ SharedStorageAccessType.documentAppend,
SharedStorageAccessType.documentDelete,
SharedStorageAccessType.workletSet,
SharedStorageAccessType.workletAppend,
-SharedStorageAccessType.workletDelete, and
-SharedStorageAccessType.workletGet.
+SharedStorageAccessType.workletDelete,
+SharedStorageAccessType.workletGet,
+SharedStorageAccessType.headerSet,
+SharedStorageAccessType.headerAppend, and
+SharedStorageAccessType.headerDelete.
*/
key?: string;
/**
* Value for a specific entry in an origin's shared storage.
Present only for SharedStorageAccessType.documentSet,
SharedStorageAccessType.documentAppend,
-SharedStorageAccessType.workletSet, and
-SharedStorageAccessType.workletAppend.
+SharedStorageAccessType.workletSet,
+SharedStorageAccessType.workletAppend,
+SharedStorageAccessType.headerSet, and
+SharedStorageAccessType.headerAppend.
*/
value?: string;
/**
* Whether or not to set an entry for a key if that key is already present.
-Present only for SharedStorageAccessType.documentSet and
-SharedStorageAccessType.workletSet.
+Present only for SharedStorageAccessType.documentSet,
+SharedStorageAccessType.workletSet, and
+SharedStorageAccessType.headerSet.
*/
ignoreIfPresent?: boolean;
}
@@ -13789,6 +13821,23 @@ int
}
export type AttributionReportingEventLevelResult = "success"|"successDroppedLowerPriority"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"deduplicated"|"excessiveAttributions"|"priorityTooLow"|"neverAttributedSource"|"excessiveReportingOrigins"|"noMatchingSourceFilterData"|"prohibitedByBrowserPolicy"|"noMatchingConfigurations"|"excessiveReports"|"falselyAttributedSource"|"reportWindowPassed"|"notRegistered"|"reportWindowNotStarted"|"noMatchingTriggerData";
export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports";
+ /**
+ * A single Related Website Set object.
+ */
+ export interface RelatedWebsiteSet {
+ /**
+ * The primary site of this set, along with the ccTLDs if there is any.
+ */
+ primarySites: string[];
+ /**
+ * The associated sites of this set, along with the ccTLDs if there is any.
+ */
+ associatedSites: string[];
+ /**
+ * The service sites of this set, along with the ccTLDs if there is any.
+ */
+ serviceSites: string[];
+ }
/**
* A cache's contents have been modified.
@@ -14216,7 +14265,13 @@ Leaves other stored data, including the issuer's Redemption Records, intact.
name: string;
}
export type getInterestGroupDetailsReturnValue = {
- details: InterestGroupDetails;
+ /**
+ * This largely corresponds to:
+https://wicg.github.io/turtledove/#dictdef-generatebidinterestgroup
+but has absolute expirationTime instead of relative lifetimeMs and
+also adds joiningOrigin.
+ */
+ details: { [key: string]: string };
}
/**
* Enables/Disables issuing of interestGroupAccessed events.
@@ -14345,6 +14400,15 @@ interestGroupAuctionNetworkRequestCreated.
}
export type setAttributionReportingTrackingReturnValue = {
}
+ /**
+ * Returns the effective Related Website Sets in use by this profile for the browser
+session. The effective Related Website Sets will not change during a browser session.
+ */
+ export type getRelatedWebsiteSetsParameters = {
+ }
+ export type getRelatedWebsiteSetsReturnValue = {
+ sets: RelatedWebsiteSet[];
+ }
}
/**
@@ -14582,10 +14646,10 @@ supported.
export type SessionID = string;
export interface TargetInfo {
targetId: TargetID;
- type: string;
/**
* List of types: https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/devtools_agent_host_impl.cc?ss=chromium&q=f:devtools%20-f:out%20%22::kTypeTab%5B%5D%22
*/
+ type: string;
title: string;
url: string;
/**
@@ -16385,7 +16449,7 @@ See also:
requestId?: Network.RequestId;
/**
* Error information
-`errorMessage` is null if `errorType` is null.
+`errorMessage` is null iff `errorType` is null.
*/
errorType?: RuleSetErrorType;
/**
@@ -19515,6 +19579,7 @@ Error was thrown.
"CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeParameters;
"CSS.getStyleSheetText": CSS.getStyleSheetTextParameters;
"CSS.getLayersForNode": CSS.getLayersForNodeParameters;
+ "CSS.getLocationForSelector": CSS.getLocationForSelectorParameters;
"CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesParameters;
"CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesParameters;
"CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeParameters;
@@ -19885,6 +19950,7 @@ Error was thrown.
"Storage.runBounceTrackingMitigations": Storage.runBounceTrackingMitigationsParameters;
"Storage.setAttributionReportingLocalTestingMode": Storage.setAttributionReportingLocalTestingModeParameters;
"Storage.setAttributionReportingTracking": Storage.setAttributionReportingTrackingParameters;
+ "Storage.getRelatedWebsiteSets": Storage.getRelatedWebsiteSetsParameters;
"SystemInfo.getInfo": SystemInfo.getInfoParameters;
"SystemInfo.getFeatureState": SystemInfo.getFeatureStateParameters;
"SystemInfo.getProcessInfo": SystemInfo.getProcessInfoParameters;
@@ -20097,6 +20163,7 @@ Error was thrown.
"CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeReturnValue;
"CSS.getStyleSheetText": CSS.getStyleSheetTextReturnValue;
"CSS.getLayersForNode": CSS.getLayersForNodeReturnValue;
+ "CSS.getLocationForSelector": CSS.getLocationForSelectorReturnValue;
"CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesReturnValue;
"CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesReturnValue;
"CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeReturnValue;
@@ -20467,6 +20534,7 @@ Error was thrown.
"Storage.runBounceTrackingMitigations": Storage.runBounceTrackingMitigationsReturnValue;
"Storage.setAttributionReportingLocalTestingMode": Storage.setAttributionReportingLocalTestingModeReturnValue;
"Storage.setAttributionReportingTracking": Storage.setAttributionReportingTrackingReturnValue;
+ "Storage.getRelatedWebsiteSets": Storage.getRelatedWebsiteSetsReturnValue;
"SystemInfo.getInfo": SystemInfo.getInfoReturnValue;
"SystemInfo.getFeatureState": SystemInfo.getFeatureStateReturnValue;
"SystemInfo.getProcessInfo": SystemInfo.getProcessInfoReturnValue;
diff --git a/packages/playwright-core/types/structs.d.ts b/packages/playwright-core/types/structs.d.ts
index eadbb2de01..9c621cff35 100644
--- a/packages/playwright-core/types/structs.d.ts
+++ b/packages/playwright-core/types/structs.d.ts
@@ -40,6 +40,6 @@ export type Unboxed =
export type PageFunction0 = string | (() => R | Promise);
export type PageFunction = string | ((arg: Unboxed) => R | Promise);
export type PageFunctionOn = string | ((on: On, arg2: Unboxed) => R | Promise);
-export type SmartHandle = T extends Node ? ElementHandle : JSHandle;
+export type SmartHandle = [T] extends [Node] ? ElementHandle : JSHandle;
export type ElementHandleForTag = ElementHandle;
export type BindingSource = { context: BrowserContext, page: Page, frame: Frame };
diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts
index 95c0ddcdd0..235de894cd 100644
--- a/packages/playwright-core/types/types.d.ts
+++ b/packages/playwright-core/types/types.d.ts
@@ -1781,26 +1781,32 @@ export interface Page {
prependListener(event: 'worker', listener: (worker: Worker) => void): this;
/**
- * Sometimes, the web page can show an overlay that obstructs elements behind it and prevents certain actions, like
- * click, from completing. When such an overlay is shown predictably, we recommend dismissing it as a part of your
- * test flow. However, sometimes such an overlay may appear non-deterministically, for example certain cookies consent
- * dialogs behave this way. In this case,
- * [page.addLocatorHandler(locator, handler)](https://playwright.dev/docs/api/class-page#page-add-locator-handler)
- * allows handling an overlay during an action that it would block.
+ * **NOTE** This method is experimental and its behavior may change in the upcoming releases.
*
- * This method registers a handler for an overlay that is executed once the locator is visible on the page. The
- * handler should get rid of the overlay so that actions blocked by it can proceed. This is useful for
- * nondeterministic interstitial pages or dialogs, like a cookie consent dialog.
+ * When testing a web page, sometimes unexpected overlays like a "Sign up" dialog appear and block actions you want to
+ * automate, e.g. clicking a button. These overlays don't always show up in the same way or at the same time, making
+ * them tricky to handle in automated tests.
*
- * Note that execution time of the handler counts towards the timeout of the action/assertion that executed the
- * handler.
+ * This method lets you set up a special function, called a handler, that activates when it detects that overlay is
+ * visible. The handler's job is to remove the overlay, allowing your test to continue as if the overlay wasn't there.
*
- * You can register multiple handlers. However, only a single handler will be running at a time. Any actions inside a
- * handler must not require another handler to run.
+ * Things to keep in mind:
+ * - When an overlay is shown predictably, we recommend explicitly waiting for it in your test and dismissing it as
+ * a part of your normal test flow, instead of using
+ * [page.addLocatorHandler(locator, handler)](https://playwright.dev/docs/api/class-page#page-add-locator-handler).
+ * - Playwright checks for the overlay every time before executing or retrying an action that requires an
+ * [actionability check](https://playwright.dev/docs/actionability), or before performing an auto-waiting assertion check. When overlay
+ * is visible, Playwright calls the handler first, and then proceeds with the action/assertion. Note that the
+ * handler is only called when you perform an action/assertion - if the overlay becomes visible but you don't
+ * perform any actions, the handler will not be triggered.
+ * - The execution time of the handler counts towards the timeout of the action/assertion that executed the handler.
+ * If your handler takes too long, it might cause timeouts.
+ * - You can register multiple handlers. However, only a single handler will be running at a time. Make sure the
+ * actions within a handler don't depend on another handler.
*
- * **NOTE** Running the interceptor will alter your page state mid-test. For example it will change the currently
- * focused element and move the mouse. Make sure that the actions that run after the interceptor are self-contained
- * and do not rely on the focus and mouse state.
For example, consider a test that calls
+ * **NOTE** Running the handler will alter your page state mid-test. For example it will change the currently focused
+ * element and move the mouse. Make sure that actions that run after the handler are self-contained and do not rely on
+ * the focus and mouse state being unchanged.
For example, consider a test that calls
* [locator.focus([options])](https://playwright.dev/docs/api/class-locator#locator-focus) followed by
* [keyboard.press(key[, options])](https://playwright.dev/docs/api/class-keyboard#keyboard-press). If your handler
* clicks a button between these two actions, the focused element most likely will be wrong, and key press will happen
@@ -1809,17 +1815,18 @@ export interface Page {
* problem.
Another example is a series of mouse actions, where
* [mouse.move(x, y[, options])](https://playwright.dev/docs/api/class-mouse#mouse-move) is followed by
* [mouse.down([options])](https://playwright.dev/docs/api/class-mouse#mouse-down). Again, when the handler runs
- * between these two actions, the mouse position will be wrong during the mouse down. Prefer methods like
- * [locator.click([options])](https://playwright.dev/docs/api/class-locator#locator-click) that are self-contained.
+ * between these two actions, the mouse position will be wrong during the mouse down. Prefer self-contained actions
+ * like [locator.click([options])](https://playwright.dev/docs/api/class-locator#locator-click) that do not rely on
+ * the state being unchanged by a handler.
*
* **Usage**
*
- * An example that closes a cookie dialog when it appears:
+ * An example that closes a "Sign up to the newsletter" dialog when it appears:
*
* ```js
* // Setup the handler.
- * await page.addLocatorHandler(page.getByRole('button', { name: 'Accept all cookies' }), async () => {
- * await page.getByRole('button', { name: 'Reject all cookies' }).click();
+ * await page.addLocatorHandler(page.getByText('Sign up to the newsletter'), async () => {
+ * await page.getByRole('button', { name: 'No thanks' }).click();
* });
*
* // Write the test as usual.
@@ -1832,7 +1839,7 @@ export interface Page {
* ```js
* // Setup the handler.
* await page.addLocatorHandler(page.getByText('Confirm your security details'), async () => {
- * await page.getByRole('button', 'Remind me later').click();
+ * await page.getByRole('button', { name: 'Remind me later' }).click();
* });
*
* // Write the test as usual.
@@ -8438,6 +8445,28 @@ export interface BrowserContext {
*/
pages(): Array;
+ /**
+ * Removes cookies from context. At least one of the removal criteria should be provided.
+ *
+ * **Usage**
+ *
+ * ```js
+ * await browserContext.removeCookies({ name: 'session-id' });
+ * await browserContext.removeCookies({ domain: 'my-origin.com' });
+ * await browserContext.removeCookies({ path: '/api/v1' });
+ * await browserContext.removeCookies({ name: 'session-id', domain: 'my-origin.com' });
+ * ```
+ *
+ * @param filter
+ */
+ removeCookies(filter: {
+ name?: string;
+
+ domain?: string;
+
+ path?: string;
+ }): Promise;
+
/**
* Routing provides the capability to modify network requests that are made by any page in the browser context. Once
* route is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted.
@@ -11433,6 +11462,24 @@ export interface Locator {
*/
elementHandles(): Promise>;
+ /**
+ * Returns a {@link FrameLocator} object pointing to the same `iframe` as this locator.
+ *
+ * Useful when you have a {@link Locator} object obtained somewhere, and later on would like to interact with the
+ * content inside the frame.
+ *
+ * **Usage**
+ *
+ * ```js
+ * const locator = page.locator('iframe[name="embedded"]');
+ * // ...
+ * const frameLocator = locator.enterFrame();
+ * await frameLocator.getByRole('button').click();
+ * ```
+ *
+ */
+ enterFrame(): FrameLocator;
+
/**
* Set a value to the input field.
*
@@ -13125,6 +13172,7 @@ export interface BrowserType {
/**
* **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the
* `headless` option will be set `false`.
+ * @deprecated Use [debugging tools](https://playwright.dev/docs/debug) instead.
*/
devtools?: boolean;
@@ -13529,6 +13577,7 @@ export interface BrowserType {
/**
* **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the
* `headless` option will be set `false`.
+ * @deprecated Use [debugging tools](https://playwright.dev/docs/debug) instead.
*/
devtools?: boolean;
@@ -17734,14 +17783,32 @@ export interface FileChooser {
* **Converting Locator to FrameLocator**
*
* If you have a {@link Locator} object pointing to an `iframe` it can be converted to {@link FrameLocator} using
- * [`:scope`](https://developer.mozilla.org/en-US/docs/Web/CSS/:scope) CSS selector:
+ * [locator.enterFrame()](https://playwright.dev/docs/api/class-locator#locator-enter-frame).
*
- * ```js
- * const frameLocator = locator.frameLocator(':scope');
- * ```
+ * **Converting FrameLocator to Locator**
*
+ * If you have a {@link FrameLocator} object it can be converted to {@link Locator} pointing to the same `iframe`
+ * using [frameLocator.exitFrame()](https://playwright.dev/docs/api/class-framelocator#frame-locator-exit-frame).
*/
export interface FrameLocator {
+ /**
+ * Returns a {@link Locator} object pointing to the same `iframe` as this frame locator.
+ *
+ * Useful when you have a {@link FrameLocator} object obtained somewhere, and later on would like to interact with the
+ * `iframe` element.
+ *
+ * **Usage**
+ *
+ * ```js
+ * const frameLocator = page.frameLocator('iframe[name="embedded"]');
+ * // ...
+ * const locator = frameLocator.exitFrame();
+ * await expect(locator).toBeVisible();
+ * ```
+ *
+ */
+ exitFrame(): Locator;
+
/**
* Returns locator to the first matching frame.
*/
@@ -18313,7 +18380,7 @@ export interface Keyboard {
* (async () => {
* const browser = await chromium.launch({
* logger: {
- * isEnabled: (name, severity) => name === 'browser',
+ * isEnabled: (name, severity) => name === 'api',
* log: (name, severity, message, args) => console.log(`${name} ${message}`)
* }
* });
@@ -20205,6 +20272,7 @@ export interface LaunchOptions {
/**
* **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the
* `headless` option will be set `false`.
+ * @deprecated Use [debugging tools](https://playwright.dev/docs/debug) instead.
*/
devtools?: boolean;
diff --git a/packages/playwright-ct-core/.eslintrc.js b/packages/playwright-ct-core/.eslintrc.js
index ae8768db65..84888f1ae3 100644
--- a/packages/playwright-ct-core/.eslintrc.js
+++ b/packages/playwright-ct-core/.eslintrc.js
@@ -1,3 +1,3 @@
module.exports = {
- extends: "../.eslintrc-with-ts-config.js",
+ extends: "../../.eslintrc-with-ts-config.js",
};
diff --git a/packages/playwright-ct-core/package.json b/packages/playwright-ct-core/package.json
index 99dd286fca..fd63ced898 100644
--- a/packages/playwright-ct-core/package.json
+++ b/packages/playwright-ct-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-core",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"description": "Playwright Component Testing Helpers",
"repository": {
"type": "git",
@@ -26,9 +26,9 @@
}
},
"dependencies": {
- "playwright-core": "1.42.0-next",
+ "playwright-core": "1.43.0-next",
"vite": "^5.0.12",
- "playwright": "1.42.0-next"
+ "playwright": "1.43.0-next"
},
"bin": {
"playwright": "cli.js"
diff --git a/packages/playwright-ct-core/src/tsxTransform.ts b/packages/playwright-ct-core/src/tsxTransform.ts
index 0a61d4e3d9..ab33c114ba 100644
--- a/packages/playwright-ct-core/src/tsxTransform.ts
+++ b/packages/playwright-ct-core/src/tsxTransform.ts
@@ -75,7 +75,7 @@ export default declare((api: BabelAPI) => {
const ext = path.extname(importNode.source.value);
// Convert all non-JS imports into refs.
- if (!allJsExtensions.has(ext)) {
+ if (artifactExtensions.has(ext)) {
for (const specifier of importNode.specifiers) {
if (t.isImportNamespaceSpecifier(specifier))
continue;
@@ -171,4 +171,29 @@ export function importInfo(importNode: T.ImportDeclaration, specifier: T.ImportS
return { localName: specifier.local.name, info: result };
}
-const allJsExtensions = new Set(['.js', '.jsx', '.cjs', '.mjs', '.ts', '.tsx', '.cts', '.mts', '']);
+const artifactExtensions = new Set([
+ // Frameworks
+ '.vue',
+ '.svelte',
+
+ // Images
+ '.jpg', '.jpeg',
+ '.png',
+ '.gif',
+ '.svg',
+ '.bmp',
+ '.webp',
+ '.ico',
+
+ // CSS
+ '.css',
+
+ // Fonts
+ '.woff', '.woff2',
+ '.ttf',
+ '.otf',
+ '.eot',
+
+ // Other assets
+ '.json',
+]);
\ No newline at end of file
diff --git a/packages/playwright-ct-core/types/component.d.ts b/packages/playwright-ct-core/types/component.d.ts
index 7502ec2f06..5bd5d7e017 100644
--- a/packages/playwright-ct-core/types/component.d.ts
+++ b/packages/playwright-ct-core/types/component.d.ts
@@ -14,8 +14,6 @@
* limitations under the License.
*/
-import type { ImportRegistry } from '../src/injected/importRegistry';
-
type JsonPrimitive = string | number | boolean | null;
type JsonValue = JsonPrimitive | JsonObject | JsonArray;
type JsonArray = JsonValue[];
diff --git a/packages/playwright-ct-react/package.json b/packages/playwright-ct-react/package.json
index 5702342623..30b8f15871 100644
--- a/packages/playwright-ct-react/package.json
+++ b/packages/playwright-ct-react/package.json
@@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-react",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"description": "Playwright Component Testing for React",
"repository": {
"type": "git",
@@ -29,11 +29,10 @@
}
},
"dependencies": {
- "@playwright/experimental-ct-core": "1.42.0-next",
+ "@playwright/experimental-ct-core": "1.43.0-next",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {
- "playwright": "cli.js",
- "pw-react": "cli.js"
+ "playwright": "cli.js"
}
}
diff --git a/packages/playwright-ct-react/registerSource.mjs b/packages/playwright-ct-react/registerSource.mjs
index 10dad4c0db..9ad2612bc4 100644
--- a/packages/playwright-ct-react/registerSource.mjs
+++ b/packages/playwright-ct-react/registerSource.mjs
@@ -40,13 +40,11 @@ function __pwRender(value) {
if (isJsxComponent(v)) {
const component = v;
const props = component.props ? __pwRender(component.props) : {};
- const {children, ...propsWithoutChildren} = props;
- /** @type {[any, any, any?]} */
- const createElementArguments = [component.type, propsWithoutChildren];
- if(children){
+ const { children, ...propsWithoutChildren } = props;
+ const createElementArguments = [propsWithoutChildren];
+ if (children)
createElementArguments.push(children);
- }
- return { result: __pwReact.createElement(...createElementArguments) };
+ return { result: __pwReact.createElement(component.type, ...createElementArguments) };
}
});
}
diff --git a/packages/playwright-ct-react17/cli.js b/packages/playwright-ct-react17/cli.js
index b6f935eb52..9cc834ee95 100755
--- a/packages/playwright-ct-react17/cli.js
+++ b/packages/playwright-ct-react17/cli.js
@@ -15,8 +15,6 @@
* limitations under the License.
*/
-const { program, initializePlugin } = require('@playwright/experimental-ct-core/lib/program');
-const { _framework } = require('./index');
+const { program } = require('@playwright/experimental-ct-core/lib/program');
-initializePlugin(_framework);
program.parse(process.argv);
diff --git a/packages/playwright-ct-react17/package.json b/packages/playwright-ct-react17/package.json
index 4e926cca74..1ac183e407 100644
--- a/packages/playwright-ct-react17/package.json
+++ b/packages/playwright-ct-react17/package.json
@@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-react17",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"description": "Playwright Component Testing for React",
"repository": {
"type": "git",
@@ -29,11 +29,10 @@
}
},
"dependencies": {
- "@playwright/experimental-ct-core": "1.42.0-next",
+ "@playwright/experimental-ct-core": "1.43.0-next",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {
- "playwright": "cli.js",
- "pw-react17": "cli.js"
+ "playwright": "cli.js"
}
}
diff --git a/packages/playwright-ct-react17/registerSource.mjs b/packages/playwright-ct-react17/registerSource.mjs
index 8dfc1d24e9..158984f3ac 100644
--- a/packages/playwright-ct-react17/registerSource.mjs
+++ b/packages/playwright-ct-react17/registerSource.mjs
@@ -40,14 +40,11 @@ function __pwRender(value) {
if (isJsxComponent(v)) {
const component = v;
const props = component.props ? __pwRender(component.props) : {};
-
- const {children, ...propsWithoutChildren} = props;
- /** @type {[any, any, any?]} */
- const createElementArguments = [component.type, propsWithoutChildren];
- if(children){
+ const { children, ...propsWithoutChildren } = props;
+ const createElementArguments = [propsWithoutChildren];
+ if (children)
createElementArguments.push(children);
- }
- return { result: __pwReact.createElement(...createElementArguments) };
+ return { result: __pwReact.createElement(component.type, ...createElementArguments) };
}
});
}
diff --git a/packages/playwright-ct-solid/cli.js b/packages/playwright-ct-solid/cli.js
index b6f935eb52..9cc834ee95 100755
--- a/packages/playwright-ct-solid/cli.js
+++ b/packages/playwright-ct-solid/cli.js
@@ -15,8 +15,6 @@
* limitations under the License.
*/
-const { program, initializePlugin } = require('@playwright/experimental-ct-core/lib/program');
-const { _framework } = require('./index');
+const { program } = require('@playwright/experimental-ct-core/lib/program');
-initializePlugin(_framework);
program.parse(process.argv);
diff --git a/packages/playwright-ct-solid/package.json b/packages/playwright-ct-solid/package.json
index ac65ccb548..54833eeba3 100644
--- a/packages/playwright-ct-solid/package.json
+++ b/packages/playwright-ct-solid/package.json
@@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-solid",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"description": "Playwright Component Testing for Solid",
"repository": {
"type": "git",
@@ -29,14 +29,13 @@
}
},
"dependencies": {
- "@playwright/experimental-ct-core": "1.42.0-next",
+ "@playwright/experimental-ct-core": "1.43.0-next",
"vite-plugin-solid": "^2.7.0"
},
"devDependencies": {
"solid-js": "^1.7.0"
},
"bin": {
- "playwright": "cli.js",
- "pw-solid": "cli.js"
+ "playwright": "cli.js"
}
}
diff --git a/packages/playwright-ct-solid/registerSource.mjs b/packages/playwright-ct-solid/registerSource.mjs
index a76792c513..d0077dd494 100644
--- a/packages/playwright-ct-solid/registerSource.mjs
+++ b/packages/playwright-ct-solid/registerSource.mjs
@@ -19,9 +19,7 @@
import { render as __pwSolidRender, createComponent as __pwSolidCreateComponent } from 'solid-js/web';
import __pwH from 'solid-js/h';
-
/** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */
-/** @typedef {() => import('solid-js').JSX.Element} FrameworkComponent */
/**
* @param {any} component
@@ -32,42 +30,20 @@ function isJsxComponent(component) {
}
/**
- * @param {any} child
+ * @param {any} value
*/
-function __pwCreateChild(child) {
- if (Array.isArray(child))
- return child.map(grandChild => __pwCreateChild(grandChild));
- if (isJsxComponent(child))
- return __pwCreateComponent(child);
- return child;
-}
-
-/**
- * @param {JsxComponent} component
- * @returns {any[] | undefined}
- */
-function __pwJsxChildArray(component) {
- if (!component.props.children)
- return;
- if (Array.isArray(component.props.children))
- return component.props.children;
- return [component.props.children];
-}
-
-/**
- * @param {JsxComponent} component
- */
-function __pwCreateComponent(component) {
- const children = __pwJsxChildArray(component)?.map(child => __pwCreateChild(child)).filter(child => {
- if (typeof child === 'string')
- return !!child.trim();
- return true;
+function __pwCreateComponent(value) {
+ return window.__pwTransformObject(value, v => {
+ if (isJsxComponent(v)) {
+ const component = v;
+ const props = component.props ? __pwCreateComponent(component.props) : {};
+ if (typeof component.type === 'string') {
+ const { children, ...propsWithoutChildren } = props;
+ return { result: __pwH(component.type, propsWithoutChildren, children) };
+ }
+ return { result: __pwSolidCreateComponent(component.type, props) };
+ }
});
-
- if (typeof component.type === 'string')
- return __pwH(component.type, component.props, children);
-
- return __pwSolidCreateComponent(component.type, { ...component.props, children });
}
const __pwUnmountKey = Symbol('unmountKey');
@@ -96,6 +72,7 @@ window.playwrightUnmount = async rootElement => {
throw new Error('Component was not mounted');
unmount();
+ delete rootElement[__pwUnmountKey];
};
window.playwrightUpdate = async (rootElement, component) => {
diff --git a/packages/playwright-ct-svelte/cli.js b/packages/playwright-ct-svelte/cli.js
index b6f935eb52..9cc834ee95 100755
--- a/packages/playwright-ct-svelte/cli.js
+++ b/packages/playwright-ct-svelte/cli.js
@@ -15,8 +15,6 @@
* limitations under the License.
*/
-const { program, initializePlugin } = require('@playwright/experimental-ct-core/lib/program');
-const { _framework } = require('./index');
+const { program } = require('@playwright/experimental-ct-core/lib/program');
-initializePlugin(_framework);
program.parse(process.argv);
diff --git a/packages/playwright-ct-svelte/package.json b/packages/playwright-ct-svelte/package.json
index f0809a3d4a..4aeaa91a11 100644
--- a/packages/playwright-ct-svelte/package.json
+++ b/packages/playwright-ct-svelte/package.json
@@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-svelte",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"description": "Playwright Component Testing for Svelte",
"repository": {
"type": "git",
@@ -29,14 +29,13 @@
}
},
"dependencies": {
- "@playwright/experimental-ct-core": "1.42.0-next",
+ "@playwright/experimental-ct-core": "1.43.0-next",
"@sveltejs/vite-plugin-svelte": "^3.0.1"
},
"devDependencies": {
"svelte": "^4.2.8"
},
"bin": {
- "playwright": "cli.js",
- "pw-svelte": "cli.js"
+ "playwright": "cli.js"
}
}
diff --git a/packages/playwright-ct-svelte/registerSource.mjs b/packages/playwright-ct-svelte/registerSource.mjs
index 4552a4ba0a..5901458813 100644
--- a/packages/playwright-ct-svelte/registerSource.mjs
+++ b/packages/playwright-ct-svelte/registerSource.mjs
@@ -42,8 +42,8 @@ function __pwCreateSlots(slots) {
for (const slotName in slots) {
const template = document
- .createRange()
- .createContextualFragment(slots[slotName]);
+ .createRange()
+ .createContextualFragment(slots[slotName]);
svelteSlots[slotName] = [createSlotFn(template)];
}
@@ -55,7 +55,8 @@ function __pwCreateSlots(slots) {
__pwInsert(target, element, anchor);
},
d: function destroy(detaching) {
- if (detaching) __pwDetach(element);
+ if (detaching)
+ __pwDetach(element);
},
l: __pwNoop,
};
@@ -108,6 +109,7 @@ window.playwrightUnmount = async rootElement => {
if (!svelteComponent)
throw new Error('Component was not mounted');
svelteComponent.$destroy();
+ delete rootElement[__pwSvelteComponentKey];
};
window.playwrightUpdate = async (rootElement, component) => {
diff --git a/packages/playwright-ct-vue/cli.js b/packages/playwright-ct-vue/cli.js
index b6f935eb52..9cc834ee95 100755
--- a/packages/playwright-ct-vue/cli.js
+++ b/packages/playwright-ct-vue/cli.js
@@ -15,8 +15,6 @@
* limitations under the License.
*/
-const { program, initializePlugin } = require('@playwright/experimental-ct-core/lib/program');
-const { _framework } = require('./index');
+const { program } = require('@playwright/experimental-ct-core/lib/program');
-initializePlugin(_framework);
program.parse(process.argv);
diff --git a/packages/playwright-ct-vue/package.json b/packages/playwright-ct-vue/package.json
index 7ab9b652cb..d125aad45d 100644
--- a/packages/playwright-ct-vue/package.json
+++ b/packages/playwright-ct-vue/package.json
@@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-vue",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"description": "Playwright Component Testing for Vue",
"repository": {
"type": "git",
@@ -29,11 +29,10 @@
}
},
"dependencies": {
- "@playwright/experimental-ct-core": "1.42.0-next",
+ "@playwright/experimental-ct-core": "1.43.0-next",
"@vitejs/plugin-vue": "^4.2.1"
},
"bin": {
- "playwright": "cli.js",
- "pw-vue": "cli.js"
+ "playwright": "cli.js"
}
}
diff --git a/packages/playwright-ct-vue/registerSource.mjs b/packages/playwright-ct-vue/registerSource.mjs
index 5256a241a4..07ce5298f4 100644
--- a/packages/playwright-ct-vue/registerSource.mjs
+++ b/packages/playwright-ct-vue/registerSource.mjs
@@ -88,6 +88,9 @@ function __pwCreateSlot(html) {
};
}
+/**
+ * @param {string | string[]} slot
+ */
function __pwSlotToFunction(slot) {
if (typeof slot === 'string')
return __pwCreateSlot(slot)();
@@ -175,7 +178,11 @@ function __pwCreateComponent(component) {
return { Component: component.type, props, slots: lastArg, listeners };
}
+/**
+ * @param {any} slots
+ */
function __pwWrapFunctions(slots) {
+ /** @type {import('vue').ComponentInternalInstance['slots']} */
const slotsWithRenderFunctions = {};
if (!Array.isArray(slots)) {
for (const [key, value] of Object.entries(slots || {}))
@@ -198,25 +205,27 @@ function __pwCreateWrapper(component) {
return wrapper;
}
-/**
- * @returns {any}
- */
-function __pwCreateDevTools() {
- return {
+function __pwSetDevTools() {
+ __pwSetDevtoolsHook({
emit(eventType, ...payload) {
- if (eventType === 'component:emit') {
- const [, componentVM, event, eventArgs] = payload;
- for (const [wrapper, listeners] of __pwAllListeners) {
- if (wrapper.component !== componentVM)
- continue;
- const listener = listeners[event];
- if (!listener)
- return;
- listener(...eventArgs);
- }
+ if (eventType !== 'component:emit')
+ return;
+
+ const [, componentVM, event, eventArgs] = payload;
+ for (const [wrapper, listeners] of __pwAllListeners) {
+ if (wrapper.component !== componentVM)
+ continue;
+ const listener = listeners[event];
+ if (!listener)
+ return;
+ listener(...eventArgs);
}
- }
- };
+ },
+ on() {},
+ off() {},
+ once() {},
+ appRecords: []
+ }, {});
}
const __pwAppKey = Symbol('appKey');
@@ -230,7 +239,7 @@ window.playwrightMount = async (component, rootElement, hooksConfig) => {
return wrapper;
}
});
- __pwSetDevtoolsHook(__pwCreateDevTools(), {});
+ __pwSetDevTools();
for (const hook of window.__pw_hooks_before_mount || [])
await hook({ app, hooksConfig });
@@ -242,13 +251,16 @@ window.playwrightMount = async (component, rootElement, hooksConfig) => {
};
window.playwrightUnmount = async rootElement => {
- const app = /** @type {import('vue').App} */ (rootElement[__pwAppKey]);
+ /** @type {import('vue').App | undefined} */
+ const app = rootElement[__pwAppKey];
if (!app)
throw new Error('Component was not mounted');
app.unmount();
+ delete rootElement[__pwAppKey];
};
window.playwrightUpdate = async (rootElement, component) => {
+ /** @type {import('vue').VNode | undefined} */
const wrapper = rootElement[__pwWrapperKey];
if (!wrapper)
throw new Error('Component was not mounted');
diff --git a/packages/playwright-ct-vue2/cli.js b/packages/playwright-ct-vue2/cli.js
index b6f935eb52..9cc834ee95 100755
--- a/packages/playwright-ct-vue2/cli.js
+++ b/packages/playwright-ct-vue2/cli.js
@@ -15,8 +15,6 @@
* limitations under the License.
*/
-const { program, initializePlugin } = require('@playwright/experimental-ct-core/lib/program');
-const { _framework } = require('./index');
+const { program } = require('@playwright/experimental-ct-core/lib/program');
-initializePlugin(_framework);
program.parse(process.argv);
diff --git a/packages/playwright-ct-vue2/package.json b/packages/playwright-ct-vue2/package.json
index 0ba67d6bd6..aed18496f7 100644
--- a/packages/playwright-ct-vue2/package.json
+++ b/packages/playwright-ct-vue2/package.json
@@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-vue2",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"description": "Playwright Component Testing for Vue2",
"repository": {
"type": "git",
@@ -29,14 +29,13 @@
}
},
"dependencies": {
- "@playwright/experimental-ct-core": "1.42.0-next",
+ "@playwright/experimental-ct-core": "1.43.0-next",
"@vitejs/plugin-vue2": "^2.2.0"
},
"devDependencies": {
"vue": "^2.7.14"
},
"bin": {
- "playwright": "cli.js",
- "pw-vue2": "cli.js"
+ "playwright": "cli.js"
}
}
diff --git a/packages/playwright-ct-vue2/registerSource.mjs b/packages/playwright-ct-vue2/registerSource.mjs
index b0a7ae71dc..19b4d41c08 100644
--- a/packages/playwright-ct-vue2/registerSource.mjs
+++ b/packages/playwright-ct-vue2/registerSource.mjs
@@ -182,6 +182,7 @@ window.playwrightUnmount = async rootElement => {
throw new Error('Component was not mounted');
component.$destroy();
component.$el.remove();
+ delete rootElement[instanceKey];
};
window.playwrightUpdate = async (element, options) => {
diff --git a/packages/playwright-firefox/cli.js b/packages/playwright-firefox/cli.js
index 86adb86a84..f46d8b409e 100755
--- a/packages/playwright-firefox/cli.js
+++ b/packages/playwright-firefox/cli.js
@@ -15,5 +15,5 @@
* limitations under the License.
*/
-const { program } = require('playwright-core/lib/program');
+const { program } = require('playwright-core/lib/cli/program');
program.parse(process.argv);
diff --git a/packages/playwright-firefox/package.json b/packages/playwright-firefox/package.json
index 83c9f54637..e2aed567e6 100644
--- a/packages/playwright-firefox/package.json
+++ b/packages/playwright-firefox/package.json
@@ -1,6 +1,6 @@
{
"name": "playwright-firefox",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"description": "A high-level API to automate Firefox",
"repository": {
"type": "git",
@@ -30,6 +30,6 @@
"install": "node install.js"
},
"dependencies": {
- "playwright-core": "1.42.0-next"
+ "playwright-core": "1.43.0-next"
}
}
diff --git a/packages/playwright-test/package.json b/packages/playwright-test/package.json
index ee88606959..c3174bde51 100644
--- a/packages/playwright-test/package.json
+++ b/packages/playwright-test/package.json
@@ -1,6 +1,6 @@
{
"name": "@playwright/test",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"description": "A high-level API to automate web browsers",
"repository": {
"type": "git",
@@ -30,6 +30,6 @@
},
"scripts": {},
"dependencies": {
- "playwright": "1.42.0-next"
+ "playwright": "1.43.0-next"
}
}
diff --git a/packages/playwright-webkit/cli.js b/packages/playwright-webkit/cli.js
index 86adb86a84..f46d8b409e 100755
--- a/packages/playwright-webkit/cli.js
+++ b/packages/playwright-webkit/cli.js
@@ -15,5 +15,5 @@
* limitations under the License.
*/
-const { program } = require('playwright-core/lib/program');
+const { program } = require('playwright-core/lib/cli/program');
program.parse(process.argv);
diff --git a/packages/playwright-webkit/package.json b/packages/playwright-webkit/package.json
index a72ba97917..a269a30da4 100644
--- a/packages/playwright-webkit/package.json
+++ b/packages/playwright-webkit/package.json
@@ -1,6 +1,6 @@
{
"name": "playwright-webkit",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"description": "A high-level API to automate WebKit",
"repository": {
"type": "git",
@@ -30,6 +30,6 @@
"install": "node install.js"
},
"dependencies": {
- "playwright-core": "1.42.0-next"
+ "playwright-core": "1.43.0-next"
}
}
diff --git a/packages/playwright/.eslintrc.js b/packages/playwright/.eslintrc.js
index 67c9b6313d..b7e707b35f 100644
--- a/packages/playwright/.eslintrc.js
+++ b/packages/playwright/.eslintrc.js
@@ -1,14 +1,5 @@
-const path = require('path');
-
module.exports = {
- extends: '../.eslintrc.js',
- parser: "@typescript-eslint/parser",
- plugins: ["@typescript-eslint", "notice"],
- parserOptions: {
- ecmaVersion: 9,
- sourceType: "module",
- project: path.join(__dirname, '..', '..', 'tsconfig.json'),
- },
+ extends: '../../.eslintrc-with-ts-config.js',
rules: {
'@typescript-eslint/no-floating-promises': 'error',
},
diff --git a/packages/playwright/package.json b/packages/playwright/package.json
index 39e4cf23ec..5072f27c14 100644
--- a/packages/playwright/package.json
+++ b/packages/playwright/package.json
@@ -1,6 +1,6 @@
{
"name": "playwright",
- "version": "1.42.0-next",
+ "version": "1.43.0-next",
"description": "A high-level API to automate web browsers",
"repository": {
"type": "git",
@@ -58,7 +58,7 @@
},
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.42.0-next"
+ "playwright-core": "1.43.0-next"
},
"optionalDependencies": {
"fsevents": "2.3.2"
diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts
index d1c86fe159..cfe294a75c 100644
--- a/packages/playwright/src/common/config.ts
+++ b/packages/playwright/src/common/config.ts
@@ -58,11 +58,6 @@ export class FullConfigInternal {
testIdMatcher?: Matcher;
defineConfigWasUsed = false;
- // TODO: when merging reports, there could be no internal config. This is very unfortunate.
- static from(config: FullConfig): FullConfigInternal | undefined {
- return (config as any)[configInternalSymbol];
- }
-
constructor(location: ConfigLocation, userConfig: Config, configCLIOverrides: ConfigCLIOverrides) {
if (configCLIOverrides.projects && userConfig.projects)
throw new Error(`Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead.`);
diff --git a/packages/playwright/src/common/configLoader.ts b/packages/playwright/src/common/configLoader.ts
index 59ba309b2e..08a9c0a48d 100644
--- a/packages/playwright/src/common/configLoader.ts
+++ b/packages/playwright/src/common/configLoader.ts
@@ -334,25 +334,33 @@ export async function loadEmptyConfigForMergeReports() {
}
export function restartWithExperimentalTsEsm(configFile: string | undefined, force: boolean = false): boolean {
- const nodeVersion = +process.versions.node.split('.')[0];
- // New experimental loader is only supported on Node 16+.
- if (nodeVersion < 16)
- return false;
- if (!configFile && !force)
- return false;
+ // Opt-out switch.
if (process.env.PW_DISABLE_TS_ESM)
return false;
- // Node.js < 20
+
+ // There are two esm loader APIs:
+ // - Older API that needs a process restart. Available in Node 16, 17, and non-latest 18, 19 and 20.
+ // - Newer API that works in-process. Available in Node 21+ and latest 18, 19 and 20.
+
+ // First check whether we have already restarted with the ESM loader from the older API.
if ((globalThis as any).__esmLoaderPortPreV20) {
// clear execArgv after restart, so that childProcess.fork in user code does not inherit our loader.
process.execArgv = execArgvWithoutExperimentalLoaderOptions();
return false;
}
- if (!force && !fileIsModule(configFile!))
- return false;
- // Node.js < 20
+ // Now check for the newer API presence.
if (!require('node:module').register) {
+ // Older API is experimental, only supported on Node 16+.
+ const nodeVersion = +process.versions.node.split('.')[0];
+ if (nodeVersion < 16)
+ return false;
+
+ // With older API requiring a process restart, do so conditionally on the config.
+ const configIsModule = !!configFile && fileIsModule(configFile);
+ if (!force && !configIsModule)
+ return false;
+
const innerProcess = (require('child_process') as typeof import('child_process')).fork(require.resolve('../../cli'), process.argv.slice(2), {
env: {
...process.env,
@@ -367,7 +375,8 @@ export function restartWithExperimentalTsEsm(configFile: string | undefined, for
});
return true;
}
- // Nodejs >= 21
+
+ // With the newer API, always enable the ESM loader, because it does not need a restart.
registerESMLoader();
return false;
}
diff --git a/packages/playwright/src/common/ipc.ts b/packages/playwright/src/common/ipc.ts
index bb35f6c06d..3c3ad40f02 100644
--- a/packages/playwright/src/common/ipc.ts
+++ b/packages/playwright/src/common/ipc.ts
@@ -15,7 +15,7 @@
*/
import util from 'util';
-import { serializeCompilationCache } from '../transform/compilationCache';
+import { type SerializedCompilationCache, serializeCompilationCache } from '../transform/compilationCache';
import type { ConfigLocation, FullConfigInternal } from './config';
import type { ReporterDescription, TestInfoError, TestStatus } from '../../types/test';
@@ -43,7 +43,7 @@ export type ConfigCLIOverrides = {
export type SerializedConfig = {
location: ConfigLocation;
configCLIOverrides: ConfigCLIOverrides;
- compilationCache: any;
+ compilationCache?: SerializedCompilationCache;
};
export type TtyParams = {
diff --git a/packages/playwright/src/common/process.ts b/packages/playwright/src/common/process.ts
index f082e958c2..23de620649 100644
--- a/packages/playwright/src/common/process.ts
+++ b/packages/playwright/src/common/process.ts
@@ -131,4 +131,26 @@ function setTtyParams(stream: WriteStream, params: TtyParams) {
count = 16;
return count <= 2 ** params.colorDepth;
})as any;
+
+ // Stubs for the rest of the methods to avoid exceptions in user code.
+ stream.clearLine = (dir: any, callback?: () => void) => {
+ callback?.();
+ return true;
+ };
+ stream.clearScreenDown = (callback?: () => void) => {
+ callback?.();
+ return true;
+ };
+ (stream as any).cursorTo = (x: number, y?: number | (() => void), callback?: () => void) => {
+ if (callback)
+ callback();
+ else if (y instanceof Function)
+ y();
+ return true;
+ };
+ stream.moveCursor = (dx: number, dy: number, callback?: () => void) => {
+ callback?.();
+ return true;
+ };
+ stream.getWindowSize = () => [stream.columns, stream.rows];
}
diff --git a/packages/playwright/src/common/suiteUtils.ts b/packages/playwright/src/common/suiteUtils.ts
index c2140a8a96..81dc9d5465 100644
--- a/packages/playwright/src/common/suiteUtils.ts
+++ b/packages/playwright/src/common/suiteUtils.ts
@@ -90,7 +90,8 @@ export function applyRepeatEachIndex(project: FullProjectInternal, fileSuite: Su
// Assign test properties with project-specific values.
fileSuite.forEachTest((test, suite) => {
if (repeatEachIndex) {
- const testIdExpression = `[project=${project.id}]${test.titlePath().join('\x1e')} (repeat:${repeatEachIndex})`;
+ const [file, ...titles] = test.titlePath();
+ const testIdExpression = `[project=${project.id}]${toPosixPath(file)}\x1e${titles.join('\x1e')} (repeat:${repeatEachIndex})`;
const testId = suite._fileId + '-' + calculateSha1(testIdExpression).slice(0, 20);
test.id = testId;
test.repeatEachIndex = repeatEachIndex;
diff --git a/packages/playwright/src/common/test.ts b/packages/playwright/src/common/test.ts
index ec67168826..0d05ac5dfd 100644
--- a/packages/playwright/src/common/test.ts
+++ b/packages/playwright/src/common/test.ts
@@ -16,7 +16,6 @@
import type { FixturePool } from './fixtures';
import type * as reporterTypes from '../../types/testReporter';
-import type { SuitePrivate } from '../../types/reporterPrivate';
import type { TestTypeImpl } from './testType';
import { rootTestType } from './testType';
import type { Annotation, FixturesWithLocation, FullProjectInternal } from './config';
@@ -40,7 +39,7 @@ export type Modifier = {
description: string | undefined
};
-export class Suite extends Base implements SuitePrivate {
+export class Suite extends Base {
location?: Location;
parent?: Suite;
_use: FixturesWithLocation[] = [];
diff --git a/packages/playwright/src/common/testType.ts b/packages/playwright/src/common/testType.ts
index 1411b249b4..35e2a4668c 100644
--- a/packages/playwright/src/common/testType.ts
+++ b/packages/playwright/src/common/testType.ts
@@ -21,7 +21,7 @@ import { wrapFunctionWithLocation } from '../transform/transform';
import type { FixturesWithLocation } from './config';
import type { Fixtures, TestType, TestDetails } from '../../types/test';
import type { Location } from '../../types/testReporter';
-import { getPackageManagerExecCommand } from 'playwright-core/lib/utils';
+import { getPackageManagerExecCommand, zones } from 'playwright-core/lib/utils';
const testTypeSymbol = Symbol('testType');
@@ -263,9 +263,16 @@ export class TestTypeImpl {
const testInfo = currentTestInfo();
if (!testInfo)
throw new Error(`test.step() can only be called from a test`);
- return testInfo._runAsStep({ category: 'test.step', title, box: options.box }, async () => {
- // Make sure that internal "step" is not leaked to the user callback.
- return await body();
+ const step = testInfo._addStep({ wallTime: Date.now(), category: 'test.step', title, box: options.box });
+ return await zones.run('stepZone', step, async () => {
+ try {
+ const result = await body();
+ step.complete({});
+ return result;
+ } catch (error) {
+ step.complete({ error });
+ throw error;
+ }
});
}
diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts
index 59b70a1f62..2c105a2640 100644
--- a/packages/playwright/src/index.ts
+++ b/packages/playwright/src/index.ts
@@ -302,8 +302,8 @@ const playwrightFixtures: Fixtures = ({
const contexts = new Map();
await use(async options => {
- const hook = hookType(testInfoImpl);
- if (hook) {
+ const hook = testInfoImpl._currentHookType();
+ if (hook === 'beforeAll' || hook === 'afterAll') {
throw new Error([
`"context" and "page" fixtures are not supported in "${hook}" since they are created on a per-test basis.`,
`If you would like to reuse a single page between tests, create context manually with browser.newContext(). See https://aka.ms/playwright/reuse-page for details.`,
@@ -396,12 +396,6 @@ const playwrightFixtures: Fixtures = ({
},
});
-function hookType(testInfo: TestInfoImpl): 'beforeAll' | 'afterAll' | undefined {
- const type = testInfo._timeoutManager.currentRunnableType();
- if (type === 'beforeAll' || type === 'afterAll')
- return type;
-}
-
type StackFrame = {
file: string,
line?: number,
diff --git a/packages/trace-viewer/src/events.ts b/packages/playwright/src/isomorphic/events.ts
similarity index 100%
rename from packages/trace-viewer/src/events.ts
rename to packages/playwright/src/isomorphic/events.ts
diff --git a/packages/playwright/src/isomorphic/teleReceiver.ts b/packages/playwright/src/isomorphic/teleReceiver.ts
index a698f2b119..e675ce336b 100644
--- a/packages/playwright/src/isomorphic/teleReceiver.ts
+++ b/packages/playwright/src/isomorphic/teleReceiver.ts
@@ -14,25 +14,19 @@
* limitations under the License.
*/
-import type { FullConfig, FullResult, Location, TestError, TestResult, TestStatus, TestStep } from '../../types/testReporter';
import type { Annotation } from '../common/config';
import type { FullProject, Metadata } from '../../types/test';
import type * as reporterTypes from '../../types/testReporter';
-import type { SuitePrivate } from '../../types/reporterPrivate';
import type { ReporterV2 } from '../reporters/reporterV2';
-import { StringInternPool } from './stringInternPool';
-export type JsonLocation = Location;
+export type StringIntern = (s: string) => string;
+export type JsonLocation = reporterTypes.Location;
export type JsonError = string;
export type JsonStackFrame = { file: string, line: number, column: number };
export type JsonStdIOType = 'stdout' | 'stderr';
-export type JsonConfig = Pick & {
- listOnly: boolean;
-};
-
-export type MergeReporterConfig = Pick;
+export type JsonConfig = Pick;
export type JsonPattern = {
s?: string;
@@ -40,18 +34,20 @@ export type JsonPattern = {
};
export type JsonProject = {
- id: string;
grep: JsonPattern[];
grepInvert: JsonPattern[];
metadata: Metadata;
name: string;
dependencies: string[];
+ // This is relative to root dir.
snapshotDir: string;
+ // This is relative to root dir.
outputDir: string;
repeatEach: number;
retries: number;
suites: JsonSuite[];
teardown?: string;
+ // This is relative to root dir.
testDir: string;
testIgnore: JsonPattern[];
testMatch: JsonPattern[];
@@ -59,13 +55,10 @@ export type JsonProject = {
};
export type JsonSuite = {
- type: 'root' | 'project' | 'file' | 'describe';
title: string;
location?: JsonLocation;
suites: JsonSuite[];
tests: JsonTestCase[];
- fileId: string | undefined;
- parallelMode: 'none' | 'default' | 'serial' | 'parallel';
};
export type JsonTestCase = {
@@ -74,11 +67,12 @@ export type JsonTestCase = {
location: JsonLocation;
retries: number;
tags?: string[];
+ repeatEachIndex: number;
};
export type JsonTestEnd = {
testId: string;
- expectedStatus: TestStatus;
+ expectedStatus: reporterTypes.TestStatus;
timeout: number;
annotations: { type: string, description?: string }[];
};
@@ -91,13 +85,13 @@ export type JsonTestResultStart = {
startTime: number;
};
-export type JsonAttachment = Omit & { base64?: string };
+export type JsonAttachment = Omit & { base64?: string };
export type JsonTestResultEnd = {
id: string;
duration: number;
- status: TestStatus;
- errors: TestError[];
+ status: reporterTypes.TestStatus;
+ errors: reporterTypes.TestError[];
attachments: JsonAttachment[];
};
@@ -107,17 +101,17 @@ export type JsonTestStepStart = {
title: string;
category: string,
startTime: number;
- location?: Location;
+ location?: reporterTypes.Location;
};
export type JsonTestStepEnd = {
id: string;
duration: number;
- error?: TestError;
+ error?: reporterTypes.TestError;
};
export type JsonFullResult = {
- status: FullResult['status'];
+ status: reporterTypes.FullResult['status'];
startTime: number;
duration: number;
};
@@ -127,25 +121,32 @@ export type JsonEvent = {
params: any
};
+type TeleReporterReceiverOptions = {
+ mergeProjects: boolean;
+ mergeTestCases: boolean;
+ resolvePath: (rootDir: string, relativePath: string) => string;
+ configOverrides?: Pick;
+ clearPreviousResultsWhenTestBegins?: boolean;
+};
+
export class TeleReporterReceiver {
private _rootSuite: TeleSuite;
- private _pathSeparator: string;
+ private _options: TeleReporterReceiverOptions;
private _reporter: Partial;
private _tests = new Map();
private _rootDir!: string;
- private _listOnly = false;
- private _clearPreviousResultsWhenTestBegins: boolean = false;
- private _reuseTestCases: boolean;
- private _reportConfig: MergeReporterConfig | undefined;
- private _config!: FullConfig;
- private _stringPool = new StringInternPool();
+ private _config!: reporterTypes.FullConfig;
- constructor(pathSeparator: string, reporter: Partial, reuseTestCases: boolean, reportConfig?: MergeReporterConfig) {
+ constructor(reporter: Partial, options: TeleReporterReceiverOptions) {
this._rootSuite = new TeleSuite('', 'root');
- this._pathSeparator = pathSeparator;
+ this._options = options;
this._reporter = reporter;
- this._reuseTestCases = reuseTestCases;
- this._reportConfig = reportConfig;
+ }
+
+ reset() {
+ this._rootSuite.suites = [];
+ this._rootSuite.tests = [];
+ this._tests.clear();
}
dispatch(message: JsonEvent): Promise | void {
@@ -192,19 +193,14 @@ export class TeleReporterReceiver {
return this._onExit();
}
- _setClearPreviousResultsWhenTestBegins() {
- this._clearPreviousResultsWhenTestBegins = true;
- }
-
private _onConfigure(config: JsonConfig) {
this._rootDir = config.rootDir;
- this._listOnly = config.listOnly;
this._config = this._parseConfig(config);
this._reporter.onConfigure?.(this._config);
}
private _onProject(project: JsonProject) {
- let projectSuite = this._rootSuite.suites.find(suite => suite.project()!.__projectId === project.id);
+ let projectSuite = this._options.mergeProjects ? this._rootSuite.suites.find(suite => suite.project()!.name === project.name) : undefined;
if (!projectSuite) {
projectSuite = new TeleSuite(project.name, 'project');
this._rootSuite.suites.push(projectSuite);
@@ -213,23 +209,6 @@ export class TeleReporterReceiver {
// Always update project in watch mode.
projectSuite._project = this._parseProject(project);
this._mergeSuitesInto(project.suites, projectSuite);
-
- // Remove deleted tests when listing. Empty suites will be auto-filtered
- // in the UI layer.
- if (this._listOnly) {
- const testIds = new Set();
- const collectIds = (suite: JsonSuite) => {
- suite.tests.map(t => t.testId).forEach(testId => testIds.add(testId));
- suite.suites.forEach(collectIds);
- };
- project.suites.forEach(collectIds);
-
- const filterTests = (suite: TeleSuite) => {
- suite.tests = suite.tests.filter(t => testIds.has(t.id));
- suite.suites.forEach(filterTests);
- };
- filterTests(projectSuite);
- }
}
private _onBegin() {
@@ -238,14 +217,13 @@ export class TeleReporterReceiver {
private _onTestBegin(testId: string, payload: JsonTestResultStart) {
const test = this._tests.get(testId)!;
- if (this._clearPreviousResultsWhenTestBegins)
+ if (this._options.clearPreviousResultsWhenTestBegins)
test._clearResults();
const testResult = test._createTestResult(payload.id);
testResult.retry = payload.retry;
testResult.workerIndex = payload.workerIndex;
testResult.parallelIndex = payload.parallelIndex;
testResult.setStartTimeNumber(payload.startTime);
- testResult.statusEx = 'running';
this._reporter.onTestBegin?.(test, testResult);
}
@@ -254,22 +232,21 @@ export class TeleReporterReceiver {
test.timeout = testEndPayload.timeout;
test.expectedStatus = testEndPayload.expectedStatus;
test.annotations = testEndPayload.annotations;
- const result = test.resultsMap.get(payload.id)!;
+ const result = test._resultsMap.get(payload.id)!;
result.duration = payload.duration;
result.status = payload.status;
- result.statusEx = payload.status;
result.errors = payload.errors;
result.error = result.errors?.[0];
result.attachments = this._parseAttachments(payload.attachments);
this._reporter.onTestEnd?.(test, result);
// Free up the memory as won't see these step ids.
- result.stepMap = new Map();
+ result._stepMap = new Map();
}
private _onStepBegin(testId: string, resultId: string, payload: JsonTestStepStart) {
const test = this._tests.get(testId)!;
- const result = test.resultsMap.get(resultId)!;
- const parentStep = payload.parentStepId ? result.stepMap.get(payload.parentStepId) : undefined;
+ const result = test._resultsMap.get(resultId)!;
+ const parentStep = payload.parentStepId ? result._stepMap.get(payload.parentStepId) : undefined;
const location = this._absoluteLocation(payload.location);
const step = new TeleTestStep(payload, parentStep, location);
@@ -277,27 +254,27 @@ export class TeleReporterReceiver {
parentStep.steps.push(step);
else
result.steps.push(step);
- result.stepMap.set(payload.id, step);
+ result._stepMap.set(payload.id, step);
this._reporter.onStepBegin?.(test, result, step);
}
private _onStepEnd(testId: string, resultId: string, payload: JsonTestStepEnd) {
const test = this._tests.get(testId)!;
- const result = test.resultsMap.get(resultId)!;
- const step = result.stepMap.get(payload.id)!;
+ const result = test._resultsMap.get(resultId)!;
+ const step = result._stepMap.get(payload.id)!;
step.duration = payload.duration;
step.error = payload.error;
this._reporter.onStepEnd?.(test, result, step);
}
- private _onError(error: TestError) {
+ private _onError(error: reporterTypes.TestError) {
this._reporter.onError?.(error);
}
private _onStdIO(type: JsonStdIOType, testId: string | undefined, resultId: string | undefined, data: string, isBase64: boolean) {
const chunk = isBase64 ? ((globalThis as any).Buffer ? Buffer.from(data, 'base64') : atob(data)) : data;
const test = testId ? this._tests.get(testId) : undefined;
- const result = test && resultId ? test.resultsMap.get(resultId) : undefined;
+ const result = test && resultId ? test._resultsMap.get(resultId) : undefined;
if (type === 'stdout') {
result?.stdout.push(chunk);
this._reporter.onStdOut?.(chunk, test, result);
@@ -316,25 +293,22 @@ export class TeleReporterReceiver {
}
private _onExit(): Promise | void {
- // Free up the memory from the string pool.
- this._stringPool = new StringInternPool();
return this._reporter.onExit?.();
}
- private _parseConfig(config: JsonConfig): FullConfig {
+ private _parseConfig(config: JsonConfig): reporterTypes.FullConfig {
const result = { ...baseFullConfig, ...config };
- if (this._reportConfig) {
- result.configFile = this._reportConfig.configFile;
- result.reportSlowTests = this._reportConfig.reportSlowTests;
- result.quiet = this._reportConfig.quiet;
- result.reporter = [...this._reportConfig.reporter];
+ if (this._options.configOverrides) {
+ result.configFile = this._options.configOverrides.configFile;
+ result.reportSlowTests = this._options.configOverrides.reportSlowTests;
+ result.quiet = this._options.configOverrides.quiet;
+ result.reporter = [...this._options.configOverrides.reporter];
}
return result;
}
private _parseProject(project: JsonProject): TeleFullProject {
return {
- __projectId: project.id,
metadata: project.metadata,
name: project.name,
outputDir: this._absolutePath(project.outputDir),
@@ -353,7 +327,7 @@ export class TeleReporterReceiver {
};
}
- private _parseAttachments(attachments: JsonAttachment[]): TestResult['attachments'] {
+ private _parseAttachments(attachments: JsonAttachment[]): reporterTypes.TestResult['attachments'] {
return attachments.map(a => {
return {
...a,
@@ -366,13 +340,11 @@ export class TeleReporterReceiver {
for (const jsonSuite of jsonSuites) {
let targetSuite = parent.suites.find(s => s.title === jsonSuite.title);
if (!targetSuite) {
- targetSuite = new TeleSuite(jsonSuite.title, jsonSuite.type);
+ targetSuite = new TeleSuite(jsonSuite.title, parent._type === 'project' ? 'file' : 'describe');
targetSuite.parent = parent;
parent.suites.push(targetSuite);
}
targetSuite.location = this._absoluteLocation(jsonSuite.location);
- targetSuite._fileId = jsonSuite.fileId;
- targetSuite._parallelMode = jsonSuite.parallelMode;
this._mergeSuitesInto(jsonSuite.suites, targetSuite);
this._mergeTestsInto(jsonSuite.tests, targetSuite);
}
@@ -380,9 +352,9 @@ export class TeleReporterReceiver {
private _mergeTestsInto(jsonTests: JsonTestCase[], parent: TeleSuite) {
for (const jsonTest of jsonTests) {
- let targetTest = this._reuseTestCases ? parent.tests.find(s => s.title === jsonTest.title) : undefined;
+ let targetTest = this._options.mergeTestCases ? parent.tests.find(s => s.title === jsonTest.title && s.repeatEachIndex === jsonTest.repeatEachIndex) : undefined;
if (!targetTest) {
- targetTest = new TeleTestCase(jsonTest.testId, jsonTest.title, this._absoluteLocation(jsonTest.location));
+ targetTest = new TeleTestCase(jsonTest.testId, jsonTest.title, this._absoluteLocation(jsonTest.location), jsonTest.repeatEachIndex);
targetTest.parent = parent;
parent.tests.push(targetTest);
this._tests.set(targetTest.id, targetTest);
@@ -399,9 +371,9 @@ export class TeleReporterReceiver {
return test;
}
- private _absoluteLocation(location: Location): Location;
- private _absoluteLocation(location?: Location): Location | undefined;
- private _absoluteLocation(location: Location | undefined): Location | undefined {
+ private _absoluteLocation(location: reporterTypes.Location): reporterTypes.Location;
+ private _absoluteLocation(location?: reporterTypes.Location): reporterTypes.Location | undefined;
+ private _absoluteLocation(location: reporterTypes.Location | undefined): reporterTypes.Location | undefined {
if (!location)
return location;
return {
@@ -413,23 +385,21 @@ export class TeleReporterReceiver {
private _absolutePath(relativePath: string): string;
private _absolutePath(relativePath?: string): string | undefined;
private _absolutePath(relativePath?: string): string | undefined {
- if (!relativePath)
- return relativePath;
- return this._stringPool.internString(this._rootDir + this._pathSeparator + relativePath);
+ if (relativePath === undefined)
+ return;
+ return this._options.resolvePath(this._rootDir, relativePath);
}
-
}
-export class TeleSuite implements SuitePrivate {
+export class TeleSuite implements reporterTypes.Suite {
title: string;
- location?: Location;
+ location?: reporterTypes.Location;
parent?: TeleSuite;
_requireFile: string = '';
suites: TeleSuite[] = [];
tests: TeleTestCase[] = [];
_timeout: number | undefined;
_retries: number | undefined;
- _fileId: string | undefined;
_project: TeleFullProject | undefined;
_parallelMode: 'none' | 'default' | 'serial' | 'parallel' = 'none';
readonly _type: 'root' | 'project' | 'file' | 'describe';
@@ -470,7 +440,7 @@ export class TeleTestCase implements reporterTypes.TestCase {
title: string;
fn = () => {};
results: TeleTestResult[] = [];
- location: Location;
+ location: reporterTypes.Location;
parent!: TeleSuite;
expectedStatus: reporterTypes.TestStatus = 'passed';
@@ -481,12 +451,13 @@ export class TeleTestCase implements reporterTypes.TestCase {
repeatEachIndex = 0;
id: string;
- resultsMap = new Map();
+ _resultsMap = new Map();
- constructor(id: string, title: string, location: Location) {
+ constructor(id: string, title: string, location: reporterTypes.Location, repeatEachIndex: number) {
this.id = id;
this.title = title;
this.location = location;
+ this.repeatEachIndex = repeatEachIndex;
}
titlePath(): string[] {
@@ -520,28 +491,28 @@ export class TeleTestCase implements reporterTypes.TestCase {
_clearResults() {
this.results = [];
- this.resultsMap.clear();
+ this._resultsMap.clear();
}
_createTestResult(id: string): TeleTestResult {
const result = new TeleTestResult(this.results.length);
this.results.push(result);
- this.resultsMap.set(id, result);
+ this._resultsMap.set(id, result);
return result;
}
}
-class TeleTestStep implements TestStep {
+class TeleTestStep implements reporterTypes.TestStep {
title: string;
category: string;
- location: Location | undefined;
- parent: TestStep | undefined;
+ location: reporterTypes.Location | undefined;
+ parent: reporterTypes.TestStep | undefined;
duration: number = -1;
- steps: TestStep[] = [];
+ steps: reporterTypes.TestStep[] = [];
private _startTime: number = 0;
- constructor(payload: JsonTestStepStart, parentStep: TestStep | undefined, location: Location | undefined) {
+ constructor(payload: JsonTestStepStart, parentStep: reporterTypes.TestStep | undefined, location: reporterTypes.Location | undefined) {
this.title = payload.title;
this.category = payload.category;
this.location = location;
@@ -571,13 +542,12 @@ class TeleTestResult implements reporterTypes.TestResult {
stdout: reporterTypes.TestResult['stdout'] = [];
stderr: reporterTypes.TestResult['stderr'] = [];
attachments: reporterTypes.TestResult['attachments'] = [];
- status: TestStatus = 'skipped';
+ status: reporterTypes.TestStatus = 'skipped';
steps: TeleTestStep[] = [];
errors: reporterTypes.TestResult['errors'] = [];
error: reporterTypes.TestResult['error'];
- stepMap: Map = new Map();
- statusEx: reporterTypes.TestResult['status'] | 'scheduled' | 'running' = 'scheduled';
+ _stepMap: Map = new Map();
private _startTime: number = 0;
@@ -598,9 +568,9 @@ class TeleTestResult implements reporterTypes.TestResult {
}
}
-export type TeleFullProject = FullProject & { __projectId: string };
+export type TeleFullProject = FullProject;
-export const baseFullConfig: FullConfig = {
+export const baseFullConfig: reporterTypes.FullConfig = {
forbidOnly: false,
fullyParallel: false,
globalSetup: null,
diff --git a/packages/playwright/src/isomorphic/testServerConnection.ts b/packages/playwright/src/isomorphic/testServerConnection.ts
new file mode 100644
index 0000000000..3a07fbcb31
--- /dev/null
+++ b/packages/playwright/src/isomorphic/testServerConnection.ts
@@ -0,0 +1,183 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { TestServerInterface, TestServerInterfaceEvents } from '@testIsomorphic/testServerInterface';
+import type * as reporterTypes from 'playwright/types/testReporter';
+import * as events from './events';
+
+export class TestServerConnection implements TestServerInterface, TestServerInterfaceEvents {
+ readonly onClose: events.Event;
+ readonly onReport: events.Event;
+ readonly onStdio: events.Event<{ type: 'stderr' | 'stdout'; text?: string | undefined; buffer?: string | undefined; }>;
+ readonly onListChanged: events.Event;
+ readonly onTestFilesChanged: events.Event<{ testFiles: string[] }>;
+ readonly onLoadTraceRequested: events.Event<{ traceUrl: string }>;
+
+ private _onCloseEmitter = new events.EventEmitter();
+ private _onReportEmitter = new events.EventEmitter();
+ private _onStdioEmitter = new events.EventEmitter<{ type: 'stderr' | 'stdout'; text?: string | undefined; buffer?: string | undefined; }>();
+ private _onListChangedEmitter = new events.EventEmitter();
+ private _onTestFilesChangedEmitter = new events.EventEmitter<{ testFiles: string[] }>();
+ private _onLoadTraceRequestedEmitter = new events.EventEmitter<{ traceUrl: string }>();
+
+ private _lastId = 0;
+ private _ws: WebSocket;
+ private _callbacks = new Map void, reject: (arg: Error) => void }>();
+ private _connectedPromise: Promise;
+
+ constructor(wsURL: string) {
+ this.onClose = this._onCloseEmitter.event;
+ this.onReport = this._onReportEmitter.event;
+ this.onStdio = this._onStdioEmitter.event;
+ this.onListChanged = this._onListChangedEmitter.event;
+ this.onTestFilesChanged = this._onTestFilesChangedEmitter.event;
+ this.onLoadTraceRequested = this._onLoadTraceRequestedEmitter.event;
+
+ this._ws = new WebSocket(wsURL);
+ this._ws.addEventListener('message', event => {
+ const message = JSON.parse(String(event.data));
+ const { id, result, error, method, params } = message;
+ if (id) {
+ const callback = this._callbacks.get(id);
+ if (!callback)
+ return;
+ this._callbacks.delete(id);
+ if (error)
+ callback.reject(new Error(error));
+ else
+ callback.resolve(result);
+ } else {
+ this._dispatchEvent(method, params);
+ }
+ });
+ const pingInterval = setInterval(() => this._sendMessage('ping').catch(() => {}), 30000);
+ this._connectedPromise = new Promise((f, r) => {
+ this._ws.addEventListener('open', () => {
+ f();
+ this._ws.send(JSON.stringify({ method: 'ready' }));
+ });
+ this._ws.addEventListener('error', r);
+ });
+ this._ws.addEventListener('close', () => {
+ this._onCloseEmitter.fire();
+ clearInterval(pingInterval);
+ });
+ }
+
+ private async _sendMessage(method: string, params?: any): Promise {
+ const logForTest = (globalThis as any).__logForTest;
+ logForTest?.({ method, params });
+
+ await this._connectedPromise;
+ const id = ++this._lastId;
+ const message = { id, method, params };
+ this._ws.send(JSON.stringify(message));
+ return new Promise((resolve, reject) => {
+ this._callbacks.set(id, { resolve, reject });
+ });
+ }
+
+ private _sendMessageNoReply(method: string, params?: any) {
+ this._sendMessage(method, params).catch(() => {});
+ }
+
+ private _dispatchEvent(method: string, params?: any) {
+ if (method === 'report')
+ this._onReportEmitter.fire(params);
+ else if (method === 'stdio')
+ this._onStdioEmitter.fire(params);
+ else if (method === 'listChanged')
+ this._onListChangedEmitter.fire(params);
+ else if (method === 'testFilesChanged')
+ this._onTestFilesChangedEmitter.fire(params);
+ }
+
+ async ping(): Promise {
+ await this._sendMessage('ping');
+ }
+
+ async pingNoReply() {
+ this._sendMessageNoReply('ping');
+ }
+
+ async watch(params: { fileNames: string[]; }): Promise {
+ await this._sendMessage('watch', params);
+ }
+
+ watchNoReply(params: { fileNames: string[]; }) {
+ this._sendMessageNoReply('watch', params);
+ }
+
+ async open(params: { location: reporterTypes.Location; }): Promise {
+ await this._sendMessage('open', params);
+ }
+
+ openNoReply(params: { location: reporterTypes.Location; }) {
+ this._sendMessageNoReply('open', params);
+ }
+
+ async resizeTerminal(params: { cols: number; rows: number; }): Promise {
+ await this._sendMessage('resizeTerminal', params);
+ }
+
+ resizeTerminalNoReply(params: { cols: number; rows: number; }) {
+ this._sendMessageNoReply('resizeTerminal', params);
+ }
+
+ async checkBrowsers(): Promise<{ hasBrowsers: boolean; }> {
+ return await this._sendMessage('checkBrowsers');
+ }
+
+ async installBrowsers(): Promise {
+ await this._sendMessage('installBrowsers');
+ }
+
+ async runGlobalSetup(): Promise<'passed' | 'failed' | 'timedout' | 'interrupted'> {
+ return await this._sendMessage('runGlobalSetup');
+ }
+
+ async runGlobalTeardown(): Promise<'passed' | 'failed' | 'timedout' | 'interrupted'> {
+ return await this._sendMessage('runGlobalTeardown');
+ }
+
+ async listFiles(): Promise<{ projects: { name: string; testDir: string; use: { testIdAttribute?: string | undefined; }; files: string[]; }[]; cliEntryPoint?: string | undefined; error?: reporterTypes.TestError | undefined; }> {
+ return await this._sendMessage('listFiles');
+ }
+
+ async listTests(params: { reporter?: string | undefined; fileNames?: string[] | undefined; }): Promise<{ report: any[] }> {
+ return await this._sendMessage('listTests', params);
+ }
+
+ async runTests(params: { reporter?: string | undefined; locations?: string[] | undefined; grep?: string | undefined; testIds?: string[] | undefined; headed?: boolean | undefined; oneWorker?: boolean | undefined; trace?: 'off' | 'on' | undefined; projects?: string[] | undefined; reuseContext?: boolean | undefined; connectWsEndpoint?: string | undefined; }): Promise<{ status: reporterTypes.FullResult['status'] }> {
+ return await this._sendMessage('runTests', params);
+ }
+
+ async findRelatedTestFiles(params: { files: string[]; }): Promise<{ testFiles: string[]; errors?: reporterTypes.TestError[] | undefined; }> {
+ return await this._sendMessage('findRelatedTestFiles', params);
+ }
+
+ async stopTests(): Promise {
+ await this._sendMessage('stopTests');
+ }
+
+ stopTestsNoReply() {
+ this._sendMessageNoReply('stopTests');
+ }
+
+ async closeGracefully(): Promise {
+ await this._sendMessage('closeGracefully');
+ }
+}
diff --git a/packages/playwright/src/isomorphic/testServerInterface.ts b/packages/playwright/src/isomorphic/testServerInterface.ts
new file mode 100644
index 0000000000..77fdd6049c
--- /dev/null
+++ b/packages/playwright/src/isomorphic/testServerInterface.ts
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type * as reporterTypes from '../../types/testReporter';
+import type { Event } from './events';
+
+export interface TestServerInterface {
+ ping(): Promise;
+
+ watch(params: {
+ fileNames: string[];
+ }): Promise;
+
+ open(params: { location: reporterTypes.Location }): Promise;
+
+ resizeTerminal(params: { cols: number, rows: number }): Promise;
+
+ checkBrowsers(): Promise<{ hasBrowsers: boolean }>;
+
+ installBrowsers(): Promise;
+
+ runGlobalSetup(): Promise;
+
+ runGlobalTeardown(): Promise;
+
+ listFiles(): Promise<{
+ projects: {
+ name: string;
+ testDir: string;
+ use: { testIdAttribute?: string };
+ files: string[];
+ }[];
+ cliEntryPoint?: string;
+ error?: reporterTypes.TestError;
+ }>;
+
+ /**
+ * Returns list of teleReporter events.
+ */
+ listTests(params: {
+ reporter?: string;
+ fileNames?: string[];
+ }): Promise<{ report: any[] }>;
+
+ runTests(params: {
+ reporter?: string;
+ locations?: string[];
+ grep?: string;
+ testIds?: string[];
+ headed?: boolean;
+ oneWorker?: boolean;
+ trace?: 'on' | 'off';
+ projects?: string[];
+ reuseContext?: boolean;
+ connectWsEndpoint?: string;
+ }): Promise<{ status: reporterTypes.FullResult['status'] }>;
+
+ findRelatedTestFiles(params: {
+ files: string[];
+ }): Promise<{ testFiles: string[]; errors?: reporterTypes.TestError[]; }>;
+
+ stopTests(): Promise;
+
+ closeGracefully(): Promise;
+}
+
+export interface TestServerInterfaceEvents {
+ onClose: Event;
+ onReport: Event;
+ onStdio: Event<{ type: 'stdout' | 'stderr', text?: string, buffer?: string }>;
+ onListChanged: Event;
+ onTestFilesChanged: Event<{ testFiles: string[] }>;
+ onLoadTraceRequested: Event<{ traceUrl: string }>;
+}
+
+export interface TestServerInterfaceEventEmitters {
+ dispatchEvent(event: 'close', params: {}): void;
+ dispatchEvent(event: 'report', params: any): void;
+ dispatchEvent(event: 'stdio', params: { type: 'stdout' | 'stderr', text?: string, buffer?: string }): void;
+ dispatchEvent(event: 'listChanged', params: {}): void;
+ dispatchEvent(event: 'testFilesChanged', params: { testFiles: string[] }): void;
+ dispatchEvent(event: 'loadTraceRequested', params: { traceUrl: string }): void;
+}
diff --git a/packages/playwright/src/isomorphic/testTree.ts b/packages/playwright/src/isomorphic/testTree.ts
new file mode 100644
index 0000000000..64f414a763
--- /dev/null
+++ b/packages/playwright/src/isomorphic/testTree.ts
@@ -0,0 +1,352 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export type TestItemStatus = 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped';
+import type * as reporterTypes from '../../types/testReporter';
+
+export type TreeItemBase = {
+ kind: 'root' | 'group' | 'case' | 'test',
+ id: string;
+ title: string;
+ location: reporterTypes.Location,
+ duration: number;
+ parent: TreeItem | undefined;
+ children: TreeItem[];
+ status: TestItemStatus;
+};
+
+export type GroupItem = TreeItemBase & {
+ kind: 'group';
+ subKind: 'folder' | 'file' | 'describe';
+ hasLoadErrors: boolean;
+ children: (TestCaseItem | GroupItem)[];
+};
+
+export type TestCaseItem = TreeItemBase & {
+ kind: 'case',
+ tests: reporterTypes.TestCase[];
+ children: TestItem[];
+ test: reporterTypes.TestCase | undefined;
+ project: reporterTypes.FullProject | undefined;
+ tags: Array;
+};
+
+export type TestItem = TreeItemBase & {
+ kind: 'test',
+ test: reporterTypes.TestCase;
+ project: reporterTypes.FullProject;
+};
+
+export type TreeItem = GroupItem | TestCaseItem | TestItem;
+
+export class TestTree {
+ rootItem: GroupItem;
+ private _treeItemById = new Map();
+ private _treeItemByTestId = new Map();
+ readonly pathSeparator: string;
+
+ constructor(rootFolder: string, rootSuite: reporterTypes.Suite | undefined, loadErrors: reporterTypes.TestError[], projectFilters: Map | undefined, pathSeparator: string) {
+ const filterProjects = projectFilters && [...projectFilters.values()].some(Boolean);
+ this.pathSeparator = pathSeparator;
+ this.rootItem = {
+ kind: 'group',
+ subKind: 'folder',
+ id: rootFolder,
+ title: '',
+ location: { file: '', line: 0, column: 0 },
+ duration: 0,
+ parent: undefined,
+ children: [],
+ status: 'none',
+ hasLoadErrors: false,
+ };
+ this._treeItemById.set(rootFolder, this.rootItem);
+
+ const visitSuite = (project: reporterTypes.FullProject, parentSuite: reporterTypes.Suite, parentGroup: GroupItem) => {
+ for (const suite of parentSuite.suites) {
+ const title = suite.title || '';
+ let group = parentGroup.children.find(item => item.kind === 'group' && item.title === title) as GroupItem | undefined;
+ if (!group) {
+ group = {
+ kind: 'group',
+ subKind: 'describe',
+ id: 'suite:' + parentSuite.titlePath().join('\x1e') + '\x1e' + title, // account for anonymous suites
+ title,
+ location: suite.location!,
+ duration: 0,
+ parent: parentGroup,
+ children: [],
+ status: 'none',
+ hasLoadErrors: false,
+ };
+ this._addChild(parentGroup, group);
+ }
+ visitSuite(project, suite, group);
+ }
+
+ for (const test of parentSuite.tests) {
+ const title = test.title;
+ let testCaseItem = parentGroup.children.find(t => t.kind !== 'group' && t.title === title) as TestCaseItem;
+ if (!testCaseItem) {
+ testCaseItem = {
+ kind: 'case',
+ id: 'test:' + test.titlePath().join('\x1e'),
+ title,
+ parent: parentGroup,
+ children: [],
+ tests: [],
+ location: test.location,
+ duration: 0,
+ status: 'none',
+ project: undefined,
+ test: undefined,
+ tags: test.tags,
+ };
+ this._addChild(parentGroup, testCaseItem);
+ }
+
+ const result = test.results[0];
+ let status: 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped' = 'none';
+ if ((result as any)?.[statusEx] === 'scheduled')
+ status = 'scheduled';
+ else if ((result as any)?.[statusEx] === 'running')
+ status = 'running';
+ else if (result?.status === 'skipped')
+ status = 'skipped';
+ else if (result?.status === 'interrupted')
+ status = 'none';
+ else if (result && test.outcome() !== 'expected')
+ status = 'failed';
+ else if (result && test.outcome() === 'expected')
+ status = 'passed';
+
+ testCaseItem.tests.push(test);
+ const testItem: TestItem = {
+ kind: 'test',
+ id: test.id,
+ title: project.name,
+ location: test.location!,
+ test,
+ parent: testCaseItem,
+ children: [],
+ status,
+ duration: test.results.length ? Math.max(0, test.results[0].duration) : 0,
+ project,
+ };
+ this._addChild(testCaseItem, testItem);
+ this._treeItemByTestId.set(test.id, testItem);
+ testCaseItem.duration = (testCaseItem.children as TestItem[]).reduce((a, b) => a + b.duration, 0);
+ }
+ };
+
+ for (const projectSuite of rootSuite?.suites || []) {
+ if (filterProjects && !projectFilters.get(projectSuite.title))
+ continue;
+ for (const fileSuite of projectSuite.suites) {
+ const fileItem = this._fileItem(fileSuite.location!.file.split(pathSeparator), true);
+ visitSuite(projectSuite.project()!, fileSuite, fileItem);
+ }
+ }
+
+ for (const loadError of loadErrors) {
+ if (!loadError.location)
+ continue;
+ const fileItem = this._fileItem(loadError.location.file.split(pathSeparator), true);
+ fileItem.hasLoadErrors = true;
+ }
+ }
+
+ private _addChild(parent: TreeItem, child: TreeItem) {
+ parent.children.push(child);
+ child.parent = parent;
+ this._treeItemById.set(child.id, child);
+ }
+
+ filterTree(filterText: string, statusFilters: Map, runningTestIds: Set | undefined) {
+ const tokens = filterText.trim().toLowerCase().split(' ');
+ const filtersStatuses = [...statusFilters.values()].some(Boolean);
+
+ const filter = (testCase: TestCaseItem) => {
+ const titleWithTags = [...testCase.tests[0].titlePath(), ...testCase.tests[0].tags].join(' ').toLowerCase();
+ if (!tokens.every(token => titleWithTags.includes(token)) && !testCase.tests.some(t => runningTestIds?.has(t.id)))
+ return false;
+ testCase.children = (testCase.children as TestItem[]).filter(test => {
+ return !filtersStatuses || runningTestIds?.has(test.test.id) || statusFilters.get(test.status);
+ });
+ testCase.tests = (testCase.children as TestItem[]).map(c => c.test);
+ return !!testCase.children.length;
+ };
+
+ const visit = (treeItem: GroupItem) => {
+ const newChildren: (GroupItem | TestCaseItem)[] = [];
+ for (const child of treeItem.children) {
+ if (child.kind === 'case') {
+ if (filter(child))
+ newChildren.push(child);
+ } else {
+ visit(child);
+ if (child.children.length || child.hasLoadErrors)
+ newChildren.push(child);
+ }
+ }
+ treeItem.children = newChildren;
+ };
+ visit(this.rootItem);
+ }
+
+ private _fileItem(filePath: string[], isFile: boolean): GroupItem {
+ if (filePath.length === 0)
+ return this.rootItem;
+ const fileName = filePath.join(this.pathSeparator);
+ const existingFileItem = this._treeItemById.get(fileName);
+ if (existingFileItem)
+ return existingFileItem as GroupItem;
+ const parentFileItem = this._fileItem(filePath.slice(0, filePath.length - 1), false);
+ const fileItem: GroupItem = {
+ kind: 'group',
+ subKind: isFile ? 'file' : 'folder',
+ id: fileName,
+ title: filePath[filePath.length - 1],
+ location: { file: fileName, line: 0, column: 0 },
+ duration: 0,
+ parent: parentFileItem,
+ children: [],
+ status: 'none',
+ hasLoadErrors: false,
+ };
+ this._addChild(parentFileItem, fileItem);
+ return fileItem;
+ }
+
+ sortAndPropagateStatus() {
+ sortAndPropagateStatus(this.rootItem);
+ }
+
+ flattenForSingleProject() {
+ const visit = (treeItem: TreeItem) => {
+ if (treeItem.kind === 'case' && treeItem.children.length === 1) {
+ treeItem.project = treeItem.children[0].project;
+ treeItem.test = treeItem.children[0].test;
+ treeItem.children = [];
+ this._treeItemByTestId.set(treeItem.test.id, treeItem);
+ } else {
+ treeItem.children.forEach(visit);
+ }
+ };
+ visit(this.rootItem);
+ }
+
+ shortenRoot() {
+ let shortRoot = this.rootItem;
+ while (shortRoot.children.length === 1 && shortRoot.children[0].kind === 'group' && shortRoot.children[0].subKind === 'folder')
+ shortRoot = shortRoot.children[0];
+ shortRoot.location = this.rootItem.location;
+ this.rootItem = shortRoot;
+ }
+
+ testIds(): Set {
+ const result = new Set();
+ const visit = (treeItem: TreeItem) => {
+ if (treeItem.kind === 'case')
+ treeItem.tests.forEach(t => result.add(t.id));
+ treeItem.children.forEach(visit);
+ };
+ visit(this.rootItem);
+ return result;
+ }
+
+ fileNames(): string[] {
+ const result = new Set();
+ const visit = (treeItem: TreeItem) => {
+ if (treeItem.kind === 'group' && treeItem.subKind === 'file')
+ result.add(treeItem.id);
+ else
+ treeItem.children.forEach(visit);
+ };
+ visit(this.rootItem);
+ return [...result];
+ }
+
+ flatTreeItems(): TreeItem[] {
+ const result: TreeItem[] = [];
+ const visit = (treeItem: TreeItem) => {
+ result.push(treeItem);
+ treeItem.children.forEach(visit);
+ };
+ visit(this.rootItem);
+ return result;
+ }
+
+ treeItemById(id: string): TreeItem | undefined {
+ return this._treeItemById.get(id);
+ }
+
+ collectTestIds(treeItem?: TreeItem): Set {
+ const testIds = new Set();
+ if (!treeItem)
+ return testIds;
+
+ const visit = (treeItem: TreeItem) => {
+ if (treeItem.kind === 'case')
+ treeItem.tests.map(t => t.id).forEach(id => testIds.add(id));
+ else if (treeItem.kind === 'test')
+ testIds.add(treeItem.id);
+ else
+ treeItem.children?.forEach(visit);
+ };
+ visit(treeItem);
+ return testIds;
+ }
+}
+
+export function sortAndPropagateStatus(treeItem: TreeItem) {
+ for (const child of treeItem.children)
+ sortAndPropagateStatus(child);
+
+ if (treeItem.kind === 'group') {
+ treeItem.children.sort((a, b) => {
+ const fc = a.location.file.localeCompare(b.location.file);
+ return fc || a.location.line - b.location.line;
+ });
+ }
+
+ let allPassed = treeItem.children.length > 0;
+ let allSkipped = treeItem.children.length > 0;
+ let hasFailed = false;
+ let hasRunning = false;
+ let hasScheduled = false;
+
+ for (const child of treeItem.children) {
+ allSkipped = allSkipped && child.status === 'skipped';
+ allPassed = allPassed && (child.status === 'passed' || child.status === 'skipped');
+ hasFailed = hasFailed || child.status === 'failed';
+ hasRunning = hasRunning || child.status === 'running';
+ hasScheduled = hasScheduled || child.status === 'scheduled';
+ }
+
+ if (hasRunning)
+ treeItem.status = 'running';
+ else if (hasScheduled)
+ treeItem.status = 'scheduled';
+ else if (hasFailed)
+ treeItem.status = 'failed';
+ else if (allSkipped)
+ treeItem.status = 'skipped';
+ else if (allPassed)
+ treeItem.status = 'passed';
+}
+
+export const statusEx = Symbol('statusEx');
diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts
index 0b6b3da8ad..55f121474f 100644
--- a/packages/playwright/src/matchers/expect.ts
+++ b/packages/playwright/src/matchers/expect.ts
@@ -267,7 +267,6 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler {
params: args[0] ? { expected: args[0] } : undefined,
wallTime,
infectParentStepsWithError: this._info.isSoft,
- isSoft: this._info.isSoft,
};
const step = testInfo._addStep(stepInfo);
@@ -275,7 +274,9 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler {
const reportStepError = (jestError: ExpectError) => {
const error = new ExpectError(jestError, customMessage, stackFrames);
step.complete({ error });
- if (!this._info.isSoft)
+ if (this._info.isSoft)
+ testInfo._failWithError(error);
+ else
throw error;
};
diff --git a/packages/playwright/src/matchers/matcherHint.ts b/packages/playwright/src/matchers/matcherHint.ts
index 229ef6ca28..17d893431c 100644
--- a/packages/playwright/src/matchers/matcherHint.ts
+++ b/packages/playwright/src/matchers/matcherHint.ts
@@ -25,7 +25,7 @@ export function matcherHint(state: ExpectMatcherContext, locator: Locator | unde
if (timeout)
header = colors.red(`Timed out ${timeout}ms waiting for `) + header;
if (locator)
- header += `Locator: ${locator}\n`;
+ header += `Locator: ${String(locator)}\n`;
return header;
}
diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts
index 3dc08ba223..01d1dfad25 100644
--- a/packages/playwright/src/matchers/matchers.ts
+++ b/packages/playwright/src/matchers/matchers.ts
@@ -40,7 +40,7 @@ export function toBeAttached(
locator: LocatorEx,
options?: { attached?: boolean, timeout?: number },
) {
- const attached = !options || options.attached === undefined || options.attached === true;
+ const attached = !options || options.attached === undefined || options.attached;
const expected = attached ? 'attached' : 'detached';
const unexpected = attached ? 'detached' : 'attached';
const arg = attached ? '' : '{ attached: false }';
@@ -54,7 +54,7 @@ export function toBeChecked(
locator: LocatorEx,
options?: { checked?: boolean, timeout?: number },
) {
- const checked = !options || options.checked === undefined || options.checked === true;
+ const checked = !options || options.checked === undefined || options.checked;
const expected = checked ? 'checked' : 'unchecked';
const unexpected = checked ? 'unchecked' : 'checked';
const arg = checked ? '' : '{ checked: false }';
@@ -78,7 +78,7 @@ export function toBeEditable(
locator: LocatorEx,
options?: { editable?: boolean, timeout?: number },
) {
- const editable = !options || options.editable === undefined || options.editable === true;
+ const editable = !options || options.editable === undefined || options.editable;
const expected = editable ? 'editable' : 'readOnly';
const unexpected = editable ? 'readOnly' : 'editable';
const arg = editable ? '' : '{ editable: false }';
@@ -102,7 +102,7 @@ export function toBeEnabled(
locator: LocatorEx,
options?: { enabled?: boolean, timeout?: number },
) {
- const enabled = !options || options.enabled === undefined || options.enabled === true;
+ const enabled = !options || options.enabled === undefined || options.enabled;
const expected = enabled ? 'enabled' : 'disabled';
const unexpected = enabled ? 'disabled' : 'enabled';
const arg = enabled ? '' : '{ enabled: false }';
@@ -136,7 +136,7 @@ export function toBeVisible(
locator: LocatorEx,
options?: { visible?: boolean, timeout?: number },
) {
- const visible = !options || options.visible === undefined || options.visible === true;
+ const visible = !options || options.visible === undefined || options.visible;
const expected = visible ? 'visible' : 'hidden';
const unexpected = visible ? 'hidden' : 'visible';
const arg = visible ? '' : '{ visible: false }';
diff --git a/packages/playwright/src/matchers/toMatchSnapshot.ts b/packages/playwright/src/matchers/toMatchSnapshot.ts
index 80e602f5ad..442483a0de 100644
--- a/packages/playwright/src/matchers/toMatchSnapshot.ts
+++ b/packages/playwright/src/matchers/toMatchSnapshot.ts
@@ -22,7 +22,9 @@ import { getComparator, sanitizeForFilePath, zones } from 'playwright-core/lib/u
import {
addSuffixToFilePath,
trimLongString, callLogText,
- expectTypes } from '../util';
+ expectTypes,
+ sanitizeFilePathBeforeExtension,
+ windowsFilesystemFriendlyLength } from '../util';
import { colors } from 'playwright-core/lib/utilsBundle';
import fs from 'fs';
import path from 'path';
@@ -73,7 +75,7 @@ const NonConfigProperties: (keyof ToHaveScreenshotOptions)[] = [
class SnapshotHelper {
readonly testInfo: TestInfoImpl;
- readonly snapshotName: string;
+ readonly outputBaseName: string;
readonly legacyExpectedPath: string;
readonly previousPath: string;
readonly snapshotPath: string;
@@ -91,7 +93,6 @@ class SnapshotHelper {
testInfo: TestInfoImpl,
matcherName: string,
locator: Locator | undefined,
- snapshotPathResolver: (...pathSegments: string[]) => string,
anonymousSnapshotExtension: string,
configOptions: ToHaveScreenshotConfigOptions,
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & ToHaveScreenshotOptions,
@@ -102,9 +103,9 @@ class SnapshotHelper {
name = nameOrOptions;
this.options = { ...optOptions };
} else {
- name = nameOrOptions.name;
- this.options = { ...nameOrOptions };
- delete (this.options as any).name;
+ const { name: nameFromOptions, ...options } = nameOrOptions;
+ this.options = options;
+ name = nameFromOptions;
}
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
@@ -122,25 +123,34 @@ class SnapshotHelper {
// // noop
// expect.toMatchSnapshot('a.png')
- let actualModifier = '';
+ let inputPathSegments: string[];
if (!name) {
const fullTitleWithoutSpec = [
...testInfo.titlePath.slice(1),
++snapshotNames.anonymousSnapshotIndex,
].join(' ');
- name = sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.' + anonymousSnapshotExtension;
- this.snapshotName = name;
+ inputPathSegments = [sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.' + anonymousSnapshotExtension];
+ // Trim the output file paths more aggresively to avoid hitting Windows filesystem limits.
+ this.outputBaseName = sanitizeForFilePath(trimLongString(fullTitleWithoutSpec, windowsFilesystemFriendlyLength)) + '.' + anonymousSnapshotExtension;
} else {
+ // We intentionally do not sanitize user-provided array of segments, but for backwards
+ // compatibility we do sanitize the name if it is a single string.
+ // See https://github.com/microsoft/playwright/pull/9156
+ inputPathSegments = Array.isArray(name) ? name : [sanitizeFilePathBeforeExtension(name)];
const joinedName = Array.isArray(name) ? name.join(path.sep) : name;
snapshotNames.namedSnapshotIndex[joinedName] = (snapshotNames.namedSnapshotIndex[joinedName] || 0) + 1;
const index = snapshotNames.namedSnapshotIndex[joinedName];
- if (index > 1) {
- actualModifier = `-${index - 1}`;
- this.snapshotName = `${joinedName}-${index - 1}`;
- } else {
- this.snapshotName = joinedName;
- }
+ if (index > 1)
+ this.outputBaseName = addSuffixToFilePath(joinedName, `-${index - 1}`);
+ else
+ this.outputBaseName = joinedName;
}
+ this.snapshotPath = testInfo.snapshotPath(...inputPathSegments);
+ const outputFile = testInfo._getOutputPath(sanitizeFilePathBeforeExtension(this.outputBaseName));
+ this.legacyExpectedPath = addSuffixToFilePath(outputFile, '-expected');
+ this.previousPath = addSuffixToFilePath(outputFile, '-previous');
+ this.actualPath = addSuffixToFilePath(outputFile, '-actual');
+ this.diffPath = addSuffixToFilePath(outputFile, '-diff');
const filteredConfigOptions = { ...configOptions };
for (const prop of NonConfigProperties)
@@ -162,16 +172,6 @@ class SnapshotHelper {
if (this.options.maxDiffPixelRatio !== undefined && (this.options.maxDiffPixelRatio < 0 || this.options.maxDiffPixelRatio > 1))
throw new Error('`maxDiffPixelRatio` option value must be between 0 and 1');
- // sanitizes path if string
- const inputPathSegments = Array.isArray(name) ? name : [addSuffixToFilePath(name, '', undefined, true)];
- const outputPathSegments = Array.isArray(name) ? name : [addSuffixToFilePath(name, actualModifier, undefined, true)];
- this.snapshotPath = snapshotPathResolver(...inputPathSegments);
- const inputFile = testInfo._getOutputPath(...inputPathSegments);
- const outputFile = testInfo._getOutputPath(...outputPathSegments);
- this.legacyExpectedPath = addSuffixToFilePath(inputFile, '-expected');
- this.previousPath = addSuffixToFilePath(outputFile, '-previous');
- this.actualPath = addSuffixToFilePath(outputFile, '-actual');
- this.diffPath = addSuffixToFilePath(outputFile, '-diff');
this.matcherName = matcherName;
this.locator = locator;
@@ -223,7 +223,7 @@ class SnapshotHelper {
if (isWriteMissingMode) {
writeFileSync(this.snapshotPath, actual);
writeFileSync(this.actualPath, actual);
- this.testInfo.attachments.push({ name: addSuffixToFilePath(this.snapshotName, '-actual'), contentType: this.mimeType, path: this.actualPath });
+ this.testInfo.attachments.push({ name: addSuffixToFilePath(this.outputBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
}
const message = `A snapshot doesn't exist at ${this.snapshotPath}${isWriteMissingMode ? ', writing actual.' : '.'}`;
if (this.updateSnapshots === 'all') {
@@ -232,7 +232,8 @@ class SnapshotHelper {
return this.createMatcherResult(message, true);
}
if (this.updateSnapshots === 'missing') {
- this.testInfo._failWithError(new Error(message), false /* isHardError */, false /* retriable */);
+ this.testInfo._hasNonRetriableError = true;
+ this.testInfo._failWithError(new Error(message));
return this.createMatcherResult('', true);
}
return this.createMatcherResult(message, false);
@@ -257,22 +258,22 @@ class SnapshotHelper {
// Copy the expectation inside the `test-results/` folder for backwards compatibility,
// so that one can upload `test-results/` directory and have all the data inside.
writeFileSync(this.legacyExpectedPath, expected);
- this.testInfo.attachments.push({ name: addSuffixToFilePath(this.snapshotName, '-expected'), contentType: this.mimeType, path: this.snapshotPath });
+ this.testInfo.attachments.push({ name: addSuffixToFilePath(this.outputBaseName, '-expected'), contentType: this.mimeType, path: this.snapshotPath });
output.push(`\nExpected: ${colors.yellow(this.snapshotPath)}`);
}
if (previous !== undefined) {
writeFileSync(this.previousPath, previous);
- this.testInfo.attachments.push({ name: addSuffixToFilePath(this.snapshotName, '-previous'), contentType: this.mimeType, path: this.previousPath });
+ this.testInfo.attachments.push({ name: addSuffixToFilePath(this.outputBaseName, '-previous'), contentType: this.mimeType, path: this.previousPath });
output.push(`Previous: ${colors.yellow(this.previousPath)}`);
}
if (actual !== undefined) {
writeFileSync(this.actualPath, actual);
- this.testInfo.attachments.push({ name: addSuffixToFilePath(this.snapshotName, '-actual'), contentType: this.mimeType, path: this.actualPath });
+ this.testInfo.attachments.push({ name: addSuffixToFilePath(this.outputBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
output.push(`Received: ${colors.yellow(this.actualPath)}`);
}
if (diff !== undefined) {
writeFileSync(this.diffPath, diff);
- this.testInfo.attachments.push({ name: addSuffixToFilePath(this.snapshotName, '-diff'), contentType: this.mimeType, path: this.diffPath });
+ this.testInfo.attachments.push({ name: addSuffixToFilePath(this.outputBaseName, '-diff'), contentType: this.mimeType, path: this.diffPath });
output.push(` Diff: ${colors.yellow(this.diffPath)}`);
}
@@ -304,10 +305,10 @@ export function toMatchSnapshot(
if (testInfo._configInternal.ignoreSnapshots)
return { pass: !this.isNot, message: () => '', name: 'toMatchSnapshot', expected: nameOrOptions };
+ const configOptions = testInfo._projectInternal.expect?.toMatchSnapshot || {};
const helper = new SnapshotHelper(
- testInfo, 'toMatchSnapshot', undefined, testInfo.snapshotPath.bind(testInfo), determineFileExtension(received),
- testInfo._projectInternal.expect?.toMatchSnapshot || {},
- nameOrOptions, optOptions);
+ testInfo, 'toMatchSnapshot', undefined, determineFileExtension(received),
+ configOptions, nameOrOptions, optOptions);
if (this.isNot) {
if (!fs.existsSync(helper.snapshotPath))
@@ -362,10 +363,7 @@ export async function toHaveScreenshot(
expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot');
const [page, locator] = pageOrLocator.constructor.name === 'Page' ? [(pageOrLocator as PageEx), undefined] : [(pageOrLocator as Locator).page() as PageEx, pageOrLocator as Locator];
const configOptions = testInfo._projectInternal.expect?.toHaveScreenshot || {};
- const snapshotPathResolver = testInfo.snapshotPath.bind(testInfo);
- const helper = new SnapshotHelper(
- testInfo, 'toHaveScreenshot', locator, snapshotPathResolver, 'png',
- configOptions, nameOrOptions, optOptions);
+ const helper = new SnapshotHelper(testInfo, 'toHaveScreenshot', locator, 'png', configOptions, nameOrOptions, optOptions);
if (!helper.snapshotPath.toLowerCase().endsWith('.png'))
throw new Error(`Screenshot name "${path.basename(helper.snapshotPath)}" must have '.png' extension`);
expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot');
diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts
index d24add96d3..bb65d1982a 100644
--- a/packages/playwright/src/program.ts
+++ b/packages/playwright/src/program.ts
@@ -34,7 +34,6 @@ export { program } from 'playwright-core/lib/cli/program';
import type { ReporterDescription } from '../types/test';
import { prepareErrorStack } from './reporters/base';
import { cacheDir } from './transform/compilationCache';
-import { runTestServer } from './runner/testServer';
function addTestCommand(program: Command) {
const command = program.command('test [test-filter...]');
@@ -108,9 +107,9 @@ function addFindRelatedTestFilesCommand(program: Command) {
function addTestServerCommand(program: Command) {
const command = program.command('test-server', { hidden: true });
command.description('start test server');
- command.action(() => {
- void runTestServer();
- });
+ command.option('--host ', 'Host to start the server on', 'localhost');
+ command.option('--port ', 'Port to start the server on', '0');
+ command.action(opts => runTestServer(opts));
}
function addShowReportCommand(program: Command) {
@@ -166,7 +165,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
const runner = new Runner(config);
let status: FullResult['status'];
if (opts.ui || opts.uiHost || opts.uiPort)
- status = await runner.uiAllTests({ host: opts.uiHost, port: opts.uiPort ? +opts.uiPort : undefined });
+ status = await runner.runUIMode({ host: opts.uiHost, port: opts.uiPort ? +opts.uiPort : undefined });
else if (process.env.PWTEST_WATCH)
status = await runner.watchAllTests();
else
@@ -176,6 +175,19 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
gracefullyProcessExitDoNotHang(exitCode);
}
+async function runTestServer(opts: { [key: string]: any }) {
+ const config = await loadConfigFromFileRestartIfNeeded(opts.config, overridesFromOptions(opts), opts.deps === false);
+ if (!config)
+ return;
+ config.cliPassWithNoTests = true;
+ const runner = new Runner(config);
+ const host = opts.host || 'localhost';
+ const port = opts.port ? +opts.port : 0;
+ const status = await runner.runTestServer({ host, port });
+ const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1);
+ gracefullyProcessExitDoNotHang(exitCode);
+}
+
export async function withRunnerAndMutedWrite(configFile: string | undefined, callback: (runner: Runner) => Promise) {
// Redefine process.stdout.write in case config decides to pollute stdio.
const stdoutWrite = process.stdout.write.bind(process.stdout);
@@ -290,7 +302,7 @@ function resolveReporter(id: string) {
return require.resolve(id, { paths: [process.cwd()] });
}
-const kTraceModes: TraceMode[] = ['on', 'off', 'on-first-retry', 'on-all-retries', 'retain-on-failure'];
+const kTraceModes: TraceMode[] = ['on', 'off', 'on-first-retry', 'on-all-retries', 'retain-on-failure', 'retain-on-first-failure'];
const testOptions: [string, string][] = [
['--browser ', `Browser to use for tests, one of "all", "chromium", "firefox" or "webkit" (default: "chromium")`],
diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts
index 91f34faf74..fff2703208 100644
--- a/packages/playwright/src/reporters/base.ts
+++ b/packages/playwright/src/reporters/base.ts
@@ -17,7 +17,6 @@
import { colors as realColors, ms as milliseconds, parseStackTraceLine } from 'playwright-core/lib/utilsBundle';
import path from 'path';
import type { FullConfig, TestCase, Suite, TestResult, TestError, FullResult, TestStep, Location } from '../../types/testReporter';
-import type { SuitePrivate } from '../../types/reporterPrivate';
import { getPackageManagerExecCommand } from 'playwright-core/lib/utils';
import type { ReporterV2 } from './reporterV2';
export type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' };
@@ -72,7 +71,7 @@ export class BaseReporter implements ReporterV2 {
suite!: Suite;
totalTestCount = 0;
result!: FullResult;
- private fileDurations = new Map();
+ private fileDurations = new Map }>();
private _omitFailures: boolean;
private _fatalErrors: TestError[] = [];
private _failureCount: number = 0;
@@ -115,16 +114,13 @@ export class BaseReporter implements ReporterV2 {
onTestEnd(test: TestCase, result: TestResult) {
if (result.status !== 'skipped' && result.status !== test.expectedStatus)
++this._failureCount;
- // Ignore any tests that are run in parallel.
- for (let suite: Suite | undefined = test.parent; suite; suite = suite.parent) {
- if ((suite as SuitePrivate)._parallelMode === 'parallel')
- return;
- }
const projectName = test.titlePath()[1];
const relativePath = relativeTestPath(this.config, test);
const fileAndProject = (projectName ? `[${projectName}] › ` : '') + relativePath;
- const duration = this.fileDurations.get(fileAndProject) || 0;
- this.fileDurations.set(fileAndProject, duration + result.duration);
+ const entry = this.fileDurations.get(fileAndProject) || { duration: 0, workers: new Set() };
+ entry.duration += result.duration;
+ entry.workers.add(result.workerIndex);
+ this.fileDurations.set(fileAndProject, entry);
}
onError(error: TestError) {
@@ -167,7 +163,8 @@ export class BaseReporter implements ReporterV2 {
protected getSlowTests(): [string, number][] {
if (!this.config.reportSlowTests)
return [];
- const fileDurations = [...this.fileDurations.entries()];
+ // Only pick durations that were served by single worker.
+ const fileDurations = [...this.fileDurations.entries()].filter(([key, value]) => value.workers.size === 1).map(([key, value]) => [key, value.duration]) as [string, number][];
fileDurations.sort((a, b) => b[1] - a[1]);
const count = Math.min(fileDurations.length, this.config.reportSlowTests.max || Number.POSITIVE_INFINITY);
const threshold = this.config.reportSlowTests.threshold;
diff --git a/packages/playwright/src/reporters/blob.ts b/packages/playwright/src/reporters/blob.ts
index 03848a4c76..019fa8b7ca 100644
--- a/packages/playwright/src/reporters/blob.ts
+++ b/packages/playwright/src/reporters/blob.ts
@@ -50,7 +50,7 @@ export class BlobReporter extends TeleReporterEmitter {
private _reportName!: string;
constructor(options: BlobReporterOptions) {
- super(message => this._messages.push(message), false);
+ super(message => this._messages.push(message));
this._options = options;
if (this._options.fileName && !this._options.fileName.endsWith('.zip'))
throw new Error(`Blob report file name must end with .zip extension: ${this._options.fileName}`);
diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts
index 3f21de3cde..e0262122d1 100644
--- a/packages/playwright/src/reporters/html.ts
+++ b/packages/playwright/src/reporters/html.ts
@@ -22,7 +22,6 @@ import type { TransformCallback } from 'stream';
import { Transform } from 'stream';
import { codeFrameColumns } from '../transform/babelBundle';
import type { FullResult, FullConfig, Location, Suite, TestCase as TestCasePublic, TestResult as TestResultPublic, TestStep as TestStepPublic, TestError } from '../../types/testReporter';
-import type { SuitePrivate } from '../../types/reporterPrivate';
import { HttpServer, assert, calculateSha1, copyFileAndMakeWritable, gracefullyProcessExitDoNotHang, removeFolders, sanitizeForFilePath, toPosixPath } from 'playwright-core/lib/utils';
import { colors, formatError, formatResultFailure, stripAnsiEscapes } from './base';
import { resolveReporterOutputPath } from '../util';
@@ -31,7 +30,6 @@ import type { ZipFile } from 'playwright-core/lib/zipBundle';
import { yazl } from 'playwright-core/lib/zipBundle';
import { mime } from 'playwright-core/lib/utilsBundle';
import type { HTMLReport, Stats, TestAttachment, TestCase, TestCaseSummary, TestFile, TestFileSummary, TestResult, TestStep } from '@html-reporter/types';
-import { FullConfigInternal } from '../common/config';
import EmptyReporter from './empty';
type TestEntry = {
@@ -53,6 +51,7 @@ type HtmlReporterOptions = {
host?: string,
port?: number,
attachmentsBaseURL?: string,
+ _mode?: string;
};
class HtmlReporter extends EmptyReporter {
@@ -125,12 +124,11 @@ class HtmlReporter extends EmptyReporter {
override async onExit() {
if (process.env.CI || !this._buildResult)
return;
-
const { ok, singleTestId } = this._buildResult;
const shouldOpen = this._open === 'always' || (!ok && this._open === 'on-failure');
if (shouldOpen) {
await showHTMLReport(this._outputFolder, this._options.host, this._options.port, singleTestId);
- } else if (!FullConfigInternal.from(this.config)?.cliListOnly) {
+ } else if (this._options._mode === 'test') {
const packageManagerCommand = getPackageManagerExecCommand();
const relativeReportPath = this._outputFolder === standaloneDefaultFolder() ? '' : ' ' + path.relative(process.cwd(), this._outputFolder);
const hostArg = this._options.host ? ` --host ${this._options.host}` : '';
@@ -227,9 +225,12 @@ class HtmlBuilder {
async build(metadata: Metadata, projectSuites: Suite[], result: FullResult, topLevelErrors: TestError[]): Promise<{ ok: boolean, singleTestId: string | undefined }> {
const data = new Map();
for (const projectSuite of projectSuites) {
+ const testDir = projectSuite.project()!.testDir;
for (const fileSuite of projectSuite.suites) {
const fileName = this._relativeLocation(fileSuite.location)!.file;
- const fileId = (fileSuite as SuitePrivate)._fileId!;
+ // Preserve file ids computed off the testDir.
+ const relativeFile = path.relative(testDir, fileSuite.location!.file);
+ const fileId = calculateSha1(toPosixPath(relativeFile)).slice(0, 20);
let fileEntry = data.get(fileId);
if (!fileEntry) {
fileEntry = {
@@ -240,7 +241,7 @@ class HtmlBuilder {
}
const { testFile, testFileSummary } = fileEntry;
const testEntries: TestEntry[] = [];
- this._processJsonSuite(fileSuite, fileId, projectSuite.project()!.name, projectSuite.project()!.metadata?.reportName, [], testEntries);
+ this._processJsonSuite(fileSuite, fileId, projectSuite.project()!.name, [], testEntries);
for (const test of testEntries) {
testFile.tests.push(test.testCase);
testFileSummary.tests.push(test.testCaseSummary);
@@ -340,25 +341,28 @@ class HtmlBuilder {
this._dataZipFile.addBuffer(Buffer.from(JSON.stringify(data)), fileName);
}
- private _processJsonSuite(suite: Suite, fileId: string, projectName: string, botName: string | undefined, path: string[], outTests: TestEntry[]) {
+ private _processJsonSuite(suite: Suite, fileId: string, projectName: string, path: string[], outTests: TestEntry[]) {
const newPath = [...path, suite.title];
- suite.suites.forEach(s => this._processJsonSuite(s, fileId, projectName, botName, newPath, outTests));
- suite.tests.forEach(t => outTests.push(this._createTestEntry(t, projectName, botName, newPath)));
+ suite.suites.forEach(s => this._processJsonSuite(s, fileId, projectName, newPath, outTests));
+ suite.tests.forEach(t => outTests.push(this._createTestEntry(fileId, t, projectName, newPath)));
}
- private _createTestEntry(test: TestCasePublic, projectName: string, botName: string | undefined, path: string[]): TestEntry {
+ private _createTestEntry(fileId: string, test: TestCasePublic, projectName: string, path: string[]): TestEntry {
const duration = test.results.reduce((a, r) => a + r.duration, 0);
const location = this._relativeLocation(test.location)!;
path = path.slice(1);
+ const [file, ...titles] = test.titlePath();
+ const testIdExpression = `[project=${projectName}]${toPosixPath(file)}\x1e${titles.join('\x1e')} (repeat:${test.repeatEachIndex})`;
+ const testId = fileId + '-' + calculateSha1(testIdExpression).slice(0, 20);
+
const results = test.results.map(r => this._createTestResult(test, r));
return {
testCase: {
- testId: test.id,
+ testId,
title: test.title,
projectName,
- botName,
location,
duration,
// Annotations can be pushed directly, with a wrong type.
@@ -370,10 +374,9 @@ class HtmlBuilder {
ok: test.outcome() === 'expected' || test.outcome() === 'flaky',
},
testCaseSummary: {
- testId: test.id,
+ testId,
title: test.title,
projectName,
- botName,
location,
duration,
// Annotations can be pushed directly, with a wrong type.
diff --git a/packages/playwright/src/reporters/merge.ts b/packages/playwright/src/reporters/merge.ts
index 9e81f92e7a..ef34c409cc 100644
--- a/packages/playwright/src/reporters/merge.ts
+++ b/packages/playwright/src/reporters/merge.ts
@@ -23,7 +23,7 @@ import { TeleReporterReceiver } from '../isomorphic/teleReceiver';
import { JsonStringInternalizer, StringInternPool } from '../isomorphic/stringInternPool';
import { createReporters } from '../runner/reporters';
import { Multiplexer } from './multiplexer';
-import { ZipFile, calculateSha1 } from 'playwright-core/lib/utils';
+import { ZipFile } from 'playwright-core/lib/utils';
import { currentBlobReportVersion, type BlobReportMetadata } from './blob';
import { relativeFilePath } from '../util';
import type { TestError } from '../../types/testReporter';
@@ -51,9 +51,14 @@ export async function createMergedReport(config: FullConfigInternal, dir: string
if (shardFiles.length === 0)
throw new Error(`No report files found in ${dir}`);
const eventData = await mergeEvents(dir, shardFiles, stringPool, printStatus, rootDirOverride);
- // If expicit config is provided, use platform path separator, otherwise use the one from the report (if any).
- const pathSep = rootDirOverride ? path.sep : (eventData.pathSeparatorFromMetadata ?? path.sep);
- const receiver = new TeleReporterReceiver(pathSep, multiplexer, false, config.config);
+ // If explicit config is provided, use platform path separator, otherwise use the one from the report (if any).
+ const pathSeparator = rootDirOverride ? path.sep : (eventData.pathSeparatorFromMetadata ?? path.sep);
+ const receiver = new TeleReporterReceiver(multiplexer, {
+ mergeProjects: false,
+ mergeTestCases: false,
+ resolvePath: (rootDir, relativePath) => stringPool.internString(rootDir + pathSeparator + relativePath),
+ configOverrides: config.config,
+ });
printStatus(`processing test events`);
const dispatchEvents = async (events: JsonEvent[]) => {
@@ -183,22 +188,15 @@ async function mergeEvents(dir: string, shardReportFiles: string[], stringPool:
return a.file.localeCompare(b.file);
});
- const saltSet = new Set();
-
printStatus(`merging events`);
const reports: ReportData[] = [];
- for (const { file, parsedEvents, metadata, localPath } of blobs) {
+ for (let i = 0; i < blobs.length; ++i) {
// Generate unique salt for each blob.
- const sha1 = calculateSha1(metadata.name || path.basename(file)).substring(0, 16);
- let salt = sha1;
- for (let i = 0; saltSet.has(salt); i++)
- salt = sha1 + '-' + i;
- saltSet.add(salt);
-
+ const { parsedEvents, metadata, localPath } = blobs[i];
const eventPatchers = new JsonEventPatchers();
- eventPatchers.patchers.push(new IdsPatcher(stringPool, metadata.name, salt));
+ eventPatchers.patchers.push(new IdsPatcher(stringPool, metadata.name, String(i)));
// Only patch path separators if we are merging reports with explicit config.
if (rootDirOverride)
eventPatchers.patchers.push(new PathSeparatorPatcher(metadata.pathSeparator));
@@ -248,7 +246,6 @@ function mergeConfigureEvents(configureEvents: JsonEvent[], rootDirOverride: str
rootDir: '',
version: '',
workers: 0,
- listOnly: false
};
for (const event of configureEvents)
config = mergeConfigs(config, event.params.config);
@@ -355,10 +352,14 @@ class UniqueFileNameGenerator {
}
class IdsPatcher {
- constructor(
- private _stringPool: StringInternPool,
- private _reportName: string | undefined,
- private _salt: string) {
+ private _stringPool: StringInternPool;
+ private _botName: string | undefined;
+ private _salt: string;
+
+ constructor(stringPool: StringInternPool, botName: string | undefined, salt: string) {
+ this._stringPool = stringPool;
+ this._botName = botName;
+ this._salt = salt;
}
patchEvent(event: JsonEvent) {
@@ -381,13 +382,18 @@ class IdsPatcher {
private _onProject(project: JsonProject) {
project.metadata = project.metadata ?? {};
- project.metadata.reportName = this._reportName;
- project.id = this._stringPool.internString(project.id + this._salt);
+ project.metadata.botName = this._botName;
project.suites.forEach(suite => this._updateTestIds(suite));
}
private _updateTestIds(suite: JsonSuite) {
- suite.tests.forEach(test => test.testId = this._mapTestId(test.testId));
+ suite.tests.forEach(test => {
+ test.testId = this._mapTestId(test.testId);
+ if (this._botName) {
+ test.tags = test.tags || [];
+ test.tags.unshift('@' + this._botName);
+ }
+ });
suite.suites.forEach(suite => this._updateTestIds(suite));
}
diff --git a/packages/playwright/src/reporters/teleEmitter.ts b/packages/playwright/src/reporters/teleEmitter.ts
index 7f4caf6ad5..2baef221aa 100644
--- a/packages/playwright/src/reporters/teleEmitter.ts
+++ b/packages/playwright/src/reporters/teleEmitter.ts
@@ -16,41 +16,43 @@
import path from 'path';
import { createGuid } from 'playwright-core/lib/utils';
-import type { SuitePrivate } from '../../types/reporterPrivate';
-import type { FullConfig, FullResult, Location, TestCase, TestError, TestResult, TestStep } from '../../types/testReporter';
-import { FullConfigInternal, getProjectId } from '../common/config';
-import type { Suite } from '../common/test';
-import type { JsonAttachment, JsonConfig, JsonEvent, JsonFullResult, JsonProject, JsonStdIOType, JsonSuite, JsonTestCase, JsonTestEnd, JsonTestResultEnd, JsonTestResultStart, JsonTestStepEnd, JsonTestStepStart } from '../isomorphic/teleReceiver';
+import type * as reporterTypes from '../../types/testReporter';
+import type * as teleReceiver from '../isomorphic/teleReceiver';
import { serializeRegexPatterns } from '../isomorphic/teleReceiver';
import type { ReporterV2 } from './reporterV2';
-export class TeleReporterEmitter implements ReporterV2 {
- private _messageSink: (message: JsonEvent) => void;
- private _rootDir!: string;
- private _skipBuffers: boolean;
+export type TeleReporterEmitterOptions = {
+ omitOutput?: boolean;
+ omitBuffers?: boolean;
+};
- constructor(messageSink: (message: JsonEvent) => void, skipBuffers: boolean) {
+export class TeleReporterEmitter implements ReporterV2 {
+ private _messageSink: (message: teleReceiver.JsonEvent) => void;
+ private _rootDir!: string;
+ private _emitterOptions: TeleReporterEmitterOptions;
+
+ constructor(messageSink: (message: teleReceiver.JsonEvent) => void, options: TeleReporterEmitterOptions = {}) {
this._messageSink = messageSink;
- this._skipBuffers = skipBuffers;
+ this._emitterOptions = options;
}
version(): 'v2' {
return 'v2';
}
- onConfigure(config: FullConfig) {
+ onConfigure(config: reporterTypes.FullConfig) {
this._rootDir = config.rootDir;
this._messageSink({ method: 'onConfigure', params: { config: this._serializeConfig(config) } });
}
- onBegin(suite: Suite) {
+ onBegin(suite: reporterTypes.Suite) {
const projects = suite.suites.map(projectSuite => this._serializeProject(projectSuite));
for (const project of projects)
this._messageSink({ method: 'onProject', params: { project } });
this._messageSink({ method: 'onBegin', params: undefined });
}
- onTestBegin(test: TestCase, result: TestResult): void {
+ onTestBegin(test: reporterTypes.TestCase, result: reporterTypes.TestResult): void {
(result as any)[idSymbol] = createGuid();
this._messageSink({
method: 'onTestBegin',
@@ -61,8 +63,8 @@ export class TeleReporterEmitter implements ReporterV2 {
});
}
- onTestEnd(test: TestCase, result: TestResult): void {
- const testEnd: JsonTestEnd = {
+ onTestEnd(test: reporterTypes.TestCase, result: reporterTypes.TestResult): void {
+ const testEnd: teleReceiver.JsonTestEnd = {
testId: test.id,
expectedStatus: test.expectedStatus,
annotations: test.annotations,
@@ -77,7 +79,7 @@ export class TeleReporterEmitter implements ReporterV2 {
});
}
- onStepBegin(test: TestCase, result: TestResult, step: TestStep): void {
+ onStepBegin(test: reporterTypes.TestCase, result: reporterTypes.TestResult, step: reporterTypes.TestStep): void {
(step as any)[idSymbol] = createGuid();
this._messageSink({
method: 'onStepBegin',
@@ -89,7 +91,7 @@ export class TeleReporterEmitter implements ReporterV2 {
});
}
- onStepEnd(test: TestCase, result: TestResult, step: TestStep): void {
+ onStepEnd(test: reporterTypes.TestCase, result: reporterTypes.TestResult, step: reporterTypes.TestStep): void {
this._messageSink({
method: 'onStepEnd',
params: {
@@ -100,22 +102,24 @@ export class TeleReporterEmitter implements ReporterV2 {
});
}
- onError(error: TestError): void {
+ onError(error: reporterTypes.TestError): void {
this._messageSink({
method: 'onError',
params: { error }
});
}
- onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult): void {
+ onStdOut(chunk: string | Buffer, test?: reporterTypes.TestCase, result?: reporterTypes.TestResult): void {
this._onStdIO('stdout', chunk, test, result);
}
- onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult): void {
+ onStdErr(chunk: string | Buffer, test?: reporterTypes.TestCase, result?: reporterTypes.TestResult): void {
this._onStdIO('stderr', chunk, test, result);
}
- private _onStdIO(type: JsonStdIOType, chunk: string | Buffer, test: void | TestCase, result: void | TestResult): void {
+ private _onStdIO(type: teleReceiver.JsonStdIOType, chunk: string | Buffer, test: void | reporterTypes.TestCase, result: void | reporterTypes.TestResult): void {
+ if (this._emitterOptions.omitOutput)
+ return;
const isBase64 = typeof chunk !== 'string';
const data = isBase64 ? chunk.toString('base64') : chunk;
this._messageSink({
@@ -124,8 +128,8 @@ export class TeleReporterEmitter implements ReporterV2 {
});
}
- async onEnd(result: FullResult) {
- const resultPayload: JsonFullResult = {
+ async onEnd(result: reporterTypes.FullResult) {
+ const resultPayload: teleReceiver.JsonFullResult = {
status: result.status,
startTime: result.startTime.getTime(),
duration: result.duration,
@@ -145,7 +149,7 @@ export class TeleReporterEmitter implements ReporterV2 {
return false;
}
- private _serializeConfig(config: FullConfig): JsonConfig {
+ private _serializeConfig(config: reporterTypes.FullConfig): teleReceiver.JsonConfig {
return {
configFile: this._relativePath(config.configFile),
globalTimeout: config.globalTimeout,
@@ -154,14 +158,12 @@ export class TeleReporterEmitter implements ReporterV2 {
rootDir: config.rootDir,
version: config.version,
workers: config.workers,
- listOnly: !!FullConfigInternal.from(config)?.cliListOnly,
};
}
- private _serializeProject(suite: Suite): JsonProject {
+ private _serializeProject(suite: reporterTypes.Suite): teleReceiver.JsonProject {
const project = suite.project()!;
- const report: JsonProject = {
- id: getProjectId(project),
+ const report: teleReceiver.JsonProject = {
metadata: project.metadata,
name: project.name,
outputDir: this._relativePath(project.outputDir),
@@ -183,12 +185,9 @@ export class TeleReporterEmitter implements ReporterV2 {
return report;
}
- private _serializeSuite(suite: Suite): JsonSuite {
+ private _serializeSuite(suite: reporterTypes.Suite): teleReceiver.JsonSuite {
const result = {
- type: suite._type,
title: suite.title,
- fileId: (suite as SuitePrivate)._fileId,
- parallelMode: (suite as SuitePrivate)._parallelMode,
location: this._relativeLocation(suite.location),
suites: suite.suites.map(s => this._serializeSuite(s)),
tests: suite.tests.map(t => this._serializeTest(t)),
@@ -196,17 +195,18 @@ export class TeleReporterEmitter implements ReporterV2 {
return result;
}
- private _serializeTest(test: TestCase): JsonTestCase {
+ private _serializeTest(test: reporterTypes.TestCase): teleReceiver.JsonTestCase {
return {
testId: test.id,
title: test.title,
location: this._relativeLocation(test.location),
retries: test.retries,
tags: test.tags,
+ repeatEachIndex: test.repeatEachIndex,
};
}
- private _serializeResultStart(result: TestResult): JsonTestResultStart {
+ private _serializeResultStart(result: reporterTypes.TestResult): teleReceiver.JsonTestResultStart {
return {
id: (result as any)[idSymbol],
retry: result.retry,
@@ -216,7 +216,7 @@ export class TeleReporterEmitter implements ReporterV2 {
};
}
- private _serializeResultEnd(result: TestResult): JsonTestResultEnd {
+ private _serializeResultEnd(result: reporterTypes.TestResult): teleReceiver.JsonTestResultEnd {
return {
id: (result as any)[idSymbol],
duration: result.duration,
@@ -226,17 +226,17 @@ export class TeleReporterEmitter implements ReporterV2 {
};
}
- _serializeAttachments(attachments: TestResult['attachments']): JsonAttachment[] {
+ _serializeAttachments(attachments: reporterTypes.TestResult['attachments']): teleReceiver.JsonAttachment[] {
return attachments.map(a => {
return {
...a,
// There is no Buffer in the browser, so there is no point in sending the data there.
- base64: (a.body && !this._skipBuffers) ? a.body.toString('base64') : undefined,
+ base64: (a.body && !this._emitterOptions.omitBuffers) ? a.body.toString('base64') : undefined,
};
});
}
- private _serializeStepStart(step: TestStep): JsonTestStepStart {
+ private _serializeStepStart(step: reporterTypes.TestStep): teleReceiver.JsonTestStepStart {
return {
id: (step as any)[idSymbol],
parentStepId: (step.parent as any)?.[idSymbol],
@@ -247,7 +247,7 @@ export class TeleReporterEmitter implements ReporterV2 {
};
}
- private _serializeStepEnd(step: TestStep): JsonTestStepEnd {
+ private _serializeStepEnd(step: reporterTypes.TestStep): teleReceiver.JsonTestStepEnd {
return {
id: (step as any)[idSymbol],
duration: step.duration,
@@ -255,9 +255,9 @@ export class TeleReporterEmitter implements ReporterV2 {
};
}
- private _relativeLocation(location: Location): Location;
- private _relativeLocation(location?: Location): Location | undefined;
- private _relativeLocation(location: Location | undefined): Location | undefined {
+ private _relativeLocation(location: reporterTypes.Location): reporterTypes.Location;
+ private _relativeLocation(location?: reporterTypes.Location): reporterTypes.Location | undefined;
+ private _relativeLocation(location: reporterTypes.Location | undefined): reporterTypes.Location | undefined {
if (!location)
return location;
return {
diff --git a/packages/playwright/src/runner/loadUtils.ts b/packages/playwright/src/runner/loadUtils.ts
index f6ecccb3d6..4b881264d2 100644
--- a/packages/playwright/src/runner/loadUtils.ts
+++ b/packages/playwright/src/runner/loadUtils.ts
@@ -316,7 +316,7 @@ export function loadGlobalHook(config: FullConfigInternal, file: string): Promis
return requireOrImportDefaultFunction(path.resolve(config.config.rootDir, file), false);
}
-export function loadReporter(config: FullConfigInternal | undefined, file: string): Promise Reporter> {
+export function loadReporter(config: FullConfigInternal, file: string): Promise Reporter> {
return requireOrImportDefaultFunction(config ? path.resolve(config.config.rootDir, file) : file, true);
}
diff --git a/packages/playwright/src/runner/reporters.ts b/packages/playwright/src/runner/reporters.ts
index 2285e9bee2..7e846c493f 100644
--- a/packages/playwright/src/runner/reporters.ts
+++ b/packages/playwright/src/runner/reporters.ts
@@ -33,7 +33,7 @@ import { BlobReporter } from '../reporters/blob';
import type { ReporterDescription } from '../../types/test';
import { type ReporterV2, wrapReporterAsV2 } from '../reporters/reporterV2';
-export async function createReporters(config: FullConfigInternal, mode: 'list' | 'run' | 'ui' | 'merge', descriptions?: ReporterDescription[]): Promise {
+export async function createReporters(config: FullConfigInternal, mode: 'list' | 'test' | 'ui' | 'merge', descriptions?: ReporterDescription[]): Promise {
const defaultReporters: { [key in BuiltInReporter]: new(arg: any) => ReporterV2 } = {
blob: BlobReporter,
dot: mode === 'list' ? ListModeReporter : DotReporter,
@@ -50,9 +50,10 @@ export async function createReporters(config: FullConfigInternal, mode: 'list' |
descriptions ??= config.config.reporter;
if (config.configCLIOverrides.additionalReporters)
descriptions = [...descriptions, ...config.configCLIOverrides.additionalReporters];
+ const runOptions = reporterOptions(config, mode);
for (const r of descriptions) {
const [name, arg] = r;
- const options = { ...arg, configDir: config.configDir };
+ const options = { ...runOptions, ...arg };
if (name in defaultReporters) {
reporters.push(new defaultReporters[name as keyof typeof defaultReporters](options));
} else {
@@ -62,7 +63,7 @@ export async function createReporters(config: FullConfigInternal, mode: 'list' |
}
if (process.env.PW_TEST_REPORTER) {
const reporterConstructor = await loadReporter(config, process.env.PW_TEST_REPORTER);
- reporters.push(wrapReporterAsV2(new reporterConstructor()));
+ reporters.push(wrapReporterAsV2(new reporterConstructor(runOptions)));
}
const someReporterPrintsToStdio = reporters.some(r => r.printsToStdio());
@@ -77,6 +78,20 @@ export async function createReporters(config: FullConfigInternal, mode: 'list' |
return reporters;
}
+export async function createReporterForTestServer(config: FullConfigInternal, file: string, mode: 'test' | 'list', messageSink: (message: any) => void): Promise {
+ const reporterConstructor = await loadReporter(config, file);
+ const runOptions = reporterOptions(config, mode, messageSink);
+ const instance = new reporterConstructor(runOptions);
+ return wrapReporterAsV2(instance);
+}
+
+function reporterOptions(config: FullConfigInternal, mode: 'list' | 'test' | 'ui' | 'merge', send?: (message: any) => void) {
+ return {
+ configDir: config.configDir,
+ _send: send,
+ };
+}
+
class ListModeReporter extends EmptyReporter {
private config!: FullConfig;
diff --git a/packages/playwright/src/runner/runner.ts b/packages/playwright/src/runner/runner.ts
index 142f0958cd..4a2d329159 100644
--- a/packages/playwright/src/runner/runner.ts
+++ b/packages/playwright/src/runner/runner.ts
@@ -16,7 +16,8 @@
*/
import path from 'path';
-import { monotonicTime } from 'playwright-core/lib/utils';
+import type { HttpServer, ManualPromise } from 'playwright-core/lib/utils';
+import { isUnderTest, monotonicTime } from 'playwright-core/lib/utils';
import type { FullResult, TestError } from '../../types/testReporter';
import { webServerPluginsForConfig } from '../plugins/webServerPlugin';
import { collectFilesForProject, filterProjects } from './projectUtils';
@@ -25,12 +26,13 @@ import { TestRun, createTaskRunner, createTaskRunnerForList } from './tasks';
import type { FullConfigInternal } from '../common/config';
import { colors } from 'playwright-core/lib/utilsBundle';
import { runWatchModeLoop } from './watchMode';
-import { runUIMode } from './uiMode';
+import { runTestServer } from './testServer';
import { InternalReporter } from '../reporters/internalReporter';
import { Multiplexer } from '../reporters/multiplexer';
import type { Suite } from '../common/test';
import { wrapReporterAsV2 } from '../reporters/reporterV2';
import { affectedTestFiles } from '../transform/compilationCache';
+import { installRootRedirect, openTraceInBrowser, openTraceViewerApp } from 'playwright-core/lib/server';
type ProjectConfigWithFiles = {
name: string;
@@ -83,7 +85,7 @@ export class Runner {
// Legacy webServer support.
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
- const reporter = new InternalReporter(new Multiplexer(await createReporters(config, listOnly ? 'list' : 'run')));
+ const reporter = new InternalReporter(new Multiplexer(await createReporters(config, listOnly ? 'list' : 'test')));
const taskRunner = listOnly ? createTaskRunnerForList(config, reporter, 'in-process', { failOnLoadErrors: true })
: createTaskRunner(config, reporter);
@@ -146,10 +148,32 @@ export class Runner {
return await runWatchModeLoop(config);
}
- async uiAllTests(options: { host?: string, port?: number }): Promise {
+ async runUIMode(options: { host?: string, port?: number }): Promise {
const config = this._config;
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
- return await runUIMode(config, options);
+ return await runTestServer(config, options, async (server: HttpServer, cancelPromise: ManualPromise) => {
+ await installRootRedirect(server, [], { webApp: 'uiMode.html' });
+ if (options.host !== undefined || options.port !== undefined) {
+ await openTraceInBrowser(server.urlPrefix());
+ } else {
+ const page = await openTraceViewerApp(server.urlPrefix(), 'chromium', {
+ headless: isUnderTest() && process.env.PWTEST_HEADED_FOR_TEST !== '1',
+ persistentContextOptions: {
+ handleSIGINT: false,
+ },
+ });
+ page.on('close', () => cancelPromise.resolve());
+ }
+ });
+ }
+
+ async runTestServer(options: { host?: string, port?: number }): Promise {
+ const config = this._config;
+ webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
+ return await runTestServer(config, options, async server => {
+ // eslint-disable-next-line no-console
+ console.log('Listening on ' + server.urlPrefix().replace('http:', 'ws:') + '/' + server.wsGuid());
+ });
}
async findRelatedTestFiles(mode: 'in-process' | 'out-of-process', files: string[]): Promise {
diff --git a/packages/playwright/src/runner/tasks.ts b/packages/playwright/src/runner/tasks.ts
index 1cd57034e4..1f890586bc 100644
--- a/packages/playwright/src/runner/tasks.ts
+++ b/packages/playwright/src/runner/tasks.ts
@@ -78,7 +78,7 @@ export function createTaskRunnerForWatchSetup(config: FullConfigInternal, report
export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: ReporterV2, additionalFileMatcher?: Matcher): TaskRunner {
const taskRunner = new TaskRunner(reporter, 0);
- taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunTestsOutsideProjectFilter: true, additionalFileMatcher }));
+ taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true, additionalFileMatcher }));
addRunTasks(taskRunner, config);
return taskRunner;
}
@@ -86,7 +86,7 @@ export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: R
export function createTaskRunnerForTestServer(config: FullConfigInternal, reporter: ReporterV2): TaskRunner {
const taskRunner = new TaskRunner(reporter, 0);
addGlobalSetupTasks(taskRunner, config);
- taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunTestsOutsideProjectFilter: true }));
+ taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true }));
addRunTasks(taskRunner, config);
return taskRunner;
}
@@ -195,10 +195,10 @@ function createRemoveOutputDirsTask(): Task {
};
}
-function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, doNotRunTestsOutsideProjectFilter?: boolean, additionalFileMatcher?: Matcher }): Task {
+function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean, additionalFileMatcher?: Matcher }): Task {
return {
setup: async (testRun, errors, softErrors) => {
- await collectProjectsAndTestFiles(testRun, !!options.doNotRunTestsOutsideProjectFilter, options.additionalFileMatcher);
+ await collectProjectsAndTestFiles(testRun, !!options.doNotRunDepsOutsideProjectFilter, options.additionalFileMatcher);
await loadFileSuites(testRun, mode, options.failOnLoadErrors ? errors : softErrors);
testRun.rootSuite = await createRootSuite(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly);
testRun.failureTracker.onRootSuite(testRun.rootSuite);
diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts
index 0b93adb7c5..b915834d24 100644
--- a/packages/playwright/src/runner/testServer.ts
+++ b/packages/playwright/src/runner/testServer.ts
@@ -1,11 +1,11 @@
/**
- * Copyright (c) Microsoft Corporation.
+ * Copyright Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,221 +14,328 @@
* limitations under the License.
*/
-import type http from 'http';
+import fs from 'fs';
import path from 'path';
-import { ManualPromise, createGuid, gracefullyProcessExitDoNotHang } from 'playwright-core/lib/utils';
-import { WSServer } from 'playwright-core/lib/utils';
-import type { WebSocket } from 'playwright-core/lib/utilsBundle';
-import type { FullResult, TestError } from 'playwright/types/testReporter';
-import { loadConfig, restartWithExperimentalTsEsm } from '../common/configLoader';
-import { InternalReporter } from '../reporters/internalReporter';
-import { Multiplexer } from '../reporters/multiplexer';
-import { createReporters } from './reporters';
-import { TestRun, createTaskRunnerForList, createTaskRunnerForTestServer } from './tasks';
-import type { ConfigCLIOverrides } from '../common/ipc';
-import { Runner } from './runner';
-import type { FindRelatedTestFilesReport } from './runner';
+import { registry, startTraceViewerServer } from 'playwright-core/lib/server';
+import { ManualPromise, gracefullyProcessExitDoNotHang, isUnderTest } from 'playwright-core/lib/utils';
+import type { Transport, HttpServer } from 'playwright-core/lib/utils';
+import type * as reporterTypes from '../../types/testReporter';
+import { collectAffectedTestFiles, dependenciesForTestFile } from '../transform/compilationCache';
import type { FullConfigInternal } from '../common/config';
-import type { TestServerInterface } from './testServerInterface';
+import { InternalReporter } from '../reporters/internalReporter';
+import { createReporterForTestServer, createReporters } from './reporters';
+import { TestRun, createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
+import { open } from 'playwright-core/lib/utilsBundle';
+import ListReporter from '../reporters/list';
+import { Multiplexer } from '../reporters/multiplexer';
+import { SigIntWatcher } from './sigIntWatcher';
+import { Watcher } from '../fsWatcher';
+import type { TestServerInterface, TestServerInterfaceEventEmitters } from '../isomorphic/testServerInterface';
+import { Runner } from './runner';
import { serializeError } from '../util';
import { prepareErrorStack } from '../reporters/base';
-import { loadReporter } from './loadUtils';
-import { wrapReporterAsV2 } from '../reporters/reporterV2';
-export async function runTestServer() {
- if (restartWithExperimentalTsEsm(undefined, true))
- return null;
- process.env.PW_TEST_HTML_REPORT_OPEN = 'never';
- const wss = new WSServer({
- onConnection(request: http.IncomingMessage, url: URL, ws: WebSocket, id: string) {
- const dispatcher = new Dispatcher(ws);
- ws.on('message', async message => {
- const { id, method, params } = JSON.parse(message.toString());
- try {
- const result = await (dispatcher as any)[method](params);
- ws.send(JSON.stringify({ id, result }));
- } catch (e) {
- // eslint-disable-next-line no-console
- console.error(e);
- }
- });
- return {
- async close() {}
- };
- },
- });
- const url = await wss.listen(0, 'localhost', '/' + createGuid());
- // eslint-disable-next-line no-console
- process.on('exit', () => wss.close().catch(console.error));
- // eslint-disable-next-line no-console
- console.log(`Listening on ${url}`);
- process.stdin.on('close', () => gracefullyProcessExitDoNotHang(0));
-}
+class TestServer {
+ private _config: FullConfigInternal;
+ private _dispatcher: TestServerDispatcher | undefined;
+ private _originalStdoutWrite: NodeJS.WriteStream['write'];
+ private _originalStderrWrite: NodeJS.WriteStream['write'];
-class Dispatcher implements TestServerInterface {
- private _testRun: { run: Promise, stop: ManualPromise } | undefined;
- private _ws: WebSocket;
+ constructor(config: FullConfigInternal) {
+ this._config = config;
+ process.env.PW_LIVE_TRACE_STACKS = '1';
+ config.cliListOnly = false;
+ config.cliPassWithNoTests = true;
+ config.config.preserveOutput = 'always';
- constructor(ws: WebSocket) {
- this._ws = ws;
+ for (const p of config.projects) {
+ p.project.retries = 0;
+ p.project.repeatEach = 1;
+ }
+ config.configCLIOverrides.use = config.configCLIOverrides.use || {};
+ config.configCLIOverrides.use.trace = { mode: 'on', sources: false, _live: true };
- process.stdout.write = ((chunk: string | Buffer, cb?: Buffer | Function, cb2?: Function) => {
- this._dispatchEvent('stdio', chunkToPayload('stdout', chunk));
- if (typeof cb === 'function')
- (cb as any)();
- if (typeof cb2 === 'function')
- (cb2 as any)();
- return true;
- }) as any;
- process.stderr.write = ((chunk: string | Buffer, cb?: Buffer | Function, cb2?: Function) => {
- this._dispatchEvent('stdio', chunkToPayload('stderr', chunk));
- if (typeof cb === 'function')
- (cb as any)();
- if (typeof cb2 === 'function')
- (cb2 as any)();
- return true;
- }) as any;
+ this._originalStdoutWrite = process.stdout.write;
+ this._originalStderrWrite = process.stderr.write;
}
- async listFiles(params: {
- configFile: string;
- }): Promise<{
- projects: {
- name: string;
- testDir: string;
- use: { testIdAttribute?: string };
- files: string[];
- }[];
- cliEntryPoint?: string;
- error?: TestError;
- }> {
+ async start(options: { host?: string, port?: number }): Promise {
+ this._dispatcher = new TestServerDispatcher(this._config);
+ return await startTraceViewerServer({ ...options, transport: this._dispatcher.transport });
+ }
+
+ async stop() {
+ await this._dispatcher?.runGlobalTeardown();
+ }
+
+ wireStdIO() {
+ if (!process.env.PWTEST_DEBUG) {
+ process.stdout.write = (chunk: string | Buffer) => {
+ this._dispatcher?._dispatchEvent('stdio', chunkToPayload('stdout', chunk));
+ return true;
+ };
+ process.stderr.write = (chunk: string | Buffer) => {
+ this._dispatcher?._dispatchEvent('stdio', chunkToPayload('stderr', chunk));
+ return true;
+ };
+ }
+ }
+
+ unwireStdIO() {
+ if (!process.env.PWTEST_DEBUG) {
+ process.stdout.write = this._originalStdoutWrite;
+ process.stderr.write = this._originalStderrWrite;
+ }
+ }
+}
+
+class TestServerDispatcher implements TestServerInterface {
+ private _config: FullConfigInternal;
+ private _globalWatcher: Watcher;
+ private _testWatcher: Watcher;
+ private _testRun: { run: Promise, stop: ManualPromise } | undefined;
+ readonly transport: Transport;
+ private _queue = Promise.resolve();
+ private _globalCleanup: (() => Promise) | undefined;
+ readonly _dispatchEvent: TestServerInterfaceEventEmitters['dispatchEvent'];
+
+ constructor(config: FullConfigInternal) {
+ this._config = config;
+ this.transport = {
+ dispatch: (method, params) => (this as any)[method](params),
+ onclose: () => {},
+ };
+ this._globalWatcher = new Watcher('deep', () => this._dispatchEvent('listChanged', {}));
+ this._testWatcher = new Watcher('flat', events => {
+ const collector = new Set();
+ events.forEach(f => collectAffectedTestFiles(f.file, collector));
+ this._dispatchEvent('testFilesChanged', { testFiles: [...collector] });
+ });
+ this._dispatchEvent = (method, params) => this.transport.sendEvent?.(method, params);
+ }
+
+ async ping() {}
+
+ async open(params: { location: reporterTypes.Location }) {
+ if (isUnderTest())
+ return;
+ // eslint-disable-next-line no-console
+ open('vscode://file/' + params.location.file + ':' + params.location.line).catch(e => console.error(e));
+ }
+
+ async resizeTerminal(params: { cols: number; rows: number; }) {
+ process.stdout.columns = params.cols;
+ process.stdout.rows = params.rows;
+ process.stderr.columns = params.cols;
+ process.stderr.columns = params.rows;
+ }
+
+ async checkBrowsers(): Promise<{ hasBrowsers: boolean; }> {
+ return { hasBrowsers: hasSomeBrowsers() };
+ }
+
+ async installBrowsers() {
+ await installBrowsers();
+ }
+
+ async runGlobalSetup(): Promise {
+ await this.runGlobalTeardown();
+
+ const reporter = new InternalReporter(new ListReporter());
+ const taskRunner = createTaskRunnerForWatchSetup(this._config, reporter);
+ reporter.onConfigure(this._config.config);
+ const testRun = new TestRun(this._config, reporter);
+ const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0);
+ await reporter.onEnd({ status });
+ await reporter.onExit();
+ if (status !== 'passed') {
+ await globalCleanup();
+ return status;
+ }
+ this._globalCleanup = globalCleanup;
+ return status;
+ }
+
+ async runGlobalTeardown() {
+ const result = (await this._globalCleanup?.()) || 'passed';
+ this._globalCleanup = undefined;
+ return result;
+ }
+
+ async listFiles() {
try {
- const config = await this._loadConfig(params.configFile);
- const runner = new Runner(config);
+ const runner = new Runner(this._config);
return runner.listTestFiles();
} catch (e) {
- const error: TestError = serializeError(e);
+ const error: reporterTypes.TestError = serializeError(e);
error.location = prepareErrorStack(e.stack).location;
return { projects: [], error };
}
}
- async listTests(params: {
- configFile: string;
- locations: string[];
- reporter: string;
- env: NodeJS.ProcessEnv;
- }) {
- const config = await this._loadConfig(params.configFile);
- config.cliArgs = params.locations || [];
- const wireReporter = await this._createReporter(params.reporter);
- const reporter = new InternalReporter(new Multiplexer([wireReporter]));
- const taskRunner = createTaskRunnerForList(config, reporter, 'out-of-process', { failOnLoadErrors: true });
- const testRun = new TestRun(config, reporter);
- reporter.onConfigure(config.config);
-
- const taskStatus = await taskRunner.run(testRun, 0);
- let status: FullResult['status'] = testRun.failureTracker.result();
- if (status === 'passed' && taskStatus !== 'passed')
- status = taskStatus;
- const modifiedResult = await reporter.onEnd({ status });
- if (modifiedResult && modifiedResult.status)
- status = modifiedResult.status;
- await reporter.onExit();
+ async listTests(params: { reporter?: string; fileNames: string[]; }) {
+ let report: any[] = [];
+ this._queue = this._queue.then(async () => {
+ report = await this._innerListTests(params);
+ }).catch(printInternalError);
+ await this._queue;
+ return { report };
}
- async test(params: {
- configFile: string;
- locations: string[];
- reporter: string;
- env: NodeJS.ProcessEnv;
- headed?: boolean;
- oneWorker?: boolean;
- trace?: 'on' | 'off';
- projects?: string[];
- grep?: string;
- reuseContext?: boolean;
- connectWsEndpoint?: string;
- }) {
- await this._stopTests();
+ private async _innerListTests(params: { reporter?: string; fileNames?: string[]; }) {
+ const report: any[] = [];
+ const wireReporter = await createReporterForTestServer(this._config, params.reporter || require.resolve('./uiModeReporter'), 'list', e => report.push(e));
+ const reporter = new InternalReporter(wireReporter);
+ this._config.cliArgs = params.fileNames || [];
+ this._config.cliListOnly = true;
+ this._config.testIdMatcher = undefined;
+ const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process', { failOnLoadErrors: false });
+ const testRun = new TestRun(this._config, reporter);
+ reporter.onConfigure(this._config.config);
+ const status = await taskRunner.run(testRun, 0);
+ await reporter.onEnd({ status });
+ await reporter.onExit();
- const overrides: ConfigCLIOverrides = {
- repeatEach: 1,
- retries: 0,
- preserveOutputDir: true,
- use: {
- trace: params.trace,
- headless: params.headed ? false : undefined,
- _optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined,
- _optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined,
- },
- workers: params.oneWorker ? 1 : undefined,
- };
+ const projectDirs = new Set();
+ const projectOutputs = new Set();
+ for (const p of this._config.projects) {
+ projectDirs.add(p.project.testDir);
+ projectOutputs.add(p.project.outputDir);
+ }
- const config = await this._loadConfig(params.configFile, overrides);
- config.cliListOnly = false;
- config.cliArgs = params.locations || [];
- config.cliGrep = params.grep;
- config.cliProjectFilter = params.projects?.length ? params.projects : undefined;
+ const result = await resolveCtDirs(this._config);
+ if (result) {
+ projectDirs.add(result.templateDir);
+ projectOutputs.add(result.outDir);
+ }
- const wireReporter = await this._createReporter(params.reporter);
- const configReporters = await createReporters(config, 'run');
- const reporter = new InternalReporter(new Multiplexer([...configReporters, wireReporter]));
- const taskRunner = createTaskRunnerForTestServer(config, reporter);
- const testRun = new TestRun(config, reporter);
- reporter.onConfigure(config.config);
+ this._globalWatcher.update([...projectDirs], [...projectOutputs], false);
+ return report;
+ }
+
+ async runTests(params: { reporter?: string; locations?: string[] | undefined; grep?: string | undefined; testIds?: string[] | undefined; headed?: boolean | undefined; oneWorker?: boolean | undefined; trace?: 'off' | 'on' | undefined; projects?: string[] | undefined; reuseContext?: boolean | undefined; connectWsEndpoint?: string | undefined; }) {
+ let status: reporterTypes.FullResult['status'];
+ this._queue = this._queue.then(async () => {
+ status = await this._innerRunTests(params).catch(printInternalError) || 'failed';
+ });
+ await this._queue;
+ return { status: status! };
+ }
+
+ private async _innerRunTests(params: { reporter?: string; locations?: string[] | undefined; grep?: string | undefined; testIds?: string[] | undefined; headed?: boolean | undefined; oneWorker?: boolean | undefined; trace?: 'off' | 'on' | undefined; projects?: string[] | undefined; reuseContext?: boolean | undefined; connectWsEndpoint?: string | undefined; }): Promise {
+ await this.stopTests();
+ const { testIds, projects, locations, grep } = params;
+
+ const testIdSet = testIds ? new Set(testIds) : null;
+ this._config.cliArgs = locations ? locations : [];
+ this._config.cliGrep = grep;
+ this._config.cliListOnly = false;
+ this._config.cliProjectFilter = projects?.length ? projects : undefined;
+ this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id);
+
+ const reporters = await createReporters(this._config, 'ui');
+ reporters.push(await createReporterForTestServer(this._config, params.reporter || require.resolve('./uiModeReporter'), 'list', e => this._dispatchEvent('report', e)));
+ const reporter = new InternalReporter(new Multiplexer(reporters));
+ const taskRunner = createTaskRunnerForWatch(this._config, reporter);
+ const testRun = new TestRun(this._config, reporter);
+ reporter.onConfigure(this._config.config);
const stop = new ManualPromise();
const run = taskRunner.run(testRun, 0, stop).then(async status => {
await reporter.onEnd({ status });
await reporter.onExit();
this._testRun = undefined;
+ this._config.testIdMatcher = undefined;
return status;
});
this._testRun = { run, stop };
- await run;
+ return await run;
}
- async findRelatedTestFiles(params: {
- configFile: string;
- files: string[];
- }): Promise {
- const config = await this._loadConfig(params.configFile);
- const runner = new Runner(config);
+ async watch(params: { fileNames: string[]; }) {
+ const files = new Set();
+ for (const fileName of params.fileNames) {
+ files.add(fileName);
+ dependenciesForTestFile(fileName).forEach(file => files.add(file));
+ }
+ this._testWatcher.update([...files], [], true);
+ }
+
+ findRelatedTestFiles(params: { files: string[]; }): Promise<{ testFiles: string[]; errors?: reporterTypes.TestError[] | undefined; }> {
+ const runner = new Runner(this._config);
return runner.findRelatedTestFiles('out-of-process', params.files);
}
- async stop(params: {
- configFile: string;
- }) {
- await this._stopTests();
+ async stopTests() {
+ this._testRun?.stop?.resolve();
+ await this._testRun?.run;
}
async closeGracefully() {
gracefullyProcessExitDoNotHang(0);
}
-
- private async _stopTests() {
- this._testRun?.stop?.resolve();
- await this._testRun?.run;
- }
-
- private _dispatchEvent(method: string, params: any) {
- this._ws.send(JSON.stringify({ method, params }));
- }
-
- private async _loadConfig(configFile: string, overrides?: ConfigCLIOverrides): Promise {
- return loadConfig({ resolvedConfigFile: configFile, configDir: path.dirname(configFile) }, overrides);
- }
-
- private async _createReporter(file: string) {
- const reporterConstructor = await loadReporter(undefined, file);
- const instance = new reporterConstructor((message: any) => this._dispatchEvent('report', message));
- return wrapReporterAsV2(instance);
- }
}
-function chunkToPayload(type: 'stdout' | 'stderr', chunk: Buffer | string) {
+export async function runTestServer(config: FullConfigInternal, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise) => Promise): Promise {
+ const testServer = new TestServer(config);
+ const cancelPromise = new ManualPromise();
+ const sigintWatcher = new SigIntWatcher();
+ void sigintWatcher.promise().then(() => cancelPromise.resolve());
+ try {
+ const server = await testServer.start(options);
+ await openUI(server, cancelPromise);
+ testServer.wireStdIO();
+ await cancelPromise;
+ } finally {
+ testServer.unwireStdIO();
+ await testServer.stop();
+ sigintWatcher.disarm();
+ }
+ return sigintWatcher.hadSignal() ? 'interrupted' : 'passed';
+}
+
+type StdioPayload = {
+ type: 'stdout' | 'stderr';
+ text?: string;
+ buffer?: string;
+};
+
+function chunkToPayload(type: 'stdout' | 'stderr', chunk: Buffer | string): StdioPayload {
if (chunk instanceof Buffer)
return { type, buffer: chunk.toString('base64') };
return { type, text: chunk };
}
+
+function hasSomeBrowsers(): boolean {
+ for (const browserName of ['chromium', 'webkit', 'firefox']) {
+ try {
+ registry.findExecutable(browserName)!.executablePathOrDie('javascript');
+ return true;
+ } catch {
+ }
+ }
+ return false;
+}
+
+async function installBrowsers() {
+ const executables = registry.defaultExecutables();
+ await registry.install(executables, false);
+}
+
+function printInternalError(e: Error) {
+ // eslint-disable-next-line no-console
+ console.error('Internal error:', e);
+}
+
+// TODO: remove CT dependency.
+export async function resolveCtDirs(config: FullConfigInternal) {
+ const use = config.config.projects[0].use as any;
+ const relativeTemplateDir = use.ctTemplateDir || 'playwright';
+ const templateDir = await fs.promises.realpath(path.normalize(path.join(config.configDir, relativeTemplateDir))).catch(() => undefined);
+ if (!templateDir)
+ return null;
+ const outDir = use.ctCacheDir ? path.resolve(config.configDir, use.ctCacheDir) : path.resolve(templateDir, '.cache');
+ return {
+ outDir,
+ templateDir
+ };
+}
diff --git a/packages/playwright/src/runner/testServerInterface.ts b/packages/playwright/src/runner/testServerInterface.ts
deleted file mode 100644
index 1e844d8123..0000000000
--- a/packages/playwright/src/runner/testServerInterface.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * Copyright (c) Microsoft Corporation.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import type { TestError } from '../../types/testReporter';
-
-export interface TestServerInterface {
- listFiles(params: {
- configFile: string;
- }): Promise<{
- projects: {
- name: string;
- testDir: string;
- use: { testIdAttribute?: string };
- files: string[];
- }[];
- cliEntryPoint?: string;
- error?: TestError;
- }>;
-
- listTests(params: {
- configFile: string;
- locations: string[];
- reporter: string;
- }): Promise;
-
- test(params: {
- configFile: string;
- locations: string[];
- reporter: string;
- headed?: boolean;
- oneWorker?: boolean;
- trace?: 'on' | 'off';
- projects?: string[];
- grep?: string;
- reuseContext?: boolean;
- connectWsEndpoint?: string;
- }): Promise;
-
- findRelatedTestFiles(params: {
- configFile: string;
- files: string[];
- }): Promise<{ testFiles: string[]; errors?: TestError[]; }>;
-
- stop(params: {
- configFile: string;
- }): Promise;
-
- closeGracefully(): Promise;
-}
-
-export interface TestServerEvents {
- on(event: 'report', listener: (params: any) => void): void;
- on(event: 'stdio', listener: (params: { type: 'stdout' | 'stderr', text?: string, buffer?: string }) => void): void;
-}
diff --git a/packages/playwright/src/runner/uiMode.ts b/packages/playwright/src/runner/uiMode.ts
deleted file mode 100644
index d0071e14f1..0000000000
--- a/packages/playwright/src/runner/uiMode.ts
+++ /dev/null
@@ -1,272 +0,0 @@
-/**
- * Copyright Microsoft Corporation. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { openTraceViewerApp, openTraceInBrowser, registry } from 'playwright-core/lib/server';
-import { isUnderTest, ManualPromise } from 'playwright-core/lib/utils';
-import type { FullResult } from '../../types/testReporter';
-import { collectAffectedTestFiles, dependenciesForTestFile } from '../transform/compilationCache';
-import type { FullConfigInternal } from '../common/config';
-import { InternalReporter } from '../reporters/internalReporter';
-import { TeleReporterEmitter } from '../reporters/teleEmitter';
-import { createReporters } from './reporters';
-import { TestRun, createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
-import { open } from 'playwright-core/lib/utilsBundle';
-import ListReporter from '../reporters/list';
-import type { OpenTraceViewerOptions, Transport } from 'playwright-core/lib/server/trace/viewer/traceViewer';
-import { Multiplexer } from '../reporters/multiplexer';
-import { SigIntWatcher } from './sigIntWatcher';
-import { Watcher } from '../fsWatcher';
-
-class UIMode {
- private _config: FullConfigInternal;
- private _transport!: Transport;
- private _testRun: { run: Promise, stop: ManualPromise } | undefined;
- globalCleanup: (() => Promise) | undefined;
- private _globalWatcher: Watcher;
- private _testWatcher: Watcher;
- private _originalStdoutWrite: NodeJS.WriteStream['write'];
- private _originalStderrWrite: NodeJS.WriteStream['write'];
-
- constructor(config: FullConfigInternal) {
- this._config = config;
- process.env.PW_LIVE_TRACE_STACKS = '1';
- config.cliListOnly = false;
- config.cliPassWithNoTests = true;
- config.config.preserveOutput = 'always';
-
- for (const p of config.projects) {
- p.project.retries = 0;
- p.project.repeatEach = 1;
- }
- config.configCLIOverrides.use = config.configCLIOverrides.use || {};
- config.configCLIOverrides.use.trace = { mode: 'on', sources: false, _live: true };
-
- this._originalStdoutWrite = process.stdout.write;
- this._originalStderrWrite = process.stderr.write;
- this._globalWatcher = new Watcher('deep', () => this._dispatchEvent('listChanged', {}));
- this._testWatcher = new Watcher('flat', events => {
- const collector = new Set();
- events.forEach(f => collectAffectedTestFiles(f.file, collector));
- this._dispatchEvent('testFilesChanged', { testFileNames: [...collector] });
- });
- }
-
- async runGlobalSetup(): Promise {
- const reporter = new InternalReporter(new ListReporter());
- const taskRunner = createTaskRunnerForWatchSetup(this._config, reporter);
- reporter.onConfigure(this._config.config);
- const testRun = new TestRun(this._config, reporter);
- const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0);
- await reporter.onEnd({ status });
- await reporter.onExit();
- if (status !== 'passed') {
- await globalCleanup();
- return status;
- }
- this.globalCleanup = globalCleanup;
- return status;
- }
-
- async showUI(options: { host?: string, port?: number }, cancelPromise: ManualPromise) {
- let queue = Promise.resolve();
-
- this._transport = {
- dispatch: async (method, params) => {
- if (method === 'ping')
- return;
-
- if (method === 'watch') {
- this._watchFiles(params.fileNames);
- return;
- }
- if (method === 'open' && params.location) {
- open('vscode://file/' + params.location).catch(e => this._originalStderrWrite.call(process.stderr, String(e)));
- return;
- }
- if (method === 'resizeTerminal') {
- process.stdout.columns = params.cols;
- process.stdout.rows = params.rows;
- process.stderr.columns = params.cols;
- process.stderr.columns = params.rows;
- return;
- }
- if (method === 'stop') {
- void this._stopTests();
- return;
- }
- if (method === 'checkBrowsers')
- return { hasBrowsers: hasSomeBrowsers() };
- if (method === 'installBrowsers') {
- await installBrowsers();
- return;
- }
-
- queue = queue.then(() => this._queueListOrRun(method, params));
- await queue;
- },
-
- onclose: () => { },
- };
- const openOptions: OpenTraceViewerOptions = {
- app: 'uiMode.html',
- headless: isUnderTest() && process.env.PWTEST_HEADED_FOR_TEST !== '1',
- transport: this._transport,
- host: options.host,
- port: options.port,
- persistentContextOptions: {
- handleSIGINT: false,
- },
- };
- if (options.host !== undefined || options.port !== undefined) {
- await openTraceInBrowser([], openOptions);
- } else {
- const page = await openTraceViewerApp([], 'chromium', openOptions);
- page.on('close', () => cancelPromise.resolve());
- }
-
- if (!process.env.PWTEST_DEBUG) {
- process.stdout.write = (chunk: string | Buffer) => {
- this._dispatchEvent('stdio', chunkToPayload('stdout', chunk));
- return true;
- };
- process.stderr.write = (chunk: string | Buffer) => {
- this._dispatchEvent('stdio', chunkToPayload('stderr', chunk));
- return true;
- };
- }
- await cancelPromise;
-
- if (!process.env.PWTEST_DEBUG) {
- process.stdout.write = this._originalStdoutWrite;
- process.stderr.write = this._originalStderrWrite;
- }
- }
-
- private async _queueListOrRun(method: string, params: any) {
- if (method === 'list')
- await this._listTests();
- if (method === 'run')
- await this._runTests(params.testIds, params.projects);
- }
-
- private _dispatchEvent(method: string, params?: any) {
- this._transport.sendEvent?.(method, params);
- }
-
- private async _listTests() {
- const reporter = new InternalReporter(new TeleReporterEmitter(e => this._dispatchEvent(e.method, e.params), true));
- this._config.cliListOnly = true;
- this._config.testIdMatcher = undefined;
- const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process', { failOnLoadErrors: false });
- const testRun = new TestRun(this._config, reporter);
- reporter.onConfigure(this._config.config);
- const status = await taskRunner.run(testRun, 0);
- await reporter.onEnd({ status });
- await reporter.onExit();
-
- const projectDirs = new Set();
- const projectOutputs = new Set();
- for (const p of this._config.projects) {
- projectDirs.add(p.project.testDir);
- projectOutputs.add(p.project.outputDir);
- }
- this._globalWatcher.update([...projectDirs], [...projectOutputs], false);
- }
-
- private async _runTests(testIds: string[], projects: string[]) {
- await this._stopTests();
-
- const testIdSet = testIds ? new Set(testIds) : null;
- this._config.cliListOnly = false;
- this._config.cliProjectFilter = projects.length ? projects : undefined;
- this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id);
-
- const reporters = await createReporters(this._config, 'ui');
- reporters.push(new TeleReporterEmitter(e => this._dispatchEvent(e.method, e.params), true));
- const reporter = new InternalReporter(new Multiplexer(reporters));
- const taskRunner = createTaskRunnerForWatch(this._config, reporter);
- const testRun = new TestRun(this._config, reporter);
- reporter.onConfigure(this._config.config);
- const stop = new ManualPromise();
- const run = taskRunner.run(testRun, 0, stop).then(async status => {
- await reporter.onEnd({ status });
- await reporter.onExit();
- this._testRun = undefined;
- this._config.testIdMatcher = undefined;
- return status;
- });
- this._testRun = { run, stop };
- await run;
- }
-
- private _watchFiles(fileNames: string[]) {
- const files = new Set();
- for (const fileName of fileNames) {
- files.add(fileName);
- dependenciesForTestFile(fileName).forEach(file => files.add(file));
- }
- this._testWatcher.update([...files], [], true);
- }
-
- private async _stopTests() {
- this._testRun?.stop?.resolve();
- await this._testRun?.run;
- }
-}
-
-export async function runUIMode(config: FullConfigInternal, options: { host?: string, port?: number }): Promise {
- const uiMode = new UIMode(config);
- const globalSetupStatus = await uiMode.runGlobalSetup();
- if (globalSetupStatus !== 'passed')
- return globalSetupStatus;
- const cancelPromise = new ManualPromise();
- const sigintWatcher = new SigIntWatcher();
- void sigintWatcher.promise().then(() => cancelPromise.resolve());
- try {
- await uiMode.showUI(options, cancelPromise);
- } finally {
- sigintWatcher.disarm();
- }
- return await uiMode.globalCleanup?.() || (sigintWatcher.hadSignal() ? 'interrupted' : 'passed');
-}
-
-type StdioPayload = {
- type: 'stdout' | 'stderr';
- text?: string;
- buffer?: string;
-};
-
-function chunkToPayload(type: 'stdout' | 'stderr', chunk: Buffer | string): StdioPayload {
- if (chunk instanceof Buffer)
- return { type, buffer: chunk.toString('base64') };
- return { type, text: chunk };
-}
-
-function hasSomeBrowsers(): boolean {
- for (const browserName of ['chromium', 'webkit', 'firefox']) {
- try {
- registry.findExecutable(browserName)!.executablePathOrDie('javascript');
- return true;
- } catch {
- }
- }
- return false;
-}
-
-async function installBrowsers() {
- const executables = registry.defaultExecutables();
- await registry.install(executables, false);
-}
diff --git a/packages/playwright/types/reporterPrivate.ts b/packages/playwright/src/runner/uiModeReporter.ts
similarity index 72%
rename from packages/playwright/types/reporterPrivate.ts
rename to packages/playwright/src/runner/uiModeReporter.ts
index 198b2fe9ea..8de663149a 100644
--- a/packages/playwright/types/reporterPrivate.ts
+++ b/packages/playwright/src/runner/uiModeReporter.ts
@@ -14,9 +14,12 @@
* limitations under the License.
*/
-import type { Suite } from './testReporter';
+import { TeleReporterEmitter } from '../reporters/teleEmitter';
-export interface SuitePrivate extends Suite {
- _fileId: string | undefined;
- _parallelMode: 'none' | 'default' | 'serial' | 'parallel';
+class UIModeReporter extends TeleReporterEmitter {
+ constructor(options: any) {
+ super(options._send, { omitBuffers: true });
+ }
}
+
+export default UIModeReporter;
diff --git a/packages/playwright/src/third_party/tsconfig-loader.ts b/packages/playwright/src/third_party/tsconfig-loader.ts
index ffe8af1a00..d85ff32100 100644
--- a/packages/playwright/src/third_party/tsconfig-loader.ts
+++ b/packages/playwright/src/third_party/tsconfig-loader.ts
@@ -44,8 +44,11 @@ interface TsConfig {
export interface LoadedTsConfig {
tsConfigPath: string;
- baseUrl?: string;
- paths?: { [key: string]: Array };
+ paths?: {
+ mapping: { [key: string]: Array };
+ pathsBasePath: string; // absolute path
+ };
+ absoluteBaseUrl?: string;
allowJs?: boolean;
}
@@ -101,7 +104,8 @@ function resolveConfigFile(baseConfigFile: string, referencedConfigFile: string)
referencedConfigFile += '.json';
const currentDir = path.dirname(baseConfigFile);
let resolvedConfigFile = path.resolve(currentDir, referencedConfigFile);
- if (referencedConfigFile.indexOf('/') !== -1 && referencedConfigFile.indexOf('.') !== -1 && !fs.existsSync(referencedConfigFile))
+ // TODO: I don't see how this makes sense, delete in the next minor release.
+ if (referencedConfigFile.includes('/') && referencedConfigFile.includes('.') && !fs.existsSync(resolvedConfigFile))
resolvedConfigFile = path.join(currentDir, 'node_modules', referencedConfigFile);
return resolvedConfigFile;
}
@@ -117,6 +121,7 @@ function loadTsConfig(
let result: LoadedTsConfig = {
tsConfigPath: configFilePath,
};
+ // Retain result instance below, so that caching works.
visited.set(configFilePath, result);
if (!fs.existsSync(configFilePath))
@@ -130,23 +135,27 @@ function loadTsConfig(
for (const extendedConfig of extendsArray) {
const extendedConfigPath = resolveConfigFile(configFilePath, extendedConfig);
const base = loadTsConfig(extendedConfigPath, references, visited);
-
- // baseUrl should be interpreted as relative to the base tsconfig,
- // but we need to update it so it is relative to the original tsconfig being loaded
- if (base.baseUrl && base.baseUrl) {
- const extendsDir = path.dirname(extendedConfig);
- base.baseUrl = path.join(extendsDir, base.baseUrl);
- }
- result = { ...result, ...base, tsConfigPath: configFilePath };
+ // Retain result instance, so that caching works.
+ Object.assign(result, base, { tsConfigPath: configFilePath });
}
- const loadedConfig = Object.fromEntries(Object.entries({
- baseUrl: parsedConfig.compilerOptions?.baseUrl,
- paths: parsedConfig.compilerOptions?.paths,
- allowJs: parsedConfig?.compilerOptions?.allowJs,
- }).filter(([, value]) => value !== undefined));
-
- result = { ...result, ...loadedConfig };
+ if (parsedConfig.compilerOptions?.allowJs !== undefined)
+ result.allowJs = parsedConfig.compilerOptions.allowJs;
+ if (parsedConfig.compilerOptions?.paths !== undefined) {
+ // We must store pathsBasePath from the config that defines "paths" and later resolve
+ // based on this absolute path, when no "baseUrl" is specified. See tsc for reference:
+ // https://github.com/microsoft/TypeScript/blob/353ccb7688351ae33ccf6e0acb913aa30621eaf4/src/compiler/commandLineParser.ts#L3129
+ // https://github.com/microsoft/TypeScript/blob/353ccb7688351ae33ccf6e0acb913aa30621eaf4/src/compiler/moduleSpecifiers.ts#L510
+ result.paths = {
+ mapping: parsedConfig.compilerOptions.paths,
+ pathsBasePath: path.dirname(configFilePath),
+ };
+ }
+ if (parsedConfig.compilerOptions?.baseUrl !== undefined) {
+ // Follow tsc and resolve all relative file paths in the config right away.
+ // This way it is safe to inherit paths between the configs.
+ result.absoluteBaseUrl = path.resolve(path.dirname(configFilePath), parsedConfig.compilerOptions.baseUrl);
+ }
for (const ref of parsedConfig.references || [])
references.push(loadTsConfig(resolveConfigFile(configFilePath, ref.path), references, visited));
diff --git a/packages/playwright/src/transform/compilationCache.ts b/packages/playwright/src/transform/compilationCache.ts
index caa4b08d23..e0154840b0 100644
--- a/packages/playwright/src/transform/compilationCache.ts
+++ b/packages/playwright/src/transform/compilationCache.ts
@@ -27,7 +27,7 @@ export type MemoryCache = {
moduleUrl?: string;
};
-type SerializedCompilationCache = {
+export type SerializedCompilationCache = {
sourceMaps: [string, string][],
memoryCache: [string, MemoryCache][],
fileDependencies: [string, string[]][],
@@ -158,15 +158,19 @@ export function serializeCompilationCache(): SerializedCompilationCache {
};
}
-export function addToCompilationCache(payload: any) {
+export function addToCompilationCache(payload: SerializedCompilationCache) {
for (const entry of payload.sourceMaps)
sourceMaps.set(entry[0], entry[1]);
for (const entry of payload.memoryCache)
memoryCache.set(entry[0], entry[1]);
- for (const entry of payload.fileDependencies)
- fileDependencies.set(entry[0], new Set(entry[1]));
- for (const entry of payload.externalDependencies)
- externalDependencies.set(entry[0], new Set(entry[1]));
+ for (const entry of payload.fileDependencies) {
+ const existing = fileDependencies.get(entry[0]) || [];
+ fileDependencies.set(entry[0], new Set([...entry[1], ...existing]));
+ }
+ for (const entry of payload.externalDependencies) {
+ const existing = externalDependencies.get(entry[0]) || [];
+ externalDependencies.set(entry[0], new Set([...entry[1], ...existing]));
+ }
}
function calculateCachePath(filePath: string, hash: string): string {
@@ -249,9 +253,9 @@ const kPlaywrightCoveragePrefix = path.resolve(__dirname, '../../../../tests/con
export function belongsToNodeModules(file: string) {
if (file.includes(`${path.sep}node_modules${path.sep}`))
return true;
- if (file.startsWith(kPlaywrightInternalPrefix) && file.endsWith('.js'))
+ if (file.startsWith(kPlaywrightInternalPrefix) && (file.endsWith('.js') || file.endsWith('.mjs')))
return true;
- if (file.startsWith(kPlaywrightCoveragePrefix) && file.endsWith('.js'))
+ if (file.startsWith(kPlaywrightCoveragePrefix) && (file.endsWith('.js') || file.endsWith('.mjs')))
return true;
return false;
}
diff --git a/packages/playwright/src/transform/transform.ts b/packages/playwright/src/transform/transform.ts
index 82b5acdda1..2f6431c4f1 100644
--- a/packages/playwright/src/transform/transform.ts
+++ b/packages/playwright/src/transform/transform.ts
@@ -30,7 +30,7 @@ import { getFromCompilationCache, currentFileDepsCollector, belongsToNodeModules
const version = require('../../package.json').version;
type ParsedTsConfigData = {
- absoluteBaseUrl: string;
+ pathsBase?: string;
paths: { key: string, values: string[] }[];
allowJs: boolean;
};
@@ -58,16 +58,15 @@ export function transformConfig(): TransformConfig {
}
function validateTsConfig(tsconfig: LoadedTsConfig): ParsedTsConfigData {
- // Make 'baseUrl' absolute, because it is relative to the tsconfig.json, not to cwd.
// When no explicit baseUrl is set, resolve paths relative to the tsconfig file.
// See https://www.typescriptlang.org/tsconfig#paths
- const absoluteBaseUrl = path.resolve(path.dirname(tsconfig.tsConfigPath), tsconfig.baseUrl ?? '.');
+ const pathsBase = tsconfig.absoluteBaseUrl ?? tsconfig.paths?.pathsBasePath;
// Only add the catch-all mapping when baseUrl is specified
- const pathsFallback = tsconfig.baseUrl ? [{ key: '*', values: ['*'] }] : [];
+ const pathsFallback = tsconfig.absoluteBaseUrl ? [{ key: '*', values: ['*'] }] : [];
return {
allowJs: !!tsconfig.allowJs,
- absoluteBaseUrl,
- paths: Object.entries(tsconfig.paths || {}).map(([key, values]) => ({ key, values })).concat(pathsFallback)
+ pathsBase,
+ paths: Object.entries(tsconfig.paths?.mapping || {}).map(([key, values]) => ({ key, values })).concat(pathsFallback)
};
}
@@ -132,7 +131,7 @@ export function resolveHook(filename: string, specifier: string): string | undef
let candidate = value;
if (value.includes('*'))
candidate = candidate.replace('*', matchedPartOfSpecifier);
- candidate = path.resolve(tsconfig.absoluteBaseUrl, candidate);
+ candidate = path.resolve(tsconfig.pathsBase!, candidate);
const existing = resolveImportSpecifierExtension(candidate);
if (existing) {
longestPrefixLength = keyPrefix.length;
@@ -241,7 +240,7 @@ function installTransform(): () => void {
if (!shouldTransform(filename))
return code;
return transformHook(code, filename).code;
- }, { exts: ['.ts', '.tsx', '.js', '.jsx', '.mjs'] });
+ }, { exts: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.mts', '.cjs', '.cts'] });
return () => {
reverted = true;
diff --git a/packages/playwright/src/util.ts b/packages/playwright/src/util.ts
index 34907428c9..d6cabd07c6 100644
--- a/packages/playwright/src/util.ts
+++ b/packages/playwright/src/util.ts
@@ -185,6 +185,8 @@ export function expectTypes(receiver: any, types: string[], matcherName: string)
}
}
+export const windowsFilesystemFriendlyLength = 60;
+
export function trimLongString(s: string, length = 100) {
if (s.length <= length)
return s;
@@ -195,12 +197,16 @@ export function trimLongString(s: string, length = 100) {
return s.substring(0, start) + middle + s.slice(-end);
}
-export function addSuffixToFilePath(filePath: string, suffix: string, customExtension?: string, sanitize = false): string {
- const dirname = path.dirname(filePath);
+export function addSuffixToFilePath(filePath: string, suffix: string): string {
const ext = path.extname(filePath);
- const name = path.basename(filePath, ext);
- const base = path.join(dirname, name);
- return (sanitize ? sanitizeForFilePath(base) : base) + suffix + (customExtension || ext);
+ const base = filePath.substring(0, filePath.length - ext.length);
+ return base + suffix + ext;
+}
+
+export function sanitizeFilePathBeforeExtension(filePath: string): string {
+ const ext = path.extname(filePath);
+ const base = filePath.substring(0, filePath.length - ext.length);
+ return sanitizeForFilePath(base) + ext;
}
/**
@@ -208,7 +214,8 @@ export function addSuffixToFilePath(filePath: string, suffix: string, customExte
*/
export function getContainedPath(parentPath: string, subPath: string = ''): string | null {
const resolvedPath = path.resolve(parentPath, subPath);
- if (resolvedPath === parentPath || resolvedPath.startsWith(parentPath + path.sep)) return resolvedPath;
+ if (resolvedPath === parentPath || resolvedPath.startsWith(parentPath + path.sep))
+ return resolvedPath;
return null;
}
diff --git a/packages/playwright/src/worker/fixtureRunner.ts b/packages/playwright/src/worker/fixtureRunner.ts
index 85c2dddab9..f425f1cd3e 100644
--- a/packages/playwright/src/worker/fixtureRunner.ts
+++ b/packages/playwright/src/worker/fixtureRunner.ts
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-import { formatLocation, debugTest, filterStackFile } from '../util';
+import { formatLocation, filterStackFile } from '../util';
import { ManualPromise } from 'playwright-core/lib/utils';
import type { TestInfoImpl } from './testInfo';
-import type { FixtureDescription } from './timeoutManager';
+import { TimeoutManagerError, type FixtureDescription, type RunnableDescription } from './timeoutManager';
import { fixtureParameterNames, type FixturePool, type FixtureRegistration, type FixtureScope } from '../common/fixtures';
import type { WorkerInfo } from '../../types/test';
import type { Location } from '../../types/testReporter';
@@ -30,11 +30,9 @@ class Fixture {
private _useFuncFinished: ManualPromise | undefined;
private _selfTeardownComplete: Promise | undefined;
- private _teardownWithDepsComplete: Promise | undefined;
private _setupDescription: FixtureDescription;
private _teardownDescription: FixtureDescription;
- private _shouldGenerateStep = false;
- private _isInternalFixture = false;
+ private _stepInfo: { category: 'fixture', location?: Location } | undefined;
_deps = new Set();
_usages = new Set();
@@ -42,22 +40,24 @@ class Fixture {
this.runner = runner;
this.registration = registration;
this.value = null;
+ const shouldGenerateStep = !this.registration.hideStep && !this.registration.name.startsWith('_') && !this.registration.option;
+ const isInternalFixture = this.registration.location && filterStackFile(this.registration.location.file);
const title = this.registration.customTitle || this.registration.name;
+ const location = isInternalFixture ? this.registration.location : undefined;
+ this._stepInfo = shouldGenerateStep ? { category: 'fixture', location } : undefined;
this._setupDescription = {
title,
phase: 'setup',
- location: registration.location,
+ location,
slot: this.registration.timeout === undefined ? undefined : {
timeout: this.registration.timeout,
elapsed: 0,
}
};
this._teardownDescription = { ...this._setupDescription, phase: 'teardown' };
- this._shouldGenerateStep = !this.registration.hideStep && !this.registration.name.startsWith('_') && !this.registration.option;
- this._isInternalFixture = this.registration.location && filterStackFile(this.registration.location.file);
}
- async setup(testInfo: TestInfoImpl) {
+ async setup(testInfo: TestInfoImpl, runnable: RunnableDescription) {
this.runner.instanceForId.set(this.registration.id, this);
if (typeof this.registration.fn !== 'function') {
@@ -65,22 +65,13 @@ class Fixture {
return;
}
- testInfo._timeoutManager.setCurrentFixture(this._setupDescription);
- const beforeStep = this._shouldGenerateStep ? testInfo._addStep({
+ await testInfo._runAsStage({
title: `fixture: ${this.registration.name}`,
- category: 'fixture',
- location: this._isInternalFixture ? this.registration.location : undefined,
- wallTime: Date.now(),
- }) : undefined;
- try {
+ runnable: { ...runnable, fixture: this._setupDescription },
+ stepInfo: this._stepInfo,
+ }, async () => {
await this._setupInternal(testInfo);
- beforeStep?.complete({});
- } catch (error) {
- beforeStep?.complete({ error });
- throw error;
- } finally {
- testInfo._timeoutManager.setCurrentFixture(undefined);
- }
+ });
}
private async _setupInternal(testInfo: TestInfoImpl) {
@@ -107,7 +98,6 @@ class Fixture {
let called = false;
const useFuncStarted = new ManualPromise();
- debugTest(`setup ${this.registration.name}`);
const useFunc = async (value: any) => {
if (called)
throw new Error(`Cannot provide fixture value for the second time`);
@@ -134,25 +124,14 @@ class Fixture {
await useFuncStarted;
}
- async teardown(testInfo: TestInfoImpl) {
- const afterStep = this._shouldGenerateStep ? testInfo?._addStep({
- wallTime: Date.now(),
+ async teardown(testInfo: TestInfoImpl, runnable: RunnableDescription) {
+ await testInfo._runAsStage({
title: `fixture: ${this.registration.name}`,
- category: 'fixture',
- location: this._isInternalFixture ? this.registration.location : undefined,
- }) : undefined;
- testInfo._timeoutManager.setCurrentFixture(this._teardownDescription);
- try {
- if (!this._teardownWithDepsComplete)
- this._teardownWithDepsComplete = this._teardownInternal();
- await this._teardownWithDepsComplete;
- afterStep?.complete({});
- } catch (error) {
- afterStep?.complete({ error });
- throw error;
- } finally {
- testInfo._timeoutManager.setCurrentFixture(undefined);
- }
+ runnable: { ...runnable, fixture: this._teardownDescription },
+ stepInfo: this._stepInfo,
+ }, async () => {
+ await this._teardownInternal();
+ });
}
private async _teardownInternal() {
@@ -165,17 +144,21 @@ class Fixture {
this._usages.clear();
}
if (this._useFuncFinished) {
- debugTest(`teardown ${this.registration.name}`);
this._useFuncFinished.resolve();
+ this._useFuncFinished = undefined;
await this._selfTeardownComplete;
}
} finally {
- for (const dep of this._deps)
- dep._usages.delete(this);
- this.runner.instanceForId.delete(this.registration.id);
+ this._cleanupInstance();
}
}
+ _cleanupInstance() {
+ for (const dep of this._deps)
+ dep._usages.delete(this);
+ this.runner.instanceForId.delete(this.registration.id);
+ }
+
_collectFixturesInTeardownOrder(scope: FixtureScope, collector: Set) {
if (this.registration.scope !== scope)
return;
@@ -214,19 +197,36 @@ export class FixtureRunner {
collector.add(registration);
}
- async teardownScope(scope: FixtureScope, testInfo: TestInfoImpl, onFixtureError: (error: Error) => void) {
+ async teardownScope(scope: FixtureScope, testInfo: TestInfoImpl, runnable: RunnableDescription) {
// Teardown fixtures in the reverse order.
const fixtures = Array.from(this.instanceForId.values()).reverse();
const collector = new Set();
for (const fixture of fixtures)
fixture._collectFixturesInTeardownOrder(scope, collector);
- for (const fixture of collector)
- await fixture.teardown(testInfo).catch(onFixtureError);
- if (scope === 'test')
- this.testScopeClean = true;
+ try {
+ let firstError: Error | undefined;
+ for (const fixture of collector) {
+ try {
+ await fixture.teardown(testInfo, runnable);
+ } catch (error) {
+ if (error instanceof TimeoutManagerError)
+ throw error;
+ firstError = firstError ?? error;
+ }
+ }
+ if (firstError)
+ throw firstError;
+ } finally {
+ // To preserve fixtures integrity, forcefully cleanup fixtures that did not teardown
+ // due to a timeout in one of them.
+ for (const fixture of collector)
+ fixture._cleanupInstance();
+ if (scope === 'test')
+ this.testScopeClean = true;
+ }
}
- async resolveParametersForFunction(fn: Function, testInfo: TestInfoImpl, autoFixtures: 'worker' | 'test' | 'all-hooks-only'): Promise