Merge remote-tracking branch 'origin/main' into await-onapi-csi-calls
This commit is contained in:
commit
c9458e2193
28
.github/workflows/tests_electron.yml
vendored
28
.github/workflows/tests_electron.yml
vendored
|
|
@ -32,25 +32,11 @@ jobs:
|
|||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: ./.github/actions/run-test
|
||||
with:
|
||||
node-version: 18
|
||||
- run: npm ci
|
||||
env:
|
||||
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 etest
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
- run: npm run etest
|
||||
if: matrix.os != 'ubuntu-latest'
|
||||
- name: Azure Login
|
||||
uses: azure/login@v2
|
||||
if: ${{ !cancelled() && github.event_name == 'push' && github.repository == 'microsoft/playwright' }}
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
browsers-to-install: chromium
|
||||
command: npm run etest
|
||||
bot-name: "electron-${{ matrix.os }}"
|
||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
||||
|
|
|
|||
4
.github/workflows/tests_secondary.yml
vendored
4
.github/workflows/tests_secondary.yml
vendored
|
|
@ -229,7 +229,7 @@ jobs:
|
|||
PWTEST_CHANNEL: chrome
|
||||
|
||||
chromium_tot:
|
||||
name: Chromium TOT ${{ matrix.os }}
|
||||
name: Chromium tip-of-tree ${{ matrix.os }}
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
|
|
@ -250,7 +250,7 @@ jobs:
|
|||
PWTEST_CHANNEL: chromium-tip-of-tree
|
||||
|
||||
chromium_tot_headed:
|
||||
name: Chromium TOT headed ${{ matrix.os }}
|
||||
name: Chromium tip-of-tree headed ${{ matrix.os }}
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
|
|
|
|||
26
.github/workflows/tests_video.yml
vendored
26
.github/workflows/tests_video.yml
vendored
|
|
@ -26,25 +26,13 @@ jobs:
|
|||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: ./.github/actions/run-test
|
||||
with:
|
||||
node-version: 18
|
||||
- run: npm ci
|
||||
env:
|
||||
DEBUG: pw:install
|
||||
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 }}-*
|
||||
browsers-to-install: ${{ matrix.browser }} chromium
|
||||
command: npm run test -- --project=${{ matrix.browser }}-*
|
||||
bot-name: "${{ matrix.browser }}-${{ matrix.os }}"
|
||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
||||
env:
|
||||
PWTEST_VIDEO: 1
|
||||
- name: Azure Login
|
||||
uses: azure/login@v2
|
||||
if: ${{ !cancelled() && github.event_name == 'push' && github.repository == 'microsoft/playwright' }}
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 🎭 Playwright
|
||||
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop -->
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop -->
|
||||
|
||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||
|
||||
|
|
@ -8,9 +8,9 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
|||
|
||||
| | Linux | macOS | Windows |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Chromium <!-- GEN:chromium-version -->125.0.6422.41<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Chromium <!-- GEN:chromium-version -->126.0.6478.8<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| WebKit <!-- GEN:webkit-version -->17.4<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Firefox <!-- GEN:firefox-version -->125.0.1<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Firefox <!-- GEN:firefox-version -->126.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ Playwright performs a range of actionability checks on the elements before makin
|
|||
behave as expected. It auto-waits for all the relevant checks to pass and only then performs the requested action. If the required checks do not pass within the given `timeout`, action fails with the `TimeoutError`.
|
||||
|
||||
For example, for [`method: Locator.click`], Playwright will ensure that:
|
||||
- locator resolves to an exactly one element
|
||||
- locator resolves to exactly one element
|
||||
- element is [Visible]
|
||||
- element is [Stable], as in not animating or completed animation
|
||||
- element [Receives Events], as in not obscured by other elements
|
||||
|
|
|
|||
|
|
@ -185,6 +185,12 @@ context cookies from the response. The method will automatically follow redirect
|
|||
|
||||
All responses returned by [`method: APIRequestContext.get`] and similar methods are stored in the memory, so that you can later call [`method: APIResponse.body`].This method discards all its resources, calling any method on disposed [APIRequestContext] will throw an exception.
|
||||
|
||||
### option: APIRequestContext.dispose.reason
|
||||
* since: v1.45
|
||||
- `reason` <[string]>
|
||||
|
||||
The reason to be reported to the operations interrupted by the context disposure.
|
||||
|
||||
## async method: APIRequestContext.fetch
|
||||
* since: v1.16
|
||||
- returns: <[APIResponse]>
|
||||
|
|
|
|||
|
|
@ -133,16 +133,16 @@ System.out.println(browser.contexts().size()); // prints "1"
|
|||
|
||||
```python async
|
||||
browser = await pw.webkit.launch()
|
||||
print(len(browser.contexts())) # prints `0`
|
||||
print(len(browser.contexts)) # prints `0`
|
||||
context = await browser.new_context()
|
||||
print(len(browser.contexts())) # prints `1`
|
||||
print(len(browser.contexts)) # prints `1`
|
||||
```
|
||||
|
||||
```python sync
|
||||
browser = pw.webkit.launch()
|
||||
print(len(browser.contexts())) # prints `0`
|
||||
print(len(browser.contexts)) # prints `0`
|
||||
context = browser.new_context()
|
||||
print(len(browser.contexts())) # prints `1`
|
||||
print(len(browser.contexts)) # prints `1`
|
||||
```
|
||||
|
||||
```csharp
|
||||
|
|
|
|||
|
|
@ -3388,7 +3388,7 @@ Function that should be run once [`param: locator`] appears. This function shoul
|
|||
### param: Page.addLocatorHandler.handler
|
||||
* langs: csharp
|
||||
* since: v1.42
|
||||
- `handler` <[function]\([Locator]\)>
|
||||
- `handler` <[function]\([Locator]\): [Promise<any>]>
|
||||
|
||||
Function that should be run once [`param: locator`] appears. This function should get rid of the element that blocks actions like click.
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ pnpm create playwright
|
|||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
|
||||
|
|
@ -81,10 +82,40 @@ The `tests` folder contains a basic example test to help you get started with te
|
|||
|
||||
By default tests will be run on all 3 browsers, chromium, firefox and webkit using 3 workers. This can be configured in the [playwright.config file](./test-configuration.md). Tests are run in headless mode meaning no browser will open up when running the tests. Results of the tests and test logs will be shown in the terminal.
|
||||
|
||||
<Tabs
|
||||
defaultValue="npm"
|
||||
values={[
|
||||
{label: 'npm', value: 'npm'},
|
||||
{label: 'yarn', value: 'yarn'},
|
||||
{label: 'pnpm', value: 'pnpm'}
|
||||
]
|
||||
}>
|
||||
<TabItem value="npm">
|
||||
|
||||
```bash
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn">
|
||||
|
||||
```bash
|
||||
yarn playwright test
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm">
|
||||
|
||||
```bash
|
||||
pnpm exec playwright test
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||

|
||||
See our doc on [Running Tests](./running-tests.md) to learn more about running tests in headed mode, running multiple tests, running specific tests etc.
|
||||
|
||||
|
|
@ -92,19 +123,81 @@ See our doc on [Running Tests](./running-tests.md) to learn more about running t
|
|||
|
||||
After your test completes, an [HTML Reporter](./test-reporters.md#html-reporter) will be generated, which shows you a full report of your tests allowing you to filter the report by browsers, passed tests, failed tests, skipped tests and flaky tests. You can click on each test and explore the test's errors as well as each step of the test. By default, the HTML report is opened automatically if some of the tests failed.
|
||||
|
||||
<Tabs
|
||||
defaultValue="npm"
|
||||
values={[
|
||||
{label: 'npm', value: 'npm'},
|
||||
{label: 'yarn', value: 'yarn'},
|
||||
{label: 'pnpm', value: 'pnpm'}
|
||||
]
|
||||
}>
|
||||
<TabItem value="npm">
|
||||
|
||||
```bash
|
||||
npx playwright show-report
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn">
|
||||
|
||||
```bash
|
||||
yarn playwright show-report
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm">
|
||||
|
||||
```bash
|
||||
pnpm exec playwright show-report
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||

|
||||
|
||||
## Running the Example Test in UI Mode
|
||||
|
||||
Run your tests with [UI Mode](./test-ui-mode.md) for a better developer experience with time travel debugging, watch mode and more.
|
||||
|
||||
<Tabs
|
||||
defaultValue="npm"
|
||||
values={[
|
||||
{label: 'npm', value: 'npm'},
|
||||
{label: 'yarn', value: 'yarn'},
|
||||
{label: 'pnpm', value: 'pnpm'}
|
||||
]
|
||||
}>
|
||||
|
||||
<TabItem value="npm">
|
||||
|
||||
```bash
|
||||
npx playwright test --ui
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn">
|
||||
|
||||
```bash
|
||||
yarn playwright test --ui
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm">
|
||||
|
||||
```bash
|
||||
pnpm exec playwright test --ui
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||

|
||||
|
||||
Check out or [detailed guide on UI Mode](./test-ui-mode.md) to learn more about its features.
|
||||
|
|
@ -113,17 +206,84 @@ Check out or [detailed guide on UI Mode](./test-ui-mode.md) to learn more about
|
|||
|
||||
To update Playwright to the latest version run the following command:
|
||||
|
||||
<Tabs
|
||||
defaultValue="npm"
|
||||
values={[
|
||||
{label: 'npm', value: 'npm'},
|
||||
{label: 'yarn', value: 'yarn'},
|
||||
{label: 'pnpm', value: 'pnpm'}
|
||||
]
|
||||
}>
|
||||
|
||||
<TabItem value="npm">
|
||||
|
||||
```bash
|
||||
npm install -D @playwright/test@latest
|
||||
# Also download new browser binaries and their dependencies:
|
||||
npx playwright install --with-deps
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn">
|
||||
|
||||
```bash
|
||||
yarn add --dev @playwright/test@latest
|
||||
# Also download new browser binaries and their dependencies:
|
||||
yarn playwright install --with-deps
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm">
|
||||
|
||||
```bash
|
||||
pnpm install --save-dev @playwright/test@latest
|
||||
# Also download new browser binaries and their dependencies:
|
||||
pnpm exec playwright install --with-deps
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
You can always check which version of Playwright you have by running the following command:
|
||||
|
||||
<Tabs
|
||||
defaultValue="npm"
|
||||
values={[
|
||||
{label: 'npm', value: 'npm'},
|
||||
{label: 'yarn', value: 'yarn'},
|
||||
{label: 'pnpm', value: 'pnpm'}
|
||||
]
|
||||
}>
|
||||
|
||||
<TabItem value="npm">
|
||||
|
||||
```bash
|
||||
npx playwright --version
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn">
|
||||
|
||||
```bash
|
||||
yarn playwright --version
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pnpm">
|
||||
|
||||
```bash
|
||||
pnpm exec playwright --version
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
## System requirements
|
||||
|
||||
- Node.js 18+
|
||||
|
|
|
|||
|
|
@ -4,6 +4,82 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.44
|
||||
|
||||
### New APIs
|
||||
|
||||
**Accessibility assertions**
|
||||
|
||||
- [`method: LocatorAssertions.toHaveAccessibleName`] checks if the element has the specified accessible name:
|
||||
```csharp
|
||||
var locator = Page.GetByRole(AriaRole.Button);
|
||||
await Expect(locator).ToHaveAccessibleNameAsync("Submit");
|
||||
```
|
||||
|
||||
- [`method: LocatorAssertions.toHaveAccessibleDescription`] checks if the element has the specified accessible description:
|
||||
```csharp
|
||||
var locator = Page.GetByRole(AriaRole.Button);
|
||||
await Expect(locator).ToHaveAccessibleDescriptionAsync("Upload a photo");
|
||||
```
|
||||
|
||||
- [`method: LocatorAssertions.toHaveRole`] checks if the element has the specified ARIA role:
|
||||
```csharp
|
||||
var locator = Page.GetByTestId("save-button");
|
||||
await Expect(locator).ToHaveRoleAsync(AriaRole.Button);
|
||||
```
|
||||
|
||||
**Locator handler**
|
||||
|
||||
- After executing the handler added with [`method: Page.addLocatorHandler`], Playwright will now wait until the overlay that triggered the handler is not visible anymore. You can opt-out of this behavior with the new `NoWaitAfter` option.
|
||||
- You can use new `Times` option in [`method: Page.addLocatorHandler`] to specify maximum number of times the handler should be run.
|
||||
- The handler in [`method: Page.addLocatorHandler`] now accepts the locator as argument.
|
||||
- New [`method: Page.removeLocatorHandler`] method for removing previously added locator handlers.
|
||||
|
||||
```csharp
|
||||
var locator = Page.GetByText("This interstitial covers the button");
|
||||
await Page.AddLocatorHandlerAsync(locator, async (overlay) =>
|
||||
{
|
||||
await overlay.Locator("#close").ClickAsync();
|
||||
}, new() { Times = 3, NoWaitAfter = true });
|
||||
// Run your tests that can be interrupted by the overlay.
|
||||
// ...
|
||||
await Page.RemoveLocatorHandlerAsync(locator);
|
||||
```
|
||||
|
||||
**Miscellaneous options**
|
||||
|
||||
- New method [`method: FormData.append`] allows to specify repeating fields with the same name in [`Multipart`](./api/class-apirequestcontext#api-request-context-fetch-option-multipart) option in `APIRequestContext.FetchAsync()`:
|
||||
- ```
|
||||
```csharp
|
||||
var formData = Context.APIRequest.CreateFormData();
|
||||
formData.Append("file", new FilePayload()
|
||||
{
|
||||
Name = "f1.js",
|
||||
MimeType = "text/javascript",
|
||||
Buffer = System.Text.Encoding.UTF8.GetBytes("var x = 2024;")
|
||||
});
|
||||
formData.Append("file", new FilePayload()
|
||||
{
|
||||
Name = "f2.txt",
|
||||
MimeType = "text/plain",
|
||||
Buffer = System.Text.Encoding.UTF8.GetBytes("hello")
|
||||
});
|
||||
var response = await Context.APIRequest.PostAsync("https://example.com/uploadFiles", new() { Multipart = formData });
|
||||
```
|
||||
|
||||
- [`method: PageAssertions.toHaveURL`] now supports `IgnoreCase` [option](./api/class-pageassertions#page-assertions-to-have-url-option-ignore-case).
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 125.0.6422.14
|
||||
* Mozilla Firefox 125.0.1
|
||||
* WebKit 17.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 124
|
||||
* Microsoft Edge 124
|
||||
|
||||
## Version 1.43
|
||||
|
||||
### New APIs
|
||||
|
|
|
|||
|
|
@ -4,6 +4,72 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.44
|
||||
|
||||
### New APIs
|
||||
|
||||
**Accessibility assertions**
|
||||
|
||||
- [`method: LocatorAssertions.toHaveAccessibleName`] checks if the element has the specified accessible name:
|
||||
```java
|
||||
Locator locator = page.getByRole(AriaRole.BUTTON);
|
||||
assertThat(locator).hasAccessibleName("Submit");
|
||||
```
|
||||
|
||||
- [`method: LocatorAssertions.toHaveAccessibleDescription`] checks if the element has the specified accessible description:
|
||||
```java
|
||||
Locator locator = page.getByRole(AriaRole.BUTTON);
|
||||
assertThat(locator).hasAccessibleDescription("Upload a photo");
|
||||
```
|
||||
|
||||
- [`method: LocatorAssertions.toHaveRole`] checks if the element has the specified ARIA role:
|
||||
```java
|
||||
Locator locator = page.getByTestId("save-button");
|
||||
assertThat(locator).hasRole(AriaRole.BUTTON);
|
||||
```
|
||||
|
||||
**Locator handler**
|
||||
|
||||
- After executing the handler added with [`method: Page.addLocatorHandler`], Playwright will now wait until the overlay that triggered the handler is not visible anymore. You can opt-out of this behavior with the new `setNoWaitAfter` option.
|
||||
- You can use new `setTimes` option in [`method: Page.addLocatorHandler`] to specify maximum number of times the handler should be run.
|
||||
- The handler in [`method: Page.addLocatorHandler`] now accepts the locator as argument.
|
||||
- New [`method: Page.removeLocatorHandler`] method for removing previously added locator handlers.
|
||||
|
||||
```java
|
||||
Locator locator = page.getByText("This interstitial covers the button");
|
||||
page.addLocatorHandler(locator, overlay -> {
|
||||
overlay.locator("#close").click();
|
||||
}, new Page.AddLocatorHandlerOptions().setTimes(3).setNoWaitAfter(true));
|
||||
// Run your tests that can be interrupted by the overlay.
|
||||
// ...
|
||||
page.removeLocatorHandler(locator);
|
||||
```
|
||||
|
||||
**Miscellaneous options**
|
||||
|
||||
- New method [`method: FormData.append`] allows to specify repeating fields with the same name in [`setMultipart`](./api/class-requestoptions#request-options-set-multipart) option in `RequestOptions`:
|
||||
```java
|
||||
FormData formData = FormData.create();
|
||||
formData.append("file", new FilePayload("f1.js", "text/javascript",
|
||||
"var x = 2024;".getBytes(StandardCharsets.UTF_8)));
|
||||
formData.append("file", new FilePayload("f2.txt", "text/plain",
|
||||
"hello".getBytes(StandardCharsets.UTF_8)));
|
||||
APIResponse response = context.request().post("https://example.com/uploadFile", RequestOptions.create().setMultipart(formData));
|
||||
```
|
||||
|
||||
- `expect(page).toHaveURL(url)` now supports `setIgnoreCase` [option](./api/class-pageassertions#page-assertions-to-have-url-option-ignore-case).
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 125.0.6422.14
|
||||
* Mozilla Firefox 125.0.1
|
||||
* WebKit 17.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 124
|
||||
* Microsoft Edge 124
|
||||
|
||||
## Version 1.43
|
||||
|
||||
### New APIs
|
||||
|
|
|
|||
|
|
@ -4,6 +4,60 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.44
|
||||
|
||||
### New APIs
|
||||
|
||||
**Accessibility assertions**
|
||||
|
||||
- [`method: LocatorAssertions.toHaveAccessibleName`] checks if the element has the specified accessible name:
|
||||
```python
|
||||
locator = page.get_by_role("button")
|
||||
expect(locator).to_have_accessible_name("Submit")
|
||||
```
|
||||
|
||||
- [`method: LocatorAssertions.toHaveAccessibleDescription`] checks if the element has the specified accessible description:
|
||||
```python
|
||||
locator = page.get_by_role("button")
|
||||
expect(locator).to_have_accessible_description("Upload a photo")
|
||||
```
|
||||
|
||||
- [`method: LocatorAssertions.toHaveRole`] checks if the element has the specified ARIA role:
|
||||
```python
|
||||
locator = page.get_by_test_id("save-button")
|
||||
expect(locator).to_have_role("button")
|
||||
```
|
||||
|
||||
**Locator handler**
|
||||
|
||||
- After executing the handler added with [`method: Page.addLocatorHandler`], Playwright will now wait until the overlay that triggered the handler is not visible anymore. You can opt-out of this behavior with the new `no_wait_after` option.
|
||||
- You can use new `times` option in [`method: Page.addLocatorHandler`] to specify maximum number of times the handler should be run.
|
||||
- The handler in [`method: Page.addLocatorHandler`] now accepts the locator as argument.
|
||||
- New [`method: Page.removeLocatorHandler`] method for removing previously added locator handlers.
|
||||
|
||||
```python
|
||||
locator = page.get_by_text("This interstitial covers the button")
|
||||
page.add_locator_handler(locator, lambda overlay: overlay.locator("#close").click(), times=3, no_wait_after=True)
|
||||
# Run your tests that can be interrupted by the overlay.
|
||||
# ...
|
||||
page.remove_locator_handler(locator)
|
||||
```
|
||||
|
||||
**Miscellaneous options**
|
||||
|
||||
- [`method: PageAssertions.toHaveURL`] now supports `ignore_case` [option](./api/class-pageassertions#page-assertions-to-have-url-option-ignore-case).
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 125.0.6422.14
|
||||
* Mozilla Firefox 125.0.1
|
||||
* WebKit 17.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 124
|
||||
* Microsoft Edge 124
|
||||
|
||||
## Version 1.43
|
||||
|
||||
### New APIs
|
||||
|
|
|
|||
|
|
@ -83,6 +83,15 @@ To run a test with a specific title, use the `-g` flag followed by the title of
|
|||
npx playwright test -g "add a todo item"
|
||||
```
|
||||
|
||||
### Run last failed tests
|
||||
|
||||
To run only the tests that failed in the last test run, first run your tests and then run them again with the `--last-failed` flag.
|
||||
|
||||
```bash
|
||||
npx playwright test --last-failed
|
||||
```
|
||||
|
||||
|
||||
### Run tests in VS Code
|
||||
|
||||
Tests can be run right from VS Code using the [VS Code extension](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright). Once installed you can simply click the green triangle next to the test you want to run or run all tests from the testing sidebar. Check out our [Getting Started with VS Code](./getting-started-vscode.md#running-tests) guide for more details.
|
||||
|
|
|
|||
|
|
@ -71,7 +71,8 @@ import { test, expect } from '@playwright/test';
|
|||
test('basic test', {
|
||||
annotation: {
|
||||
type: 'issue',
|
||||
description: 'https://github.com/microsoft/playwright/issues/23180',
|
||||
description: 'feature tags API',
|
||||
url: 'https://github.com/microsoft/playwright/issues/23180'
|
||||
},
|
||||
}, async ({ page }) => {
|
||||
await page.goto('https://playwright.dev/');
|
||||
|
|
@ -97,7 +98,8 @@ Test title.
|
|||
- `tag` ?<[string]|[Array]<[string]>>
|
||||
- `annotation` ?<[Object]|[Array]<[Object]>>
|
||||
- `type` <[string]> Annotation type, for example `'issue'`.
|
||||
- `description` ?<[string]> Optional annotation description, for example an issue url.
|
||||
- `description` ?<[string]> Optional annotation description.
|
||||
- `url` ?<[string]> Optional for example an issue url.
|
||||
|
||||
Additional test details.
|
||||
|
||||
|
|
@ -440,6 +442,7 @@ Group title.
|
|||
- `annotation` ?<[Object]|[Array]<[Object]>>
|
||||
- `type` <[string]>
|
||||
- `description` ?<[string]>
|
||||
- `url` ?<[string]>
|
||||
|
||||
Additional details for all tests in the group.
|
||||
|
||||
|
|
@ -568,6 +571,7 @@ Group title.
|
|||
- `annotation` ?<[Object]|[Array]<[Object]>>
|
||||
- `type` <[string]>
|
||||
- `description` ?<[string]>
|
||||
- `url` ?<[string]>
|
||||
|
||||
See [`method: Test.describe`] for details description.
|
||||
|
||||
|
|
@ -623,6 +627,7 @@ Group title.
|
|||
- `annotation` ?<[Object]|[Array]<[Object]>>
|
||||
- `type` <[string]>
|
||||
- `description` ?<[string]>
|
||||
- `url` ?<[string]>
|
||||
|
||||
See [`method: Test.describe`] for details description.
|
||||
|
||||
|
|
@ -676,6 +681,7 @@ Group title.
|
|||
- `annotation` ?<[Object]|[Array]<[Object]>>
|
||||
- `type` <[string]>
|
||||
- `description` ?<[string]>
|
||||
- `url` ?<[string]>
|
||||
|
||||
See [`method: Test.describe`] for details description.
|
||||
|
||||
|
|
@ -727,6 +733,7 @@ Group title.
|
|||
- `annotation` ?<[Object]|[Array]<[Object]>>
|
||||
- `type` <[string]>
|
||||
- `description` ?<[string]>
|
||||
- `url` ?<[string]>
|
||||
|
||||
See [`method: Test.describe`] for details description.
|
||||
|
||||
|
|
@ -782,6 +789,7 @@ Group title.
|
|||
- `annotation` ?<[Object]|[Array]<[Object]>>
|
||||
- `type` <[string]>
|
||||
- `description` ?<[string]>
|
||||
- `url` ?<[string]>
|
||||
|
||||
See [`method: Test.describe`] for details description.
|
||||
|
||||
|
|
@ -839,6 +847,7 @@ Group title.
|
|||
- `annotation` ?<[Object]|[Array]<[Object]>>
|
||||
- `type` <[string]>
|
||||
- `description` ?<[string]>
|
||||
- `url` ?<[string]>
|
||||
|
||||
See [`method: Test.describe`] for details description.
|
||||
|
||||
|
|
@ -891,6 +900,7 @@ Group title.
|
|||
- `annotation` ?<[Object]|[Array]<[Object]>>
|
||||
- `type` <[string]>
|
||||
- `description` ?<[string]>
|
||||
- `url` ?<[string]>
|
||||
|
||||
See [`method: Test.describe`] for details description.
|
||||
|
||||
|
|
@ -1109,6 +1119,7 @@ Test title.
|
|||
- `annotation` ?<[Object]|[Array]<[Object]>>
|
||||
- `type` <[string]>
|
||||
- `description` ?<[string]>
|
||||
- `url` ?<[string]>
|
||||
|
||||
See [`method: Test.(call)`] for test details description.
|
||||
|
||||
|
|
@ -1214,6 +1225,7 @@ Test title.
|
|||
- `annotation` ?<[Object]|[Array]<[Object]>>
|
||||
- `type` <[string]>
|
||||
- `description` ?<[string]>
|
||||
- `url` ?<[string]>
|
||||
|
||||
See [`method: Test.(call)`] for test details description.
|
||||
|
||||
|
|
@ -1291,6 +1303,7 @@ Test title.
|
|||
- `annotation` ?<[Object]|[Array]<[Object]>>
|
||||
- `type` <[string]>
|
||||
- `description` ?<[string]>
|
||||
- `url` ?<[string]>
|
||||
|
||||
See [`method: Test.(call)`] for test details description.
|
||||
|
||||
|
|
@ -1436,6 +1449,7 @@ Test title.
|
|||
- `annotation` ?<[Object]|[Array]<[Object]>>
|
||||
- `type` <[string]>
|
||||
- `description` ?<[string]>
|
||||
- `url` ?<[string]>
|
||||
|
||||
See [`method: Test.(call)`] for test details description.
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ Complete set of Playwright Test options is available in the [configuration file]
|
|||
| Non-option arguments | Each argument is treated as a regular expression matched against the full test file path. Only tests from the files matching the pattern will be executed. Special symbols like `$` or `*` should be escaped with `\`. In many shells/terminals you may need to quote the arguments. |
|
||||
| `-c <file>` or `--config <file>`| Configuration file. If not passed, defaults to `playwright.config.ts` or `playwright.config.js` in the current directory. |
|
||||
| `--debug`| Run tests with Playwright Inspector. Shortcut for `PWDEBUG=1` environment variable and `--timeout=0 --max-failures=1 --headed --workers=1` options.|
|
||||
| `--fail-on-flaky-tests` | Fails test runs that contain flaky tests. By default flaky tests count as successes. |
|
||||
| `--forbid-only` | Whether to disallow `test.only`. Useful on CI.|
|
||||
| `--global-timeout <number>` | Total timeout for the whole test run in milliseconds. By default, there is no global timeout. Learn more about [various timeouts](./test-timeouts.md).|
|
||||
| `-g <grep>` or `--grep <grep>` | Only run tests matching this regular expression. For example, this will run `'should add to cart'` when passed `-g "add to cart"`. The regular expression will be tested against the string that consists of the test file name, `test.describe` titles if any, test title and all test tags, separated by spaces, e.g. `my-test.spec.ts my-suite my-test @smoke`. The filter does not apply to the tests from dependency projects, i.e. Playwright will still run all tests from [project dependencies](./test-projects.md#dependencies). |
|
||||
|
|
|
|||
|
|
@ -97,6 +97,15 @@ export default defineConfig({
|
|||
});
|
||||
```
|
||||
|
||||
List report supports the following configuration options and environment variables:
|
||||
|
||||
| Environment Variable Name | Reporter Config Option| Description | Default
|
||||
|---|---|---|---|
|
||||
| `PLAYWRIGHT_LIST_PRINT_STEPS` | `printSteps` | Whether to print each step on its own line. | `false`
|
||||
| `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. | `true` when terminal is in TTY mode, `false` otherwise.
|
||||
| `FORCE_COLOR` | | Whether to produce colored output. | `true` when terminal is in TTY mode, `false` otherwise.
|
||||
|
||||
|
||||
### Line reporter
|
||||
|
||||
Line reporter is more concise than the list reporter. It uses a single line to report last finished test, and prints failures when they occur. Line reporter is useful for large test suites where it shows the progress but does not spam the output by listing all the tests.
|
||||
|
|
@ -127,6 +136,14 @@ Running 124 tests using 6 workers
|
|||
[23/124] gitignore.spec.ts - should respect nested .gitignore
|
||||
```
|
||||
|
||||
Line report supports the following configuration options and environment variables:
|
||||
|
||||
| Environment Variable Name | Reporter Config Option| Description | Default
|
||||
|---|---|---|---|
|
||||
| `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. | `true` when terminal is in TTY mode, `false` otherwise.
|
||||
| `FORCE_COLOR` | | Whether to produce colored output. | `true` when terminal is in TTY mode, `false` otherwise.
|
||||
|
||||
|
||||
### Dot reporter
|
||||
|
||||
Dot reporter is very concise - it only produces a single character per successful test run. It is the default on CI and useful where you don't want a lot of output.
|
||||
|
|
@ -150,6 +167,24 @@ Running 124 tests using 6 workers
|
|||
······F·············································
|
||||
```
|
||||
|
||||
One character is displayed for each test that has run, indicating its status:
|
||||
|
||||
| Character | Description
|
||||
|---|---|
|
||||
| `·` | Passed
|
||||
| `F` | Failed
|
||||
| `×` | Failed or timed out - and will be retried
|
||||
| `±` | Passed on retry (flaky)
|
||||
| `T` | Timed out
|
||||
| `°` | Skipped
|
||||
|
||||
Dot report supports the following configuration options and environment variables:
|
||||
|
||||
| Environment Variable Name | Reporter Config Option| Description | Default
|
||||
|---|---|---|---|
|
||||
| `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. | `true` when terminal is in TTY mode, `false` otherwise.
|
||||
| `FORCE_COLOR` | | Whether to produce colored output. | `true` when terminal is in TTY mode, `false` otherwise.
|
||||
|
||||
### HTML reporter
|
||||
|
||||
HTML reporter produces a self-contained folder that contains report for the test run that can be served as a web page.
|
||||
|
|
@ -159,7 +194,7 @@ npx playwright test --reporter=html
|
|||
```
|
||||
|
||||
By default, HTML report is opened automatically if some of the tests failed. You can control this behavior via the
|
||||
`open` property in the Playwright config or the `PW_TEST_HTML_REPORT_OPEN` environmental variable. The possible values for that property are `always`, `never` and `on-failure`
|
||||
`open` property in the Playwright config or the `PLAYWRIGHT_HTML_OPEN` environmental variable. The possible values for that property are `always`, `never` and `on-failure`
|
||||
(default).
|
||||
|
||||
You can also configure `host` and `port` that are used to serve the HTML report.
|
||||
|
|
@ -173,7 +208,7 @@ export default defineConfig({
|
|||
```
|
||||
|
||||
By default, report is written into the `playwright-report` folder in the current working directory. One can override
|
||||
that location using the `PLAYWRIGHT_HTML_REPORT` environment variable or a reporter configuration.
|
||||
that location using the `PLAYWRIGHT_HTML_OUTPUT_DIR` environment variable or a reporter configuration.
|
||||
|
||||
In configuration file, pass options directly:
|
||||
|
||||
|
|
@ -207,6 +242,16 @@ Or if there is a custom folder name:
|
|||
npx playwright show-report my-report
|
||||
```
|
||||
|
||||
HTML report supports the following configuration options and environment variables:
|
||||
|
||||
| Environment Variable Name | Reporter Config Option| Description | Default
|
||||
|---|---|---|---|
|
||||
| `PLAYWRIGHT_HTML_OUTPUT_DIR` | `outputFolder` | Directory to save the report to. | `playwright-report`
|
||||
| `PLAYWRIGHT_HTML_OPEN` | `open` | When to open the html report in the browser, one of `'always'`, `'never'` or `'on-failure'` | `'on-failure'`
|
||||
| `PLAYWRIGHT_HTML_HOST` | `host` | When report opens in the browser, it will be served bound to this hostname. | `localhost`
|
||||
| `PLAYWRIGHT_HTML_PORT` | `port` | When report opens in the browser, it will be served on this port. | `9323` or any available port when `9323` is not available.
|
||||
| `PLAYWRIGHT_HTML_ATTACHMENTS_BASE_URL` | `attachmentsBaseURL` | A separate location where attachments from the `data` subdirectory are uploaded. Only needed when you upload report and `data` separately to different locations. | `data/`
|
||||
|
||||
### Blob reporter
|
||||
|
||||
Blob reports contain all the details about the test run and can be used later to produce any other report. Their primary function is to facilitate the merging of reports from [sharded tests](./test-sharding.md).
|
||||
|
|
@ -308,8 +353,8 @@ JUnit report supports following configuration options and environment variables:
|
|||
| `PLAYWRIGHT_JUNIT_OUTPUT_DIR` | | Directory to save the output file. Ignored if output file is not specified. | `cwd` or config directory.
|
||||
| `PLAYWRIGHT_JUNIT_OUTPUT_NAME` | `outputFile` | Base file name for the output, relative to the output dir. | JUnit report is printed to the stdout.
|
||||
| `PLAYWRIGHT_JUNIT_OUTPUT_FILE` | `outputFile` | Full path to the output file. If defined, `PLAYWRIGHT_JUNIT_OUTPUT_DIR` and `PLAYWRIGHT_JUNIT_OUTPUT_NAME` will be ignored. | JUnit report is printed to the stdout.
|
||||
| | `stripANSIControlSequences` | Whether to remove ANSI control sequences from the text before writing it in the report. | By default output text is added as is.
|
||||
| | `includeProjectInTestName` | Whether to include Playwright project name in every test case as a name prefix. | By default not included.
|
||||
| `PLAYWRIGHT_JUNIT_STRIP_ANSI` | `stripANSIControlSequences` | Whether to remove ANSI control sequences from the text before writing it in the report. | By default output text is added as is.
|
||||
| `PLAYWRIGHT_JUNIT_INCLUDE_PROJECT_IN_TEST_NAME` | `includeProjectInTestName` | Whether to include Playwright project name in every test case as a name prefix. | By default not included.
|
||||
| `PLAYWRIGHT_JUNIT_SUITE_ID` | | Value of the `id` attribute on the root `<testsuites/>` report entry. | Empty string.
|
||||
| `PLAYWRIGHT_JUNIT_SUITE_NAME` | | Value of the `name` attribute on the root `<testsuites/>` report entry. | Empty string.
|
||||
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ const testCase: TestCase = {
|
|||
projectName: 'chromium',
|
||||
location: { file: 'test.spec.ts', line: 42, column: 0 },
|
||||
annotations: [
|
||||
{ type: 'annotation', description: 'Annotation text' },
|
||||
{ type: 'annotation', description: 'Another annotation text' },
|
||||
{ type: 'annotation', description: 'Annotation text', url: 'example url' },
|
||||
{ type: 'annotation', description: 'Another annotation text', url: 'Another example url' },
|
||||
],
|
||||
tags: [],
|
||||
outcome: 'expected',
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export type TestFileSummary = {
|
|||
stats: Stats;
|
||||
};
|
||||
|
||||
export type TestCaseAnnotation = { type: string, description?: string };
|
||||
export type TestCaseAnnotation = { type: string, description?: string, url?: string};
|
||||
|
||||
export type TestCaseSummary = {
|
||||
testId: string,
|
||||
|
|
|
|||
|
|
@ -3,37 +3,39 @@
|
|||
"browsers": [
|
||||
{
|
||||
"name": "chromium",
|
||||
"revision": "1118",
|
||||
"revision": "1119",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "125.0.6422.41"
|
||||
"browserVersion": "126.0.6478.8"
|
||||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree",
|
||||
"revision": "1220",
|
||||
"revision": "1222",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "126.0.6467.2"
|
||||
"browserVersion": "126.0.6478.8"
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
"revision": "1449",
|
||||
"revision": "1450",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "125.0.1"
|
||||
"browserVersion": "126.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox-beta",
|
||||
"revision": "1449",
|
||||
"revision": "1450",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "126.0b1"
|
||||
"browserVersion": "127.0b3"
|
||||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2005",
|
||||
"revision": "2010",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"mac10.14": "1446",
|
||||
"mac10.15": "1616",
|
||||
"mac11": "1816",
|
||||
"mac11-arm64": "1816"
|
||||
"mac11-arm64": "1816",
|
||||
"mac12": "2009",
|
||||
"mac12-arm64": "2009"
|
||||
},
|
||||
"browserVersion": "17.4"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -143,12 +143,16 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
if (validator) {
|
||||
return async (params: any) => {
|
||||
return await this._wrapApiCall(async apiZone => {
|
||||
const { apiName, frames, csi, callCookie, wallTime } = apiZone.reported ? { apiName: undefined, csi: undefined, callCookie: undefined, frames: [], wallTime: undefined } : apiZone;
|
||||
const { apiName, frames, csi, callCookie, stepId } = apiZone.reported ? { apiName: undefined, csi: undefined, callCookie: undefined, frames: [], stepId: undefined } : apiZone;
|
||||
apiZone.reported = true;
|
||||
if (csi && apiName)
|
||||
let currentStepId = stepId;
|
||||
if (csi && apiName) {
|
||||
const out: { stepId?: string } = {};
|
||||
// this is meant to be awaited so all async hooks can be completed before the actual command gets dispatched
|
||||
await csi.onApiCallBegin(apiName, params, frames, wallTime, callCookie);
|
||||
return await this._connection.sendMessageToServer(this, prop, validator(params, '', { tChannelImpl: tChannelImplToWire, binary: this._connection.rawBuffers() ? 'buffer' : 'toBase64' }), apiName, frames, wallTime);
|
||||
await csi.onApiCallBegin(apiName, params, frames, callCookie, out);
|
||||
currentStepId = out.stepId;
|
||||
}
|
||||
return await this._connection.sendMessageToServer(this, prop, validator(params, '', { tChannelImpl: tChannelImplToWire, binary: this._connection.rawBuffers() ? 'buffer' : 'toBase64' }), apiName, frames, currentStepId);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
@ -176,7 +180,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
|
||||
// Enclosing zone could have provided the apiName and wallTime.
|
||||
const expectZone = zones.zoneData<ExpectZone>('expectZone');
|
||||
const wallTime = expectZone ? expectZone.wallTime : Date.now();
|
||||
const stepId = expectZone?.stepId;
|
||||
if (!isInternal && expectZone)
|
||||
apiName = expectZone.title;
|
||||
|
||||
|
|
@ -187,7 +191,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
|
||||
try {
|
||||
logApiCall(logger, `=> ${apiName} started`, isInternal);
|
||||
const apiZone: ApiZone = { apiName, frames, isInternal, reported: false, csi, callCookie, wallTime };
|
||||
const apiZone: ApiZone = { apiName, frames, isInternal, reported: false, csi, callCookie, stepId };
|
||||
const result = await zones.run('apiZone', apiZone, async () => await func(apiZone));
|
||||
await csi?.onApiCallEnd(callCookie);
|
||||
logApiCall(logger, `<= ${apiName} succeeded`, isInternal);
|
||||
|
|
@ -244,5 +248,5 @@ type ApiZone = {
|
|||
reported: boolean;
|
||||
csi: ClientInstrumentation | undefined;
|
||||
callCookie: any;
|
||||
wallTime: number;
|
||||
stepId?: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export interface ClientInstrumentation {
|
|||
addListener(listener: ClientInstrumentationListener): void;
|
||||
removeListener(listener: ClientInstrumentationListener): void;
|
||||
removeAllListeners(): void;
|
||||
onApiCallBegin(apiCall: string, params: Record<string, any>, frames: StackFrame[], wallTime: number, userData: any): void;
|
||||
onApiCallBegin(apiCall: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }): void;
|
||||
onApiCallEnd(userData: any, error?: Error): void;
|
||||
onDidCreateBrowserContext(context: BrowserContext): Promise<void>;
|
||||
onDidCreateRequestContext(context: APIRequestContext): Promise<void>;
|
||||
|
|
@ -32,7 +32,7 @@ export interface ClientInstrumentation {
|
|||
}
|
||||
|
||||
export interface ClientInstrumentationListener {
|
||||
onApiCallBegin?(apiName: string, params: Record<string, any>, frames: StackFrame[], wallTime: number, userData: any): void;
|
||||
onApiCallBegin?(apiName: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }): void;
|
||||
onApiCallEnd?(userData: any, error?: Error): void;
|
||||
onDidCreateBrowserContext?(context: BrowserContext): Promise<void>;
|
||||
onDidCreateRequestContext?(context: APIRequestContext): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ export class Connection extends EventEmitter {
|
|||
this._tracingCount--;
|
||||
}
|
||||
|
||||
async sendMessageToServer(object: ChannelOwner, method: string, params: any, apiName: string | undefined, frames: channels.StackFrame[], wallTime: number | undefined): Promise<any> {
|
||||
async sendMessageToServer(object: ChannelOwner, method: string, params: any, apiName: string | undefined, frames: channels.StackFrame[], stepId?: string): Promise<any> {
|
||||
if (this._closedError)
|
||||
throw this._closedError;
|
||||
if (object._wasCollected)
|
||||
|
|
@ -133,7 +133,7 @@ export class Connection extends EventEmitter {
|
|||
debugLogger.log('channel', 'SEND> ' + JSON.stringify(message));
|
||||
}
|
||||
const location = frames[0] ? { file: frames[0].file, line: frames[0].line, column: frames[0].column } : undefined;
|
||||
const metadata: channels.Metadata = { wallTime, apiName, location, internal: !apiName };
|
||||
const metadata: channels.Metadata = { apiName, location, internal: !apiName, stepId };
|
||||
if (this._tracingCount && frames && type !== 'LocalUtils')
|
||||
this._localUtils?._channel.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {});
|
||||
// We need to exit zones before calling into the server, otherwise
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ export class APIRequest implements api.APIRequest {
|
|||
export class APIRequestContext extends ChannelOwner<channels.APIRequestContextChannel> implements api.APIRequestContext {
|
||||
_request?: APIRequest;
|
||||
readonly _tracing: Tracing;
|
||||
private _closeReason: string | undefined;
|
||||
|
||||
static from(channel: channels.APIRequestContextChannel): APIRequestContext {
|
||||
return (channel as any)._object;
|
||||
|
|
@ -99,9 +100,10 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
await this.dispose();
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
async dispose(options: { reason?: string } = {}): Promise<void> {
|
||||
this._closeReason = options.reason;
|
||||
await this._instrumentation.onWillCloseRequestContext(this);
|
||||
await this._channel.dispose();
|
||||
await this._channel.dispose(options);
|
||||
this._tracing._resetStackCounter();
|
||||
this._request?._contexts.delete(this);
|
||||
}
|
||||
|
|
@ -156,6 +158,8 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
|
||||
async _innerFetch(options: FetchOptions & { url?: string, request?: api.Request } = {}): Promise<APIResponse> {
|
||||
return await this._wrapApiCall(async () => {
|
||||
if (this._closeReason)
|
||||
throw new Error(this._closeReason);
|
||||
assert(options.request || typeof options.url === 'string', 'First argument must be either URL string or Request');
|
||||
assert((options.data === undefined ? 0 : 1) + (options.form === undefined ? 0 : 1) + (options.multipart === undefined ? 0 : 1) <= 1, `Only one of 'data', 'form' or 'multipart' can be specified`);
|
||||
assert(options.maxRedirects === undefined || options.maxRedirects >= 0, `'maxRedirects' should be greater than or equal to '0'`);
|
||||
|
|
|
|||
|
|
@ -101,7 +101,6 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
|
|||
if (this._redirectedFrom)
|
||||
this._redirectedFrom._redirectedTo = this;
|
||||
this._provisionalHeaders = new RawHeaders(initializer.headers);
|
||||
this._fallbackOverrides.postDataBuffer = initializer.postData;
|
||||
this._timing = {
|
||||
startTime: 0,
|
||||
domainLookupStart: -1,
|
||||
|
|
@ -128,11 +127,11 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
|
|||
}
|
||||
|
||||
postData(): string | null {
|
||||
return this._fallbackOverrides.postDataBuffer?.toString('utf-8') || null;
|
||||
return (this._fallbackOverrides.postDataBuffer || this._initializer.postData)?.toString('utf-8') || null;
|
||||
}
|
||||
|
||||
postDataBuffer(): Buffer | null {
|
||||
return this._fallbackOverrides.postDataBuffer || null;
|
||||
return this._fallbackOverrides.postDataBuffer || this._initializer.postData || null;
|
||||
}
|
||||
|
||||
postDataJSON(): Object | null {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ scheme.Metadata = tObject({
|
|||
})),
|
||||
apiName: tOptional(tString),
|
||||
internal: tOptional(tBoolean),
|
||||
wallTime: tOptional(tNumber),
|
||||
stepId: tOptional(tString),
|
||||
});
|
||||
scheme.ClientSideCallMetadata = tObject({
|
||||
id: tNumber,
|
||||
|
|
@ -207,7 +207,9 @@ scheme.APIRequestContextDisposeAPIResponseParams = tObject({
|
|||
fetchUid: tString,
|
||||
});
|
||||
scheme.APIRequestContextDisposeAPIResponseResult = tOptional(tObject({}));
|
||||
scheme.APIRequestContextDisposeParams = tOptional(tObject({}));
|
||||
scheme.APIRequestContextDisposeParams = tObject({
|
||||
reason: tOptional(tString),
|
||||
});
|
||||
scheme.APIRequestContextDisposeResult = tOptional(tObject({}));
|
||||
scheme.APIResponse = tObject({
|
||||
fetchUid: tString,
|
||||
|
|
|
|||
|
|
@ -317,7 +317,7 @@ export class CRNetworkManager {
|
|||
requestPausedSessionInfo!.session._sendMayFail('Fetch.fulfillRequest', {
|
||||
requestId: requestPausedEvent.requestId,
|
||||
responseCode: 204,
|
||||
responsePhrase: network.STATUS_TEXTS['204'],
|
||||
responsePhrase: network.statusText(204),
|
||||
responseHeaders,
|
||||
body: '',
|
||||
});
|
||||
|
|
@ -376,6 +376,10 @@ export class CRNetworkManager {
|
|||
if (response.body || !expectedLength)
|
||||
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
|
||||
|
||||
// Make sure no network requests sent while reading the body for fulfilled requests.
|
||||
if (request._route?._fulfilled)
|
||||
return Buffer.from('');
|
||||
|
||||
// For <link prefetch we are going to receive empty body with non-empty content-length expectation. Reach out for the actual content.
|
||||
const resource = await session.send('Network.loadNetworkResource', { url: request.request.url(), frameId: this._serviceWorker ? undefined : request.request.frame()!._id, options: { disableCache: false, includeCredentials: true } });
|
||||
const chunks: Buffer[] = [];
|
||||
|
|
@ -595,6 +599,7 @@ class RouteImpl implements network.RouteDelegate {
|
|||
private readonly _session: CRSession;
|
||||
private _interceptionId: string;
|
||||
_alreadyContinuedParams: Protocol.Fetch.continueRequestParameters | undefined;
|
||||
_fulfilled: boolean = false;
|
||||
|
||||
constructor(session: CRSession, interceptionId: string) {
|
||||
this._session = session;
|
||||
|
|
@ -615,6 +620,7 @@ class RouteImpl implements network.RouteDelegate {
|
|||
}
|
||||
|
||||
async fulfill(response: types.NormalizedFulfillResponse) {
|
||||
this._fulfilled = true;
|
||||
const body = response.isBase64 ? response.body : Buffer.from(response.body).toString('base64');
|
||||
|
||||
const responseHeaders = splitSetCookieHeader(response.headers);
|
||||
|
|
@ -622,7 +628,7 @@ class RouteImpl implements network.RouteDelegate {
|
|||
await this._session.send('Fetch.fulfillRequest', {
|
||||
requestId: this._interceptionId!,
|
||||
responseCode: response.status,
|
||||
responsePhrase: network.STATUS_TEXTS[String(response.status)],
|
||||
responsePhrase: network.statusText(response.status),
|
||||
responseHeaders,
|
||||
body,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -841,6 +841,7 @@ CORS RFC1918 enforcement.
|
|||
clientSecurityState?: Network.ClientSecurityState;
|
||||
}
|
||||
export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation"|"InvalidInfoHeader"|"NoRegisterSourceHeader"|"NoRegisterTriggerHeader"|"NoRegisterOsSourceHeader"|"NoRegisterOsTriggerHeader";
|
||||
export type SharedDictionaryError = "UseErrorCrossOriginNoCorsRequest"|"UseErrorDictionaryLoadFailure"|"UseErrorMatchingDictionaryNotUsed"|"UseErrorUnexpectedContentDictionaryHeader"|"WriteErrorCossOriginNoCorsRequest"|"WriteErrorDisallowedBySettings"|"WriteErrorExpiredResponse"|"WriteErrorFeatureDisabled"|"WriteErrorInsufficientResources"|"WriteErrorInvalidMatchField"|"WriteErrorInvalidStructuredHeader"|"WriteErrorNavigationRequest"|"WriteErrorNoMatchField"|"WriteErrorNonListMatchDestField"|"WriteErrorNonSecureContext"|"WriteErrorNonStringIdField"|"WriteErrorNonStringInMatchDestList"|"WriteErrorNonStringMatchField"|"WriteErrorNonTokenTypeField"|"WriteErrorRequestAborted"|"WriteErrorShuttingDown"|"WriteErrorTooLongIdField"|"WriteErrorUnsupportedType";
|
||||
/**
|
||||
* Details for issues around "Attribution Reporting API" usage.
|
||||
Explainer: https://github.com/WICG/attribution-reporting-api
|
||||
|
|
@ -870,6 +871,10 @@ instead of "limited-quirks".
|
|||
url: string;
|
||||
location?: SourceCodeLocation;
|
||||
}
|
||||
export interface SharedDictionaryIssueDetails {
|
||||
sharedDictionaryError: SharedDictionaryError;
|
||||
request: AffectedRequest;
|
||||
}
|
||||
export type GenericIssueErrorType = "CrossOriginPortalPostMessageError"|"FormLabelForNameError"|"FormDuplicateIdForInputError"|"FormInputWithNoLabelError"|"FormAutocompleteAttributeEmptyError"|"FormEmptyIdAndNameAttributesForInputError"|"FormAriaLabelledByToNonExistingId"|"FormInputAssignedAutocompleteValueToIdOrNameAttributeError"|"FormLabelHasNeitherForNorNestedInput"|"FormLabelForMatchesNonExistingIdError"|"FormInputHasWrongButWellIntendedAutocompleteValueError"|"ResponseWasBlockedByORB";
|
||||
/**
|
||||
* Depending on the concrete errorType, different properties are set.
|
||||
|
|
@ -997,7 +1002,7 @@ registrations being ignored.
|
|||
optional fields in InspectorIssueDetails to convey more specific
|
||||
information about the kind of issue.
|
||||
*/
|
||||
export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue";
|
||||
export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue";
|
||||
/**
|
||||
* This struct holds a list of optional fields with additional information
|
||||
specific to the kind of issue. When adding a new issue code, please also
|
||||
|
|
@ -1024,6 +1029,7 @@ add a new optional field to this type.
|
|||
stylesheetLoadingIssueDetails?: StylesheetLoadingIssueDetails;
|
||||
propertyRuleIssueDetails?: PropertyRuleIssueDetails;
|
||||
federatedAuthUserInfoRequestIssueDetails?: FederatedAuthUserInfoRequestIssueDetails;
|
||||
sharedDictionaryIssueDetails?: SharedDictionaryIssueDetails;
|
||||
}
|
||||
/**
|
||||
* A unique id for a DevTools inspector issue. Allows other entities (e.g.
|
||||
|
|
@ -1121,6 +1127,33 @@ using Audits.issueAdded event.
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines commands and events for browser extensions. Available if the client
|
||||
is connected using the --remote-debugging-pipe flag and
|
||||
the --enable-unsafe-extension-debugging flag is set.
|
||||
*/
|
||||
export module Extensions {
|
||||
|
||||
|
||||
/**
|
||||
* Installs an unpacked extension from the filesystem similar to
|
||||
--load-extension CLI flags. Returns extension ID once the extension
|
||||
has been installed.
|
||||
*/
|
||||
export type loadUnpackedParameters = {
|
||||
/**
|
||||
* Absolute file path.
|
||||
*/
|
||||
path: string;
|
||||
}
|
||||
export type loadUnpackedReturnValue = {
|
||||
/**
|
||||
* Extension id.
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines commands and events for Autofill.
|
||||
*/
|
||||
|
|
@ -3450,7 +3483,7 @@ front-end.
|
|||
/**
|
||||
* Pseudo element type.
|
||||
*/
|
||||
export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-markers"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new";
|
||||
export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-markers"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new";
|
||||
/**
|
||||
* Shadow root type.
|
||||
*/
|
||||
|
|
@ -4426,6 +4459,25 @@ appear on top of all other content.
|
|||
*/
|
||||
nodeIds: NodeId[];
|
||||
}
|
||||
/**
|
||||
* Returns the NodeId of the matched element according to certain relations.
|
||||
*/
|
||||
export type getElementByRelationParameters = {
|
||||
/**
|
||||
* Id of the node from which to query the relation.
|
||||
*/
|
||||
nodeId: NodeId;
|
||||
/**
|
||||
* Type of relation to get.
|
||||
*/
|
||||
relation: "PopoverTarget";
|
||||
}
|
||||
export type getElementByRelationReturnValue = {
|
||||
/**
|
||||
* NodeId of the element matching the queried relation.
|
||||
*/
|
||||
nodeId: NodeId;
|
||||
}
|
||||
/**
|
||||
* Re-does the last undone action.
|
||||
*/
|
||||
|
|
@ -8309,8 +8361,16 @@ records.
|
|||
*/
|
||||
export type ServiceWorkerRouterSource = "network"|"cache"|"fetch-event"|"race-network-and-fetch-handler";
|
||||
export interface ServiceWorkerRouterInfo {
|
||||
ruleIdMatched: number;
|
||||
matchedSourceType: ServiceWorkerRouterSource;
|
||||
/**
|
||||
* ID of the rule matched. If there is a matched rule, this field will
|
||||
be set, otherwiser no value will be set.
|
||||
*/
|
||||
ruleIdMatched?: number;
|
||||
/**
|
||||
* The router source of the matched rule. If there is a matched rule, this
|
||||
field will be set, otherwise no value will be set.
|
||||
*/
|
||||
matchedSourceType?: ServiceWorkerRouterSource;
|
||||
}
|
||||
/**
|
||||
* HTTP response data.
|
||||
|
|
@ -8385,7 +8445,10 @@ records.
|
|||
*/
|
||||
fromEarlyHints?: boolean;
|
||||
/**
|
||||
* Information about how Service Worker Static Router was used.
|
||||
* Information about how ServiceWorker Static Router API was used. If this
|
||||
field is set with `matchedSourceType` field, a matching rule is found.
|
||||
If this field is set without `matchedSource`, no matching rule is found.
|
||||
Otherwise, the API is not used.
|
||||
*/
|
||||
serviceWorkerRouterInfo?: ServiceWorkerRouterInfo;
|
||||
/**
|
||||
|
|
@ -11167,7 +11230,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-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";
|
||||
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"|"xr-spatial-tracking";
|
||||
/**
|
||||
* Reason for a permissions policy feature to be disabled.
|
||||
*/
|
||||
|
|
@ -11755,7 +11818,7 @@ https://github.com/WICG/manifest-incubations/blob/gh-pages/scope_extensions-expl
|
|||
/**
|
||||
* 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"|"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";
|
||||
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"|"RequestedByWebViewClient";
|
||||
/**
|
||||
* Types of not restored reasons for back-forward cache.
|
||||
*/
|
||||
|
|
@ -12334,7 +12397,7 @@ option, use with caution.
|
|||
This API always waits for the manifest to be loaded.
|
||||
If manifestId is provided, and it does not match the manifest of the
|
||||
current document, this API errors out.
|
||||
If there isn’t a loaded page, this API errors out immediately.
|
||||
If there is not a loaded page, this API errors out immediately.
|
||||
*/
|
||||
export type getAppManifestParameters = {
|
||||
manifestId?: string;
|
||||
|
|
@ -16690,7 +16753,7 @@ possible for multiple rule sets and links to trigger a single attempt.
|
|||
/**
|
||||
* List of FinalStatus reasons for Prerender2.
|
||||
*/
|
||||
export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl";
|
||||
export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled";
|
||||
/**
|
||||
* Preloading status values, see also PreloadingTriggeringOutcome. This
|
||||
status is shared by prefetchStatusUpdated and prerenderStatusUpdated.
|
||||
|
|
@ -16921,6 +16984,52 @@ https://web.dev/learn/pwa/web-app-manifest.
|
|||
badgeCount: number;
|
||||
fileHandlers: FileHandler[];
|
||||
}
|
||||
/**
|
||||
* Installs the given manifest identity, optionally using the given install_url
|
||||
or IWA bundle location.
|
||||
|
||||
TODO(crbug.com/337872319) Support IWA to meet the following specific
|
||||
requirement.
|
||||
IWA-specific install description: If the manifest_id is isolated-app://,
|
||||
install_url_or_bundle_url is required, and can be either an http(s) URL or
|
||||
file:// URL pointing to a signed web bundle (.swbn). The .swbn file's
|
||||
signing key must correspond to manifest_id. If Chrome is not in IWA dev
|
||||
mode, the installation will fail, regardless of the state of the allowlist.
|
||||
*/
|
||||
export type installParameters = {
|
||||
manifestId: string;
|
||||
/**
|
||||
* The location of the app or bundle overriding the one derived from the
|
||||
manifestId.
|
||||
*/
|
||||
installUrlOrBundleUrl?: string;
|
||||
}
|
||||
export type installReturnValue = {
|
||||
}
|
||||
/**
|
||||
* Uninstals the given manifest_id and closes any opened app windows.
|
||||
*/
|
||||
export type uninstallParameters = {
|
||||
manifestId: string;
|
||||
}
|
||||
export type uninstallReturnValue = {
|
||||
}
|
||||
/**
|
||||
* Launches the installed web app, or an url in the same web app instead of the
|
||||
default start url if it is provided. Returns a tab / web contents based
|
||||
Target.TargetID which can be used to attach to via Target.attachToTarget or
|
||||
similar APIs.
|
||||
*/
|
||||
export type launchParameters = {
|
||||
manifestId: string;
|
||||
url?: string;
|
||||
}
|
||||
export type launchReturnValue = {
|
||||
/**
|
||||
* ID of the tab target created as a result.
|
||||
*/
|
||||
targetId: Target.TargetID;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -19773,6 +19882,7 @@ Error was thrown.
|
|||
"Audits.enable": Audits.enableParameters;
|
||||
"Audits.checkContrast": Audits.checkContrastParameters;
|
||||
"Audits.checkFormsIssues": Audits.checkFormsIssuesParameters;
|
||||
"Extensions.loadUnpacked": Extensions.loadUnpackedParameters;
|
||||
"Autofill.trigger": Autofill.triggerParameters;
|
||||
"Autofill.setAddresses": Autofill.setAddressesParameters;
|
||||
"Autofill.disable": Autofill.disableParameters;
|
||||
|
|
@ -19870,6 +19980,7 @@ Error was thrown.
|
|||
"DOM.querySelector": DOM.querySelectorParameters;
|
||||
"DOM.querySelectorAll": DOM.querySelectorAllParameters;
|
||||
"DOM.getTopLayerElements": DOM.getTopLayerElementsParameters;
|
||||
"DOM.getElementByRelation": DOM.getElementByRelationParameters;
|
||||
"DOM.redo": DOM.redoParameters;
|
||||
"DOM.removeAttribute": DOM.removeAttributeParameters;
|
||||
"DOM.removeNode": DOM.removeNodeParameters;
|
||||
|
|
@ -20256,6 +20367,9 @@ Error was thrown.
|
|||
"FedCm.dismissDialog": FedCm.dismissDialogParameters;
|
||||
"FedCm.resetCooldown": FedCm.resetCooldownParameters;
|
||||
"PWA.getOsAppState": PWA.getOsAppStateParameters;
|
||||
"PWA.install": PWA.installParameters;
|
||||
"PWA.uninstall": PWA.uninstallParameters;
|
||||
"PWA.launch": PWA.launchParameters;
|
||||
"Console.clearMessages": Console.clearMessagesParameters;
|
||||
"Console.disable": Console.disableParameters;
|
||||
"Console.enable": Console.enableParameters;
|
||||
|
|
@ -20361,6 +20475,7 @@ Error was thrown.
|
|||
"Audits.enable": Audits.enableReturnValue;
|
||||
"Audits.checkContrast": Audits.checkContrastReturnValue;
|
||||
"Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue;
|
||||
"Extensions.loadUnpacked": Extensions.loadUnpackedReturnValue;
|
||||
"Autofill.trigger": Autofill.triggerReturnValue;
|
||||
"Autofill.setAddresses": Autofill.setAddressesReturnValue;
|
||||
"Autofill.disable": Autofill.disableReturnValue;
|
||||
|
|
@ -20458,6 +20573,7 @@ Error was thrown.
|
|||
"DOM.querySelector": DOM.querySelectorReturnValue;
|
||||
"DOM.querySelectorAll": DOM.querySelectorAllReturnValue;
|
||||
"DOM.getTopLayerElements": DOM.getTopLayerElementsReturnValue;
|
||||
"DOM.getElementByRelation": DOM.getElementByRelationReturnValue;
|
||||
"DOM.redo": DOM.redoReturnValue;
|
||||
"DOM.removeAttribute": DOM.removeAttributeReturnValue;
|
||||
"DOM.removeNode": DOM.removeNodeReturnValue;
|
||||
|
|
@ -20844,6 +20960,9 @@ Error was thrown.
|
|||
"FedCm.dismissDialog": FedCm.dismissDialogReturnValue;
|
||||
"FedCm.resetCooldown": FedCm.resetCooldownReturnValue;
|
||||
"PWA.getOsAppState": PWA.getOsAppStateReturnValue;
|
||||
"PWA.install": PWA.installReturnValue;
|
||||
"PWA.uninstall": PWA.uninstallReturnValue;
|
||||
"PWA.launch": PWA.launchReturnValue;
|
||||
"Console.clearMessages": Console.clearMessagesReturnValue;
|
||||
"Console.disable": Console.disableReturnValue;
|
||||
"Console.enable": Console.enableReturnValue;
|
||||
|
|
|
|||
|
|
@ -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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 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/126.0.6478.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/125.0.6422.41 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/126.0.6478.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/125.0.6422.41 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/126.0.6478.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/125.0.6422.41 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/126.0.6478.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/125.0.6422.41 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/126.0.6478.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/125.0.6422.41 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/126.0.6478.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/125.0.6422.41 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 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/126.0.6478.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/125.0.6422.41 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/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 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/126.0.6478.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/125.0.6422.41 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/126.0.6478.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/125.0.6422.41 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 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/126.0.6478.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/125.0.6422.41 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/126.0.6478.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/125.0.6422.41 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/126.0.6478.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/125.0.6422.41 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/126.0.6478.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/125.0.6422.41 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/126.0.6478.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/125.0.6422.41 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/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Safari/537.36 Edg/125.0.6422.41",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Safari/537.36 Edg/126.0.6478.8",
|
||||
"screen": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1472,7 +1472,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Firefox HiDPI": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0.1) Gecko/20100101 Firefox/125.0.1",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0",
|
||||
"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/125.0.6422.41 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.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/125.0.6422.41 Safari/537.36 Edg/125.0.6422.41",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.8 Safari/537.36 Edg/126.0.6478.8",
|
||||
"screen": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
@ -1532,7 +1532,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Firefox": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0.1) Gecko/20100101 Firefox/125.0.1",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0",
|
||||
"screen": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
|
|||
|
|
@ -286,10 +286,10 @@ export class DispatcherConnection {
|
|||
const sdkObject = dispatcher._object instanceof SdkObject ? dispatcher._object : undefined;
|
||||
const callMetadata: CallMetadata = {
|
||||
id: `call@${id}`,
|
||||
wallTime: validMetadata.wallTime || Date.now(),
|
||||
location: validMetadata.location,
|
||||
apiName: validMetadata.apiName,
|
||||
internal: validMetadata.internal,
|
||||
stepId: validMetadata.stepId,
|
||||
objectId: sdkObject?.guid,
|
||||
pageId: sdkObject?.attribution?.page?.guid,
|
||||
frameId: sdkObject?.attribution?.frame?.guid,
|
||||
|
|
|
|||
|
|
@ -198,9 +198,9 @@ export class APIRequestContextDispatcher extends Dispatcher<APIRequestContext, c
|
|||
return this._object.storageState();
|
||||
}
|
||||
|
||||
async dispose(_: channels.APIRequestContextDisposeParams, metadata: CallMetadata): Promise<void> {
|
||||
async dispose(params: channels.APIRequestContextDisposeParams, metadata: CallMetadata): Promise<void> {
|
||||
metadata.potentiallyClosesScope = true;
|
||||
await this._object.dispose();
|
||||
await this._object.dispose(params);
|
||||
this._dispose();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ export class Electron extends SdkObject {
|
|||
return controller.run(async progress => {
|
||||
let app: ElectronApplication | undefined = undefined;
|
||||
// --remote-debugging-port=0 must be the last playwright's argument, loader.ts relies on it.
|
||||
const electronArguments = ['--inspect=0', '--remote-debugging-port=0', ...args];
|
||||
let electronArguments = ['--inspect=0', '--remote-debugging-port=0', ...args];
|
||||
|
||||
if (os.platform() === 'linux') {
|
||||
const runningAsRoot = process.geteuid && process.geteuid() === 0;
|
||||
|
|
@ -195,6 +195,16 @@ export class Electron extends SdkObject {
|
|||
// Packaged apps might have their own command line handling.
|
||||
electronArguments.unshift('-r', require.resolve('./loader'));
|
||||
}
|
||||
let shell = false;
|
||||
if (process.platform === 'win32') {
|
||||
// On Windows in order to run .cmd files, shell: true is required.
|
||||
// https://github.com/nodejs/node/issues/52554
|
||||
shell = true;
|
||||
// On Windows, we need to quote the executable path due to shell: true.
|
||||
command = `"${command}"`;
|
||||
// On Windows, we need to quote the arguments due to shell: true.
|
||||
electronArguments = electronArguments.map(arg => `"${arg}"`);
|
||||
}
|
||||
|
||||
// When debugging Playwright test that runs Electron, NODE_OPTIONS
|
||||
// will make the debugger attach to Electron's Node. But Playwright
|
||||
|
|
@ -208,9 +218,7 @@ export class Electron extends SdkObject {
|
|||
progress.log(message);
|
||||
browserLogsCollector.log(message);
|
||||
},
|
||||
// On Windows in order to run .cmd files, shell: true is required.
|
||||
// https://github.com/nodejs/node/issues/52554
|
||||
shell: process.platform === 'win32',
|
||||
shell,
|
||||
stdio: 'pipe',
|
||||
cwd: options.cwd,
|
||||
tempDirectories: [artifactsDir],
|
||||
|
|
@ -221,6 +229,8 @@ export class Electron extends SdkObject {
|
|||
onExit: () => app?.emit(ElectronApplication.Events.Close),
|
||||
});
|
||||
|
||||
// All waitForLines must be started immediately.
|
||||
// Otherwise the lines might come before we are ready.
|
||||
const waitForXserverError = new Promise(async (resolve, reject) => {
|
||||
waitForLine(progress, launchedProcess, /Unable to open X display/).then(() => reject(new Error([
|
||||
'Unable to open X display!',
|
||||
|
|
@ -232,17 +242,20 @@ export class Electron extends SdkObject {
|
|||
progress.metadata.log
|
||||
].join('\n')))).catch(() => {});
|
||||
});
|
||||
const nodeMatchPromise = waitForLine(progress, launchedProcess, /^Debugger listening on (ws:\/\/.*)$/);
|
||||
const chromeMatchPromise = waitForLine(progress, launchedProcess, /^DevTools listening on (ws:\/\/.*)$/);
|
||||
const debuggerDisconnectPromise = waitForLine(progress, launchedProcess, /Waiting for the debugger to disconnect\.\.\./);
|
||||
|
||||
const nodeMatch = await waitForLine(progress, launchedProcess, /^Debugger listening on (ws:\/\/.*)$/);
|
||||
const nodeMatch = await nodeMatchPromise;
|
||||
const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]);
|
||||
const nodeConnection = new CRConnection(nodeTransport, helper.debugProtocolLogger(), browserLogsCollector);
|
||||
|
||||
// Immediately release exiting process under debug.
|
||||
waitForLine(progress, launchedProcess, /Waiting for the debugger to disconnect\.\.\./).then(() => {
|
||||
debuggerDisconnectPromise.then(() => {
|
||||
nodeTransport.close();
|
||||
}).catch(() => {});
|
||||
const chromeMatch = await Promise.race([
|
||||
waitForLine(progress, launchedProcess, /^DevTools listening on (ws:\/\/.*)$/),
|
||||
chromeMatchPromise,
|
||||
waitForXserverError,
|
||||
]) as RegExpMatchArray;
|
||||
const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]);
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
|
||||
abstract tracing(): Tracing;
|
||||
|
||||
abstract dispose(): Promise<void>;
|
||||
abstract dispose(options: { reason?: string }): Promise<void>;
|
||||
|
||||
abstract _defaultOptions(): FetchRequestOptions;
|
||||
abstract _addCookies(cookies: channels.NetworkCookie[]): Promise<void>;
|
||||
|
|
@ -336,12 +336,15 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
redirectOptions.rejectUnauthorized = false;
|
||||
|
||||
// HTTP-redirect fetch step 4: If locationURL is null, then return response.
|
||||
if (response.headers.location) {
|
||||
// Best-effort UTF-8 decoding, per spec it's US-ASCII only, but browsers are more lenient.
|
||||
// Node.js parses it as Latin1 via std::v8::String, so we convert it to UTF-8.
|
||||
const locationHeaderValue = Buffer.from(response.headers.location ?? '', 'latin1').toString('utf8');
|
||||
if (locationHeaderValue) {
|
||||
let locationURL;
|
||||
try {
|
||||
locationURL = new URL(response.headers.location, url);
|
||||
locationURL = new URL(locationHeaderValue, url);
|
||||
} catch (error) {
|
||||
reject(new Error(`uri requested responds with an invalid redirect URL: ${response.headers.location}`));
|
||||
reject(new Error(`uri requested responds with an invalid redirect URL: ${locationHeaderValue}`));
|
||||
request.destroy();
|
||||
return;
|
||||
}
|
||||
|
|
@ -483,7 +486,8 @@ export class BrowserContextAPIRequestContext extends APIRequestContext {
|
|||
return this._context.tracing;
|
||||
}
|
||||
|
||||
override async dispose() {
|
||||
override async dispose(options: { reason?: string }) {
|
||||
this._closeReason = options.reason;
|
||||
this.fetchResponses.clear();
|
||||
}
|
||||
|
||||
|
|
@ -552,7 +556,8 @@ export class GlobalAPIRequestContext extends APIRequestContext {
|
|||
return this._tracing;
|
||||
}
|
||||
|
||||
override async dispose() {
|
||||
override async dispose(options: { reason?: string }) {
|
||||
this._closeReason = options.reason;
|
||||
await this._tracing.flush();
|
||||
await this._tracing.deleteTmpTracesDir();
|
||||
this._disposeImpl();
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ class FFRouteImpl implements network.RouteDelegate {
|
|||
await this._session.sendMayFail('Network.fulfillInterceptedRequest', {
|
||||
requestId: this._request._id,
|
||||
status: response.status,
|
||||
statusText: network.STATUS_TEXTS[String(response.status)] || '',
|
||||
statusText: network.statusText(response.status),
|
||||
headers: response.headers,
|
||||
base64body,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ export class Highlight {
|
|||
const document = injectedScript.document;
|
||||
this._isUnderTest = injectedScript.isUnderTest;
|
||||
this._glassPaneElement = document.createElement('x-pw-glass');
|
||||
this._glassPaneElement.popover = 'manual';
|
||||
this._glassPaneElement.style.position = 'fixed';
|
||||
this._glassPaneElement.style.top = '0';
|
||||
this._glassPaneElement.style.right = '0';
|
||||
|
|
@ -65,12 +64,6 @@ export class Highlight {
|
|||
this._glassPaneElement.style.pointerEvents = 'none';
|
||||
this._glassPaneElement.style.display = 'flex';
|
||||
this._glassPaneElement.style.backgroundColor = 'transparent';
|
||||
this._glassPaneElement.style.width = 'inherit';
|
||||
this._glassPaneElement.style.height = 'inherit';
|
||||
this._glassPaneElement.style.padding = '0';
|
||||
this._glassPaneElement.style.margin = '0';
|
||||
this._glassPaneElement.style.border = 'none';
|
||||
this._glassPaneElement.style.overflow = 'hidden';
|
||||
for (const eventName of ['click', 'auxclick', 'dragstart', 'input', 'keydown', 'keyup', 'pointerdown', 'pointerup', 'mousedown', 'mouseup', 'mouseleave', 'focus', 'scroll']) {
|
||||
this._glassPaneElement.addEventListener(eventName, e => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -98,8 +91,6 @@ export class Highlight {
|
|||
|
||||
install() {
|
||||
this._injectedScript.document.documentElement.appendChild(this._glassPaneElement);
|
||||
// Popover is not supported in WebKit-macOS < 14.0
|
||||
this._glassPaneElement.showPopover?.();
|
||||
}
|
||||
|
||||
setLanguage(language: Language) {
|
||||
|
|
@ -116,8 +107,6 @@ export class Highlight {
|
|||
uninstall() {
|
||||
if (this._rafRequest)
|
||||
cancelAnimationFrame(this._rafRequest);
|
||||
// Popover is not supported in WebKit-macOS < 14.0
|
||||
this._glassPaneElement.hidePopover?.();
|
||||
this._glassPaneElement.remove();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -325,8 +325,10 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, i
|
|||
const cssToken: SelectorToken = { engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: kCSSTagNameScore };
|
||||
for (const alternative of alternatives)
|
||||
candidates.push([cssToken, { engine: 'internal:has-text', selector: escapeForTextSelector(alternative.text, false), score: kTextScore - alternative.scoreBouns }]);
|
||||
if (text.length <= 80)
|
||||
candidates.push([cssToken, { engine: 'internal:has-text', selector: '/^' + escapeRegExp(text) + '$/', score: kTextScoreRegex }]);
|
||||
if (text.length <= 80) {
|
||||
const re = new RegExp('^' + escapeRegExp(text) + '$');
|
||||
candidates.push([cssToken, { engine: 'internal:has-text', selector: escapeForTextSelector(re, false), score: kTextScoreRegex }]);
|
||||
}
|
||||
}
|
||||
|
||||
const ariaRole = getAriaRole(element);
|
||||
|
|
|
|||
|
|
@ -106,7 +106,6 @@ export function serverSideCallMetadata(): CallMetadata {
|
|||
id: '',
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
wallTime: Date.now(),
|
||||
type: 'Internal',
|
||||
method: '',
|
||||
params: {},
|
||||
|
|
|
|||
|
|
@ -616,7 +616,7 @@ export interface RouteDelegate {
|
|||
}
|
||||
|
||||
// List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes.
|
||||
export const STATUS_TEXTS: { [status: string]: string } = {
|
||||
const STATUS_TEXTS: { [status: string]: string } = {
|
||||
'100': 'Continue',
|
||||
'101': 'Switching Protocols',
|
||||
'102': 'Processing',
|
||||
|
|
@ -682,6 +682,10 @@ export const STATUS_TEXTS: { [status: string]: string } = {
|
|||
'511': 'Network Authentication Required',
|
||||
};
|
||||
|
||||
export function statusText(status: number): string {
|
||||
return STATUS_TEXTS[String(status)] || 'Unknown';
|
||||
}
|
||||
|
||||
export function singleHeader(name: string, value: string): HeadersArray {
|
||||
return [{ name, value }];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -605,7 +605,6 @@ class ContextRecorder extends EventEmitter {
|
|||
frameId: frame.guid,
|
||||
startTime: monotonicTime(),
|
||||
endTime: 0,
|
||||
wallTime: Date.now(),
|
||||
type: 'Frame',
|
||||
method: action,
|
||||
params,
|
||||
|
|
|
|||
|
|
@ -137,17 +137,17 @@ const DOWNLOAD_PATHS: Record<BrowserName | InternalTool, DownloadPaths> = {
|
|||
'debian11-arm64': 'builds/firefox/%s/firefox-debian-11-arm64.zip',
|
||||
'debian12-x64': 'builds/firefox/%s/firefox-debian-12.zip',
|
||||
'debian12-arm64': 'builds/firefox/%s/firefox-debian-12-arm64.zip',
|
||||
'mac10.13': 'builds/firefox/%s/firefox-mac-13.zip',
|
||||
'mac10.14': 'builds/firefox/%s/firefox-mac-13.zip',
|
||||
'mac10.15': 'builds/firefox/%s/firefox-mac-13.zip',
|
||||
'mac11': 'builds/firefox/%s/firefox-mac-13.zip',
|
||||
'mac11-arm64': 'builds/firefox/%s/firefox-mac-13-arm64.zip',
|
||||
'mac12': 'builds/firefox/%s/firefox-mac-13.zip',
|
||||
'mac12-arm64': 'builds/firefox/%s/firefox-mac-13-arm64.zip',
|
||||
'mac13': 'builds/firefox/%s/firefox-mac-13.zip',
|
||||
'mac13-arm64': 'builds/firefox/%s/firefox-mac-13-arm64.zip',
|
||||
'mac14': 'builds/firefox/%s/firefox-mac-13.zip',
|
||||
'mac14-arm64': 'builds/firefox/%s/firefox-mac-13-arm64.zip',
|
||||
'mac10.13': 'builds/firefox/%s/firefox-mac.zip',
|
||||
'mac10.14': 'builds/firefox/%s/firefox-mac.zip',
|
||||
'mac10.15': 'builds/firefox/%s/firefox-mac.zip',
|
||||
'mac11': 'builds/firefox/%s/firefox-mac.zip',
|
||||
'mac11-arm64': 'builds/firefox/%s/firefox-mac-arm64.zip',
|
||||
'mac12': 'builds/firefox/%s/firefox-mac.zip',
|
||||
'mac12-arm64': 'builds/firefox/%s/firefox-mac-arm64.zip',
|
||||
'mac13': 'builds/firefox/%s/firefox-mac.zip',
|
||||
'mac13-arm64': 'builds/firefox/%s/firefox-mac-arm64.zip',
|
||||
'mac14': 'builds/firefox/%s/firefox-mac.zip',
|
||||
'mac14-arm64': 'builds/firefox/%s/firefox-mac-arm64.zip',
|
||||
'win64': 'builds/firefox/%s/firefox-win64.zip',
|
||||
},
|
||||
'firefox-beta': {
|
||||
|
|
@ -162,17 +162,17 @@ const DOWNLOAD_PATHS: Record<BrowserName | InternalTool, DownloadPaths> = {
|
|||
'debian11-arm64': 'builds/firefox-beta/%s/firefox-beta-debian-11-arm64.zip',
|
||||
'debian12-x64': 'builds/firefox-beta/%s/firefox-beta-debian-12.zip',
|
||||
'debian12-arm64': 'builds/firefox-beta/%s/firefox-beta-debian-12-arm64.zip',
|
||||
'mac10.13': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip',
|
||||
'mac10.14': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip',
|
||||
'mac10.15': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip',
|
||||
'mac11': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip',
|
||||
'mac11-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-13-arm64.zip',
|
||||
'mac12': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip',
|
||||
'mac12-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-13-arm64.zip',
|
||||
'mac13': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip',
|
||||
'mac13-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-13-arm64.zip',
|
||||
'mac14': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip',
|
||||
'mac14-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-13-arm64.zip',
|
||||
'mac10.13': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
|
||||
'mac10.14': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
|
||||
'mac10.15': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
|
||||
'mac11': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
|
||||
'mac11-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip',
|
||||
'mac12': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
|
||||
'mac12-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip',
|
||||
'mac13': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
|
||||
'mac13-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip',
|
||||
'mac14': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
|
||||
'mac14-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip',
|
||||
'win64': 'builds/firefox-beta/%s/firefox-beta-win64.zip',
|
||||
},
|
||||
'webkit': {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ import type { ConsoleMessage } from '../../console';
|
|||
import { Dispatcher } from '../../dispatchers/dispatcher';
|
||||
import { serializeError } from '../../errors';
|
||||
|
||||
const version: trace.VERSION = 6;
|
||||
const version: trace.VERSION = 7;
|
||||
|
||||
export type TracerOptions = {
|
||||
name?: string;
|
||||
|
|
@ -100,10 +100,12 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
|||
this._contextCreatedEvent = {
|
||||
version,
|
||||
type: 'context-options',
|
||||
origin: 'library',
|
||||
browserName: '',
|
||||
options: {},
|
||||
platform: process.platform,
|
||||
wallTime: 0,
|
||||
monotonicTime: 0,
|
||||
sdkLanguage: context.attribution.playwright.options.sdkLanguage,
|
||||
testIdAttributeName
|
||||
};
|
||||
|
|
@ -177,7 +179,12 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
|||
this._allocateNewTraceFile(this._state);
|
||||
|
||||
this._fs.mkdir(path.dirname(this._state.traceFile));
|
||||
const event: trace.TraceEvent = { ...this._contextCreatedEvent, title: options.title, wallTime: Date.now() };
|
||||
const event: trace.TraceEvent = {
|
||||
...this._contextCreatedEvent,
|
||||
title: options.title,
|
||||
wallTime: Date.now(),
|
||||
monotonicTime: monotonicTime()
|
||||
};
|
||||
this._fs.appendFile(this._state.traceFile, JSON.stringify(event) + '\n');
|
||||
|
||||
this._context.instrumentation.addListener(this, this._context);
|
||||
|
|
@ -535,7 +542,7 @@ function createBeforeActionTraceEvent(metadata: CallMetadata): trace.BeforeActio
|
|||
class: metadata.type,
|
||||
method: metadata.method,
|
||||
params: metadata.params,
|
||||
wallTime: metadata.wallTime,
|
||||
stepId: metadata.stepId,
|
||||
pageId: metadata.pageId,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ export async function installRootRedirect(server: HttpServer, traceUrls: string[
|
|||
for (const reporter of options.reporter || [])
|
||||
params.append('reporter', reporter);
|
||||
|
||||
const urlPath = `/trace/${options.webApp || 'index.html'}?${params.toString()}`;
|
||||
const urlPath = `./trace/${options.webApp || 'index.html'}?${params.toString()}`;
|
||||
server.routePath('/', (_, response) => {
|
||||
response.statusCode = 302;
|
||||
response.setHeader('Location', urlPath);
|
||||
|
|
@ -145,7 +145,7 @@ export async function runTraceViewerApp(traceUrls: string[], browserName: string
|
|||
validateTraceUrls(traceUrls);
|
||||
const server = await startTraceViewerServer(options);
|
||||
await installRootRedirect(server, traceUrls, options);
|
||||
const page = await openTraceViewerApp(server.urlPrefix(), browserName, options);
|
||||
const page = await openTraceViewerApp(server.urlPrefix('precise'), browserName, options);
|
||||
if (exitOnClose)
|
||||
page.on('close', () => gracefullyProcessExitDoNotHang(0));
|
||||
return page;
|
||||
|
|
@ -155,7 +155,7 @@ export async function runTraceInBrowser(traceUrls: string[], options: TraceViewe
|
|||
validateTraceUrls(traceUrls);
|
||||
const server = await startTraceViewerServer(options);
|
||||
await installRootRedirect(server, traceUrls, options);
|
||||
await openTraceInBrowser(server.urlPrefix());
|
||||
await openTraceInBrowser(server.urlPrefix('human-readable'));
|
||||
}
|
||||
|
||||
export async function openTraceViewerApp(url: string, browserName: string, options?: TraceViewerAppOptions): Promise<Page> {
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export class WKInterceptableRequest {
|
|||
requestStart: timingPayload ? wkMillisToRoundishMillis(timingPayload.requestStart) : -1,
|
||||
responseStart: timingPayload ? wkMillisToRoundishMillis(timingPayload.responseStart) : -1,
|
||||
};
|
||||
const setCookieSeparator = process.platform === 'darwin' ? ',' : '\n';
|
||||
const setCookieSeparator = process.platform === 'darwin' ? ',' : 'playwright-set-cookie-separator';
|
||||
const response = new network.Response(this.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers, ',', setCookieSeparator), timing, getResponseBody, responsePayload.source === 'service-worker');
|
||||
|
||||
// No raw response headers in WebKit, use "provisional" ones.
|
||||
|
|
@ -128,7 +128,7 @@ export class WKRouteImpl implements network.RouteDelegate {
|
|||
await this._session.sendMayFail('Network.interceptRequestWithResponse', {
|
||||
requestId: this._requestId,
|
||||
status: response.status,
|
||||
statusText: network.STATUS_TEXTS[String(response.status)],
|
||||
statusText: network.statusText(response.status),
|
||||
mimeType,
|
||||
headers,
|
||||
base64Encoded: response.isBase64,
|
||||
|
|
|
|||
|
|
@ -21,9 +21,13 @@ export function getFromENV(name: string): string | undefined {
|
|||
return value;
|
||||
}
|
||||
|
||||
export function getAsBooleanFromENV(name: string): boolean {
|
||||
export function getAsBooleanFromENV(name: string, defaultValue?: boolean | undefined): boolean {
|
||||
const value = getFromENV(name);
|
||||
return !!value && value !== 'false' && value !== '0';
|
||||
if (value === 'false' || value === '0')
|
||||
return false;
|
||||
if (value)
|
||||
return true;
|
||||
return !!defaultValue;
|
||||
}
|
||||
|
||||
export function getPackageManager() {
|
||||
|
|
|
|||
|
|
@ -34,14 +34,14 @@ export type Transport = {
|
|||
|
||||
export class HttpServer {
|
||||
private _server: http.Server;
|
||||
private _urlPrefix: string;
|
||||
private _urlPrefixPrecise: string = '';
|
||||
private _urlPrefixHumanReadable: 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;
|
||||
constructor() {
|
||||
this._server = createHttpServer(this._onRequest.bind(this));
|
||||
}
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ export class HttpServer {
|
|||
return this._wsGuid;
|
||||
}
|
||||
|
||||
async start(options: { port?: number, preferredPort?: number, host?: string } = {}): Promise<string> {
|
||||
async start(options: { port?: number, preferredPort?: number, host?: string } = {}): Promise<void> {
|
||||
assert(!this._started, 'server already started');
|
||||
this._started = true;
|
||||
|
||||
|
|
@ -121,24 +121,23 @@ export class HttpServer {
|
|||
|
||||
const address = this._server.address();
|
||||
assert(address, 'Could not bind server socket');
|
||||
if (!this._urlPrefix) {
|
||||
if (typeof address === 'string') {
|
||||
this._urlPrefix = address;
|
||||
} else {
|
||||
this._port = address.port;
|
||||
const resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
|
||||
this._urlPrefix = `http://${resolvedHost}:${address.port}`;
|
||||
}
|
||||
if (typeof address === 'string') {
|
||||
this._urlPrefixPrecise = address;
|
||||
this._urlPrefixHumanReadable = address;
|
||||
} else {
|
||||
this._port = address.port;
|
||||
const resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
|
||||
this._urlPrefixPrecise = `http://${resolvedHost}:${address.port}`;
|
||||
this._urlPrefixHumanReadable = `http://${host}:${address.port}`;
|
||||
}
|
||||
return this._urlPrefix;
|
||||
}
|
||||
|
||||
async stop() {
|
||||
await new Promise(cb => this._server!.close(cb));
|
||||
}
|
||||
|
||||
urlPrefix(): string {
|
||||
return this._urlPrefix;
|
||||
urlPrefix(purpose: 'human-readable' | 'precise'): string {
|
||||
return purpose === 'human-readable' ? this._urlPrefixHumanReadable : this._urlPrefixPrecise;
|
||||
}
|
||||
|
||||
serveFile(request: http.IncomingMessage, response: http.ServerResponse, absoluteFilePath: string, headers?: { [name: string]: string }): boolean {
|
||||
|
|
|
|||
|
|
@ -140,5 +140,5 @@ Call log:
|
|||
|
||||
export type ExpectZone = {
|
||||
title: string;
|
||||
wallTime: number;
|
||||
stepId: string;
|
||||
};
|
||||
|
|
|
|||
137
packages/playwright-core/types/protocol.d.ts
vendored
137
packages/playwright-core/types/protocol.d.ts
vendored
|
|
@ -841,6 +841,7 @@ CORS RFC1918 enforcement.
|
|||
clientSecurityState?: Network.ClientSecurityState;
|
||||
}
|
||||
export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation"|"InvalidInfoHeader"|"NoRegisterSourceHeader"|"NoRegisterTriggerHeader"|"NoRegisterOsSourceHeader"|"NoRegisterOsTriggerHeader";
|
||||
export type SharedDictionaryError = "UseErrorCrossOriginNoCorsRequest"|"UseErrorDictionaryLoadFailure"|"UseErrorMatchingDictionaryNotUsed"|"UseErrorUnexpectedContentDictionaryHeader"|"WriteErrorCossOriginNoCorsRequest"|"WriteErrorDisallowedBySettings"|"WriteErrorExpiredResponse"|"WriteErrorFeatureDisabled"|"WriteErrorInsufficientResources"|"WriteErrorInvalidMatchField"|"WriteErrorInvalidStructuredHeader"|"WriteErrorNavigationRequest"|"WriteErrorNoMatchField"|"WriteErrorNonListMatchDestField"|"WriteErrorNonSecureContext"|"WriteErrorNonStringIdField"|"WriteErrorNonStringInMatchDestList"|"WriteErrorNonStringMatchField"|"WriteErrorNonTokenTypeField"|"WriteErrorRequestAborted"|"WriteErrorShuttingDown"|"WriteErrorTooLongIdField"|"WriteErrorUnsupportedType";
|
||||
/**
|
||||
* Details for issues around "Attribution Reporting API" usage.
|
||||
Explainer: https://github.com/WICG/attribution-reporting-api
|
||||
|
|
@ -870,6 +871,10 @@ instead of "limited-quirks".
|
|||
url: string;
|
||||
location?: SourceCodeLocation;
|
||||
}
|
||||
export interface SharedDictionaryIssueDetails {
|
||||
sharedDictionaryError: SharedDictionaryError;
|
||||
request: AffectedRequest;
|
||||
}
|
||||
export type GenericIssueErrorType = "CrossOriginPortalPostMessageError"|"FormLabelForNameError"|"FormDuplicateIdForInputError"|"FormInputWithNoLabelError"|"FormAutocompleteAttributeEmptyError"|"FormEmptyIdAndNameAttributesForInputError"|"FormAriaLabelledByToNonExistingId"|"FormInputAssignedAutocompleteValueToIdOrNameAttributeError"|"FormLabelHasNeitherForNorNestedInput"|"FormLabelForMatchesNonExistingIdError"|"FormInputHasWrongButWellIntendedAutocompleteValueError"|"ResponseWasBlockedByORB";
|
||||
/**
|
||||
* Depending on the concrete errorType, different properties are set.
|
||||
|
|
@ -997,7 +1002,7 @@ registrations being ignored.
|
|||
optional fields in InspectorIssueDetails to convey more specific
|
||||
information about the kind of issue.
|
||||
*/
|
||||
export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue";
|
||||
export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue";
|
||||
/**
|
||||
* This struct holds a list of optional fields with additional information
|
||||
specific to the kind of issue. When adding a new issue code, please also
|
||||
|
|
@ -1024,6 +1029,7 @@ add a new optional field to this type.
|
|||
stylesheetLoadingIssueDetails?: StylesheetLoadingIssueDetails;
|
||||
propertyRuleIssueDetails?: PropertyRuleIssueDetails;
|
||||
federatedAuthUserInfoRequestIssueDetails?: FederatedAuthUserInfoRequestIssueDetails;
|
||||
sharedDictionaryIssueDetails?: SharedDictionaryIssueDetails;
|
||||
}
|
||||
/**
|
||||
* A unique id for a DevTools inspector issue. Allows other entities (e.g.
|
||||
|
|
@ -1121,6 +1127,33 @@ using Audits.issueAdded event.
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines commands and events for browser extensions. Available if the client
|
||||
is connected using the --remote-debugging-pipe flag and
|
||||
the --enable-unsafe-extension-debugging flag is set.
|
||||
*/
|
||||
export module Extensions {
|
||||
|
||||
|
||||
/**
|
||||
* Installs an unpacked extension from the filesystem similar to
|
||||
--load-extension CLI flags. Returns extension ID once the extension
|
||||
has been installed.
|
||||
*/
|
||||
export type loadUnpackedParameters = {
|
||||
/**
|
||||
* Absolute file path.
|
||||
*/
|
||||
path: string;
|
||||
}
|
||||
export type loadUnpackedReturnValue = {
|
||||
/**
|
||||
* Extension id.
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines commands and events for Autofill.
|
||||
*/
|
||||
|
|
@ -3450,7 +3483,7 @@ front-end.
|
|||
/**
|
||||
* Pseudo element type.
|
||||
*/
|
||||
export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-markers"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new";
|
||||
export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-markers"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new";
|
||||
/**
|
||||
* Shadow root type.
|
||||
*/
|
||||
|
|
@ -4426,6 +4459,25 @@ appear on top of all other content.
|
|||
*/
|
||||
nodeIds: NodeId[];
|
||||
}
|
||||
/**
|
||||
* Returns the NodeId of the matched element according to certain relations.
|
||||
*/
|
||||
export type getElementByRelationParameters = {
|
||||
/**
|
||||
* Id of the node from which to query the relation.
|
||||
*/
|
||||
nodeId: NodeId;
|
||||
/**
|
||||
* Type of relation to get.
|
||||
*/
|
||||
relation: "PopoverTarget";
|
||||
}
|
||||
export type getElementByRelationReturnValue = {
|
||||
/**
|
||||
* NodeId of the element matching the queried relation.
|
||||
*/
|
||||
nodeId: NodeId;
|
||||
}
|
||||
/**
|
||||
* Re-does the last undone action.
|
||||
*/
|
||||
|
|
@ -8309,8 +8361,16 @@ records.
|
|||
*/
|
||||
export type ServiceWorkerRouterSource = "network"|"cache"|"fetch-event"|"race-network-and-fetch-handler";
|
||||
export interface ServiceWorkerRouterInfo {
|
||||
ruleIdMatched: number;
|
||||
matchedSourceType: ServiceWorkerRouterSource;
|
||||
/**
|
||||
* ID of the rule matched. If there is a matched rule, this field will
|
||||
be set, otherwiser no value will be set.
|
||||
*/
|
||||
ruleIdMatched?: number;
|
||||
/**
|
||||
* The router source of the matched rule. If there is a matched rule, this
|
||||
field will be set, otherwise no value will be set.
|
||||
*/
|
||||
matchedSourceType?: ServiceWorkerRouterSource;
|
||||
}
|
||||
/**
|
||||
* HTTP response data.
|
||||
|
|
@ -8385,7 +8445,10 @@ records.
|
|||
*/
|
||||
fromEarlyHints?: boolean;
|
||||
/**
|
||||
* Information about how Service Worker Static Router was used.
|
||||
* Information about how ServiceWorker Static Router API was used. If this
|
||||
field is set with `matchedSourceType` field, a matching rule is found.
|
||||
If this field is set without `matchedSource`, no matching rule is found.
|
||||
Otherwise, the API is not used.
|
||||
*/
|
||||
serviceWorkerRouterInfo?: ServiceWorkerRouterInfo;
|
||||
/**
|
||||
|
|
@ -11167,7 +11230,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-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";
|
||||
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"|"xr-spatial-tracking";
|
||||
/**
|
||||
* Reason for a permissions policy feature to be disabled.
|
||||
*/
|
||||
|
|
@ -11755,7 +11818,7 @@ https://github.com/WICG/manifest-incubations/blob/gh-pages/scope_extensions-expl
|
|||
/**
|
||||
* 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"|"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";
|
||||
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"|"RequestedByWebViewClient";
|
||||
/**
|
||||
* Types of not restored reasons for back-forward cache.
|
||||
*/
|
||||
|
|
@ -12334,7 +12397,7 @@ option, use with caution.
|
|||
This API always waits for the manifest to be loaded.
|
||||
If manifestId is provided, and it does not match the manifest of the
|
||||
current document, this API errors out.
|
||||
If there isn’t a loaded page, this API errors out immediately.
|
||||
If there is not a loaded page, this API errors out immediately.
|
||||
*/
|
||||
export type getAppManifestParameters = {
|
||||
manifestId?: string;
|
||||
|
|
@ -16690,7 +16753,7 @@ possible for multiple rule sets and links to trigger a single attempt.
|
|||
/**
|
||||
* List of FinalStatus reasons for Prerender2.
|
||||
*/
|
||||
export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl";
|
||||
export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled";
|
||||
/**
|
||||
* Preloading status values, see also PreloadingTriggeringOutcome. This
|
||||
status is shared by prefetchStatusUpdated and prerenderStatusUpdated.
|
||||
|
|
@ -16921,6 +16984,52 @@ https://web.dev/learn/pwa/web-app-manifest.
|
|||
badgeCount: number;
|
||||
fileHandlers: FileHandler[];
|
||||
}
|
||||
/**
|
||||
* Installs the given manifest identity, optionally using the given install_url
|
||||
or IWA bundle location.
|
||||
|
||||
TODO(crbug.com/337872319) Support IWA to meet the following specific
|
||||
requirement.
|
||||
IWA-specific install description: If the manifest_id is isolated-app://,
|
||||
install_url_or_bundle_url is required, and can be either an http(s) URL or
|
||||
file:// URL pointing to a signed web bundle (.swbn). The .swbn file's
|
||||
signing key must correspond to manifest_id. If Chrome is not in IWA dev
|
||||
mode, the installation will fail, regardless of the state of the allowlist.
|
||||
*/
|
||||
export type installParameters = {
|
||||
manifestId: string;
|
||||
/**
|
||||
* The location of the app or bundle overriding the one derived from the
|
||||
manifestId.
|
||||
*/
|
||||
installUrlOrBundleUrl?: string;
|
||||
}
|
||||
export type installReturnValue = {
|
||||
}
|
||||
/**
|
||||
* Uninstals the given manifest_id and closes any opened app windows.
|
||||
*/
|
||||
export type uninstallParameters = {
|
||||
manifestId: string;
|
||||
}
|
||||
export type uninstallReturnValue = {
|
||||
}
|
||||
/**
|
||||
* Launches the installed web app, or an url in the same web app instead of the
|
||||
default start url if it is provided. Returns a tab / web contents based
|
||||
Target.TargetID which can be used to attach to via Target.attachToTarget or
|
||||
similar APIs.
|
||||
*/
|
||||
export type launchParameters = {
|
||||
manifestId: string;
|
||||
url?: string;
|
||||
}
|
||||
export type launchReturnValue = {
|
||||
/**
|
||||
* ID of the tab target created as a result.
|
||||
*/
|
||||
targetId: Target.TargetID;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -19773,6 +19882,7 @@ Error was thrown.
|
|||
"Audits.enable": Audits.enableParameters;
|
||||
"Audits.checkContrast": Audits.checkContrastParameters;
|
||||
"Audits.checkFormsIssues": Audits.checkFormsIssuesParameters;
|
||||
"Extensions.loadUnpacked": Extensions.loadUnpackedParameters;
|
||||
"Autofill.trigger": Autofill.triggerParameters;
|
||||
"Autofill.setAddresses": Autofill.setAddressesParameters;
|
||||
"Autofill.disable": Autofill.disableParameters;
|
||||
|
|
@ -19870,6 +19980,7 @@ Error was thrown.
|
|||
"DOM.querySelector": DOM.querySelectorParameters;
|
||||
"DOM.querySelectorAll": DOM.querySelectorAllParameters;
|
||||
"DOM.getTopLayerElements": DOM.getTopLayerElementsParameters;
|
||||
"DOM.getElementByRelation": DOM.getElementByRelationParameters;
|
||||
"DOM.redo": DOM.redoParameters;
|
||||
"DOM.removeAttribute": DOM.removeAttributeParameters;
|
||||
"DOM.removeNode": DOM.removeNodeParameters;
|
||||
|
|
@ -20256,6 +20367,9 @@ Error was thrown.
|
|||
"FedCm.dismissDialog": FedCm.dismissDialogParameters;
|
||||
"FedCm.resetCooldown": FedCm.resetCooldownParameters;
|
||||
"PWA.getOsAppState": PWA.getOsAppStateParameters;
|
||||
"PWA.install": PWA.installParameters;
|
||||
"PWA.uninstall": PWA.uninstallParameters;
|
||||
"PWA.launch": PWA.launchParameters;
|
||||
"Console.clearMessages": Console.clearMessagesParameters;
|
||||
"Console.disable": Console.disableParameters;
|
||||
"Console.enable": Console.enableParameters;
|
||||
|
|
@ -20361,6 +20475,7 @@ Error was thrown.
|
|||
"Audits.enable": Audits.enableReturnValue;
|
||||
"Audits.checkContrast": Audits.checkContrastReturnValue;
|
||||
"Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue;
|
||||
"Extensions.loadUnpacked": Extensions.loadUnpackedReturnValue;
|
||||
"Autofill.trigger": Autofill.triggerReturnValue;
|
||||
"Autofill.setAddresses": Autofill.setAddressesReturnValue;
|
||||
"Autofill.disable": Autofill.disableReturnValue;
|
||||
|
|
@ -20458,6 +20573,7 @@ Error was thrown.
|
|||
"DOM.querySelector": DOM.querySelectorReturnValue;
|
||||
"DOM.querySelectorAll": DOM.querySelectorAllReturnValue;
|
||||
"DOM.getTopLayerElements": DOM.getTopLayerElementsReturnValue;
|
||||
"DOM.getElementByRelation": DOM.getElementByRelationReturnValue;
|
||||
"DOM.redo": DOM.redoReturnValue;
|
||||
"DOM.removeAttribute": DOM.removeAttributeReturnValue;
|
||||
"DOM.removeNode": DOM.removeNodeReturnValue;
|
||||
|
|
@ -20844,6 +20960,9 @@ Error was thrown.
|
|||
"FedCm.dismissDialog": FedCm.dismissDialogReturnValue;
|
||||
"FedCm.resetCooldown": FedCm.resetCooldownReturnValue;
|
||||
"PWA.getOsAppState": PWA.getOsAppStateReturnValue;
|
||||
"PWA.install": PWA.installReturnValue;
|
||||
"PWA.uninstall": PWA.uninstallReturnValue;
|
||||
"PWA.launch": PWA.launchReturnValue;
|
||||
"Console.clearMessages": Console.clearMessagesReturnValue;
|
||||
"Console.disable": Console.disableReturnValue;
|
||||
"Console.enable": Console.enableReturnValue;
|
||||
|
|
|
|||
8
packages/playwright-core/types/types.d.ts
vendored
8
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -15846,8 +15846,14 @@ export interface APIRequestContext {
|
|||
* and similar methods are stored in the memory, so that you can later call
|
||||
* [apiResponse.body()](https://playwright.dev/docs/api/class-apiresponse#api-response-body).This method discards all
|
||||
* its resources, calling any method on disposed {@link APIRequestContext} will throw an exception.
|
||||
* @param options
|
||||
*/
|
||||
dispose(): Promise<void>;
|
||||
dispose(options?: {
|
||||
/**
|
||||
* The reason to be reported to the operations interrupted by the context disposure.
|
||||
*/
|
||||
reason?: string;
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and
|
||||
|
|
|
|||
|
|
@ -86,6 +86,8 @@ window.playwrightMount = async (component, rootElement, hooksConfig) => {
|
|||
window.playwrightUnmount = async rootElement => {
|
||||
if (!__pwReactDOM.unmountComponentAtNode(rootElement))
|
||||
throw new Error('Component was not mounted');
|
||||
|
||||
__pwRootRegistry.delete(rootElement);
|
||||
};
|
||||
|
||||
window.playwrightUpdate = async (rootElement, component) => {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export type FixturesWithLocation = {
|
|||
fixtures: Fixtures;
|
||||
location: Location;
|
||||
};
|
||||
export type Annotation = { type: string, description?: string };
|
||||
export type Annotation = { type: string, description?: string, url?: string };
|
||||
|
||||
export const defaultTimeout = 30000;
|
||||
|
||||
|
|
@ -52,6 +52,7 @@ export class FullConfigInternal {
|
|||
cliProjectFilter?: string[];
|
||||
cliListOnly = false;
|
||||
cliPassWithNoTests?: boolean;
|
||||
cliFailOnFlakyTests?: boolean;
|
||||
testIdMatcher?: Matcher;
|
||||
defineConfigWasUsed = false;
|
||||
|
||||
|
|
@ -265,7 +266,7 @@ export function toReporters(reporters: BuiltInReporter | ReporterDescription[] |
|
|||
export const builtInReporters = ['list', 'line', 'dot', 'json', 'junit', 'null', 'github', 'html', 'blob', 'markdown'] as const;
|
||||
export type BuiltInReporter = typeof builtInReporters[number];
|
||||
|
||||
export type ContextReuseMode = 'none' | 'force' | 'when-possible';
|
||||
export type ContextReuseMode = 'none' | 'when-possible';
|
||||
|
||||
function resolveScript(id: string | undefined, rootDir: string): string | undefined {
|
||||
if (!id)
|
||||
|
|
|
|||
|
|
@ -44,11 +44,12 @@ export class ProcessRunner {
|
|||
}
|
||||
}
|
||||
|
||||
let closed = false;
|
||||
let gracefullyCloseCalled = false;
|
||||
let forceExitInitiated = false;
|
||||
|
||||
sendMessageToParent({ method: 'ready' });
|
||||
|
||||
process.on('disconnect', gracefullyCloseAndExit);
|
||||
process.on('disconnect', () => gracefullyCloseAndExit(true));
|
||||
process.on('SIGINT', () => {});
|
||||
process.on('SIGTERM', () => {});
|
||||
|
||||
|
|
@ -76,7 +77,7 @@ process.on('message', async (message: any) => {
|
|||
const keys = new Set([...Object.keys(process.env), ...Object.keys(startingEnv)]);
|
||||
const producedEnv: EnvProducedPayload = [...keys].filter(key => startingEnv[key] !== process.env[key]).map(key => [key, process.env[key] ?? null]);
|
||||
sendMessageToParent({ method: '__env_produced__', params: producedEnv });
|
||||
await gracefullyCloseAndExit();
|
||||
await gracefullyCloseAndExit(false);
|
||||
return;
|
||||
}
|
||||
if (message.method === '__dispatch__') {
|
||||
|
|
@ -92,19 +93,24 @@ process.on('message', async (message: any) => {
|
|||
}
|
||||
});
|
||||
|
||||
async function gracefullyCloseAndExit() {
|
||||
if (closed)
|
||||
return;
|
||||
closed = true;
|
||||
// Force exit after 30 seconds.
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
setTimeout(() => process.exit(0), 30000);
|
||||
// Meanwhile, try to gracefully shutdown.
|
||||
await processRunner?.gracefullyClose().catch(() => {});
|
||||
if (processName)
|
||||
await stopProfiling(processName).catch(() => {});
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
process.exit(0);
|
||||
const kForceExitTimeout = +(process.env.PWTEST_FORCE_EXIT_TIMEOUT || 30000);
|
||||
|
||||
async function gracefullyCloseAndExit(forceExit: boolean) {
|
||||
if (forceExit && !forceExitInitiated) {
|
||||
forceExitInitiated = true;
|
||||
// Force exit after 30 seconds.
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
setTimeout(() => process.exit(0), kForceExitTimeout);
|
||||
}
|
||||
if (!gracefullyCloseCalled) {
|
||||
gracefullyCloseCalled = true;
|
||||
// Meanwhile, try to gracefully shutdown.
|
||||
await processRunner?.gracefullyClose().catch(() => {});
|
||||
if (processName)
|
||||
await stopProfiling(processName).catch(() => {});
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
function sendMessageToParent(message: { method: string, params?: any }) {
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ export class TestTypeImpl {
|
|||
const testInfo = currentTestInfo();
|
||||
if (!testInfo)
|
||||
throw new Error(`test.step() can only be called from a test`);
|
||||
const step = testInfo._addStep({ wallTime: Date.now(), category: 'test.step', title, box: options.box });
|
||||
const step = testInfo._addStep({ category: 'test.step', title, box: options.box });
|
||||
return await zones.run('stepZone', step, async () => {
|
||||
try {
|
||||
const result = await body();
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||
const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot);
|
||||
await artifactsRecorder.willStartTest(testInfo as TestInfoImpl);
|
||||
const csiListener: ClientInstrumentationListener = {
|
||||
onApiCallBegin: (apiName: string, params: Record<string, any>, frames: StackFrame[], wallTime: number, userData: any) => {
|
||||
onApiCallBegin: (apiName: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }) => {
|
||||
const testInfo = currentTestInfo();
|
||||
if (!testInfo || apiName.includes('setTestIdAttribute'))
|
||||
return { userObject: null };
|
||||
|
|
@ -257,9 +257,9 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||
title: renderApiCall(apiName, params),
|
||||
apiName,
|
||||
params,
|
||||
wallTime,
|
||||
});
|
||||
userData.userObject = step;
|
||||
out.stepId = step.stepId;
|
||||
},
|
||||
onApiCallEnd: (userData: any, error?: Error) => {
|
||||
const step = userData.userObject;
|
||||
|
|
@ -356,8 +356,8 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||
_reuseContext: [async ({ video, _optionContextReuseMode }, use) => {
|
||||
let mode = _optionContextReuseMode;
|
||||
if (process.env.PW_TEST_REUSE_CONTEXT)
|
||||
mode = process.env.PW_TEST_REUSE_CONTEXT === 'when-possible' ? 'when-possible' : (process.env.PW_TEST_REUSE_CONTEXT ? 'force' : 'none');
|
||||
const reuse = mode === 'force' || (mode === 'when-possible' && normalizeVideoMode(video) === 'off');
|
||||
mode = 'when-possible';
|
||||
const reuse = mode === 'when-possible' && normalizeVideoMode(video) === 'off';
|
||||
await use(reuse);
|
||||
}, { scope: 'worker', _title: 'context' } as any],
|
||||
|
||||
|
|
@ -392,7 +392,17 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||
request: async ({ playwright }, use) => {
|
||||
const request = await playwright.request.newContext();
|
||||
await use(request);
|
||||
await request.dispose();
|
||||
const hook = (test.info() as TestInfoImpl)._currentHookType();
|
||||
if (hook === 'beforeAll') {
|
||||
await request.dispose({ reason: [
|
||||
`Fixture { request } from beforeAll cannot be reused in a test.`,
|
||||
` - Recommended fix: use a separate { request } in the test.`,
|
||||
` - Alternatively, manually create APIRequestContext in beforeAll and dispose it in afterAll.`,
|
||||
`See https://playwright.dev/docs/api-testing#sending-api-requests-from-ui-tests for more details.`,
|
||||
].join('\n') });
|
||||
} else {
|
||||
await request.dispose();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ export interface TestServerInterface {
|
|||
timeout?: number,
|
||||
reporters?: string[],
|
||||
trace?: 'on' | 'off';
|
||||
video?: 'on' | 'off';
|
||||
projects?: string[];
|
||||
reuseContext?: boolean;
|
||||
connectWsEndpoint?: string;
|
||||
|
|
|
|||
|
|
@ -259,7 +259,6 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
|||
|
||||
const defaultTitle = `expect${this._info.isPoll ? '.poll' : ''}${this._info.isSoft ? '.soft' : ''}${this._info.isNot ? '.not' : ''}.${matcherName}${argsSuffix}`;
|
||||
const title = customMessage || defaultTitle;
|
||||
const wallTime = Date.now();
|
||||
|
||||
// This looks like it is unnecessary, but it isn't - we need to filter
|
||||
// out all the frames that belong to the test runner from caught runtime errors.
|
||||
|
|
@ -270,7 +269,6 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
|||
category: 'expect',
|
||||
title: trimLongString(title, 1024),
|
||||
params: args[0] ? { expected: args[0] } : undefined,
|
||||
wallTime,
|
||||
infectParentStepsWithError: this._info.isSoft,
|
||||
};
|
||||
|
||||
|
|
@ -295,7 +293,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
|||
// so they behave like a retriable step.
|
||||
const result = (matcherName === 'toPass' || this._info.isPoll) ?
|
||||
zones.run('stepZone', step, callback) :
|
||||
zones.run<ExpectZone, any>('expectZone', { title, wallTime }, callback);
|
||||
zones.run<ExpectZone, any>('expectZone', { title, stepId: step.stepId }, callback);
|
||||
if (result instanceof Promise)
|
||||
return result.then(finalizer).catch(reportStepError);
|
||||
finalizer();
|
||||
|
|
|
|||
|
|
@ -194,6 +194,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
|||
config.cliListOnly = !!opts.list;
|
||||
config.cliProjectFilter = opts.project || undefined;
|
||||
config.cliPassWithNoTests = !!opts.passWithNoTests;
|
||||
config.cliFailOnFlakyTests = !!opts.failOnFlakyTests;
|
||||
|
||||
const runner = new Runner(config);
|
||||
let status: FullResult['status'];
|
||||
|
|
@ -336,6 +337,7 @@ const testOptions: [string, string][] = [
|
|||
['--browser <browser>', `Browser to use for tests, one of "all", "chromium", "firefox" or "webkit" (default: "chromium")`],
|
||||
['-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`],
|
||||
['--debug', `Run tests with Playwright Inspector. Shortcut for "PWDEBUG=1" environment variable and "--timeout=0 --max-failures=1 --headed --workers=1" options`],
|
||||
['--fail-on-flaky-tests', `Fail if any test is flagged as flaky (default: false)`],
|
||||
['--forbid-only', `Fail if test.only is called (default: false)`],
|
||||
['--fully-parallel', `Run all tests in parallel (default: false)`],
|
||||
['--global-timeout <timeout>', `Maximum time this test suite can run in milliseconds (default: unlimited)`],
|
||||
|
|
|
|||
|
|
@ -45,27 +45,41 @@ type TestSummary = {
|
|||
fatalErrors: TestError[];
|
||||
};
|
||||
|
||||
export const isTTY = !!process.env.PWTEST_TTY_WIDTH || process.stdout.isTTY;
|
||||
export const ttyWidth = process.env.PWTEST_TTY_WIDTH ? parseInt(process.env.PWTEST_TTY_WIDTH, 10) : process.stdout.columns || 0;
|
||||
let useColors = isTTY;
|
||||
if (process.env.DEBUG_COLORS === '0'
|
||||
|| process.env.DEBUG_COLORS === 'false'
|
||||
|| process.env.FORCE_COLOR === '0'
|
||||
|| process.env.FORCE_COLOR === 'false')
|
||||
useColors = false;
|
||||
else if (process.env.DEBUG_COLORS || process.env.FORCE_COLOR)
|
||||
useColors = true;
|
||||
export const { isTTY, ttyWidth, colors } = (() => {
|
||||
let isTTY = !!process.stdout.isTTY;
|
||||
let ttyWidth = process.stdout.columns || 0;
|
||||
if (process.env.PLAYWRIGHT_FORCE_TTY === 'false' || process.env.PLAYWRIGHT_FORCE_TTY === '0') {
|
||||
isTTY = false;
|
||||
ttyWidth = 0;
|
||||
} else if (process.env.PLAYWRIGHT_FORCE_TTY === 'true' || process.env.PLAYWRIGHT_FORCE_TTY === '1') {
|
||||
isTTY = true;
|
||||
ttyWidth = process.stdout.columns || 100;
|
||||
} else if (process.env.PLAYWRIGHT_FORCE_TTY) {
|
||||
isTTY = true;
|
||||
ttyWidth = +process.env.PLAYWRIGHT_FORCE_TTY;
|
||||
if (isNaN(ttyWidth))
|
||||
ttyWidth = 100;
|
||||
}
|
||||
|
||||
export const colors = useColors ? realColors : {
|
||||
bold: (t: string) => t,
|
||||
cyan: (t: string) => t,
|
||||
dim: (t: string) => t,
|
||||
gray: (t: string) => t,
|
||||
green: (t: string) => t,
|
||||
red: (t: string) => t,
|
||||
yellow: (t: string) => t,
|
||||
enabled: false,
|
||||
};
|
||||
let useColors = isTTY;
|
||||
if (process.env.DEBUG_COLORS === '0' || process.env.DEBUG_COLORS === 'false' ||
|
||||
process.env.FORCE_COLOR === '0' || process.env.FORCE_COLOR === 'false')
|
||||
useColors = false;
|
||||
else if (process.env.DEBUG_COLORS || process.env.FORCE_COLOR)
|
||||
useColors = true;
|
||||
|
||||
const colors = useColors ? realColors : {
|
||||
bold: (t: string) => t,
|
||||
cyan: (t: string) => t,
|
||||
dim: (t: string) => t,
|
||||
gray: (t: string) => t,
|
||||
green: (t: string) => t,
|
||||
red: (t: string) => t,
|
||||
yellow: (t: string) => t,
|
||||
enabled: false,
|
||||
};
|
||||
return { isTTY, ttyWidth, colors };
|
||||
})();
|
||||
|
||||
export class BaseReporter implements ReporterV2 {
|
||||
config!: FullConfig;
|
||||
|
|
|
|||
|
|
@ -62,14 +62,14 @@ class HtmlReporter extends EmptyReporter {
|
|||
private _outputFolder!: string;
|
||||
private _attachmentsBaseURL!: string;
|
||||
private _open: string | undefined;
|
||||
private _port: number | undefined;
|
||||
private _host: string | undefined;
|
||||
private _buildResult: { ok: boolean, singleTestId: string | undefined } | undefined;
|
||||
private _topLevelErrors: TestError[] = [];
|
||||
|
||||
constructor(options: HtmlReporterOptions) {
|
||||
super();
|
||||
this._options = options;
|
||||
if (options._mode === 'test')
|
||||
process.env.PW_HTML_REPORT = '1';
|
||||
}
|
||||
|
||||
override printsToStdio() {
|
||||
|
|
@ -81,9 +81,11 @@ class HtmlReporter extends EmptyReporter {
|
|||
}
|
||||
|
||||
override onBegin(suite: Suite) {
|
||||
const { outputFolder, open, attachmentsBaseURL } = this._resolveOptions();
|
||||
const { outputFolder, open, attachmentsBaseURL, host, port } = this._resolveOptions();
|
||||
this._outputFolder = outputFolder;
|
||||
this._open = open;
|
||||
this._host = host;
|
||||
this._port = port;
|
||||
this._attachmentsBaseURL = attachmentsBaseURL;
|
||||
const reportedWarnings = new Set<string>();
|
||||
for (const project of this.config.projects) {
|
||||
|
|
@ -104,12 +106,14 @@ class HtmlReporter extends EmptyReporter {
|
|||
this.suite = suite;
|
||||
}
|
||||
|
||||
_resolveOptions(): { outputFolder: string, open: HtmlReportOpenOption, attachmentsBaseURL: string } {
|
||||
_resolveOptions(): { outputFolder: string, open: HtmlReportOpenOption, attachmentsBaseURL: string, host: string | undefined, port: number | undefined } {
|
||||
const outputFolder = reportFolderFromEnv() ?? resolveReporterOutputPath('playwright-report', this._options.configDir, this._options.outputFolder);
|
||||
return {
|
||||
outputFolder,
|
||||
open: getHtmlReportOptionProcessEnv() || this._options.open || 'on-failure',
|
||||
attachmentsBaseURL: this._options.attachmentsBaseURL || 'data/'
|
||||
attachmentsBaseURL: process.env.PLAYWRIGHT_HTML_ATTACHMENTS_BASE_URL || this._options.attachmentsBaseURL || 'data/',
|
||||
host: process.env.PLAYWRIGHT_HTML_HOST || this._options.host,
|
||||
port: process.env.PLAYWRIGHT_HTML_PORT ? +process.env.PLAYWRIGHT_HTML_PORT : this._options.port,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -135,12 +139,12 @@ class HtmlReporter extends EmptyReporter {
|
|||
const { ok, singleTestId } = this._buildResult;
|
||||
const shouldOpen = !this._options._isTestServer && (this._open === 'always' || (!ok && this._open === 'on-failure'));
|
||||
if (shouldOpen) {
|
||||
await showHTMLReport(this._outputFolder, this._options.host, this._options.port, singleTestId);
|
||||
await showHTMLReport(this._outputFolder, this._host, this._port, singleTestId);
|
||||
} 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}` : '';
|
||||
const portArg = this._options.port ? ` --port ${this._options.port}` : '';
|
||||
const hostArg = this._host ? ` --host ${this._host}` : '';
|
||||
const portArg = this._port ? ` --port ${this._port}` : '';
|
||||
console.log('');
|
||||
console.log('To open last HTML report run:');
|
||||
console.log(colors.cyan(`
|
||||
|
|
@ -151,18 +155,18 @@ class HtmlReporter extends EmptyReporter {
|
|||
}
|
||||
|
||||
function reportFolderFromEnv(): string | undefined {
|
||||
if (process.env[`PLAYWRIGHT_HTML_REPORT`])
|
||||
return path.resolve(process.cwd(), process.env[`PLAYWRIGHT_HTML_REPORT`]);
|
||||
return undefined;
|
||||
// Note: PLAYWRIGHT_HTML_REPORT is for backwards compatibility.
|
||||
const envValue = process.env.PLAYWRIGHT_HTML_OUTPUT_DIR || process.env.PLAYWRIGHT_HTML_REPORT;
|
||||
return envValue ? path.resolve(envValue) : undefined;
|
||||
}
|
||||
|
||||
function getHtmlReportOptionProcessEnv(): HtmlReportOpenOption | undefined {
|
||||
const processKey = 'PW_TEST_HTML_REPORT_OPEN';
|
||||
const htmlOpenEnv = process.env[processKey];
|
||||
// Note: PW_TEST_HTML_REPORT_OPEN is for backwards compatibility.
|
||||
const htmlOpenEnv = process.env.PLAYWRIGHT_HTML_OPEN || process.env.PW_TEST_HTML_REPORT_OPEN;
|
||||
if (!htmlOpenEnv)
|
||||
return undefined;
|
||||
if (!isHtmlReportOption(htmlOpenEnv)) {
|
||||
console.log(colors.red(`Configuration Error: HTML reporter Invalid value for ${processKey}: ${htmlOpenEnv}. Valid values are: ${htmlReportOptions.join(', ')}`));
|
||||
console.log(colors.red(`Configuration Error: HTML reporter Invalid value for PLAYWRIGHT_HTML_OPEN: ${htmlOpenEnv}. Valid values are: ${htmlReportOptions.join(', ')}`));
|
||||
return undefined;
|
||||
}
|
||||
return htmlOpenEnv;
|
||||
|
|
@ -182,7 +186,8 @@ export async function showHTMLReport(reportFolder: string | undefined, host: str
|
|||
return;
|
||||
}
|
||||
const server = startHtmlReportServer(folder);
|
||||
let url = await server.start({ port, host, preferredPort: port ? undefined : 9323 });
|
||||
await server.start({ port, host, preferredPort: port ? undefined : 9323 });
|
||||
let url = server.urlPrefix('human-readable');
|
||||
console.log('');
|
||||
console.log(colors.cyan(` Serving HTML report at ${url}. Press Ctrl+C to quit.`));
|
||||
if (testId)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import path from 'path';
|
|||
import type { FullConfig, FullResult, Suite, TestCase } from '../../types/testReporter';
|
||||
import { formatFailure, resolveOutputFile, stripAnsiEscapes } from './base';
|
||||
import EmptyReporter from './empty';
|
||||
import { getAsBooleanFromENV } from 'playwright-core/lib/utils';
|
||||
|
||||
type JUnitOptions = {
|
||||
outputFile?: string,
|
||||
|
|
@ -42,8 +43,8 @@ class JUnitReporter extends EmptyReporter {
|
|||
|
||||
constructor(options: JUnitOptions) {
|
||||
super();
|
||||
this.stripANSIControlSequences = options.stripANSIControlSequences || false;
|
||||
this.includeProjectInTestName = options.includeProjectInTestName || false;
|
||||
this.stripANSIControlSequences = getAsBooleanFromENV('PLAYWRIGHT_JUNIT_STRIP_ANSI', !!options.stripANSIControlSequences);
|
||||
this.includeProjectInTestName = getAsBooleanFromENV('PLAYWRIGHT_JUNIT_INCLUDE_PROJECT_IN_TEST_NAME', !!options.includeProjectInTestName);
|
||||
this.configDir = options.configDir;
|
||||
this.resolvedOutputFile = resolveOutputFile('JUNIT', options)?.outputFile;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
import { ms as milliseconds } from 'playwright-core/lib/utilsBundle';
|
||||
import { colors, BaseReporter, formatError, formatTestTitle, isTTY, stepSuffix, stripAnsiEscapes, ttyWidth } from './base';
|
||||
import type { FullResult, Suite, TestCase, TestError, TestResult, TestStep } from '../../types/testReporter';
|
||||
import { getAsBooleanFromENV } from 'playwright-core/lib/utils';
|
||||
|
||||
// Allow it in the Visual Studio Code Terminal and the new Windows Terminal
|
||||
const DOES_NOT_SUPPORT_UTF8_IN_TERMINAL = process.platform === 'win32' && process.env.TERM_PROGRAM !== 'vscode' && !process.env.WT_SESSION;
|
||||
|
|
@ -33,9 +34,9 @@ class ListReporter extends BaseReporter {
|
|||
private _needNewLine = false;
|
||||
private _printSteps: boolean;
|
||||
|
||||
constructor(options: { omitFailures?: boolean, printSteps?: boolean } = {}) {
|
||||
super(options);
|
||||
this._printSteps = isTTY && (options.printSteps || !!process.env.PW_TEST_DEBUG_REPORTERS_PRINT_STEPS);
|
||||
constructor(options: { printSteps?: boolean } = {}) {
|
||||
super();
|
||||
this._printSteps = isTTY && getAsBooleanFromENV('PLAYWRIGHT_LIST_PRINT_STEPS', options.printSteps);
|
||||
}
|
||||
|
||||
override printsToStdio() {
|
||||
|
|
@ -229,8 +230,6 @@ class ListReporter extends BaseReporter {
|
|||
// Go down if needed.
|
||||
if (row !== this._lastRow)
|
||||
process.stdout.write(`\u001B[${this._lastRow - row}E`);
|
||||
if (process.env.PWTEST_TTY_WIDTH)
|
||||
process.stdout.write('\n'); // For testing.
|
||||
}
|
||||
|
||||
private _testPrefix(index: string, statusMark: string) {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,15 @@ export class FailureTracker {
|
|||
}
|
||||
|
||||
result(): 'failed' | 'passed' {
|
||||
return this._hasWorkerErrors || this.hasReachedMaxFailures() || this._rootSuite?.allTests().some(test => !test.ok()) ? 'failed' : 'passed';
|
||||
return this._hasWorkerErrors || this.hasReachedMaxFailures() || this.hasFailedTests() || (this._config.cliFailOnFlakyTests && this.hasFlakyTests()) ? 'failed' : 'passed';
|
||||
}
|
||||
|
||||
hasFailedTests() {
|
||||
return this._rootSuite?.allTests().some(test => !test.ok());
|
||||
}
|
||||
|
||||
hasFlakyTests() {
|
||||
return this._rootSuite?.allTests().some(test => (test.outcome() === 'flaky'));
|
||||
}
|
||||
|
||||
maxFailures() {
|
||||
|
|
|
|||
|
|
@ -308,6 +308,7 @@ class TestServerDispatcher implements TestServerInterface {
|
|||
reporter: params.reporters ? params.reporters.map(r => [r]) : undefined,
|
||||
use: {
|
||||
trace: params.trace === 'on' ? { mode: 'on', sources: false, _live: true } : (params.trace === 'off' ? 'off' : undefined),
|
||||
video: params.video === 'on' ? 'on' : (params.video === 'off' ? 'off' : undefined),
|
||||
headless: params.headed ? false : undefined,
|
||||
_optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined,
|
||||
_optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined,
|
||||
|
|
@ -417,9 +418,9 @@ export async function runUIMode(configFile: string | undefined, options: TraceVi
|
|||
return await innerRunTestServer(configLocation, options, async (server: HttpServer, cancelPromise: ManualPromise<void>) => {
|
||||
await installRootRedirect(server, [], { ...options, webApp: 'uiMode.html' });
|
||||
if (options.host !== undefined || options.port !== undefined) {
|
||||
await openTraceInBrowser(server.urlPrefix());
|
||||
await openTraceInBrowser(server.urlPrefix('human-readable'));
|
||||
} else {
|
||||
const page = await openTraceViewerApp(server.urlPrefix(), 'chromium', {
|
||||
const page = await openTraceViewerApp(server.urlPrefix('precise'), 'chromium', {
|
||||
headless: isUnderTest() && process.env.PWTEST_HEADED_FOR_TEST !== '1',
|
||||
persistentContextOptions: {
|
||||
handleSIGINT: false,
|
||||
|
|
@ -434,7 +435,7 @@ export async function runTestServer(configFile: string | undefined, options: { h
|
|||
const configLocation = resolveConfigLocation(configFile);
|
||||
return await innerRunTestServer(configLocation, options, async server => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Listening on ' + server.urlPrefix().replace('http:', 'ws:') + '/' + server.wsGuid());
|
||||
console.log('Listening on ' + server.urlPrefix('precise').replace('http:', 'ws:') + '/' + server.wsGuid());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,8 +33,7 @@ export interface TestStepInternal {
|
|||
complete(result: { error?: Error, attachments?: Attachment[] }): void;
|
||||
stepId: string;
|
||||
title: string;
|
||||
category: 'hook' | 'fixture' | 'test.step' | string;
|
||||
wallTime: number;
|
||||
category: 'hook' | 'fixture' | 'test.step' | 'expect' | string;
|
||||
location?: Location;
|
||||
boxedStack?: StackFrame[];
|
||||
steps: TestStepInternal[];
|
||||
|
|
@ -322,11 +321,11 @@ export class TestInfoImpl implements TestInfo {
|
|||
parentStepId: parentStep ? parentStep.stepId : undefined,
|
||||
title: data.title,
|
||||
category: data.category,
|
||||
wallTime: data.wallTime,
|
||||
wallTime: Date.now(),
|
||||
location: data.location,
|
||||
};
|
||||
this._onStepBegin(payload);
|
||||
this._tracing.appendBeforeActionForStep(stepId, parentStep?.stepId, data.apiName || data.title, data.params, data.wallTime, data.location ? [data.location] : []);
|
||||
this._tracing.appendBeforeActionForStep(stepId, parentStep?.stepId, data.apiName || data.title, data.params, data.location ? [data.location] : []);
|
||||
return step;
|
||||
}
|
||||
|
||||
|
|
@ -355,7 +354,7 @@ export class TestInfoImpl implements TestInfo {
|
|||
const location = stage.runnable?.location ? ` at "${formatLocation(stage.runnable.location)}"` : ``;
|
||||
debugTest(`started stage "${stage.title}"${location}`);
|
||||
}
|
||||
stage.step = stage.stepInfo ? this._addStep({ ...stage.stepInfo, title: stage.title, wallTime: Date.now(), isStage: true }) : undefined;
|
||||
stage.step = stage.stepInfo ? this._addStep({ ...stage.stepInfo, title: stage.title, isStage: true }) : undefined;
|
||||
this._stages.push(stage);
|
||||
|
||||
try {
|
||||
|
|
@ -420,7 +419,6 @@ export class TestInfoImpl implements TestInfo {
|
|||
const step = this._addStep({
|
||||
title: `attach "${name}"`,
|
||||
category: 'attach',
|
||||
wallTime: Date.now(),
|
||||
});
|
||||
this._attachmentsPush(attachment);
|
||||
this._onAttach({
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import type { TestInfoImpl } from './testInfo';
|
|||
|
||||
export type Attachment = TestInfo['attachments'][0];
|
||||
export const testTraceEntryName = 'test.trace';
|
||||
const version: trace.VERSION = 7;
|
||||
let traceOrdinal = 0;
|
||||
|
||||
type TraceFixtureValue = PlaywrightWorkerOptions['trace'] | undefined;
|
||||
|
|
@ -41,11 +42,24 @@ export class TestTracing {
|
|||
private _temporaryTraceFiles: string[] = [];
|
||||
private _artifactsDir: string;
|
||||
private _tracesDir: string;
|
||||
private _contextCreatedEvent: trace.ContextCreatedTraceEvent;
|
||||
|
||||
constructor(testInfo: TestInfoImpl, artifactsDir: string) {
|
||||
this._testInfo = testInfo;
|
||||
this._artifactsDir = artifactsDir;
|
||||
this._tracesDir = path.join(this._artifactsDir, 'traces');
|
||||
this._contextCreatedEvent = {
|
||||
version,
|
||||
type: 'context-options',
|
||||
origin: 'testRunner',
|
||||
browserName: '',
|
||||
options: {},
|
||||
platform: process.platform,
|
||||
wallTime: Date.now(),
|
||||
monotonicTime: monotonicTime(),
|
||||
sdkLanguage: 'javascript',
|
||||
};
|
||||
this._appendTraceEvent(this._contextCreatedEvent);
|
||||
}
|
||||
|
||||
private _shouldCaptureTrace() {
|
||||
|
|
@ -220,12 +234,11 @@ export class TestTracing {
|
|||
});
|
||||
}
|
||||
|
||||
appendBeforeActionForStep(callId: string, parentId: string | undefined, apiName: string, params: Record<string, any> | undefined, wallTime: number, stack: StackFrame[]) {
|
||||
appendBeforeActionForStep(callId: string, parentId: string | undefined, apiName: string, params: Record<string, any> | undefined, stack: StackFrame[]) {
|
||||
this._appendTraceEvent({
|
||||
type: 'before',
|
||||
callId,
|
||||
parentId,
|
||||
wallTime,
|
||||
startTime: monotonicTime(),
|
||||
class: 'Test',
|
||||
method: 'step',
|
||||
|
|
|
|||
|
|
@ -100,12 +100,17 @@ export class WorkerMain extends ProcessRunner {
|
|||
override async gracefullyClose() {
|
||||
try {
|
||||
await this._stop();
|
||||
// Ignore top-level errors, they are already inside TestInfo.errors.
|
||||
const fakeTestInfo = new TestInfoImpl(this._config, this._project, this._params, undefined, 0, () => {}, () => {}, () => {});
|
||||
const runnable = { type: 'teardown' } as const;
|
||||
// We have to load the project to get the right deadline below.
|
||||
await this._loadIfNeeded();
|
||||
await this._teardownScopes();
|
||||
await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => this._loadIfNeeded()).catch(() => {});
|
||||
await this._fixtureRunner.teardownScope('test', fakeTestInfo, runnable).catch(() => {});
|
||||
await this._fixtureRunner.teardownScope('worker', fakeTestInfo, runnable).catch(() => {});
|
||||
// Close any other browsers launched in this process. This includes anything launched
|
||||
// manually in the test/hooks and internal browsers like Playwright Inspector.
|
||||
await gracefullyCloseAll();
|
||||
await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => gracefullyCloseAll()).catch(() => {});
|
||||
this._fatalErrors.push(...fakeTestInfo.errors);
|
||||
} catch (e) {
|
||||
this._fatalErrors.push(serializeError(e));
|
||||
}
|
||||
|
|
@ -144,15 +149,6 @@ export class WorkerMain extends ProcessRunner {
|
|||
}
|
||||
}
|
||||
|
||||
private async _teardownScopes() {
|
||||
const fakeTestInfo = new TestInfoImpl(this._config, this._project, this._params, undefined, 0, () => {}, () => {}, () => {});
|
||||
const runnable = { type: 'teardown' } as const;
|
||||
// Ignore top-level errors, they are already inside TestInfo.errors.
|
||||
await this._fixtureRunner.teardownScope('test', fakeTestInfo, runnable).catch(() => {});
|
||||
await this._fixtureRunner.teardownScope('worker', fakeTestInfo, runnable).catch(() => {});
|
||||
this._fatalErrors.push(...fakeTestInfo.errors);
|
||||
}
|
||||
|
||||
unhandledError(error: Error | any) {
|
||||
// No current test - fatal error.
|
||||
if (!this._currentTest) {
|
||||
|
|
|
|||
6
packages/playwright/types/test.d.ts
vendored
6
packages/playwright/types/test.d.ts
vendored
|
|
@ -2156,7 +2156,8 @@ interface TestFunction<TestArgs> {
|
|||
* test('basic test', {
|
||||
* annotation: {
|
||||
* type: 'issue',
|
||||
* description: 'https://github.com/microsoft/playwright/issues/23180',
|
||||
* description: 'feature tags API',
|
||||
* url: 'https://github.com/microsoft/playwright/issues/23180'
|
||||
* },
|
||||
* }, async ({ page }) => {
|
||||
* await page.goto('https://playwright.dev/');
|
||||
|
|
@ -2232,7 +2233,8 @@ interface TestFunction<TestArgs> {
|
|||
* test('basic test', {
|
||||
* annotation: {
|
||||
* type: 'issue',
|
||||
* description: 'https://github.com/microsoft/playwright/issues/23180',
|
||||
* description: 'feature tags API',
|
||||
* url: 'https://github.com/microsoft/playwright/issues/23180'
|
||||
* },
|
||||
* }, async ({ page }) => {
|
||||
* await page.goto('https://playwright.dev/');
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ export type CallMetadata = {
|
|||
// Service-side is making a call to itself, this metadata does not go
|
||||
// through the dispatcher, so is always excluded from inspector / tracing.
|
||||
isServerSide?: boolean;
|
||||
// Client wall time.
|
||||
wallTime: number;
|
||||
// Test runner step id.
|
||||
stepId?: string;
|
||||
location?: { file: string, line?: number, column?: number };
|
||||
log: string[];
|
||||
error?: SerializedError;
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ export type Metadata = {
|
|||
},
|
||||
apiName?: string,
|
||||
internal?: boolean,
|
||||
wallTime?: number,
|
||||
stepId?: string,
|
||||
};
|
||||
|
||||
export type ClientSideCallMetadata = {
|
||||
|
|
@ -309,7 +309,7 @@ export interface APIRequestContextChannel extends APIRequestContextEventTarget,
|
|||
fetchLog(params: APIRequestContextFetchLogParams, metadata?: CallMetadata): Promise<APIRequestContextFetchLogResult>;
|
||||
storageState(params?: APIRequestContextStorageStateParams, metadata?: CallMetadata): Promise<APIRequestContextStorageStateResult>;
|
||||
disposeAPIResponse(params: APIRequestContextDisposeAPIResponseParams, metadata?: CallMetadata): Promise<APIRequestContextDisposeAPIResponseResult>;
|
||||
dispose(params?: APIRequestContextDisposeParams, metadata?: CallMetadata): Promise<APIRequestContextDisposeResult>;
|
||||
dispose(params: APIRequestContextDisposeParams, metadata?: CallMetadata): Promise<APIRequestContextDisposeResult>;
|
||||
}
|
||||
export type APIRequestContextFetchParams = {
|
||||
url: string,
|
||||
|
|
@ -372,8 +372,12 @@ export type APIRequestContextDisposeAPIResponseOptions = {
|
|||
|
||||
};
|
||||
export type APIRequestContextDisposeAPIResponseResult = void;
|
||||
export type APIRequestContextDisposeParams = {};
|
||||
export type APIRequestContextDisposeOptions = {};
|
||||
export type APIRequestContextDisposeParams = {
|
||||
reason?: string,
|
||||
};
|
||||
export type APIRequestContextDisposeOptions = {
|
||||
reason?: string,
|
||||
};
|
||||
export type APIRequestContextDisposeResult = void;
|
||||
|
||||
export interface APIRequestContextEvents {
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ Metadata:
|
|||
column: number?
|
||||
apiName: string?
|
||||
internal: boolean?
|
||||
# The number of milliseconds elapsed since midnight, January 1, 1970 UTC.
|
||||
wallTime: number?
|
||||
# Test runner step id.
|
||||
stepId: string?
|
||||
|
||||
ClientSideCallMetadata:
|
||||
type: object
|
||||
|
|
@ -330,6 +330,8 @@ APIRequestContext:
|
|||
fetchUid: string
|
||||
|
||||
dispose:
|
||||
parameters:
|
||||
reason: string?
|
||||
|
||||
|
||||
APIResponse:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export type ContextEntry = {
|
|||
browserName: string;
|
||||
channel?: string;
|
||||
platform?: string;
|
||||
wallTime?: number;
|
||||
wallTime: number;
|
||||
sdkLanguage?: Language;
|
||||
testIdAttributeName?: string;
|
||||
title?: string;
|
||||
|
|
@ -58,6 +58,7 @@ export function createEmptyContext(): ContextEntry {
|
|||
origin: 'testRunner',
|
||||
traceUrl: '',
|
||||
startTime: Number.MAX_SAFE_INTEGER,
|
||||
wallTime: Number.MAX_SAFE_INTEGER,
|
||||
endTime: 0,
|
||||
browserName: '',
|
||||
options: {
|
||||
|
|
@ -71,6 +72,6 @@ export function createEmptyContext(): ContextEntry {
|
|||
events: [],
|
||||
errors: [],
|
||||
stdio: [],
|
||||
hasSource: false
|
||||
hasSource: false,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import type * as trace from '@trace/trace';
|
|||
import type * as traceV3 from './versions/traceV3';
|
||||
import type * as traceV4 from './versions/traceV4';
|
||||
import type * as traceV5 from './versions/traceV5';
|
||||
import type * as traceV6 from './versions/traceV6';
|
||||
import type { ActionEntry, ContextEntry, PageEntry } from './entries';
|
||||
import type { SnapshotStorage } from './snapshotStorage';
|
||||
|
||||
|
|
@ -71,12 +72,13 @@ export class TraceModernizer {
|
|||
switch (event.type) {
|
||||
case 'context-options': {
|
||||
this._version = event.version;
|
||||
contextEntry.origin = 'library';
|
||||
contextEntry.origin = event.origin;
|
||||
contextEntry.browserName = event.browserName;
|
||||
contextEntry.channel = event.channel;
|
||||
contextEntry.title = event.title;
|
||||
contextEntry.platform = event.platform;
|
||||
contextEntry.wallTime = event.wallTime;
|
||||
contextEntry.startTime = event.monotonicTime;
|
||||
contextEntry.sdkLanguage = event.sdkLanguage;
|
||||
contextEntry.options = event.options;
|
||||
contextEntry.testIdAttributeName = event.testIdAttributeName;
|
||||
|
|
@ -145,11 +147,11 @@ export class TraceModernizer {
|
|||
break;
|
||||
}
|
||||
case 'resource-snapshot':
|
||||
this._snapshotStorage!.addResource(event.snapshot);
|
||||
this._snapshotStorage.addResource(event.snapshot);
|
||||
contextEntry.resources.push(event.snapshot);
|
||||
break;
|
||||
case 'frame-snapshot':
|
||||
this._snapshotStorage!.addFrameSnapshot(event.snapshot);
|
||||
this._snapshotStorage.addFrameSnapshot(event.snapshot);
|
||||
break;
|
||||
}
|
||||
// Make sure there is a page entry for each page, even without screencast frames,
|
||||
|
|
@ -170,12 +172,18 @@ export class TraceModernizer {
|
|||
}
|
||||
}
|
||||
|
||||
private _processedContextCreatedEvent() {
|
||||
return this._version !== undefined;
|
||||
}
|
||||
|
||||
private _modernize(event: any): trace.TraceEvent[] {
|
||||
if (this._version === undefined)
|
||||
// In trace 6->7 we also need to modernize context-options event.
|
||||
let version = this._version || event.version;
|
||||
if (version === undefined)
|
||||
return [event];
|
||||
const lastVersion: trace.VERSION = 6;
|
||||
const lastVersion: trace.VERSION = 7;
|
||||
let events = [event];
|
||||
for (let version = this._version; version < lastVersion; ++version)
|
||||
for (; version < lastVersion; ++version)
|
||||
events = (this as any)[`_modernize_${version}_to_${version + 1}`].call(this, events);
|
||||
return events;
|
||||
}
|
||||
|
|
@ -341,8 +349,8 @@ export class TraceModernizer {
|
|||
return event;
|
||||
}
|
||||
|
||||
_modernize_5_to_6(events: traceV5.TraceEvent[]): trace.TraceEvent[] {
|
||||
const result: trace.TraceEvent[] = [];
|
||||
_modernize_5_to_6(events: traceV5.TraceEvent[]): traceV6.TraceEvent[] {
|
||||
const result: traceV6.TraceEvent[] = [];
|
||||
for (const event of events) {
|
||||
result.push(event);
|
||||
if (event.type !== 'after' || !event.log.length)
|
||||
|
|
@ -358,4 +366,35 @@ export class TraceModernizer {
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
_modernize_6_to_7(events: traceV6.TraceEvent[]): trace.TraceEvent[] {
|
||||
const result: trace.TraceEvent[] = [];
|
||||
if (!this._processedContextCreatedEvent() && events[0].type !== 'context-options') {
|
||||
const event: trace.ContextCreatedTraceEvent = {
|
||||
type: 'context-options',
|
||||
origin: 'testRunner',
|
||||
version: 7,
|
||||
browserName: '',
|
||||
options: {},
|
||||
platform: process.platform,
|
||||
wallTime: 0,
|
||||
monotonicTime: 0,
|
||||
sdkLanguage: 'javascript',
|
||||
};
|
||||
result.push(event);
|
||||
}
|
||||
for (const event of events) {
|
||||
if (event.type === 'context-options') {
|
||||
result.push({ ...event, monotonicTime: 0, origin: 'library' });
|
||||
continue;
|
||||
}
|
||||
// Take wall and monotonic time from the first event.
|
||||
if (!this._contextEntry.wallTime && event.type === 'before')
|
||||
this._contextEntry.wallTime = event.wallTime;
|
||||
if (!this._contextEntry.startTime && event.type === 'before')
|
||||
this._contextEntry.startTime = event.startTime;
|
||||
result.push(event);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,10 @@ import { CopyToClipboard } from './copyToClipboard';
|
|||
import { asLocator } from '@isomorphic/locatorGenerators';
|
||||
import type { Language } from '@isomorphic/locatorGenerators';
|
||||
import { PlaceholderPanel } from './placeholderPanel';
|
||||
import type { ActionTraceEventInContext } from './modelUtil';
|
||||
|
||||
export const CallTab: React.FunctionComponent<{
|
||||
action: ActionTraceEvent | undefined,
|
||||
action: ActionTraceEventInContext | undefined,
|
||||
sdkLanguage: Language | undefined,
|
||||
}> = ({ action, sdkLanguage }) => {
|
||||
if (!action)
|
||||
|
|
@ -34,7 +35,8 @@ export const CallTab: React.FunctionComponent<{
|
|||
// Strip down the waitForEventInfo data, we never need it.
|
||||
delete params.info;
|
||||
const paramKeys = Object.keys(params);
|
||||
const wallTime = action.wallTime ? new Date(action.wallTime).toLocaleString() : null;
|
||||
const timeMillis = action.startTime + (action.context.wallTime - action.context.startTime);
|
||||
const wallTime = new Date(timeMillis).toLocaleString();
|
||||
const duration = action.endTime ? msToString(action.endTime - action.startTime) : 'Timed Out';
|
||||
|
||||
return <div className='call-tab'>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export const LogTab: React.FunctionComponent<{
|
|||
if (!action || !action.log.length)
|
||||
return [];
|
||||
const log = action.log;
|
||||
const wallTimeOffset = action.wallTime - action.startTime;
|
||||
const wallTimeOffset = action.context.wallTime - action.context.startTime;
|
||||
const entries: { message: string, time: string }[] = [];
|
||||
for (let i = 0; i < log.length; ++i) {
|
||||
let time = '';
|
||||
|
|
|
|||
|
|
@ -157,6 +157,8 @@ function indexModel(context: ContextEntry) {
|
|||
}
|
||||
for (const event of context.events)
|
||||
(event as any)[contextSymbol] = context;
|
||||
for (const resource of context.resources)
|
||||
(resource as any)[contextSymbol] = context;
|
||||
}
|
||||
|
||||
function mergeActionsAndUpdateTiming(contexts: ContextEntry[]) {
|
||||
|
|
@ -215,23 +217,33 @@ function mergeActionsAndUpdateTimingSameTrace(contexts: ContextEntry[]) {
|
|||
const libraryContexts = contexts.filter(context => context.origin === 'library');
|
||||
const testRunnerContexts = contexts.filter(context => context.origin === 'testRunner');
|
||||
|
||||
// Library actions are replaced with corresponding test runner steps. Matching with
|
||||
// the test runner steps enables us to find parent steps.
|
||||
// - In the newer versions the actions are matched by explicit step id stored in the
|
||||
// library context actions.
|
||||
// - In the older versions the step id is not stored and the match is perfomed based on
|
||||
// action name and wallTime.
|
||||
const matchByStepId = libraryContexts.some(c => c.actions.some(a => !!a.stepId));
|
||||
|
||||
for (const context of libraryContexts) {
|
||||
for (const action of context.actions)
|
||||
map.set(`${action.apiName}@${action.wallTime}`, { ...action, context });
|
||||
for (const action of context.actions) {
|
||||
const key = matchByStepId ? action.stepId! : `${action.apiName}@${(action as any).wallTime}`;
|
||||
map.set(key, { ...action, context });
|
||||
}
|
||||
}
|
||||
|
||||
// Protocol call aka library contexts have startTime/endTime as server-side times.
|
||||
// Step aka test runner contexts have startTime/endTime as client-side times.
|
||||
// Adjust startTime/endTime on the library contexts to align them with the test
|
||||
// runner steps.
|
||||
const delta = monotonicTimeDeltaBetweenLibraryAndRunner(testRunnerContexts, map);
|
||||
const delta = monotonicTimeDeltaBetweenLibraryAndRunner(testRunnerContexts, map, matchByStepId);
|
||||
if (delta)
|
||||
adjustMonotonicTime(libraryContexts, delta);
|
||||
|
||||
const nonPrimaryIdToPrimaryId = new Map<string, string>();
|
||||
for (const context of testRunnerContexts) {
|
||||
for (const action of context.actions) {
|
||||
const key = `${action.apiName}@${action.wallTime}`;
|
||||
const key = matchByStepId ? action.callId : `${action.apiName}@${(action as any).wallTime}`;
|
||||
const existing = map.get(key);
|
||||
if (existing) {
|
||||
nonPrimaryIdToPrimaryId.set(action.callId, existing.callId);
|
||||
|
|
@ -276,7 +288,7 @@ function adjustMonotonicTime(contexts: ContextEntry[], monotonicTimeDelta: numbe
|
|||
}
|
||||
}
|
||||
|
||||
function monotonicTimeDeltaBetweenLibraryAndRunner(nonPrimaryContexts: ContextEntry[], libraryActions: Map<string, ActionTraceEventInContext>) {
|
||||
function monotonicTimeDeltaBetweenLibraryAndRunner(nonPrimaryContexts: ContextEntry[], libraryActions: Map<string, ActionTraceEventInContext>, matchByStepId: boolean) {
|
||||
// We cannot rely on wall time or monotonic time to be the in sync
|
||||
// between library and test runner contexts. So we find first action
|
||||
// that is present in both runner and library contexts and use it
|
||||
|
|
@ -286,7 +298,7 @@ function monotonicTimeDeltaBetweenLibraryAndRunner(nonPrimaryContexts: ContextEn
|
|||
for (const action of context.actions) {
|
||||
if (!action.startTime)
|
||||
continue;
|
||||
const key = `${action.apiName}@${action.wallTime}`;
|
||||
const key = matchByStepId ? action.stepId! : `${action.apiName}@${(action as any).wallTime}`;
|
||||
const libraryAction = libraryActions.get(key);
|
||||
if (libraryAction)
|
||||
return action.startTime - libraryAction.startTime;
|
||||
|
|
@ -320,7 +332,7 @@ export function idForAction(action: ActionTraceEvent) {
|
|||
return `${action.pageId || 'none'}:${action.callId}`;
|
||||
}
|
||||
|
||||
export function context(action: ActionTraceEvent | trace.EventTraceEvent): ContextEntry {
|
||||
export function context(action: ActionTraceEvent | trace.EventTraceEvent | ResourceSnapshot): ContextEntry {
|
||||
return (action as any)[contextSymbol];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,12 +21,14 @@ import './networkTab.css';
|
|||
import { NetworkResourceDetails } from './networkResourceDetails';
|
||||
import { bytesToString, msToString } from '@web/uiUtils';
|
||||
import { PlaceholderPanel } from './placeholderPanel';
|
||||
import type { MultiTraceModel } from './modelUtil';
|
||||
import { context, type MultiTraceModel } from './modelUtil';
|
||||
import { GridView, type RenderedGridCell } from '@web/components/gridView';
|
||||
import { SplitView } from '@web/components/splitView';
|
||||
import type { ContextEntry } from '../entries';
|
||||
|
||||
type NetworkTabModel = {
|
||||
resources: Entry[],
|
||||
contextIdMap: ContextIdMap,
|
||||
};
|
||||
|
||||
type RenderedEntry = {
|
||||
|
|
@ -39,6 +41,7 @@ type RenderedEntry = {
|
|||
start: number,
|
||||
route: string,
|
||||
resource: Entry,
|
||||
contextId: string,
|
||||
};
|
||||
type ColumnName = keyof RenderedEntry;
|
||||
type Sorting = { by: ColumnName, negate: boolean};
|
||||
|
|
@ -54,7 +57,8 @@ export function useNetworkTabModel(model: MultiTraceModel | undefined, selectedT
|
|||
});
|
||||
return filtered;
|
||||
}, [model, selectedTime]);
|
||||
return { resources };
|
||||
const contextIdMap = React.useMemo(() => new ContextIdMap(model), [model]);
|
||||
return { resources, contextIdMap };
|
||||
}
|
||||
|
||||
export const NetworkTab: React.FunctionComponent<{
|
||||
|
|
@ -66,11 +70,11 @@ export const NetworkTab: React.FunctionComponent<{
|
|||
const [selectedEntry, setSelectedEntry] = React.useState<RenderedEntry | undefined>(undefined);
|
||||
|
||||
const { renderedEntries } = React.useMemo(() => {
|
||||
const renderedEntries = networkModel.resources.map(entry => renderEntry(entry, boundaries));
|
||||
const renderedEntries = networkModel.resources.map(entry => renderEntry(entry, boundaries, networkModel.contextIdMap));
|
||||
if (sorting)
|
||||
sort(renderedEntries, sorting);
|
||||
return { renderedEntries };
|
||||
}, [networkModel.resources, sorting, boundaries]);
|
||||
}, [networkModel.resources, networkModel.contextIdMap, sorting, boundaries]);
|
||||
|
||||
if (!networkModel.resources.length)
|
||||
return <PlaceholderPanel text='No network calls' />;
|
||||
|
|
@ -81,7 +85,7 @@ export const NetworkTab: React.FunctionComponent<{
|
|||
selectedItem={selectedEntry}
|
||||
onSelected={item => setSelectedEntry(item)}
|
||||
onHighlighted={item => onEntryHovered(item?.resource)}
|
||||
columns={selectedEntry ? ['name'] : ['name', 'method', 'status', 'contentType', 'duration', 'size', 'start', 'route']}
|
||||
columns={visibleColumns(!!selectedEntry, renderedEntries)}
|
||||
columnTitle={columnTitle}
|
||||
columnWidth={columnWidth}
|
||||
isError={item => item.status.code >= 400}
|
||||
|
|
@ -100,6 +104,8 @@ export const NetworkTab: React.FunctionComponent<{
|
|||
};
|
||||
|
||||
const columnTitle = (column: ColumnName) => {
|
||||
if (column === 'contextId')
|
||||
return 'Source';
|
||||
if (column === 'name')
|
||||
return 'Name';
|
||||
if (column === 'method')
|
||||
|
|
@ -128,10 +134,28 @@ const columnWidth = (column: ColumnName) => {
|
|||
return 60;
|
||||
if (column === 'contentType')
|
||||
return 200;
|
||||
if (column === 'contextId')
|
||||
return 60;
|
||||
return 100;
|
||||
};
|
||||
|
||||
function visibleColumns(entrySelected: boolean, renderedEntries: RenderedEntry[]): (keyof RenderedEntry)[] {
|
||||
if (entrySelected)
|
||||
return ['name'];
|
||||
const columns: (keyof RenderedEntry)[] = [];
|
||||
if (hasMultipleContexts(renderedEntries))
|
||||
columns.push('contextId');
|
||||
columns.push('name', 'method', 'status', 'contentType', 'duration', 'size', 'start', 'route');
|
||||
return columns;
|
||||
}
|
||||
|
||||
const renderCell = (entry: RenderedEntry, column: ColumnName): RenderedGridCell => {
|
||||
if (column === 'contextId') {
|
||||
return {
|
||||
body: entry.contextId,
|
||||
title: entry.name.url,
|
||||
};
|
||||
}
|
||||
if (column === 'name') {
|
||||
return {
|
||||
body: entry.name.name,
|
||||
|
|
@ -159,7 +183,57 @@ const renderCell = (entry: RenderedEntry, column: ColumnName): RenderedGridCell
|
|||
return { body: '' };
|
||||
};
|
||||
|
||||
const renderEntry = (resource: Entry, boundaries: Boundaries): RenderedEntry => {
|
||||
class ContextIdMap {
|
||||
private _pagerefToShortId = new Map<string, string>();
|
||||
private _contextToId = new Map<ContextEntry, string>();
|
||||
private _lastPageId = 0;
|
||||
private _lastApiRequestContextId = 0;
|
||||
|
||||
constructor(model: MultiTraceModel | undefined) {}
|
||||
|
||||
contextId(resource: Entry): string {
|
||||
if (resource.pageref)
|
||||
return this._pageId(resource.pageref);
|
||||
else if (resource._apiRequest)
|
||||
return this._apiRequestContextId(resource);
|
||||
return '';
|
||||
}
|
||||
|
||||
private _pageId(pageref: string): string {
|
||||
let shortId = this._pagerefToShortId.get(pageref);
|
||||
if (!shortId) {
|
||||
++this._lastPageId;
|
||||
shortId = 'page#' + this._lastPageId;
|
||||
this._pagerefToShortId.set(pageref, shortId);
|
||||
}
|
||||
return shortId;
|
||||
}
|
||||
|
||||
private _apiRequestContextId(resource: Entry): string {
|
||||
const contextEntry = context(resource);
|
||||
if (!contextEntry)
|
||||
return '';
|
||||
let contextId = this._contextToId.get(contextEntry);
|
||||
if (!contextId) {
|
||||
++this._lastApiRequestContextId;
|
||||
contextId = 'api#' + this._lastApiRequestContextId;
|
||||
this._contextToId.set(contextEntry, contextId);
|
||||
}
|
||||
return contextId;
|
||||
}
|
||||
}
|
||||
|
||||
function hasMultipleContexts(renderedEntries: RenderedEntry[]): boolean {
|
||||
const contextIds = new Set<string>();
|
||||
for (const entry of renderedEntries) {
|
||||
contextIds.add(entry.contextId);
|
||||
if (contextIds.size > 1)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const renderEntry = (resource: Entry, boundaries: Boundaries, contextIdGenerator: ContextIdMap): RenderedEntry => {
|
||||
const routeStatus = formatRouteStatus(resource);
|
||||
let resourceName: string;
|
||||
try {
|
||||
|
|
@ -184,7 +258,8 @@ const renderEntry = (resource: Entry, boundaries: Boundaries): RenderedEntry =>
|
|||
size: resource.response._transferSize! > 0 ? resource.response._transferSize! : resource.response.bodySize,
|
||||
start: resource._monotonicTime! - boundaries.minimum,
|
||||
route: routeStatus,
|
||||
resource
|
||||
resource,
|
||||
contextId: contextIdGenerator.contextId(resource),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -249,4 +324,7 @@ function comparator(sortBy: ColumnName) {
|
|||
return a.route.localeCompare(b.route);
|
||||
};
|
||||
}
|
||||
|
||||
if (sortBy === 'contextId')
|
||||
return (a: RenderedEntry, b: RenderedEntry) => a.contextId.localeCompare(b.contextId);
|
||||
}
|
||||
|
|
|
|||
239
packages/trace-viewer/src/versions/traceV6.ts
Normal file
239
packages/trace-viewer/src/versions/traceV6.ts
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
/**
|
||||
* 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 { Entry as ResourceSnapshot } from '../../../trace/src/har';
|
||||
|
||||
type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl';
|
||||
type Point = { x: number, y: number };
|
||||
type Size = { width: number, height: number };
|
||||
|
||||
type StackFrame = {
|
||||
file: string,
|
||||
line: number,
|
||||
column: number,
|
||||
function?: string,
|
||||
};
|
||||
|
||||
type SerializedValue = {
|
||||
n?: number,
|
||||
b?: boolean,
|
||||
s?: string,
|
||||
v?: 'null' | 'undefined' | 'NaN' | 'Infinity' | '-Infinity' | '-0',
|
||||
d?: string,
|
||||
u?: string,
|
||||
bi?: string,
|
||||
m?: SerializedValue,
|
||||
se?: SerializedValue,
|
||||
r?: {
|
||||
p: string,
|
||||
f: string,
|
||||
},
|
||||
a?: SerializedValue[],
|
||||
o?: {
|
||||
k: string,
|
||||
v: SerializedValue,
|
||||
}[],
|
||||
h?: number,
|
||||
id?: number,
|
||||
ref?: number,
|
||||
};
|
||||
|
||||
type SerializedError = {
|
||||
error?: {
|
||||
message: string,
|
||||
name: string,
|
||||
stack?: string,
|
||||
},
|
||||
value?: SerializedValue,
|
||||
};
|
||||
|
||||
type NodeSnapshot =
|
||||
// Text node.
|
||||
string |
|
||||
// Subtree reference, "x snapshots ago, node #y". Could point to a text node.
|
||||
// Only nodes that are not references are counted, starting from zero, using post-order traversal.
|
||||
[ [number, number] ] |
|
||||
// Just node name.
|
||||
[ string ] |
|
||||
// Node name, attributes, child nodes.
|
||||
// Unfortunately, we cannot make this type definition recursive, therefore "any".
|
||||
[ string, { [attr: string]: string }, ...any ];
|
||||
|
||||
|
||||
type ResourceOverride = {
|
||||
url: string,
|
||||
sha1?: string,
|
||||
ref?: number
|
||||
};
|
||||
|
||||
type FrameSnapshot = {
|
||||
snapshotName?: string,
|
||||
callId: string,
|
||||
pageId: string,
|
||||
frameId: string,
|
||||
frameUrl: string,
|
||||
timestamp: number,
|
||||
collectionTime: number,
|
||||
doctype?: string,
|
||||
html: NodeSnapshot,
|
||||
resourceOverrides: ResourceOverride[],
|
||||
viewport: { width: number, height: number },
|
||||
isMainFrame: boolean,
|
||||
};
|
||||
|
||||
export type BrowserContextEventOptions = {
|
||||
viewport?: Size,
|
||||
deviceScaleFactor?: number,
|
||||
isMobile?: boolean,
|
||||
userAgent?: string,
|
||||
};
|
||||
|
||||
export type ContextCreatedTraceEvent = {
|
||||
version: number,
|
||||
type: 'context-options',
|
||||
browserName: string,
|
||||
channel?: string,
|
||||
platform: string,
|
||||
wallTime: number,
|
||||
title?: string,
|
||||
options: BrowserContextEventOptions,
|
||||
sdkLanguage?: Language,
|
||||
testIdAttributeName?: string,
|
||||
};
|
||||
|
||||
export type ScreencastFrameTraceEvent = {
|
||||
type: 'screencast-frame',
|
||||
pageId: string,
|
||||
sha1: string,
|
||||
width: number,
|
||||
height: number,
|
||||
timestamp: number,
|
||||
};
|
||||
|
||||
export type BeforeActionTraceEvent = {
|
||||
type: 'before',
|
||||
callId: string;
|
||||
startTime: number;
|
||||
apiName: string;
|
||||
class: string;
|
||||
method: string;
|
||||
params: Record<string, any>;
|
||||
wallTime: number;
|
||||
beforeSnapshot?: string;
|
||||
stack?: StackFrame[];
|
||||
pageId?: string;
|
||||
parentId?: string;
|
||||
};
|
||||
|
||||
export type InputActionTraceEvent = {
|
||||
type: 'input',
|
||||
callId: string;
|
||||
inputSnapshot?: string;
|
||||
point?: Point;
|
||||
};
|
||||
|
||||
export type AfterActionTraceEventAttachment = {
|
||||
name: string;
|
||||
contentType: string;
|
||||
path?: string;
|
||||
sha1?: string;
|
||||
base64?: string;
|
||||
};
|
||||
|
||||
export type AfterActionTraceEvent = {
|
||||
type: 'after',
|
||||
callId: string;
|
||||
endTime: number;
|
||||
afterSnapshot?: string;
|
||||
error?: SerializedError['error'];
|
||||
attachments?: AfterActionTraceEventAttachment[];
|
||||
result?: any;
|
||||
point?: Point;
|
||||
};
|
||||
|
||||
export type LogTraceEvent = {
|
||||
type: 'log',
|
||||
callId: string;
|
||||
time: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type EventTraceEvent = {
|
||||
type: 'event',
|
||||
time: number;
|
||||
class: string;
|
||||
method: string;
|
||||
params: any;
|
||||
pageId?: string;
|
||||
};
|
||||
|
||||
export type ConsoleMessageTraceEvent = {
|
||||
type: 'console';
|
||||
time: number;
|
||||
pageId?: string;
|
||||
messageType: string,
|
||||
text: string,
|
||||
args?: { preview: string, value: any }[],
|
||||
location: {
|
||||
url: string,
|
||||
lineNumber: number,
|
||||
columnNumber: number,
|
||||
},
|
||||
};
|
||||
|
||||
export type ResourceSnapshotTraceEvent = {
|
||||
type: 'resource-snapshot',
|
||||
snapshot: ResourceSnapshot,
|
||||
};
|
||||
|
||||
export type FrameSnapshotTraceEvent = {
|
||||
type: 'frame-snapshot',
|
||||
snapshot: FrameSnapshot,
|
||||
};
|
||||
|
||||
export type ActionTraceEvent = {
|
||||
type: 'action',
|
||||
} & Omit<BeforeActionTraceEvent, 'type'>
|
||||
& Omit<AfterActionTraceEvent, 'type'>
|
||||
& Omit<InputActionTraceEvent, 'type'>;
|
||||
|
||||
export type StdioTraceEvent = {
|
||||
type: 'stdout' | 'stderr';
|
||||
timestamp: number;
|
||||
text?: string;
|
||||
base64?: string;
|
||||
};
|
||||
|
||||
export type ErrorTraceEvent = {
|
||||
type: 'error';
|
||||
message: string;
|
||||
stack?: StackFrame[];
|
||||
};
|
||||
|
||||
export type TraceEvent =
|
||||
ContextCreatedTraceEvent |
|
||||
ScreencastFrameTraceEvent |
|
||||
ActionTraceEvent |
|
||||
BeforeActionTraceEvent |
|
||||
InputActionTraceEvent |
|
||||
AfterActionTraceEvent |
|
||||
EventTraceEvent |
|
||||
LogTraceEvent |
|
||||
ConsoleMessageTraceEvent |
|
||||
ResourceSnapshotTraceEvent |
|
||||
FrameSnapshotTraceEvent |
|
||||
StdioTraceEvent |
|
||||
ErrorTraceEvent;
|
||||
|
|
@ -21,7 +21,7 @@ import type { FrameSnapshot, ResourceSnapshot } from './snapshot';
|
|||
export type Size = { width: number, height: number };
|
||||
|
||||
// Make sure you add _modernize_N_to_N1(event: any) to traceModel.ts.
|
||||
export type VERSION = 6;
|
||||
export type VERSION = 7;
|
||||
|
||||
export type BrowserContextEventOptions = {
|
||||
viewport?: Size,
|
||||
|
|
@ -33,10 +33,12 @@ export type BrowserContextEventOptions = {
|
|||
export type ContextCreatedTraceEvent = {
|
||||
version: number,
|
||||
type: 'context-options',
|
||||
origin: 'testRunner' | 'library',
|
||||
browserName: string,
|
||||
channel?: string,
|
||||
platform: string,
|
||||
wallTime: number,
|
||||
monotonicTime: number,
|
||||
title?: string,
|
||||
options: BrowserContextEventOptions,
|
||||
sdkLanguage?: Language,
|
||||
|
|
@ -60,7 +62,7 @@ export type BeforeActionTraceEvent = {
|
|||
class: string;
|
||||
method: string;
|
||||
params: Record<string, any>;
|
||||
wallTime: number;
|
||||
stepId?: string;
|
||||
beforeSnapshot?: string;
|
||||
stack?: StackFrame[];
|
||||
pageId?: string;
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export function useMeasure<T extends Element>() {
|
|||
}
|
||||
|
||||
export function msToString(ms: number): string {
|
||||
if (!isFinite(ms))
|
||||
if (ms < 0 || !isFinite(ms))
|
||||
return '-';
|
||||
|
||||
if (ms === 0)
|
||||
|
|
|
|||
|
|
@ -23,3 +23,10 @@ test('unmount twice throws an error', async ({ mount }) => {
|
|||
await component.unmount();
|
||||
await expect(component.unmount()).rejects.toThrowError('Component was not mounted');
|
||||
});
|
||||
|
||||
test('mount then unmount then mount', async ({ mount }) => {
|
||||
let component = await mount(<Button title="Submit" />);
|
||||
await component.unmount();
|
||||
component = await mount(<Button title="Save" />);
|
||||
await expect(component).toContainText('Save');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,3 +23,10 @@ test('unmount twice throws an error', async ({ mount }) => {
|
|||
await component.unmount();
|
||||
await expect(component.unmount()).rejects.toThrowError('Component was not mounted');
|
||||
});
|
||||
|
||||
test('mount then unmount then mount', async ({ mount }) => {
|
||||
let component = await mount(<Button title="Submit" />);
|
||||
await component.unmount();
|
||||
component = await mount(<Button title="Save" />);
|
||||
await expect(component).toContainText('Save');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,3 +23,10 @@ test('unmount twice throws an error', async ({ mount }) => {
|
|||
await component.unmount();
|
||||
await expect(component.unmount()).rejects.toThrowError('Component was not mounted');
|
||||
});
|
||||
|
||||
test('mount then unmount then mount', async ({ mount }) => {
|
||||
let component = await mount(<Button title="Submit" />);
|
||||
await component.unmount();
|
||||
component = await mount(<Button title="Save" />);
|
||||
await expect(component).toContainText('Save');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,3 +31,18 @@ test('unmount twice throws an error', async ({ mount }) => {
|
|||
await component.unmount();
|
||||
await expect(component.unmount()).rejects.toThrowError('Component was not mounted');
|
||||
});
|
||||
|
||||
test('mount then unmount then mount', async ({ mount }) => {
|
||||
let component = await mount(Button, {
|
||||
props: {
|
||||
title: 'Submit',
|
||||
},
|
||||
});
|
||||
await component.unmount();
|
||||
component = await mount(Button, {
|
||||
props: {
|
||||
title: 'Save',
|
||||
},
|
||||
});
|
||||
await expect(component).toContainText('Save');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { test, expect } from '@playwright/experimental-ct-vue';
|
||||
import MultiRoot from '@/components/MultiRoot.vue';
|
||||
import Button from '@/components/Button.vue';
|
||||
|
||||
test('unmount a multi root component', async ({ mount, page }) => {
|
||||
const component = await mount(MultiRoot);
|
||||
|
|
@ -9,3 +10,19 @@ test('unmount a multi root component', async ({ mount, page }) => {
|
|||
await expect(page.locator('#root')).not.toContainText('root 1');
|
||||
await expect(page.locator('#root')).not.toContainText('root 2');
|
||||
});
|
||||
|
||||
test('mount then unmount then mount', async ({ mount }) => {
|
||||
let component = await mount(Button, {
|
||||
props: {
|
||||
title: 'Submit',
|
||||
},
|
||||
});
|
||||
await component.unmount();
|
||||
component = await mount(Button, {
|
||||
props: {
|
||||
title: 'Save',
|
||||
},
|
||||
});
|
||||
await expect(component).toContainText('Save');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -31,3 +31,18 @@ test('unmount twice throws an error', async ({ mount }) => {
|
|||
await component.unmount();
|
||||
await expect(component.unmount()).rejects.toThrowError('Component was not mounted');
|
||||
});
|
||||
|
||||
test('mount then unmount then mount', async ({ mount }) => {
|
||||
let component = await mount(Button, {
|
||||
props: {
|
||||
title: 'Submit',
|
||||
},
|
||||
});
|
||||
await component.unmount();
|
||||
component = await mount(Button, {
|
||||
props: {
|
||||
title: 'Save',
|
||||
},
|
||||
});
|
||||
await expect(component).toContainText('Save');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,3 +16,10 @@ test('unmount twice throws an error', async ({ mount }) => {
|
|||
await component.unmount();
|
||||
await expect(component.unmount()).rejects.toThrowError('Component was not mounted');
|
||||
});
|
||||
|
||||
test('mount then unmount then mount', async ({ mount }) => {
|
||||
let component = await mount(<Button title="Submit" />);
|
||||
await component.unmount();
|
||||
component = await mount(<Button title="Save" />);
|
||||
await expect(component).toContainText('Save');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,3 +21,18 @@ test('unmount twice throws an error', async ({ mount }) => {
|
|||
await component.unmount();
|
||||
await expect(component.unmount()).rejects.toThrowError('Component was not mounted');
|
||||
});
|
||||
|
||||
test('mount then unmount then mount', async ({ mount }) => {
|
||||
let component = await mount(Button, {
|
||||
props: {
|
||||
title: 'Submit',
|
||||
},
|
||||
});
|
||||
await component.unmount();
|
||||
component = await mount(Button, {
|
||||
props: {
|
||||
title: 'Save',
|
||||
},
|
||||
});
|
||||
await expect(component).toContainText('Save');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,3 +13,10 @@ test('unmount twice throws an error', async ({ mount }) => {
|
|||
await component.unmount();
|
||||
await expect(component.unmount()).rejects.toThrowError('Component was not mounted');
|
||||
});
|
||||
|
||||
test('mount then unmount then mount', async ({ mount }) => {
|
||||
let component = await mount(<Button title="Submit" />);
|
||||
await component.unmount();
|
||||
component = await mount(<Button title="Save" />);
|
||||
await expect(component).toContainText('Save');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
import { test } from './npmTest';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
test('electron should work', async ({ exec, tsc, writeFiles }) => {
|
||||
await exec('npm i playwright electron@19.0.11');
|
||||
|
|
@ -24,3 +26,16 @@ test('electron should work', async ({ exec, tsc, writeFiles }) => {
|
|||
});
|
||||
await tsc('test.ts');
|
||||
});
|
||||
|
||||
test('electron should work with special characters in path', async ({ exec, tmpWorkspace }) => {
|
||||
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30755' });
|
||||
const folderName = path.join(tmpWorkspace, '!@#$% тест with spaces and 😊');
|
||||
|
||||
await exec('npm i playwright electron@19.0.11');
|
||||
await fs.promises.mkdir(folderName);
|
||||
for (const file of ['electron-app.js', 'sanity-electron.js'])
|
||||
await fs.promises.copyFile(path.join(tmpWorkspace, file), path.join(folderName, file));
|
||||
await exec('node sanity-electron.js', {
|
||||
cwd: path.join(folderName)
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -198,6 +198,20 @@ it('should follow redirects', async ({ context, server }) => {
|
|||
expect(await response.json()).toEqual({ foo: 'bar' });
|
||||
});
|
||||
|
||||
it('should follow redirects correctly when Location header contains UTF-8 characters', async ({ context, server }) => {
|
||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30903' });
|
||||
server.setRoute('/redirect', (req, res) => {
|
||||
// Node.js only allows US-ASCII, so we can't send invalid headers directly. Sending it as a raw response instead.
|
||||
res.socket.write('HTTP/1.1 301 Moved Permanently\r\n');
|
||||
res.socket.write(`Location: ${server.PREFIX}/empty.html?message=マスクПривет\r\n`);
|
||||
res.socket.write('\r\n');
|
||||
res.socket.uncork();
|
||||
res.socket.end();
|
||||
});
|
||||
const response = await context.request.get(server.PREFIX + '/redirect');
|
||||
expect(response.url()).toBe(server.PREFIX + '/empty.html?' + new URLSearchParams({ message: 'マスクПривет' }));
|
||||
});
|
||||
|
||||
it('should add cookies from Set-Cookie header', async ({ context, page, server }) => {
|
||||
server.setRoute('/setcookie.html', (req, res) => {
|
||||
res.setHeader('Set-Cookie', ['session=value', 'foo=bar; max-age=3600']);
|
||||
|
|
@ -794,21 +808,6 @@ it('should respect timeout after redirects', async function({ context, server })
|
|||
expect(error.message).toContain(`Request timed out after 100ms`);
|
||||
});
|
||||
|
||||
it('should throw on a redirect with an invalid URL', async ({ context, server }) => {
|
||||
server.setRedirect('/redirect', '/test');
|
||||
server.setRoute('/test', (req, res) => {
|
||||
// Node.js prevents us from responding with an invalid header, therefore we manually write the response.
|
||||
const conn = res.connection!;
|
||||
conn.write('HTTP/1.1 302\r\n');
|
||||
conn.write('Location: https://здравствуйте/\r\n');
|
||||
conn.write('\r\n');
|
||||
conn.uncork();
|
||||
conn.end();
|
||||
});
|
||||
const error = await context.request.get(server.PREFIX + '/redirect').catch(e => e);
|
||||
expect(error.message).toContain('apiRequestContext.get: uri requested responds with an invalid redirect URL');
|
||||
});
|
||||
|
||||
it('should not hang on a brotli encoded Range request', async ({ context, server }) => {
|
||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/18190' });
|
||||
it.skip(+process.versions.node.split('.')[0] < 18);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ it('SharedArrayBuffer should work @smoke', async function({ contextFactory, http
|
|||
expect(await page.evaluate(() => typeof SharedArrayBuffer)).toBe('function');
|
||||
});
|
||||
|
||||
it('Web Assembly should work @smoke', async function({ page, server }) {
|
||||
it('Web Assembly should work @smoke', async ({ page, server, browserName, platform }) => {
|
||||
it.fixme(browserName === 'webkit' && platform === 'win32', 'Windows JIT is disabled: https://bugs.webkit.org/show_bug.cgi?id=273854');
|
||||
await page.goto(server.PREFIX + '/wasm/table2.html');
|
||||
expect(await page.evaluate('loadTable()')).toBe('42, 83');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -203,6 +203,15 @@ it.describe('selector generator', () => {
|
|||
expect(await generate(page, 'div div')).toBe(`div >> internal:has-text=/^Hello world$/`);
|
||||
});
|
||||
|
||||
it('should use internal:has-text with regexp with a quote', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<span>Hello'world</span>
|
||||
<div><div>Hello'<span>world</span></div>extra</div>
|
||||
<a>Goodbye'<span>world</span></a>
|
||||
`);
|
||||
expect(await generate(page, 'div div')).toBe(`div >> internal:has-text=/^Hello\\'world$/`);
|
||||
});
|
||||
|
||||
it('should chain text after parent', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<div>Hello <span>world</span></div>
|
||||
|
|
|
|||
|
|
@ -19,11 +19,9 @@ import { test as it, expect } from './pageTest';
|
|||
import { attachFrame } from '../config/utils';
|
||||
import fs from 'fs';
|
||||
|
||||
function adjustServerHeaders(headers: Object, browserName: string, channel: string) {
|
||||
if (browserName === 'firefox' && channel === 'firefox-beta') {
|
||||
// This is a new experimental feature, only enabled in Firefox Beta for now.
|
||||
function adjustServerHeaders(headers: Object, browserName: string) {
|
||||
if (browserName === 'firefox')
|
||||
delete headers['priority'];
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +88,7 @@ it('should return headers', async ({ page, server, browserName }) => {
|
|||
expect(response.request().headers()['user-agent']).toContain('WebKit');
|
||||
});
|
||||
|
||||
it('should get the same headers as the server', async ({ page, server, browserName, platform, isElectron, browserMajorVersion, channel }) => {
|
||||
it('should get the same headers as the server', async ({ page, server, browserName, platform, isElectron, browserMajorVersion }) => {
|
||||
it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99');
|
||||
it.fail(browserName === 'webkit' && platform === 'win32', 'Curl does not show accept-encoding and accept-language');
|
||||
let serverRequest;
|
||||
|
|
@ -100,10 +98,10 @@ it('should get the same headers as the server', async ({ page, server, browserNa
|
|||
});
|
||||
const response = await page.goto(server.PREFIX + '/empty.html');
|
||||
const headers = await response.request().allHeaders();
|
||||
expect(headers).toEqual(adjustServerHeaders(serverRequest.headers, browserName, channel));
|
||||
expect(headers).toEqual(adjustServerHeaders(serverRequest.headers, browserName));
|
||||
});
|
||||
|
||||
it('should not return allHeaders() until they are available', async ({ page, server, browserName, platform, isElectron, browserMajorVersion, channel }) => {
|
||||
it('should not return allHeaders() until they are available', async ({ page, server, browserName, platform, isElectron, browserMajorVersion }) => {
|
||||
it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99');
|
||||
it.fail(browserName === 'webkit' && platform === 'win32', 'Curl does not show accept-encoding and accept-language');
|
||||
|
||||
|
|
@ -122,13 +120,13 @@ it('should not return allHeaders() until they are available', async ({ page, ser
|
|||
|
||||
await page.goto(server.PREFIX + '/empty.html');
|
||||
const requestHeaders = await requestHeadersPromise;
|
||||
expect(requestHeaders).toEqual(adjustServerHeaders(serverRequest.headers, browserName, channel));
|
||||
expect(requestHeaders).toEqual(adjustServerHeaders(serverRequest.headers, browserName));
|
||||
|
||||
const responseHeaders = await responseHeadersPromise;
|
||||
expect(responseHeaders['foo']).toBe('bar');
|
||||
});
|
||||
|
||||
it('should get the same headers as the server CORS', async ({ page, server, browserName, platform, isElectron, browserMajorVersion, channel }) => {
|
||||
it('should get the same headers as the server CORS', async ({ page, server, browserName, platform, isElectron, browserMajorVersion, }) => {
|
||||
it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99');
|
||||
it.fail(browserName === 'webkit' && platform === 'win32', 'Curl does not show accept-encoding and accept-language');
|
||||
|
||||
|
|
@ -147,7 +145,7 @@ it('should get the same headers as the server CORS', async ({ page, server, brow
|
|||
expect(text).toBe('done');
|
||||
const response = await responsePromise;
|
||||
const headers = await response.request().allHeaders();
|
||||
expect(headers).toEqual(adjustServerHeaders(serverRequest.headers, browserName, channel));
|
||||
expect(headers).toEqual(adjustServerHeaders(serverRequest.headers, browserName));
|
||||
});
|
||||
|
||||
it('should not get preflight CORS requests when intercepting', async ({ page, server, browserName, isAndroid }) => {
|
||||
|
|
@ -408,10 +406,9 @@ it('should report raw headers', async ({ page, server, browserName, platform, is
|
|||
return { name, value: values[0] };
|
||||
});
|
||||
}
|
||||
if (browserName === 'firefox' && channel === 'firefox-beta') {
|
||||
// This is a new experimental feature, only enabled in Firefox Beta for now.
|
||||
if (browserName === 'firefox')
|
||||
expectedHeaders = expectedHeaders.filter(({ name }) => name.toLowerCase() !== 'priority');
|
||||
}
|
||||
|
||||
res.end();
|
||||
});
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
|
|
|||
|
|
@ -477,3 +477,46 @@ it('should intercept css variable with background url', async ({ page, server })
|
|||
await page.waitForTimeout(1000);
|
||||
expect(interceptedRequests).toBe(1);
|
||||
});
|
||||
|
||||
it('continue should not change multipart/form-data body', async ({ page, server, browserName }) => {
|
||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/19158' });
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
server.setRoute('/upload', (request, response) => {
|
||||
response.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
response.end('done');
|
||||
});
|
||||
async function sendFormData() {
|
||||
const reqPromise = server.waitForRequest('/upload');
|
||||
const status = await page.evaluate(async () => {
|
||||
const newFile = new File(['file content'], 'file.txt');
|
||||
const formData = new FormData();
|
||||
formData.append('file', newFile);
|
||||
const response = await fetch('/upload', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData,
|
||||
});
|
||||
return response.status;
|
||||
});
|
||||
const req = await reqPromise;
|
||||
expect(status).toBe(200);
|
||||
return req;
|
||||
}
|
||||
const reqBefore = await sendFormData();
|
||||
await page.route('**/*', async route => {
|
||||
await route.continue();
|
||||
});
|
||||
const reqAfter = await sendFormData();
|
||||
const fileContent = [
|
||||
'Content-Disposition: form-data; name=\"file\"; filename=\"file.txt\"',
|
||||
'Content-Type: application/octet-stream',
|
||||
'',
|
||||
'file content',
|
||||
'------'].join('\r\n');
|
||||
expect.soft((await reqBefore.postBody).toString('utf8')).toContain(fileContent);
|
||||
expect.soft((await reqAfter.postBody).toString('utf8')).toContain(fileContent);
|
||||
// Firefox sends a bit longer boundary.
|
||||
const expectedLength = browserName === 'firefox' ? '246' : '208';
|
||||
expect.soft(reqBefore.headers['content-length']).toBe(expectedLength);
|
||||
expect.soft(reqAfter.headers['content-length']).toBe(expectedLength);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -78,8 +78,9 @@ it('should work with status code 422', async ({ page, server }) => {
|
|||
expect(await page.evaluate(() => document.body.textContent)).toBe('Yo, page!');
|
||||
});
|
||||
|
||||
it('should throw exception if status code is not supported', async ({ page, server, browserName }) => {
|
||||
it('should fulfill with unuassigned status codes', async ({ page, server, browserName }) => {
|
||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/28490' });
|
||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30773' });
|
||||
let fulfillPromiseCallback;
|
||||
const fulfillPromise = new Promise<Error|undefined>(f => fulfillPromiseCallback = f);
|
||||
await page.route('**/data.json', route => {
|
||||
|
|
@ -89,14 +90,14 @@ it('should throw exception if status code is not supported', async ({ page, serv
|
|||
}).catch(e => e));
|
||||
});
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
page.evaluate(url => fetch(url), server.PREFIX + '/data.json').catch(() => {});
|
||||
const response = await page.evaluate(async url => {
|
||||
const { status, statusText } = await fetch(url);
|
||||
return { status, statusText };
|
||||
}, server.PREFIX + '/data.json');
|
||||
const error = await fulfillPromise;
|
||||
if (browserName === 'chromium') {
|
||||
expect(error).toBeTruthy();
|
||||
expect(error.message).toContain(' Invalid http status code or phrase');
|
||||
} else {
|
||||
expect(error).toBe(undefined);
|
||||
}
|
||||
expect(error).toBe(undefined);
|
||||
expect(response.status).toBe(430);
|
||||
expect(response.statusText).toBe('Unknown');
|
||||
});
|
||||
|
||||
it('should not throw if request was cancelled by the page', async ({ page, server }) => {
|
||||
|
|
@ -456,3 +457,30 @@ it('should fulfill with gzip and readback', {
|
|||
await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)');
|
||||
expect(await response.text()).toContain(`<div>hello, world!</div>`);
|
||||
});
|
||||
|
||||
it('should not go to the network for fulfilled requests body', {
|
||||
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30760' },
|
||||
}, async ({ page, server, browserName }) => {
|
||||
await page.route('**/one-style.css', async route => {
|
||||
return route.fulfill({
|
||||
status: 404,
|
||||
contentType: 'text/plain',
|
||||
body: 'Not Found! (mocked)',
|
||||
});
|
||||
});
|
||||
|
||||
let serverHit = false;
|
||||
server.setRoute('/one-style.css', (req, res) => {
|
||||
serverHit = true;
|
||||
res.setHeader('Content-Type', 'text/css');
|
||||
res.end('body { background-color: green; }');
|
||||
});
|
||||
|
||||
const responsePromise = page.waitForResponse('**/one-style.css');
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
const response = await responsePromise;
|
||||
const body = await response.body();
|
||||
expect(body).toBeTruthy();
|
||||
expect(serverHit).toBe(false);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -221,6 +221,17 @@ it.describe('page screenshot', () => {
|
|||
expect(screenshot).toMatchSnapshot('screenshot-grid-fullpage.png');
|
||||
});
|
||||
|
||||
it('should take fullPage screenshots and mask elements outside of it', async ({ page, server }) => {
|
||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30770' });
|
||||
await page.setViewportSize({ width: 500, height: 500 });
|
||||
await page.goto(server.PREFIX + '/grid.html');
|
||||
const screenshot = await page.screenshot({
|
||||
fullPage: true,
|
||||
mask: [page.locator('.box').nth(144)],
|
||||
});
|
||||
expect(screenshot).toMatchSnapshot('screenshot-grid-fullpage-mask-outside-viewport.png');
|
||||
});
|
||||
|
||||
it('should restore viewport after fullPage screenshot', async ({ page, server }) => {
|
||||
await page.setViewportSize({ width: 500, height: 500 });
|
||||
await page.goto(server.PREFIX + '/grid.html');
|
||||
|
|
@ -482,36 +493,6 @@ it.describe('page screenshot', () => {
|
|||
})).toMatchSnapshot('should-mask-inside-iframe.png');
|
||||
});
|
||||
|
||||
it('should mask inside <dialog />', async ({ page, server, browserName, isElectron }) => {
|
||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/29878' });
|
||||
it.skip(browserName === 'webkit' && process.platform === 'darwin' && parseInt(os.release().split('.')[0], 10) < 23, 'SDKAlignedBehavior::PopoverAttributeEnabled is only enabled in macOS 14.0+');
|
||||
it.fixme(isElectron, 'Requires a more recent Electron version');
|
||||
|
||||
await page.setViewportSize({ width: 500, height: 500 });
|
||||
await page.goto(server.PREFIX + '/grid.html');
|
||||
await page.evaluate(() => {
|
||||
const elements = document.body.innerHTML;
|
||||
document.body.innerHTML = '';
|
||||
// Move all the elements of the body (grid elements) into a <dialog /> which lives on the Top-Layer.
|
||||
const dialog = document.createElement('dialog');
|
||||
dialog.style.padding = '0';
|
||||
dialog.style.margin = '0';
|
||||
dialog.style.border = 'none';
|
||||
dialog.style.maxWidth = 'inherit';
|
||||
dialog.style.maxHeight = 'inherit';
|
||||
dialog.style.outline = 'none';
|
||||
document.body.appendChild(dialog);
|
||||
dialog.innerHTML = elements;
|
||||
dialog.showModal();
|
||||
});
|
||||
expect(await page.screenshot({
|
||||
mask: [
|
||||
page.locator('div').nth(5),
|
||||
page.frameLocator('#frame1').locator('div').nth(12),
|
||||
],
|
||||
})).toMatchSnapshot('should-mask-inside-iframe.png');
|
||||
});
|
||||
|
||||
it('should mask in parallel', async ({ page, server }) => {
|
||||
await page.setViewportSize({ width: 500, height: 500 });
|
||||
await attachFrame(page, 'frame1', server.PREFIX + '/grid.html');
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
|
|
@ -39,9 +39,10 @@ test('should support import assertions', async ({ runInlineTest }) => {
|
|||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
import packageJSON from './package.json' assert { type: 'json' };
|
||||
console.log('imported value: ' + packageJSON.foo);
|
||||
export default { };
|
||||
`,
|
||||
'package.json': JSON.stringify({ type: 'module' }),
|
||||
'package.json': JSON.stringify({ type: 'module', foo: 'bar' }),
|
||||
'a.esm.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
|
|
@ -53,23 +54,28 @@ test('should support import assertions', async ({ runInlineTest }) => {
|
|||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.stdout).toContain('imported value: bar');
|
||||
});
|
||||
|
||||
test('should support import attributes', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
import packageJSON from './package.json' with { type: 'json' };
|
||||
console.log('imported value (config): ' + packageJSON.foo);
|
||||
export default { };
|
||||
`,
|
||||
'package.json': JSON.stringify({ type: 'module' }),
|
||||
'package.json': JSON.stringify({ type: 'module', foo: 'bar' }),
|
||||
'a.test.ts': `
|
||||
import config from './config.json' with { type: 'json' };
|
||||
import config from './package.json' with { type: 'json' };
|
||||
console.log('imported value (test): ' + config.foo);
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('pass', async () => {});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.stdout).toContain('imported value (config): bar');
|
||||
expect(result.stdout).toContain('imported value (test): bar');
|
||||
});
|
||||
|
||||
test('should import esm from ts when package.json has type module in experimental mode', async ({ runInlineTest }) => {
|
||||
|
|
|
|||
|
|
@ -128,6 +128,19 @@ test('should allow flaky', async ({ runInlineTest }) => {
|
|||
expect(result.flaky).toBe(1);
|
||||
});
|
||||
|
||||
test('failOnFlakyTests flag disallows flaky', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.js': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('flake', async ({}, testInfo) => {
|
||||
expect(testInfo.retry).toBe(1);
|
||||
});
|
||||
`,
|
||||
}, { 'retries': 1, 'fail-on-flaky-tests': true });
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.flaky).toBe(1);
|
||||
});
|
||||
|
||||
test('should fail on unexpected pass', async ({ runInlineTest }) => {
|
||||
const { exitCode, failed, output } = await runInlineTest({
|
||||
'unexpected-pass.spec.js': `
|
||||
|
|
|
|||
|
|
@ -224,6 +224,7 @@ export function cleanEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|||
GITHUB_SHA: undefined,
|
||||
// END: Reserved CI
|
||||
PW_TEST_HTML_REPORT_OPEN: undefined,
|
||||
PLAYWRIGHT_HTML_OPEN: undefined,
|
||||
PW_TEST_REPORTER: undefined,
|
||||
PW_TEST_REPORTER_WS_ENDPOINT: undefined,
|
||||
PW_TEST_SOURCE_TRANSFORM: undefined,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue