Merge branch 'main' into mockingproxy-headers-only
This commit is contained in:
commit
a5f4be6533
|
|
@ -3,9 +3,21 @@ name: Roll Browser into Playwright
|
|||
on:
|
||||
repository_dispatch:
|
||||
types: [roll_into_pw]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
browser:
|
||||
description: 'Browser name, e.g. chromium'
|
||||
required: true
|
||||
type: string
|
||||
revision:
|
||||
description: 'Browser revision without v prefix, e.g. 1234'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
||||
BROWSER: ${{ github.event.client_payload.browser || github.event.inputs.browser }}
|
||||
REVISION: ${{ github.event.client_payload.revision || github.event.inputs.revision }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
|
@ -24,19 +36,19 @@ jobs:
|
|||
run: npx playwright install-deps
|
||||
- name: Roll to new revision
|
||||
run: |
|
||||
./utils/roll_browser.js ${{ github.event.client_payload.browser }} ${{ github.event.client_payload.revision }}
|
||||
./utils/roll_browser.js $BROWSER $REVISION
|
||||
npm run build
|
||||
- name: Prepare branch
|
||||
id: prepare-branch
|
||||
run: |
|
||||
BRANCH_NAME="roll-into-pw-${{ github.event.client_payload.browser }}/${{ github.event.client_payload.revision }}"
|
||||
BRANCH_NAME="roll-into-pw-${BROWSER}/${REVISION}"
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
git config --global user.name github-actions
|
||||
git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
git add .
|
||||
git commit -m "feat(${{ github.event.client_payload.browser }}): roll to r${{ github.event.client_payload.revision }}"
|
||||
git push origin $BRANCH_NAME
|
||||
git commit -m "feat(${BROWSER}): roll to r${REVISION}"
|
||||
git push origin $BRANCH_NAME --force
|
||||
- name: Create Pull Request
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
|
|
@ -47,7 +59,7 @@ jobs:
|
|||
repo: 'playwright',
|
||||
head: 'microsoft:${{ steps.prepare-branch.outputs.BRANCH_NAME }}',
|
||||
base: 'main',
|
||||
title: 'feat(${{ github.event.client_payload.browser }}): roll to r${{ github.event.client_payload.revision }}',
|
||||
title: 'feat(${{ env.BROWSER }}): roll to r${{ env.REVISION }}',
|
||||
});
|
||||
await github.rest.issues.addLabels({
|
||||
owner: 'microsoft',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 🎭 Playwright
|
||||
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
|
||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
|||
|
||||
| | Linux | macOS | Windows |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Chromium <!-- GEN:chromium-version -->133.0.6943.27<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Chromium <!-- GEN:chromium-version -->133.0.6943.35<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Firefox <!-- GEN:firefox-version -->134.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,25 @@ Methods like [`method: APIRequestContext.get`] take the base URL into considerat
|
|||
- `localStorage` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `value` <[string]>
|
||||
- `indexedDB` ?<[Array]<[Object]>> indexedDB to set for context
|
||||
- `name` <[string]> database name
|
||||
- `version` <[int]> database version
|
||||
- `stores` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `keyPath` ?<[string]>
|
||||
- `keyPathArray` ?<[Array]<[string]>>
|
||||
- `autoIncrement` <[boolean]>
|
||||
- `indexes` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `keyPath` ?<[string]>
|
||||
- `keyPathArray` ?<[Array]<[string]>>
|
||||
- `unique` <[boolean]>
|
||||
- `multiEntry` <[boolean]>
|
||||
- `records` <[Array]<[Object]>>
|
||||
- `key` ?<[Object]>
|
||||
- `keyEncoded` ?<[Object]> if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
- `value` <[Object]>
|
||||
- `valueEncoded` ?<[Object]> if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
|
||||
Populates context with given storage state. This option can be used to initialize context with logged-in information
|
||||
obtained via [`method: BrowserContext.storageState`] or [`method: APIRequestContext.storageState`]. Either a path to the
|
||||
|
|
|
|||
|
|
@ -880,6 +880,25 @@ context cookies from the response. The method will automatically follow redirect
|
|||
- `localStorage` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `value` <[string]>
|
||||
- `indexedDB` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `version` <[int]>
|
||||
- `stores` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `keyPath` ?<[string]>
|
||||
- `keyPathArray` ?<[Array]<[string]>>
|
||||
- `autoIncrement` <[boolean]>
|
||||
- `indexes` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `keyPath` ?<[string]>
|
||||
- `keyPathArray` ?<[Array]<[string]>>
|
||||
- `unique` <[boolean]>
|
||||
- `multiEntry` <[boolean]>
|
||||
- `records` <[Array]<[Object]>>
|
||||
- `key` ?<[Object]>
|
||||
- `keyEncoded` ?<[Object]> if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
- `value` <[Object]>
|
||||
- `valueEncoded` ?<[Object]> if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
|
||||
Returns storage state for this request context, contains current cookies and local storage snapshot if it was passed to the constructor.
|
||||
|
||||
|
|
|
|||
|
|
@ -1511,8 +1511,27 @@ Whether to emulate network being offline for the browser context.
|
|||
- `localStorage` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `value` <[string]>
|
||||
- `indexedDB` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `version` <[int]>
|
||||
- `stores` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `keyPath` ?<[string]>
|
||||
- `keyPathArray` ?<[Array]<[string]>>
|
||||
- `autoIncrement` <[boolean]>
|
||||
- `indexes` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `keyPath` ?<[string]>
|
||||
- `keyPathArray` ?<[Array]<[string]>>
|
||||
- `unique` <[boolean]>
|
||||
- `multiEntry` <[boolean]>
|
||||
- `records` <[Array]<[Object]>>
|
||||
- `key` ?<[Object]>
|
||||
- `keyEncoded` ?<[Object]> if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
- `value` <[Object]>
|
||||
- `valueEncoded` ?<[Object]> if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
|
||||
Returns storage state for this browser context, contains current cookies and local storage snapshot.
|
||||
Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot.
|
||||
|
||||
## async method: BrowserContext.storageState
|
||||
* since: v1.8
|
||||
|
|
|
|||
|
|
@ -89,13 +89,17 @@ class BrowserTypeExamples
|
|||
* since: v1.8
|
||||
- returns: <[Browser]>
|
||||
|
||||
This method attaches Playwright to an existing browser instance. When connecting to another browser launched via `BrowserType.launchServer` in Node.js, the major and minor version needs to match the client version (1.2.3 → is compatible with 1.2.x).
|
||||
This method attaches Playwright to an existing browser instance created via `BrowserType.launchServer` in Node.js.
|
||||
|
||||
:::note
|
||||
The major and minor version of the Playwright instance that connects needs to match the version of Playwright that launches the browser (1.2.3 → is compatible with 1.2.x).
|
||||
:::
|
||||
|
||||
### param: BrowserType.connect.wsEndpoint
|
||||
* since: v1.10
|
||||
- `wsEndpoint` <[string]>
|
||||
|
||||
A browser websocket endpoint to connect to.
|
||||
A Playwright browser websocket endpoint to connect to. You obtain this endpoint via `BrowserServer.wsEndpoint`.
|
||||
|
||||
### option: BrowserType.connect.headers
|
||||
* since: v1.11
|
||||
|
|
@ -152,6 +156,10 @@ The default browser context is accessible via [`method: Browser.contexts`].
|
|||
Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
|
||||
:::
|
||||
|
||||
:::note
|
||||
This connection is significantly lower fidelity than the Playwright protocol connection via [`method: BrowserType.connect`]. If you are experiencing issues or attempting to use advanced functionality, you probably want to use [`method: BrowserType.connect`].
|
||||
:::
|
||||
|
||||
**Usage**
|
||||
|
||||
```js
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ Additional locator to match.
|
|||
- returns: <[string]>
|
||||
|
||||
Captures the aria snapshot of the given element.
|
||||
Read more about [aria snapshots](../aria-snapshots.md) and [`method: LocatorAssertions.toMatchAriaSnapshot#1`] for the corresponding assertion.
|
||||
Read more about [aria snapshots](../aria-snapshots.md) and [`method: LocatorAssertions.toMatchAriaSnapshot`] for the corresponding assertion.
|
||||
|
||||
**Usage**
|
||||
|
||||
|
|
|
|||
|
|
@ -240,6 +240,24 @@ Expected accessible description.
|
|||
### option: LocatorAssertions.NotToHaveAccessibleDescription.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
* since: v1.44
|
||||
|
||||
## async method: LocatorAssertions.NotToHaveAccessibleErrorMessage
|
||||
* since: v1.50
|
||||
* langs: python
|
||||
|
||||
The opposite of [`method: LocatorAssertions.toHaveAccessibleErrorMessage`].
|
||||
|
||||
### param: LocatorAssertions.NotToHaveAccessibleErrorMessage.errorMessage
|
||||
* since: v1.50
|
||||
- `errorMessage` <[string]|[RegExp]>
|
||||
|
||||
Expected accessible error message.
|
||||
|
||||
### option: LocatorAssertions.NotToHaveAccessibleErrorMessage.ignoreCase = %%-assertions-ignore-case-%%
|
||||
* since: v1.50
|
||||
|
||||
### option: LocatorAssertions.NotToHaveAccessibleErrorMessage.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
* since: v1.50
|
||||
|
||||
|
||||
## async method: LocatorAssertions.NotToHaveAccessibleName
|
||||
* since: v1.44
|
||||
|
|
@ -446,7 +464,7 @@ Expected options currently selected.
|
|||
* since: v1.49
|
||||
* langs: python
|
||||
|
||||
The opposite of [`method: LocatorAssertions.toMatchAriaSnapshot#1`].
|
||||
The opposite of [`method: LocatorAssertions.toMatchAriaSnapshot`].
|
||||
|
||||
### param: LocatorAssertions.NotToMatchAriaSnapshot.expected
|
||||
* since: v1.49
|
||||
|
|
@ -2180,7 +2198,7 @@ Expected options currently selected.
|
|||
* since: v1.23
|
||||
|
||||
|
||||
## async method: LocatorAssertions.toMatchAriaSnapshot#1
|
||||
## async method: LocatorAssertions.toMatchAriaSnapshot
|
||||
* since: v1.49
|
||||
* langs:
|
||||
- alias-java: matchesAriaSnapshot
|
||||
|
|
@ -2229,14 +2247,14 @@ assertThat(page.locator("body")).matchesAriaSnapshot("""
|
|||
""");
|
||||
```
|
||||
|
||||
### param: LocatorAssertions.toMatchAriaSnapshot#1.expected
|
||||
### param: LocatorAssertions.toMatchAriaSnapshot.expected
|
||||
* since: v1.49
|
||||
- `expected` <string>
|
||||
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot#1.timeout = %%-js-assertions-timeout-%%
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%%
|
||||
* since: v1.49
|
||||
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot#1.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
* since: v1.49
|
||||
|
||||
## async method: LocatorAssertions.toMatchAriaSnapshot#2
|
||||
|
|
@ -2245,27 +2263,13 @@ assertThat(page.locator("body")).matchesAriaSnapshot("""
|
|||
|
||||
Asserts that the target element matches the given [accessibility snapshot](../aria-snapshots.md).
|
||||
|
||||
Snapshot is stored in a separate `.yml` file in a location configured by `expect.toMatchAriaSnapshot.pathTemplate` and/or `snapshotPathTemplate` properties in the configuration file.
|
||||
|
||||
**Usage**
|
||||
|
||||
```js
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot();
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
|
||||
```
|
||||
|
||||
```python async
|
||||
await expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml')
|
||||
```
|
||||
|
||||
```python sync
|
||||
expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml')
|
||||
```
|
||||
|
||||
```csharp
|
||||
await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(new { Path = "/path/to/snapshot.yml" });
|
||||
```
|
||||
|
||||
```java
|
||||
assertThat(page.locator("body")).matchesAriaSnapshot(new LocatorAssertions.MatchesAriaSnapshotOptions().setPath("/path/to/snapshot.yml"));
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'body.yml' });
|
||||
```
|
||||
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot#2.name
|
||||
|
|
@ -2273,7 +2277,7 @@ assertThat(page.locator("body")).matchesAriaSnapshot(new LocatorAssertions.Match
|
|||
* langs: js
|
||||
- `name` <[string]>
|
||||
|
||||
Name of the snapshot to store in the snapshot (screenshot) folder corresponding to this test.
|
||||
Name of the snapshot to store in the snapshot folder corresponding to this test.
|
||||
Generates sequential names if not specified.
|
||||
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-js-assertions-timeout-%%
|
||||
|
|
|
|||
|
|
@ -1309,6 +1309,18 @@ Emulates `'forced-colors'` media feature, supported values are `'active'` and `'
|
|||
* langs: csharp, python
|
||||
- `forcedColors` <[ForcedColors]<"active"|"none"|"null">>
|
||||
|
||||
### option: Page.emulateMedia.contrast
|
||||
* since: v1.51
|
||||
* langs: js, java
|
||||
- `contrast` <null|[Contrast]<"no-preference"|"more">>
|
||||
|
||||
Emulates `'prefers-contrast'` media feature, supported values are `'no-preference'`, `'more'`. Passing `null` disables contrast emulation.
|
||||
|
||||
### option: Page.emulateMedia.contrast
|
||||
* since: v1.51
|
||||
* langs: csharp, python
|
||||
- `contrast` <[Contrast]<"no-preference"|"more"|"null">>
|
||||
|
||||
## async method: Page.evalOnSelector
|
||||
* since: v1.9
|
||||
* discouraged: This method does not wait for the element to pass actionability
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
# class: WebSocket
|
||||
* since: v1.8
|
||||
|
||||
The [WebSocket] class represents websocket connections in the page.
|
||||
The [WebSocket] class represents WebSocket connections within a page. It provides the ability to inspect and manipulate the data being transmitted and received.
|
||||
|
||||
If you want to intercept or modify WebSocket frames, consider using [WebSocketRoute].
|
||||
|
||||
## event: WebSocket.close
|
||||
* since: v1.8
|
||||
|
|
|
|||
|
|
@ -259,11 +259,30 @@ Specify environment variables that will be visible to the browser. Defaults to `
|
|||
- `httpOnly` <[boolean]>
|
||||
- `secure` <[boolean]>
|
||||
- `sameSite` <[SameSiteAttribute]<"Strict"|"Lax"|"None">> sameSite flag
|
||||
- `origins` <[Array]<[Object]>> localStorage to set for context
|
||||
- `origins` <[Array]<[Object]>>
|
||||
- `origin` <[string]>
|
||||
- `localStorage` <[Array]<[Object]>>
|
||||
- `localStorage` <[Array]<[Object]>> localStorage to set for context
|
||||
- `name` <[string]>
|
||||
- `value` <[string]>
|
||||
- `indexedDB` ?<[Array]<[Object]>> indexedDB to set for context
|
||||
- `name` <[string]> database name
|
||||
- `version` <[int]> database version
|
||||
- `stores` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `keyPath` ?<[string]>
|
||||
- `keyPathArray` ?<[Array]<[string]>>
|
||||
- `autoIncrement` <[boolean]>
|
||||
- `indexes` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `keyPath` ?<[string]>
|
||||
- `keyPathArray` ?<[Array]<[string]>>
|
||||
- `unique` <[boolean]>
|
||||
- `multiEntry` <[boolean]>
|
||||
- `records` <[Array]<[Object]>>
|
||||
- `key` ?<[Object]>
|
||||
- `keyEncoded` ?<[Object]> if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
- `value` <[Object]>
|
||||
- `valueEncoded` ?<[Object]> if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
|
||||
Learn more about [storage state and auth](../auth.md).
|
||||
|
||||
|
|
@ -673,6 +692,18 @@ Emulates `'forced-colors'` media feature, supported values are `'active'`, `'non
|
|||
|
||||
Emulates `'forced-colors'` media feature, supported values are `'active'`, `'none'`. See [`method: Page.emulateMedia`] for more details. Passing `'null'` resets emulation to system defaults. Defaults to `'none'`.
|
||||
|
||||
## context-option-contrast
|
||||
* langs: js, java
|
||||
- `contrast` <null|[ForcedColors]<"no-preference"|"more">>
|
||||
|
||||
Emulates `'prefers-contrast'` media feature, supported values are `'no-preference'`, `'more'`. See [`method: Page.emulateMedia`] for more details. Passing `null` resets emulation to system defaults. Defaults to `'no-preference'`.
|
||||
|
||||
## context-option-contrast-csharp-python
|
||||
* langs: csharp, python
|
||||
- `contrast` <[ForcedColors]<"no-preference"|"more"|"null">>
|
||||
|
||||
Emulates `'prefers-contrast'` media feature, supported values are `'no-preference'`, `'more'`. See [`method: Page.emulateMedia`] for more details. Passing `'null'` resets emulation to system defaults. Defaults to `'no-preference'`.
|
||||
|
||||
## context-option-logger
|
||||
* langs: js
|
||||
- `logger` <[Logger]>
|
||||
|
|
@ -973,6 +1004,8 @@ between the same pixel in compared images, between zero (strict) and one (lax),
|
|||
- %%-context-option-reducedMotion-csharp-python-%%
|
||||
- %%-context-option-forcedColors-%%
|
||||
- %%-context-option-forcedColors-csharp-python-%%
|
||||
- %%-context-option-contrast-%%
|
||||
- %%-context-option-contrast-csharp-python-%%
|
||||
- %%-context-option-logger-%%
|
||||
- %%-context-option-videospath-%%
|
||||
- %%-context-option-videosize-%%
|
||||
|
|
@ -1758,7 +1791,9 @@ await Expect(Page.GetByTitle("Issues count")).toHaveText("25 issues");
|
|||
- `type` ?<[string]>
|
||||
* langs: js
|
||||
|
||||
This option configures a template controlling location of snapshots generated by [`method: PageAssertions.toHaveScreenshot#1`] and [`method: SnapshotAssertions.toMatchSnapshot#1`].
|
||||
This option configures a template controlling location of snapshots generated by [`method: PageAssertions.toHaveScreenshot#1`], [`method: LocatorAssertions.toMatchAriaSnapshot#2`] and [`method: SnapshotAssertions.toMatchSnapshot#1`].
|
||||
|
||||
You can configure templates for each assertion separately in [`property: TestConfig.expect`].
|
||||
|
||||
**Usage**
|
||||
|
||||
|
|
@ -1767,7 +1802,19 @@ import { defineConfig } from '@playwright/test';
|
|||
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
|
||||
// Single template for all assertions
|
||||
snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
|
||||
|
||||
// Assertion-specific templates
|
||||
expect: {
|
||||
toHaveScreenshot: {
|
||||
pathTemplate: '{testDir}/__screenshots__{/projectName}/{testFilePath}/{arg}{ext}',
|
||||
},
|
||||
toMatchAriaSnapshot: {
|
||||
pathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -1798,22 +1845,22 @@ test.describe('suite', () => {
|
|||
|
||||
The list of supported tokens:
|
||||
|
||||
* `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated snapshot name.
|
||||
* `{arg}` - Relative snapshot path **without extension**. This comes from the arguments passed to `toHaveScreenshot()`, `toMatchAriaSnapshot()` or `toMatchSnapshot()`; if called without arguments, this will be an auto-generated snapshot name.
|
||||
* Value: `foo/bar/baz`
|
||||
* `{ext}` - snapshot extension (with dots)
|
||||
* `{ext}` - Snapshot extension (with the leading dot).
|
||||
* Value: `.png`
|
||||
* `{platform}` - The value of `process.platform`.
|
||||
* `{projectName}` - Project's file-system-sanitized name, if any.
|
||||
* Value: `''` (empty string).
|
||||
* `{snapshotDir}` - Project's [`property: TestConfig.snapshotDir`].
|
||||
* `{snapshotDir}` - Project's [`property: TestProject.snapshotDir`].
|
||||
* Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
|
||||
* `{testDir}` - Project's [`property: TestConfig.testDir`].
|
||||
* Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with config)
|
||||
* `{testDir}` - Project's [`property: TestProject.testDir`].
|
||||
* Value: `/home/playwright/tests` (absolute path since `testDir` is resolved relative to directory with config)
|
||||
* `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
|
||||
* Value: `page`
|
||||
* `{testFileName}` - Test file name with extension.
|
||||
* Value: `page-click.spec.ts`
|
||||
* `{testFilePath}` - Relative path from `testDir` to **test file**
|
||||
* `{testFilePath}` - Relative path from `testDir` to **test file**.
|
||||
* Value: `page/page-click.spec.ts`
|
||||
* `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
|
||||
* Value: `suite-test-should-work`
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
|
|||
|
||||
## Overview
|
||||
|
||||
With the Playwright Snapshot testing you can assert the accessibility tree of a page against a predefined snapshot template.
|
||||
With Playwright's Snapshot testing you can assert the accessibility tree of a page against a predefined snapshot template.
|
||||
|
||||
```js
|
||||
await page.goto('https://playwright.dev/');
|
||||
|
|
@ -79,12 +79,12 @@ confirms that an input field has the expected value.
|
|||
Assertion tests are specific and generally check the current state of an element or property
|
||||
against an expected, predefined state.
|
||||
They work well for predictable, single-value checks but are limited in scope when testing the
|
||||
broader structure or variations.
|
||||
broader structure or variations.
|
||||
|
||||
**Advantages**
|
||||
- **Clarity**: The intent of the test is explicit and easy to understand.
|
||||
- **Specificity**: Tests focus on particular aspects of functionality, making them more robust
|
||||
against unrelated changes.
|
||||
against unrelated changes.
|
||||
- **Debugging**: Failures provide targeted feedback, pointing directly to the problematic aspect.
|
||||
|
||||
**Disadvantages**
|
||||
|
|
@ -154,7 +154,7 @@ structure of a page, use the [Chrome DevTools Accessibility Pane](https://develo
|
|||
|
||||
## Snapshot matching
|
||||
|
||||
The [`method: LocatorAssertions.toMatchAriaSnapshot#1`] assertion method in Playwright compares the accessible
|
||||
The [`method: LocatorAssertions.toMatchAriaSnapshot`] assertion method in Playwright compares the accessible
|
||||
structure of the locator scope with a predefined aria snapshot template, helping validate the page's state against
|
||||
testing requirements.
|
||||
|
||||
|
|
@ -222,7 +222,7 @@ attributes.
|
|||
```
|
||||
|
||||
In this example, the button role is matched, but the accessible name ("Submit") is not specified, allowing the test to
|
||||
pass regardless of the button’s label.
|
||||
pass regardless of the button's label.
|
||||
|
||||
<hr/>
|
||||
|
||||
|
|
@ -280,12 +280,12 @@ support regex patterns.
|
|||
|
||||
## Generating snapshots
|
||||
|
||||
Creating aria snapshots in Playwright helps ensure and maintain your application’s structure.
|
||||
Creating aria snapshots in Playwright helps ensure and maintain your application's structure.
|
||||
You can generate snapshots in various ways depending on your testing setup and workflow.
|
||||
|
||||
### 1. Generating snapshots with the Playwright code generator
|
||||
### Generating snapshots with the Playwright code generator
|
||||
|
||||
If you’re using Playwright’s [Code Generator](./codegen.md), generating aria snapshots is streamlined with its
|
||||
If you're using Playwright's [Code Generator](./codegen.md), generating aria snapshots is streamlined with its
|
||||
interactive interface:
|
||||
|
||||
- **"Assert snapshot" Action**: In the code generator, you can use the "Assert snapshot" action to automatically create
|
||||
|
|
@ -296,20 +296,18 @@ recorded test flow.
|
|||
aria snapshot for a selected locator, letting you explore, inspect, and verify element roles, attributes, and
|
||||
accessible names to aid snapshot creation and review.
|
||||
|
||||
### 2. Updating snapshots with `@playwright/test` and the `--update-snapshots` flag
|
||||
### Updating snapshots with `@playwright/test` and the `--update-snapshots` flag
|
||||
* langs: js
|
||||
|
||||
When using the Playwright test runner (`@playwright/test`), you can automatically update snapshots by running tests with
|
||||
the `--update-snapshots` flag:
|
||||
When using the Playwright test runner (`@playwright/test`), you can automatically update snapshots with the `--update-snapshots` flag, `-u` for short.
|
||||
|
||||
Running tests with the `--update-snapshots` flag will update snapshots that did not match. Matching snapshots will not be updated.
|
||||
|
||||
```bash
|
||||
npx playwright test --update-snapshots
|
||||
```
|
||||
|
||||
This command regenerates snapshots for assertions, including aria snapshots, replacing outdated ones. It’s
|
||||
useful when application structure changes require new snapshots as a baseline. Note that Playwright will wait for the
|
||||
maximum expect timeout specified in the test runner configuration to ensure the
|
||||
page is settled before taking the snapshot. It might be necessary to adjust the `--timeout` if the test hits the timeout
|
||||
while generating snapshots.
|
||||
Updating snapshots is useful when application structure changes require new snapshots as a baseline. Note that Playwright will wait for the maximum expect timeout specified in the test runner configuration to ensure the page is settled before taking the snapshot. It might be necessary to adjust the `--timeout` if the test hits the timeout while generating snapshots.
|
||||
|
||||
#### Empty template for snapshot generation
|
||||
|
||||
|
|
@ -329,10 +327,40 @@ When updating snapshots, Playwright creates patch files that capture differences
|
|||
applied, and committed to source control, allowing teams to track structural changes over time and ensure updates are
|
||||
consistent with application requirements.
|
||||
|
||||
### 3. Using the `Locator.ariaSnapshot` method
|
||||
The way source code is updated can be changed using the `--update-source-method` flag. There are several options available:
|
||||
|
||||
- **"patch"** (default): Generates a unified diff file that can be applied to the source code using `git apply`.
|
||||
- **"3way"**: Generates merge conflict markers in your source code, allowing you to choose whether to accept changes.
|
||||
- **"overwrite"**: Overwrites the source code with the new snapshot values.
|
||||
|
||||
```bash
|
||||
npx playwright test --update-snapshots --update-source-mode=3way
|
||||
```
|
||||
|
||||
#### Snapshots as separate files
|
||||
|
||||
To store your snapshots in a separate file, use the `toMatchAriaSnapshot` method with the `name` option, specifying a `.yml` file extension.
|
||||
|
||||
```js
|
||||
await expect(page.getByRole('main')).toMatchAriaSnapshot({ name: 'main-snapshot.yml' });
|
||||
```
|
||||
|
||||
By default, snapshots from a test file `example.spec.ts` are placed in the `example.spec.ts-snapshots` directory. As snapshots should be the same across browsers, only one snapshot is saved even if testing with multiple browsers. Should you wish, you can customize the [snapshot path template](./api/class-testconfig#test-config-snapshot-path-template) using the following configuration:
|
||||
|
||||
```js
|
||||
export default defineConfig({
|
||||
expect: {
|
||||
toMatchAriaSnapshot: {
|
||||
pathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Using the `Locator.ariaSnapshot` method
|
||||
|
||||
The [`method: Locator.ariaSnapshot`] method allows you to programmatically create a YAML representation of accessible
|
||||
elements within a locator’s scope, especially helpful for generating snapshots dynamically during test execution.
|
||||
elements within a locator's scope, especially helpful for generating snapshots dynamically during test execution.
|
||||
|
||||
**Example**:
|
||||
|
||||
|
|
@ -361,7 +389,7 @@ var snapshot = await page.Locator("body").AriaSnapshotAsync();
|
|||
Console.WriteLine(snapshot);
|
||||
```
|
||||
|
||||
This command outputs the aria snapshot within the specified locator’s scope in YAML format, which you can validate
|
||||
This command outputs the aria snapshot within the specified locator's scope in YAML format, which you can validate
|
||||
or store as needed.
|
||||
|
||||
## Accessibility tree examples
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ await page.goto('https://github.com/login')
|
|||
# Interact with login form
|
||||
await page.get_by_label("Username or email address").fill("username")
|
||||
await page.get_by_label("Password").fill("password")
|
||||
await page.page.get_by_role("button", name="Sign in").click()
|
||||
await page.get_by_role("button", name="Sign in").click()
|
||||
# Continue with the test
|
||||
```
|
||||
|
||||
|
|
@ -266,9 +266,9 @@ existing authentication state instead.
|
|||
Playwright provides a way to reuse the signed-in state in the tests. That way you can log
|
||||
in only once and then skip the log in step for all of the tests.
|
||||
|
||||
Web apps use cookie-based or token-based authentication, where authenticated state is stored as [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) or in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage). Playwright provides [`method: BrowserContext.storageState`] method that can be used to retrieve storage state from authenticated contexts and then create new contexts with prepopulated state.
|
||||
Web apps use cookie-based or token-based authentication, where authenticated state is stored as [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage) or in [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API). Playwright provides [`method: BrowserContext.storageState`] method that can be used to retrieve storage state from authenticated contexts and then create new contexts with prepopulated state.
|
||||
|
||||
Cookies and local storage state can be used across different browsers. They depend on your application's authentication model: some apps might require both cookies and local storage.
|
||||
Cookies, local storage and IndexedDB state can be used across different browsers. They depend on your application's authentication model which may require some combination of cookies, local storage or IndexedDB.
|
||||
|
||||
The following code snippet retrieves state from an authenticated context and creates a new context with that state.
|
||||
|
||||
|
|
@ -583,7 +583,7 @@ test('admin and user', async ({ adminPage, userPage }) => {
|
|||
|
||||
### Session storage
|
||||
|
||||
Reusing authenticated state covers [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) and [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage) based authentication. Rarely, [session storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) is used for storing information associated with the signed-in state. Session storage is specific to a particular domain and is not persisted across page loads. Playwright does not provide API to persist session storage, but the following snippet can be used to save/load session storage.
|
||||
Reusing authenticated state covers [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage) and [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) based authentication. Rarely, [session storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) is used for storing information associated with the signed-in state. Session storage is specific to a particular domain and is not persisted across page loads. Playwright does not provide API to persist session storage, but the following snippet can be used to save/load session storage.
|
||||
|
||||
```js
|
||||
// Get session storage and store as env variable
|
||||
|
|
|
|||
|
|
@ -804,7 +804,7 @@ Sharding in CircleCI is indexed with 0 which means that you will need to overrid
|
|||
executor: pw-noble-development
|
||||
parallelism: 4
|
||||
steps:
|
||||
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npx playwright test -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
|
||||
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npx playwright test --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
|
||||
```
|
||||
|
||||
### Jenkins
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ pwsh bin/Debug/netX/playwright.ps1 codegen --timezone="Europe/Rome" --geolocatio
|
|||
|
||||
### Preserve authenticated state
|
||||
|
||||
Run `codegen` with `--save-storage` to save [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) and [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) at the end of the session. This is useful to separately record an authentication step and reuse it later when recording more tests.
|
||||
Run `codegen` with `--save-storage` to save [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) and [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) data at the end of the session. This is useful to separately record an authentication step and reuse it later when recording more tests.
|
||||
|
||||
```bash js
|
||||
npx playwright codegen github.com/microsoft/playwright --save-storage=auth.json
|
||||
|
|
@ -375,7 +375,7 @@ Make sure you only use the `auth.json` locally as it contains sensitive informat
|
|||
|
||||
#### Load authenticated state
|
||||
|
||||
Run with `--load-storage` to consume the previously loaded storage from the `auth.json`. This way, all [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) and [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) will be restored, bringing most web apps to the authenticated state without the need to login again. This means you can continue generating tests from the logged in state.
|
||||
Run with `--load-storage` to consume the previously loaded storage from the `auth.json`. This way, all [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) and [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) data will be restored, bringing most web apps to the authenticated state without the need to login again. This means you can continue generating tests from the logged in state.
|
||||
|
||||
```bash js
|
||||
npx playwright codegen --load-storage=auth.json github.com/microsoft/playwright
|
||||
|
|
|
|||
|
|
@ -4,12 +4,43 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.50
|
||||
|
||||
### Support for Xunit
|
||||
|
||||
* Support for xUnit 2.8+ via [Microsoft.Playwright.Xunit](https://www.nuget.org/packages/Microsoft.Playwright.Xunit). Follow our [Getting Started](./intro.md) guide to learn more.
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* Added method [`method: LocatorAssertions.toHaveAccessibleErrorMessage`] to assert the Locator points to an element with a given [aria errormessage](https://w3c.github.io/aria/#aria-errormessage).
|
||||
|
||||
### UI updates
|
||||
|
||||
* New button in Codegen for picking elements to produce aria snapshots.
|
||||
* Additional details (such as keys pressed) are now displayed alongside action API calls in traces.
|
||||
* Display of `canvas` content in traces is error-prone. Display is now disabled by default, and can be enabled via the `Display canvas content` UI setting.
|
||||
* `Call` and `Network` panels now display additional time information.
|
||||
|
||||
### Breaking
|
||||
|
||||
* [`method: LocatorAssertions.toBeEditable`] and [`method: Locator.isEditable`] now throw if the target element is not `<input>`, `<select>`, or a number of other editable elements.
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 133.0.6943.16
|
||||
* Mozilla Firefox 134.0
|
||||
* WebKit 18.2
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 132
|
||||
* Microsoft Edge 132
|
||||
|
||||
## Version 1.49
|
||||
|
||||
### Aria snapshots
|
||||
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#1`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
|
||||
```csharp
|
||||
await page.GotoAsync("https://playwright.dev");
|
||||
|
|
|
|||
|
|
@ -4,11 +4,39 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.50
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* Added method [`method: LocatorAssertions.toHaveAccessibleErrorMessage`] to assert the Locator points to an element with a given [aria errormessage](https://w3c.github.io/aria/#aria-errormessage).
|
||||
|
||||
### UI updates
|
||||
|
||||
* New button in Codegen for picking elements to produce aria snapshots.
|
||||
* Additional details (such as keys pressed) are now displayed alongside action API calls in traces.
|
||||
* Display of `canvas` content in traces is error-prone. Display is now disabled by default, and can be enabled via the `Display canvas content` UI setting.
|
||||
* `Call` and `Network` panels now display additional time information.
|
||||
|
||||
### Breaking
|
||||
|
||||
* [`method: LocatorAssertions.toBeEditable`] and [`method: Locator.isEditable`] now throw if the target element is not `<input>`, `<select>`, or a number of other editable elements.
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 133.0.6943.16
|
||||
* Mozilla Firefox 134.0
|
||||
* WebKit 18.2
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 132
|
||||
* Microsoft Edge 132
|
||||
|
||||
## Version 1.49
|
||||
|
||||
### Aria snapshots
|
||||
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#1`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
|
||||
```java
|
||||
page.navigate("https://playwright.dev");
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
|
|||
```
|
||||
|
||||
* New method [`method: Test.step.skip`] to disable execution of a test step.
|
||||
|
||||
|
||||
```js
|
||||
test('some test', async ({ page }) => {
|
||||
await test.step('before running step', async () => {
|
||||
|
|
@ -49,11 +49,12 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
|
|||
|
||||
* Option [`property: TestConfig.webServer`] added a `gracefulShutdown` field for specifying a process kill signal other than the default `SIGKILL`.
|
||||
* Exposed [`property: TestStep.attachments`] from the reporter API to allow retrieval of all attachments created by that step.
|
||||
* New option `pathTemplate` for `toHaveScreenshot` and `toMatchAriaSnapshot` assertions in the [`property: TestConfig.expect`] configuration.
|
||||
|
||||
### UI updates
|
||||
|
||||
* Updated default HTML reporter to improve display of attachments.
|
||||
* New button for picking elements to produce aria snapshots.
|
||||
* New button in Codegen for picking elements to produce aria snapshots.
|
||||
* Additional details (such as keys pressed) are now displayed alongside action API calls in traces.
|
||||
* Display of `canvas` content in traces is error-prone. Display is now disabled by default, and can be enabled via the `Display canvas content` UI setting.
|
||||
* `Call` and `Network` panels now display additional time information.
|
||||
|
|
@ -83,7 +84,7 @@ This version was also tested against the following stable channels:
|
|||
|
||||
### Aria snapshots
|
||||
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#1`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
|
||||
```js
|
||||
await page.goto('https://playwright.dev');
|
||||
|
|
|
|||
|
|
@ -4,11 +4,43 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.50
|
||||
|
||||
### Async Pytest Plugin
|
||||
|
||||
* [Playwright's Pytest plugin](./test-runners.md) now has support for [Async Fixtures](https://playwright.dev/python/docs/test-runners#async-fixtures).
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* Added method [`method: LocatorAssertions.toHaveAccessibleErrorMessage`] to assert the Locator points to an element with a given [aria errormessage](https://w3c.github.io/aria/#aria-errormessage).
|
||||
|
||||
### UI updates
|
||||
|
||||
* New button in Codegen for picking elements to produce aria snapshots.
|
||||
* Additional details (such as keys pressed) are now displayed alongside action API calls in traces.
|
||||
* Display of `canvas` content in traces is error-prone. Display is now disabled by default, and can be enabled via the `Display canvas content` UI setting.
|
||||
* `Call` and `Network` panels now display additional time information.
|
||||
|
||||
### Breaking
|
||||
|
||||
* [`method: LocatorAssertions.toBeEditable`] and [`method: Locator.isEditable`] now throw if the target element is not `<input>`, `<select>`, or a number of other editable elements.
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 133.0.6943.16
|
||||
* Mozilla Firefox 134.0
|
||||
* WebKit 18.2
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 132
|
||||
* Microsoft Edge 132
|
||||
|
||||
## Version 1.49
|
||||
|
||||
### Aria snapshots
|
||||
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#1`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
|
||||
```python
|
||||
page.goto("https://playwright.dev")
|
||||
|
|
|
|||
|
|
@ -1751,7 +1751,7 @@ Step name.
|
|||
|
||||
### param: Test.step.body
|
||||
* since: v1.10
|
||||
- `body` <[function]\(\):[Promise]<[any]>>
|
||||
- `body` <[function]\([TestStepInfo]\):[Promise]<[any]>>
|
||||
|
||||
Step body.
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ export default defineConfig({
|
|||
- `scale` ?<[ScreenshotScale]<"css"|"device">> See [`option: Page.screenshot.scale`] in [`method: Page.screenshot`]. Defaults to `"css"`.
|
||||
- `stylePath` ?<[string]|[Array]<[string]>> See [`option: Page.screenshot.style`] in [`method: Page.screenshot`].
|
||||
- `threshold` ?<[float]> An acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and `1` (lax). `"pixelmatch"` comparator computes color difference in [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
|
||||
- `pathTemplate` ?<[string]> A template controlling location of the screenshots. See [`property: TestConfig.snapshotPathTemplate`] for details.
|
||||
- `toMatchAriaSnapshot` ?<[Object]> Configuration for the [`method: LocatorAssertions.toMatchAriaSnapshot#2`] method.
|
||||
- `pathTemplate` ?<[string]> A template controlling location of the aria snapshots. See [`property: TestConfig.snapshotPathTemplate`] for details.
|
||||
- `toMatchSnapshot` ?<[Object]> Configuration for the [`method: SnapshotAssertions.toMatchSnapshot#1`] method.
|
||||
- `maxDiffPixels` ?<[int]> An acceptable amount of pixels that could be different, unset by default.
|
||||
- `maxDiffPixelRatio` ?<[float]> An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default.
|
||||
|
|
@ -234,7 +237,9 @@ export default defineConfig({
|
|||
* since: v1.10
|
||||
- type: ?<[Metadata]>
|
||||
|
||||
Metadata that will be put directly to the test report serialized as JSON.
|
||||
Metadata contains key-value pairs to be included in the report. For example, HTML report will display it as key-value pairs, and JSON report will include metadata serialized as json.
|
||||
|
||||
See also [`property: TestConfig.populateGitInfo`] that populates metadata.
|
||||
|
||||
**Usage**
|
||||
|
||||
|
|
@ -242,7 +247,7 @@ Metadata that will be put directly to the test report serialized as JSON.
|
|||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
metadata: 'acceptance tests',
|
||||
metadata: { title: 'acceptance tests' },
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -325,7 +330,9 @@ This path will serve as the base directory for each test file snapshot directory
|
|||
* since: v1.51
|
||||
- type: ?<[boolean]>
|
||||
|
||||
Whether to populate [`property: TestConfig.metadata`] with Git info. The metadata will automatically appear in the HTML report and is available in Reporter API.
|
||||
Whether to populate `'git.commit.info'` field of the [`property: TestConfig.metadata`] with Git commit info and CI/CD information.
|
||||
|
||||
This information will appear in the HTML and JSON reports and is available in the Reporter API.
|
||||
|
||||
**Usage**
|
||||
|
||||
|
|
@ -647,7 +654,7 @@ export default defineConfig({
|
|||
- `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
|
||||
- `gracefulShutdown` ?<[Object]> How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal: 'SIGTERM', timeout: 500 }`, the process group is sent a `SIGTERM` signal, followed by `SIGKILL` if it doesn't exit within 500ms. You can also use `SIGINT` as the signal instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't support `SIGTERM` and `SIGINT` signals, so this option is ignored on Windows. Note that shutting down a Docker container requires `SIGTERM`.
|
||||
- `signal` <["SIGINT"|"SIGTERM"]>
|
||||
- `timeout` <[int]>
|
||||
- `timeout` <[int]>
|
||||
- `url` ?<[string]> The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is checked. Either `port` or `url` should be specified.
|
||||
|
||||
Launch a development web server (or multiple) during the tests.
|
||||
|
|
|
|||
|
|
@ -98,6 +98,9 @@ export default defineConfig({
|
|||
- `caret` ?<[ScreenshotCaret]<"hide"|"initial">> See [`option: Page.screenshot.caret`] in [`method: Page.screenshot`]. Defaults to `"hide"`.
|
||||
- `scale` ?<[ScreenshotScale]<"css"|"device">> See [`option: Page.screenshot.scale`] in [`method: Page.screenshot`]. Defaults to `"css"`.
|
||||
- `stylePath` ?<[string]|[Array]<[string]>> See [`option: Page.screenshot.style`] in [`method: Page.screenshot`].
|
||||
- `pathTemplate` ?<[string]> A template controlling location of the screenshots. See [`property: TestProject.snapshotPathTemplate`] for details.
|
||||
- `toMatchAriaSnapshot` ?<[Object]> Configuration for the [`method: LocatorAssertions.toMatchAriaSnapshot#2`] method.
|
||||
- `pathTemplate` ?<[string]> A template controlling location of the aria snapshots. See [`property: TestProject.snapshotPathTemplate`] for details.
|
||||
- `toMatchSnapshot` ?<[Object]> Configuration for the [`method: SnapshotAssertions.toMatchSnapshot#1`] method.
|
||||
- `threshold` ?<[float]> an acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and `1` (lax). `"pixelmatch"` comparator computes color difference in [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
|
||||
- `maxDiffPixels` ?<[int]> an acceptable amount of pixels that could be different, unset by default.
|
||||
|
|
@ -180,6 +183,10 @@ Metadata that will be put directly to the test report serialized as JSON.
|
|||
|
||||
Project name is visible in the report and during test execution.
|
||||
|
||||
:::warning
|
||||
Playwright executes the configuration file multiple times. Do not dynamically produce non-stable values in your configuration.
|
||||
:::
|
||||
|
||||
## property: TestProject.snapshotDir
|
||||
* since: v1.10
|
||||
- type: ?<[string]>
|
||||
|
|
|
|||
103
docs/src/test-api/class-teststepinfo.md
Normal file
103
docs/src/test-api/class-teststepinfo.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# class: TestStepInfo
|
||||
* since: v1.51
|
||||
* langs: js
|
||||
|
||||
`TestStepInfo` contains information about currently running test step. It is passed as an argument to the step function. `TestStepInfo` provides utilities to control test step execution.
|
||||
|
||||
```js
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('basic test', async ({ page, browserName }, TestStepInfo) => {
|
||||
await test.step('check some behavior', async step => {
|
||||
await step.skip(browserName === 'webkit', 'The feature is not available in WebKit');
|
||||
// ... rest of the step code
|
||||
await page.check('input');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## async method: TestStepInfo.attach
|
||||
* since: v1.51
|
||||
|
||||
Attach a value or a file from disk to the current test step. Some reporters show test step attachments. Either [`option: path`] or [`option: body`] must be specified, but not both. Calling this method will attribute the attachment to the step, as opposed to [`method: TestInfo.attach`] which stores all attachments at the test level.
|
||||
|
||||
For example, you can attach a screenshot to the test step:
|
||||
|
||||
```js
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('basic test', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev');
|
||||
await test.step('check page rendering', async step => {
|
||||
const screenshot = await page.screenshot();
|
||||
await step.attach('screenshot', { body: screenshot, contentType: 'image/png' });
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Or you can attach files returned by your APIs:
|
||||
|
||||
```js
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { download } from './my-custom-helpers';
|
||||
|
||||
test('basic test', async ({}) => {
|
||||
await test.step('check download behavior', async step => {
|
||||
const tmpPath = await download('a');
|
||||
await step.attach('downloaded', { path: tmpPath });
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
:::note
|
||||
[`method: TestStepInfo.attach`] automatically takes care of copying attached files to a
|
||||
location that is accessible to reporters. You can safely remove the attachment
|
||||
after awaiting the attach call.
|
||||
:::
|
||||
|
||||
### param: TestStepInfo.attach.name
|
||||
* since: v1.51
|
||||
- `name` <[string]>
|
||||
|
||||
Attachment name. The name will also be sanitized and used as the prefix of file name
|
||||
when saving to disk.
|
||||
|
||||
### option: TestStepInfo.attach.body
|
||||
* since: v1.51
|
||||
- `body` <[string]|[Buffer]>
|
||||
|
||||
Attachment body. Mutually exclusive with [`option: path`].
|
||||
|
||||
### option: TestStepInfo.attach.contentType
|
||||
* since: v1.51
|
||||
- `contentType` <[string]>
|
||||
|
||||
Content type of this attachment to properly present in the report, for example `'application/json'` or `'image/png'`. If omitted, content type is inferred based on the [`option: path`], or defaults to `text/plain` for [string] attachments and `application/octet-stream` for [Buffer] attachments.
|
||||
|
||||
### option: TestStepInfo.attach.path
|
||||
* since: v1.51
|
||||
- `path` <[string]>
|
||||
|
||||
Path on the filesystem to the attached file. Mutually exclusive with [`option: body`].
|
||||
|
||||
## method: TestStepInfo.skip#1
|
||||
* since: v1.51
|
||||
|
||||
Unconditionally skip the currently running step. Test step is immediately aborted. This is similar to [`method: Test.step.skip`].
|
||||
|
||||
## method: TestStepInfo.skip#2
|
||||
* since: v1.51
|
||||
|
||||
Conditionally skips the currently running step with an optional description. This is similar to [`method: Test.step.skip`].
|
||||
|
||||
### param: TestStepInfo.skip#2.condition
|
||||
* since: v1.51
|
||||
- `condition` <[boolean]>
|
||||
|
||||
A skip condition. Test step is skipped when the condition is `true`.
|
||||
|
||||
### param: TestStepInfo.skip#2.description
|
||||
* since: v1.51
|
||||
- `description` ?<[string]>
|
||||
|
||||
Optional description that will be reflected in a test report.
|
||||
|
|
@ -110,7 +110,7 @@ Complete set of Playwright Test options is available in the [configuration file]
|
|||
| `--ui` | Run tests in interactive UI mode. |
|
||||
| `--ui-host <host>` | Host to serve UI on; specifying this option opens UI in a browser tab. |
|
||||
| `--ui-port <port>` | Port to serve UI on, 0 for any free port; specifying this option opens UI in a browser tab. |
|
||||
| `-u` or `--update-snapshots [mode]` | Update snapshots with actual results. Possible values are "all", "changed", "missing", and "none". Not passing defaults to "missing"; passing without a value defaults to "changed". |
|
||||
| `-u` or `--update-snapshots [mode]` | Update snapshots with actual results. Possible values are "all", "changed", "missing", and "none". Running tests without the flag defaults to "missing"; running tests with the flag but without a value defaults to "changed". |
|
||||
| `--update-source-method [mode]` | Update snapshots with actual results. Possible values are "patch" (default), "3way" and "overwrite". "Patch" creates a unified diff file that can be used to update the source code later. "3way" generates merge conflict markers in source code. "Overwrite" overwrites the source code with the new snapshot values.|
|
||||
| `-j <workers>` or `--workers <workers>` | Number of concurrent workers or percentage of logical CPU cores, use 1 to run in a single worker (default: 50%). |
|
||||
| `-x` | Stop after the first failure. |
|
||||
|
|
|
|||
|
|
@ -50,6 +50,14 @@ Start time of this particular test step.
|
|||
|
||||
List of steps inside this step.
|
||||
|
||||
## property: TestStep.annotations
|
||||
* since: v1.51
|
||||
- type: <[Array]<[Object]>>
|
||||
- `type` <[string]> Annotation type, for example `'skip'`.
|
||||
- `description` ?<[string]> Optional description.
|
||||
|
||||
The list of annotations applicable to the current test step.
|
||||
|
||||
## property: TestStep.attachments
|
||||
* since: v1.50
|
||||
- type: <[Array]<[Object]>>
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ See the [guides for CI providers](./ci.md) to deploy your tests to CI/CD.
|
|||
## Async Fixtures
|
||||
|
||||
If you want to use async fixtures, you can use the [`pytest-playwright-asyncio`](https://pypi.org/project/pytest-playwright-asyncio/) plugin.
|
||||
Make sure to use `pytest-asyncio>=0.24.0` and make your tests use of [`loop_scope=sesion`](https://pytest-asyncio.readthedocs.io/en/latest/how-to-guides/run_session_tests_in_same_loop.html).
|
||||
Make sure to use `pytest-asyncio>=0.24.0` and make your tests use of [`loop_scope=session`](https://pytest-asyncio.readthedocs.io/en/latest/how-to-guides/run_session_tests_in_same_loop.html).
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ The snapshot name `example-test-1-chromium-darwin.png` consists of a few parts:
|
|||
|
||||
- `chromium-darwin` - the browser name and the platform. Screenshots differ between browsers and platforms due to different rendering, fonts and more, so you will need different snapshots for them. If you use multiple projects in your [configuration file](./test-configuration.md), project name will be used instead of `chromium`.
|
||||
|
||||
The snapshot name and path can be configured with [`snapshotPathTemplate`](./api/class-testproject#test-project-snapshot-path-template) in the playwright config.
|
||||
The snapshot name and path can be configured with [`property: TestConfig.snapshotPathTemplate`] in the playwright config.
|
||||
|
||||
## Updating screenshots
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
"lint": "npm run eslint && npm run tsc && npm run doc && npm run check-deps && node utils/generate_channels.js && node utils/generate_types/ && npm run lint-tests && npm run test-types && npm run lint-packages",
|
||||
"lint-packages": "node utils/workspace.js --ensure-consistent",
|
||||
"lint-tests": "node utils/lint_tests.js",
|
||||
"flint": "concurrently \"npm run eslint\" \"npm run tsc\" \"npm run doc\" \"npm run check-deps\" \"node utils/generate_channels.js\" \"node utils/generate_types/\" \"npm run lint-tests\" \"npm run test-types\" \"npm run lint-packages\"",
|
||||
"flint": "concurrently \"npm run eslint\" \"npm run tsc\" \"npm run doc\" \"npm run check-deps\" \"node utils/generate_channels.js\" \"node utils/generate_types/\" \"npm run lint-tests\" \"npm run test-types\" \"npm run lint-packages\" \"node utils/doclint/linting-code-snippets/cli.js --js-only\"",
|
||||
"clean": "node utils/build/clean.js",
|
||||
"build": "node utils/build/build.js",
|
||||
"watch": "node utils/build/build.js --watch --lint",
|
||||
|
|
|
|||
|
|
@ -69,22 +69,6 @@ export const blank = () => {
|
|||
return <svg className='octicon' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'></svg>;
|
||||
};
|
||||
|
||||
export const externalLink = () => {
|
||||
return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16'><path fillRule='evenodd' d='M10.604 1h4.146a.25.25 0 01.25.25v4.146a.25.25 0 01-.427.177L13.03 4.03 9.28 7.78a.75.75 0 01-1.06-1.06l3.75-3.75-1.543-1.543A.25.25 0 0110.604 1zM3.75 2A1.75 1.75 0 002 3.75v8.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 12.25v-3.5a.75.75 0 00-1.5 0v3.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-8.5a.25.25 0 01.25-.25h3.5a.75.75 0 000-1.5h-3.5z'></path></svg>;
|
||||
};
|
||||
|
||||
export const calendar = () => {
|
||||
return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16'><path fillRule='evenodd' d='M4.75 0a.75.75 0 01.75.75V2h5V.75a.75.75 0 011.5 0V2h1.25c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0113.25 16H2.75A1.75 1.75 0 011 14.25V3.75C1 2.784 1.784 2 2.75 2H4V.75A.75.75 0 014.75 0zm0 3.5h8.5a.25.25 0 01.25.25V6h-11V3.75a.25.25 0 01.25-.25h2zm-2.25 4v6.75c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25V7.5h-11z'></path></svg>;
|
||||
};
|
||||
|
||||
export const person = () => {
|
||||
return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16'><path fillRule='evenodd' d='M10.5 5a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zm.061 3.073a4 4 0 10-5.123 0 6.004 6.004 0 00-3.431 5.142.75.75 0 001.498.07 4.5 4.5 0 018.99 0 .75.75 0 101.498-.07 6.005 6.005 0 00-3.432-5.142z'></path></svg>;
|
||||
};
|
||||
|
||||
export const commit = () => {
|
||||
return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16'><path fillRule='evenodd' d='M10.5 7.75a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zm1.43.75a4.002 4.002 0 01-7.86 0H.75a.75.75 0 110-1.5h3.32a4.001 4.001 0 017.86 0h3.32a.75.75 0 110 1.5h-3.32z'></path></svg>;
|
||||
};
|
||||
|
||||
export const image = () => {
|
||||
return <svg className='octicon' viewBox='0 0 48 48' version='1.1' width='20' height='20' aria-hidden='true'>
|
||||
<path xmlns='http://www.w3.org/2000/svg' d='M11.85 32H36.2l-7.35-9.95-6.55 8.7-4.6-6.45ZM7 40q-1.2 0-2.1-.9Q4 38.2 4 37V11q0-1.2.9-2.1Q5.8 8 7 8h34q1.2 0 2.1.9.9.9.9 2.1v26q0 1.2-.9 2.1-.9.9-2.1.9Zm0-29v26-26Zm34 26V11H7v26Z'/>
|
||||
|
|
|
|||
41
packages/html-reporter/src/metadataView.css
Normal file
41
packages/html-reporter/src/metadataView.css
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.metadata-toggle {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.metadata-view {
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.metadata-separator {
|
||||
height: 1px;
|
||||
border-bottom: 1px solid var(--color-border-default);
|
||||
}
|
||||
|
||||
.metadata-view .copy-value-container {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.git-commit-info a {
|
||||
color: var(--color-fg-default);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
|
@ -17,21 +17,19 @@
|
|||
import * as React from 'react';
|
||||
import './colors.css';
|
||||
import './common.css';
|
||||
import * as icons from './icons';
|
||||
import { AutoChip } from './chip';
|
||||
import './reportView.css';
|
||||
import './theme.css';
|
||||
import './metadataView.css';
|
||||
import type { Metadata } from '@playwright/test';
|
||||
import type { GitCommitInfo } from '@testIsomorphic/types';
|
||||
import { CopyToClipboardContainer } from './copyToClipboard';
|
||||
import { linkifyText } from '@web/renderUtils';
|
||||
|
||||
export type Metainfo = {
|
||||
'revision.id'?: string;
|
||||
'revision.author'?: string;
|
||||
'revision.email'?: string;
|
||||
'revision.subject'?: string;
|
||||
'revision.timestamp'?: number | Date;
|
||||
'revision.link'?: string;
|
||||
'ci.link'?: string;
|
||||
'timestamp'?: number
|
||||
};
|
||||
type MetadataEntries = [string, unknown][];
|
||||
|
||||
export function filterMetadata(metadata: Metadata): MetadataEntries {
|
||||
// TODO: do not plumb actualWorkers through metadata.
|
||||
return Object.entries(metadata).filter(([key]) => key !== 'actualWorkers');
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error: Error | null, errorInfo: React.ErrorInfo | null }> {
|
||||
override state: { error: Error | null, errorInfo: React.ErrorInfo | null } = {
|
||||
|
|
@ -46,12 +44,12 @@ class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error
|
|||
override render() {
|
||||
if (this.state.error || this.state.errorInfo) {
|
||||
return (
|
||||
<AutoChip header={'Commit Metainfo Error'} dataTestId='metadata-error'>
|
||||
<p>An error was encountered when trying to render Commit Metainfo. Please file a GitHub issue to report this error.</p>
|
||||
<div className='metadata-view p-3'>
|
||||
<p>An error was encountered when trying to render metadata.</p>
|
||||
<p>
|
||||
<pre style={{ overflow: 'scroll' }}>{this.state.error?.message}<br/>{this.state.error?.stack}<br/>{this.state.errorInfo?.componentStack}</pre>
|
||||
</p>
|
||||
</AutoChip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -59,79 +57,50 @@ class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error
|
|||
}
|
||||
}
|
||||
|
||||
export const MetadataView: React.FC<Metainfo> = metadata => <ErrorBoundary><InnerMetadataView {...metadata} /></ErrorBoundary>;
|
||||
|
||||
const InnerMetadataView: React.FC<Metainfo> = metadata => {
|
||||
if (!Object.keys(metadata).find(k => k.startsWith('revision.') || k.startsWith('ci.')))
|
||||
return null;
|
||||
|
||||
return (
|
||||
<AutoChip header={
|
||||
<span>
|
||||
{metadata['revision.id'] && <span style={{ float: 'right' }}>
|
||||
{metadata['revision.id'].slice(0, 7)}
|
||||
</span>}
|
||||
{metadata['revision.subject'] || 'Commit Metainfo'}
|
||||
</span>} initialExpanded={false} dataTestId='metadata-chip'>
|
||||
{metadata['revision.subject'] &&
|
||||
<MetadataViewItem
|
||||
testId='revision.subject'
|
||||
content={<span>{metadata['revision.subject']}</span>}
|
||||
/>
|
||||
}
|
||||
{metadata['revision.id'] &&
|
||||
<MetadataViewItem
|
||||
testId='revision.id'
|
||||
content={<span>{metadata['revision.id']}</span>}
|
||||
href={metadata['revision.link']}
|
||||
icon='commit'
|
||||
/>
|
||||
}
|
||||
{(metadata['revision.author'] || metadata['revision.email']) &&
|
||||
<MetadataViewItem
|
||||
content={`${metadata['revision.author']} ${metadata['revision.email']}`}
|
||||
icon='person'
|
||||
/>
|
||||
}
|
||||
{metadata['revision.timestamp'] &&
|
||||
<MetadataViewItem
|
||||
testId='revision.timestamp'
|
||||
content={
|
||||
<>
|
||||
{Intl.DateTimeFormat(undefined, { dateStyle: 'full' }).format(metadata['revision.timestamp'])}
|
||||
{' '}
|
||||
{Intl.DateTimeFormat(undefined, { timeStyle: 'long' }).format(metadata['revision.timestamp'])}
|
||||
</>
|
||||
}
|
||||
icon='calendar'
|
||||
/>
|
||||
}
|
||||
{metadata['ci.link'] &&
|
||||
<MetadataViewItem
|
||||
content='CI/CD Logs'
|
||||
href={metadata['ci.link']}
|
||||
icon='externalLink'
|
||||
/>
|
||||
}
|
||||
{metadata['timestamp'] &&
|
||||
<MetadataViewItem
|
||||
content={<span style={{ color: 'var(--color-fg-subtle)' }}>
|
||||
Report generated on {Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(metadata['timestamp'])}
|
||||
</span>}></MetadataViewItem>
|
||||
}
|
||||
</AutoChip>
|
||||
);
|
||||
export const MetadataView: React.FC<{ metadataEntries: MetadataEntries }> = ({ metadataEntries }) => {
|
||||
return <ErrorBoundary><InnerMetadataView metadataEntries={metadataEntries}/></ErrorBoundary>;
|
||||
};
|
||||
|
||||
const MetadataViewItem: React.FC<{ content: JSX.Element | string; icon?: keyof typeof icons, href?: string, testId?: string }> = ({ content, icon, href, testId }) => {
|
||||
return (
|
||||
<div className='my-1 hbox' data-testid={testId} >
|
||||
<div className='mr-2'>
|
||||
{icons[icon || 'blank']()}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
{href ? <a href={href} target='_blank' rel='noopener noreferrer'>{content}</a> : content}
|
||||
const InnerMetadataView: React.FC<{ metadataEntries: MetadataEntries }> = ({ metadataEntries }) => {
|
||||
const gitCommitInfo = metadataEntries.find(([key]) => key === 'git.commit.info')?.[1] as GitCommitInfo | undefined;
|
||||
const entries = metadataEntries.filter(([key]) => key !== 'git.commit.info');
|
||||
if (!gitCommitInfo && !entries.length)
|
||||
return null;
|
||||
return <div className='metadata-view'>
|
||||
{gitCommitInfo && <>
|
||||
<GitCommitInfoView info={gitCommitInfo}/>
|
||||
{entries.length > 0 && <div className='metadata-separator' />}
|
||||
</>}
|
||||
{entries.map(([key, value]) => {
|
||||
const valueString = typeof value !== 'object' || value === null || value === undefined ? String(value) : JSON.stringify(value);
|
||||
const trimmedValue = valueString.length > 1000 ? valueString.slice(0, 1000) + '\u2026' : valueString;
|
||||
return <div className='m-1 ml-5' key={key}>
|
||||
<span style={{ fontWeight: 'bold' }} title={key}>{key}</span>
|
||||
{valueString && <CopyToClipboardContainer value={valueString}>: <span title={trimmedValue}>{linkifyText(trimmedValue)}</span></CopyToClipboardContainer>}
|
||||
</div>;
|
||||
})}
|
||||
</div>;
|
||||
};
|
||||
|
||||
const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => {
|
||||
const email = info['revision.email'] ? ` <${info['revision.email']}>` : '';
|
||||
const author = `${info['revision.author'] || ''}${email}`;
|
||||
const shortTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(info['revision.timestamp']);
|
||||
const longTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(info['revision.timestamp']);
|
||||
return <div className='hbox pl-4 pr-2 git-commit-info' style={{ alignItems: 'center' }}>
|
||||
<div className='vbox'>
|
||||
<a className='m-2' href={info['revision.link']} target='_blank' rel='noopener noreferrer'>
|
||||
<span title={info['revision.subject'] || ''}>{info['revision.subject'] || ''}</span>
|
||||
</a>
|
||||
<div className='hbox m-2 mt-1'>
|
||||
<div className='mr-1'>{author}</div>
|
||||
<div title={longTimestamp}> on {shortTimestamp}</div>
|
||||
{info['ci.link'] && <><span className='mx-2'>·</span><a href={info['ci.link']} target='_blank' rel='noopener noreferrer' title='CI/CD logs'>logs</a></>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
{!!info['revision.link'] && <a href={info['revision.link']} target='_blank' rel='noopener noreferrer'>
|
||||
<span title='View commit details'>{info['revision.id']?.slice(0, 7) || 'unknown'}</span>
|
||||
</a>}
|
||||
{!info['revision.link'] && !!info['revision.id'] && <span>{info['revision.id'].slice(0, 7)}</span>}
|
||||
</div>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ import { HeaderView } from './headerView';
|
|||
import { Route, SearchParamsContext } from './links';
|
||||
import type { LoadedReport } from './loadedReport';
|
||||
import './reportView.css';
|
||||
import type { Metainfo } from './metadataView';
|
||||
import { MetadataView } from './metadataView';
|
||||
import { TestCaseView } from './testCaseView';
|
||||
import { TestFilesHeader, TestFilesView } from './testFilesView';
|
||||
import './theme.css';
|
||||
|
|
@ -50,6 +48,7 @@ export const ReportView: React.FC<{
|
|||
const searchParams = React.useContext(SearchParamsContext);
|
||||
const [expandedFiles, setExpandedFiles] = React.useState<Map<string, boolean>>(new Map());
|
||||
const [filterText, setFilterText] = React.useState(searchParams.get('q') || '');
|
||||
const [metadataVisible, setMetadataVisible] = React.useState(false);
|
||||
|
||||
const testIdToFileIdMap = React.useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
|
|
@ -76,9 +75,8 @@ export const ReportView: React.FC<{
|
|||
return <div className='htmlreport vbox px-4 pb-4'>
|
||||
<main>
|
||||
{report?.json() && <HeaderView stats={report.json().stats} filterText={filterText} setFilterText={setFilterText}></HeaderView>}
|
||||
{report?.json().metadata && <MetadataView {...report?.json().metadata as Metainfo} />}
|
||||
<Route predicate={testFilesRoutePredicate}>
|
||||
<TestFilesHeader report={report?.json()} filteredStats={filteredStats} />
|
||||
<TestFilesHeader report={report?.json()} filteredStats={filteredStats} metadataVisible={metadataVisible} toggleMetadataVisible={() => setMetadataVisible(visible => !visible)}/>
|
||||
<TestFilesView
|
||||
tests={filteredTests.files}
|
||||
expandedFiles={expandedFiles}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import './testFileView.css';
|
|||
import { msToString } from './utils';
|
||||
import { AutoChip } from './chip';
|
||||
import { TestErrorView } from './testErrorView';
|
||||
import * as icons from './icons';
|
||||
import { filterMetadata, MetadataView } from './metadataView';
|
||||
|
||||
export const TestFilesView: React.FC<{
|
||||
tests: TestFileSummary[],
|
||||
|
|
@ -62,17 +64,24 @@ export const TestFilesView: React.FC<{
|
|||
export const TestFilesHeader: React.FC<{
|
||||
report: HTMLReport | undefined,
|
||||
filteredStats?: FilteredStats,
|
||||
}> = ({ report, filteredStats }) => {
|
||||
metadataVisible: boolean,
|
||||
toggleMetadataVisible: () => void,
|
||||
}> = ({ report, filteredStats, metadataVisible, toggleMetadataVisible }) => {
|
||||
if (!report)
|
||||
return;
|
||||
const metadataEntries = filterMetadata(report.metadata || {});
|
||||
return <>
|
||||
<div className='mt-2 mx-1' style={{ display: 'flex' }}>
|
||||
<div className='mx-1' style={{ display: 'flex', marginTop: 10 }}>
|
||||
{metadataEntries.length > 0 && <div className='metadata-toggle' role='button' onClick={toggleMetadataVisible} title={metadataVisible ? 'Hide metadata' : 'Show metadata'}>
|
||||
{metadataVisible ? icons.downArrow() : icons.rightArrow()}Metadata
|
||||
</div>}
|
||||
{report.projectNames.length === 1 && !!report.projectNames[0] && <div data-testid='project-name' style={{ color: 'var(--color-fg-subtle)' }}>Project: {report.projectNames[0]}</div>}
|
||||
{filteredStats && <div data-testid='filtered-tests-count' style={{ color: 'var(--color-fg-subtle)', padding: '0 10px' }}>Filtered: {filteredStats.total} {!!filteredStats.total && ('(' + msToString(filteredStats.duration) + ')')}</div>}
|
||||
<div style={{ flex: 'auto' }}></div>
|
||||
<div data-testid='overall-time' style={{ color: 'var(--color-fg-subtle)', marginRight: '10px' }}>{report ? new Date(report.startTime).toLocaleString() : ''}</div>
|
||||
<div data-testid='overall-duration' style={{ color: 'var(--color-fg-subtle)' }}>Total time: {msToString(report.duration ?? 0)}</div>
|
||||
</div>
|
||||
{metadataVisible && <MetadataView metadataEntries={metadataEntries}/>}
|
||||
{!!report.errors.length && <AutoChip header='Errors' dataTestId='report-errors'>
|
||||
{report.errors.map((error, index) => <TestErrorView key={'test-report-error-message-' + index} error={error}></TestErrorView>)}
|
||||
</AutoChip>}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"@protocol/*": ["../protocol/src/*"],
|
||||
"@web/*": ["../web/src/*"],
|
||||
"@playwright/*": ["../playwright/src/*"],
|
||||
"@testIsomorphic/*": ["../playwright/src/isomorphic/*"],
|
||||
"playwright-core/lib/*": ["../playwright-core/src/*"],
|
||||
"playwright/lib/*": ["../playwright/src/*"],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ This project incorporates components from the projects listed below. The origina
|
|||
|
||||
- @types/node@17.0.24 (https://github.com/DefinitelyTyped/DefinitelyTyped)
|
||||
- @types/yauzl@2.10.0 (https://github.com/DefinitelyTyped/DefinitelyTyped)
|
||||
- agent-base@7.1.3 (https://github.com/TooTallNate/proxy-agents)
|
||||
- agent-base@6.0.2 (https://github.com/TooTallNate/node-agent-base)
|
||||
- balanced-match@1.0.2 (https://github.com/juliangruber/balanced-match)
|
||||
- brace-expansion@1.1.11 (https://github.com/juliangruber/brace-expansion)
|
||||
- buffer-crc32@0.2.13 (https://github.com/brianloveswords/buffer-crc32)
|
||||
|
|
@ -24,7 +24,7 @@ This project incorporates components from the projects listed below. The origina
|
|||
- fd-slicer@1.1.0 (https://github.com/andrewrk/node-fd-slicer)
|
||||
- get-stream@5.2.0 (https://github.com/sindresorhus/get-stream)
|
||||
- graceful-fs@4.2.10 (https://github.com/isaacs/node-graceful-fs)
|
||||
- https-proxy-agent@7.0.6 (https://github.com/TooTallNate/proxy-agents)
|
||||
- https-proxy-agent@5.0.1 (https://github.com/TooTallNate/node-https-proxy-agent)
|
||||
- ip-address@9.0.5 (https://github.com/beaugunderson/ip-address)
|
||||
- is-docker@2.2.1 (https://github.com/sindresorhus/is-docker)
|
||||
- is-wsl@2.2.0 (https://github.com/sindresorhus/is-wsl)
|
||||
|
|
@ -43,7 +43,7 @@ This project incorporates components from the projects listed below. The origina
|
|||
- retry@0.12.0 (https://github.com/tim-kos/node-retry)
|
||||
- signal-exit@3.0.7 (https://github.com/tapjs/signal-exit)
|
||||
- smart-buffer@4.2.0 (https://github.com/JoshGlazebrook/smart-buffer)
|
||||
- socks-proxy-agent@8.0.5 (https://github.com/TooTallNate/proxy-agents)
|
||||
- socks-proxy-agent@6.1.1 (https://github.com/TooTallNate/node-socks-proxy-agent)
|
||||
- socks@2.8.3 (https://github.com/JoshGlazebrook/socks)
|
||||
- sprintf-js@1.1.3 (https://github.com/alexei/sprintf.js)
|
||||
- stack-utils@2.0.5 (https://github.com/tapjs/stack-utils)
|
||||
|
|
@ -105,11 +105,128 @@ MIT License
|
|||
=========================================
|
||||
END OF @types/yauzl@2.10.0 AND INFORMATION
|
||||
|
||||
%% agent-base@7.1.3 NOTICES AND INFORMATION BEGIN HERE
|
||||
%% agent-base@6.0.2 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
agent-base
|
||||
==========
|
||||
### Turn a function into an [`http.Agent`][http.Agent] instance
|
||||
[](https://github.com/TooTallNate/node-agent-base/actions?workflow=Node+CI)
|
||||
|
||||
This module provides an `http.Agent` generator. That is, you pass it an async
|
||||
callback function, and it returns a new `http.Agent` instance that will invoke the
|
||||
given callback function when sending outbound HTTP requests.
|
||||
|
||||
#### Some subclasses:
|
||||
|
||||
Here's some more interesting uses of `agent-base`.
|
||||
Send a pull request to list yours!
|
||||
|
||||
* [`http-proxy-agent`][http-proxy-agent]: An HTTP(s) proxy `http.Agent` implementation for HTTP endpoints
|
||||
* [`https-proxy-agent`][https-proxy-agent]: An HTTP(s) proxy `http.Agent` implementation for HTTPS endpoints
|
||||
* [`pac-proxy-agent`][pac-proxy-agent]: A PAC file proxy `http.Agent` implementation for HTTP and HTTPS
|
||||
* [`socks-proxy-agent`][socks-proxy-agent]: A SOCKS proxy `http.Agent` implementation for HTTP and HTTPS
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Install with `npm`:
|
||||
|
||||
``` bash
|
||||
$ npm install agent-base
|
||||
```
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
Here's a minimal example that creates a new `net.Socket` connection to the server
|
||||
for every HTTP request (i.e. the equivalent of `agent: false` option):
|
||||
|
||||
```js
|
||||
var net = require('net');
|
||||
var tls = require('tls');
|
||||
var url = require('url');
|
||||
var http = require('http');
|
||||
var agent = require('agent-base');
|
||||
|
||||
var endpoint = 'http://nodejs.org/api/';
|
||||
var parsed = url.parse(endpoint);
|
||||
|
||||
// This is the important part!
|
||||
parsed.agent = agent(function (req, opts) {
|
||||
var socket;
|
||||
// `secureEndpoint` is true when using the https module
|
||||
if (opts.secureEndpoint) {
|
||||
socket = tls.connect(opts);
|
||||
} else {
|
||||
socket = net.connect(opts);
|
||||
}
|
||||
return socket;
|
||||
});
|
||||
|
||||
// Everything else works just like normal...
|
||||
http.get(parsed, function (res) {
|
||||
console.log('"response" event!', res.headers);
|
||||
res.pipe(process.stdout);
|
||||
});
|
||||
```
|
||||
|
||||
Returning a Promise or using an `async` function is also supported:
|
||||
|
||||
```js
|
||||
agent(async function (req, opts) {
|
||||
await sleep(1000);
|
||||
// etc…
|
||||
});
|
||||
```
|
||||
|
||||
Return another `http.Agent` instance to "pass through" the responsibility
|
||||
for that HTTP request to that agent:
|
||||
|
||||
```js
|
||||
agent(function (req, opts) {
|
||||
return opts.secureEndpoint ? https.globalAgent : http.globalAgent;
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
## Agent(Function callback[, Object options]) → [http.Agent][]
|
||||
|
||||
Creates a base `http.Agent` that will execute the callback function `callback`
|
||||
for every HTTP request that it is used as the `agent` for. The callback function
|
||||
is responsible for creating a `stream.Duplex` instance of some kind that will be
|
||||
used as the underlying socket in the HTTP request.
|
||||
|
||||
The `options` object accepts the following properties:
|
||||
|
||||
* `timeout` - Number - Timeout for the `callback()` function in milliseconds. Defaults to Infinity (optional).
|
||||
|
||||
The callback function should have the following signature:
|
||||
|
||||
### callback(http.ClientRequest req, Object options, Function cb) → undefined
|
||||
|
||||
The ClientRequest `req` can be accessed to read request headers and
|
||||
and the path, etc. The `options` object contains the options passed
|
||||
to the `http.request()`/`https.request()` function call, and is formatted
|
||||
to be directly passed to `net.connect()`/`tls.connect()`, or however
|
||||
else you want a Socket to be created. Pass the created socket to
|
||||
the callback function `cb` once created, and the HTTP request will
|
||||
continue to proceed.
|
||||
|
||||
If the `https` module is used to invoke the HTTP request, then the
|
||||
`secureEndpoint` property on `options` _will be set to `true`_.
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
|
||||
Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
|
@ -129,8 +246,14 @@ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
[http-proxy-agent]: https://github.com/TooTallNate/node-http-proxy-agent
|
||||
[https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent
|
||||
[pac-proxy-agent]: https://github.com/TooTallNate/node-pac-proxy-agent
|
||||
[socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent
|
||||
[http.Agent]: https://nodejs.org/api/http.html#http_class_http_agent
|
||||
=========================================
|
||||
END OF agent-base@7.1.3 AND INFORMATION
|
||||
END OF agent-base@6.0.2 AND INFORMATION
|
||||
|
||||
%% balanced-match@1.0.2 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
|
|
@ -542,11 +665,124 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|||
=========================================
|
||||
END OF graceful-fs@4.2.10 AND INFORMATION
|
||||
|
||||
%% https-proxy-agent@7.0.6 NOTICES AND INFORMATION BEGIN HERE
|
||||
%% https-proxy-agent@5.0.1 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
https-proxy-agent
|
||||
================
|
||||
### An HTTP(s) proxy `http.Agent` implementation for HTTPS
|
||||
[](https://github.com/TooTallNate/node-https-proxy-agent/actions?workflow=Node+CI)
|
||||
|
||||
This module provides an `http.Agent` implementation that connects to a specified
|
||||
HTTP or HTTPS proxy server, and can be used with the built-in `https` module.
|
||||
|
||||
Specifically, this `Agent` implementation connects to an intermediary "proxy"
|
||||
server and issues the [CONNECT HTTP method][CONNECT], which tells the proxy to
|
||||
open a direct TCP connection to the destination server.
|
||||
|
||||
Since this agent implements the CONNECT HTTP method, it also works with other
|
||||
protocols that use this method when connecting over proxies (i.e. WebSockets).
|
||||
See the "Examples" section below for more.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Install with `npm`:
|
||||
|
||||
``` bash
|
||||
$ npm install https-proxy-agent
|
||||
```
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
#### `https` module example
|
||||
|
||||
``` js
|
||||
var url = require('url');
|
||||
var https = require('https');
|
||||
var HttpsProxyAgent = require('https-proxy-agent');
|
||||
|
||||
// HTTP/HTTPS proxy to connect to
|
||||
var proxy = process.env.http_proxy || 'http://168.63.76.32:3128';
|
||||
console.log('using proxy server %j', proxy);
|
||||
|
||||
// HTTPS endpoint for the proxy to connect to
|
||||
var endpoint = process.argv[2] || 'https://graph.facebook.com/tootallnate';
|
||||
console.log('attempting to GET %j', endpoint);
|
||||
var options = url.parse(endpoint);
|
||||
|
||||
// create an instance of the `HttpsProxyAgent` class with the proxy server information
|
||||
var agent = new HttpsProxyAgent(proxy);
|
||||
options.agent = agent;
|
||||
|
||||
https.get(options, function (res) {
|
||||
console.log('"response" event!', res.headers);
|
||||
res.pipe(process.stdout);
|
||||
});
|
||||
```
|
||||
|
||||
#### `ws` WebSocket connection example
|
||||
|
||||
``` js
|
||||
var url = require('url');
|
||||
var WebSocket = require('ws');
|
||||
var HttpsProxyAgent = require('https-proxy-agent');
|
||||
|
||||
// HTTP/HTTPS proxy to connect to
|
||||
var proxy = process.env.http_proxy || 'http://168.63.76.32:3128';
|
||||
console.log('using proxy server %j', proxy);
|
||||
|
||||
// WebSocket endpoint for the proxy to connect to
|
||||
var endpoint = process.argv[2] || 'ws://echo.websocket.org';
|
||||
var parsed = url.parse(endpoint);
|
||||
console.log('attempting to connect to WebSocket %j', endpoint);
|
||||
|
||||
// create an instance of the `HttpsProxyAgent` class with the proxy server information
|
||||
var options = url.parse(proxy);
|
||||
|
||||
var agent = new HttpsProxyAgent(options);
|
||||
|
||||
// finally, initiate the WebSocket connection
|
||||
var socket = new WebSocket(endpoint, { agent: agent });
|
||||
|
||||
socket.on('open', function () {
|
||||
console.log('"open" event!');
|
||||
socket.send('hello world');
|
||||
});
|
||||
|
||||
socket.on('message', function (data, flags) {
|
||||
console.log('"message" event! %j %j', data, flags);
|
||||
socket.close();
|
||||
});
|
||||
```
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
### new HttpsProxyAgent(Object options)
|
||||
|
||||
The `HttpsProxyAgent` class implements an `http.Agent` subclass that connects
|
||||
to the specified "HTTP(s) proxy server" in order to proxy HTTPS and/or WebSocket
|
||||
requests. This is achieved by using the [HTTP `CONNECT` method][CONNECT].
|
||||
|
||||
The `options` argument may either be a string URI of the proxy server to use, or an
|
||||
"options" object with more specific properties:
|
||||
|
||||
* `host` - String - Proxy host to connect to (may use `hostname` as well). Required.
|
||||
* `port` - Number - Proxy port to connect to. Required.
|
||||
* `protocol` - String - If `https:`, then use TLS to connect to the proxy.
|
||||
* `headers` - Object - Additional HTTP headers to be sent on the HTTP CONNECT method.
|
||||
* Any other options given are passed to the `net.connect()`/`tls.connect()` functions.
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
|
||||
Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
|
@ -566,8 +802,10 @@ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
[CONNECT]: http://en.wikipedia.org/wiki/HTTP_tunnel#HTTP_CONNECT_Tunneling
|
||||
=========================================
|
||||
END OF https-proxy-agent@7.0.6 AND INFORMATION
|
||||
END OF https-proxy-agent@5.0.1 AND INFORMATION
|
||||
|
||||
%% ip-address@9.0.5 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
|
|
@ -1005,11 +1243,141 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|||
=========================================
|
||||
END OF smart-buffer@4.2.0 AND INFORMATION
|
||||
|
||||
%% socks-proxy-agent@8.0.5 NOTICES AND INFORMATION BEGIN HERE
|
||||
%% socks-proxy-agent@6.1.1 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
socks-proxy-agent
|
||||
================
|
||||
### A SOCKS proxy `http.Agent` implementation for HTTP and HTTPS
|
||||
[](https://github.com/TooTallNate/node-socks-proxy-agent/actions?workflow=Node+CI)
|
||||
|
||||
This module provides an `http.Agent` implementation that connects to a
|
||||
specified SOCKS proxy server, and can be used with the built-in `http`
|
||||
and `https` modules.
|
||||
|
||||
It can also be used in conjunction with the `ws` module to establish a WebSocket
|
||||
connection over a SOCKS proxy. See the "Examples" section below.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Install with `npm`:
|
||||
|
||||
``` bash
|
||||
$ npm install socks-proxy-agent
|
||||
```
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
#### TypeScript example
|
||||
|
||||
```ts
|
||||
import https from 'https';
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||
|
||||
const info = {
|
||||
host: 'br41.nordvpn.com',
|
||||
userId: 'your-name@gmail.com',
|
||||
password: 'abcdef12345124'
|
||||
};
|
||||
const agent = new SocksProxyAgent(info);
|
||||
|
||||
https.get('https://jsonip.org', { agent }, (res) => {
|
||||
console.log(res.headers);
|
||||
res.pipe(process.stdout);
|
||||
});
|
||||
```
|
||||
|
||||
#### `http` module example
|
||||
|
||||
```js
|
||||
var url = require('url');
|
||||
var http = require('http');
|
||||
var SocksProxyAgent = require('socks-proxy-agent');
|
||||
|
||||
// SOCKS proxy to connect to
|
||||
var proxy = process.env.socks_proxy || 'socks://127.0.0.1:1080';
|
||||
console.log('using proxy server %j', proxy);
|
||||
|
||||
// HTTP endpoint for the proxy to connect to
|
||||
var endpoint = process.argv[2] || 'http://nodejs.org/api/';
|
||||
console.log('attempting to GET %j', endpoint);
|
||||
var opts = url.parse(endpoint);
|
||||
|
||||
// create an instance of the `SocksProxyAgent` class with the proxy server information
|
||||
var agent = new SocksProxyAgent(proxy);
|
||||
opts.agent = agent;
|
||||
|
||||
http.get(opts, function (res) {
|
||||
console.log('"response" event!', res.headers);
|
||||
res.pipe(process.stdout);
|
||||
});
|
||||
```
|
||||
|
||||
#### `https` module example
|
||||
|
||||
```js
|
||||
var url = require('url');
|
||||
var https = require('https');
|
||||
var SocksProxyAgent = require('socks-proxy-agent');
|
||||
|
||||
// SOCKS proxy to connect to
|
||||
var proxy = process.env.socks_proxy || 'socks://127.0.0.1:1080';
|
||||
console.log('using proxy server %j', proxy);
|
||||
|
||||
// HTTP endpoint for the proxy to connect to
|
||||
var endpoint = process.argv[2] || 'https://encrypted.google.com/';
|
||||
console.log('attempting to GET %j', endpoint);
|
||||
var opts = url.parse(endpoint);
|
||||
|
||||
// create an instance of the `SocksProxyAgent` class with the proxy server information
|
||||
var agent = new SocksProxyAgent(proxy);
|
||||
opts.agent = agent;
|
||||
|
||||
https.get(opts, function (res) {
|
||||
console.log('"response" event!', res.headers);
|
||||
res.pipe(process.stdout);
|
||||
});
|
||||
```
|
||||
|
||||
#### `ws` WebSocket connection example
|
||||
|
||||
``` js
|
||||
var WebSocket = require('ws');
|
||||
var SocksProxyAgent = require('socks-proxy-agent');
|
||||
|
||||
// SOCKS proxy to connect to
|
||||
var proxy = process.env.socks_proxy || 'socks://127.0.0.1:1080';
|
||||
console.log('using proxy server %j', proxy);
|
||||
|
||||
// WebSocket endpoint for the proxy to connect to
|
||||
var endpoint = process.argv[2] || 'ws://echo.websocket.org';
|
||||
console.log('attempting to connect to WebSocket %j', endpoint);
|
||||
|
||||
// create an instance of the `SocksProxyAgent` class with the proxy server information
|
||||
var agent = new SocksProxyAgent(proxy);
|
||||
|
||||
// initiate the WebSocket connection
|
||||
var socket = new WebSocket(endpoint, { agent: agent });
|
||||
|
||||
socket.on('open', function () {
|
||||
console.log('"open" event!');
|
||||
socket.send('hello world');
|
||||
});
|
||||
|
||||
socket.on('message', function (data, flags) {
|
||||
console.log('"message" event! %j %j', data, flags);
|
||||
socket.close();
|
||||
});
|
||||
```
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
|
||||
Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
|
@ -1030,7 +1398,7 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
=========================================
|
||||
END OF socks-proxy-agent@8.0.5 AND INFORMATION
|
||||
END OF socks-proxy-agent@6.1.1 AND INFORMATION
|
||||
|
||||
%% socks@2.8.3 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
|
|
|
|||
|
|
@ -3,31 +3,31 @@
|
|||
"browsers": [
|
||||
{
|
||||
"name": "chromium",
|
||||
"revision": "1156",
|
||||
"revision": "1157",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "133.0.6943.27"
|
||||
"browserVersion": "133.0.6943.35"
|
||||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree",
|
||||
"revision": "1297",
|
||||
"revision": "1300",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "134.0.6974.0"
|
||||
"browserVersion": "134.0.6998.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
"revision": "1472",
|
||||
"revision": "1474",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "134.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox-beta",
|
||||
"revision": "1468",
|
||||
"revision": "1470",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "133.0b9"
|
||||
"browserVersion": "135.0b10"
|
||||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2125",
|
||||
"revision": "2130",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"debian11-x64": "2105",
|
||||
|
|
|
|||
351
packages/playwright-core/bundles/utils/package-lock.json
generated
351
packages/playwright-core/bundles/utils/package-lock.json
generated
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "utils-bundle",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 2,
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
"diff": "^7.0.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"graceful-fs": "4.2.10",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"https-proxy-agent": "5.0.1",
|
||||
"jpeg-js": "0.4.4",
|
||||
"mime": "^3.0.0",
|
||||
"minimatch": "^3.1.2",
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
"proxy-from-env": "1.1.0",
|
||||
"retry": "0.12.0",
|
||||
"signal-exit": "3.0.7",
|
||||
"socks-proxy-agent": "8.0.5",
|
||||
"socks-proxy-agent": "6.1.1",
|
||||
"stack-utils": "2.0.5",
|
||||
"ws": "8.17.1",
|
||||
"yaml": "^2.6.0"
|
||||
|
|
@ -140,12 +140,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
||||
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
|
||||
"license": "MIT",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
|
|
@ -241,16 +243,15 @@
|
|||
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
|
|
@ -400,17 +401,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/socks-proxy-agent": {
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
|
||||
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
|
||||
"license": "MIT",
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz",
|
||||
"integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "^4.3.4",
|
||||
"socks": "^2.8.3"
|
||||
"agent-base": "^6.0.2",
|
||||
"debug": "^4.3.1",
|
||||
"socks": "^2.6.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
|
|
@ -460,312 +460,5 @@
|
|||
"node": ">= 14"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/debug": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
|
||||
"integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"@types/diff": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/diff/-/diff-6.0.0.tgz",
|
||||
"integrity": "sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mime": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
|
||||
"integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/minimatch": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
|
||||
"integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/ms": {
|
||||
"version": "0.7.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
||||
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "17.0.25",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.25.tgz",
|
||||
"integrity": "sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/pngjs": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.1.tgz",
|
||||
"integrity": "sha512-J39njbdW1U/6YyVXvC9+1iflZghP8jgRf2ndYghdJb5xL49LYDB+1EuAxfbuJ2IBbWIL3AjHPQhgaTxT3YaYeg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/progress": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/progress/-/progress-2.0.5.tgz",
|
||||
"integrity": "sha512-ZYYVc/kSMkhH9W/4dNK/sLNra3cnkfT2nJyOAIDY+C2u6w72wa0s1aXAezVtbTsnN8HID1uhXCrLwDE2ZXpplg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/proper-lockfile": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
||||
"integrity": "sha512-kd4LMvcnpYkspDcp7rmXKedn8iJSCoa331zRRamUp5oanKt/CefbEGPQP7G89enz7sKD4bvsr8mHSsC8j5WOvA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/retry": "*"
|
||||
}
|
||||
},
|
||||
"@types/proxy-from-env": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/proxy-from-env/-/proxy-from-env-1.0.1.tgz",
|
||||
"integrity": "sha512-luG++TFHyS61eKcfkR1CVV6a1GMNXDjtqEQIIfaSHax75xp0HU3SlezjOi1yqubJwrG8e9DeW59n6wTblIDwFg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/retry": {
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
|
||||
"integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/stack-utils": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/ws": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.2.tgz",
|
||||
"integrity": "sha512-NOn5eIcgWLOo6qW8AcuLZ7G8PycXu0xTxxkS6Q18VWFxgPUSOwV0pBj2a/4viNZVu25i7RIB7GttdkAIUUXOOg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"agent-base": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
||||
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
|
||||
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
|
||||
},
|
||||
"commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"define-lazy-prop": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
|
||||
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="
|
||||
},
|
||||
"diff": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
|
||||
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw=="
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "16.4.5",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
||||
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg=="
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
|
||||
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.10",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
|
||||
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
|
||||
},
|
||||
"https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"requires": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
}
|
||||
},
|
||||
"ip-address": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
||||
"requires": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"is-docker": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
||||
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="
|
||||
},
|
||||
"is-wsl": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"requires": {
|
||||
"is-docker": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"jpeg-js": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
||||
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="
|
||||
},
|
||||
"jsbn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
|
||||
},
|
||||
"mime": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
|
||||
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"open": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz",
|
||||
"integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==",
|
||||
"requires": {
|
||||
"define-lazy-prop": "^2.0.0",
|
||||
"is-docker": "^2.1.1",
|
||||
"is-wsl": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"pngjs": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
|
||||
"integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="
|
||||
},
|
||||
"progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="
|
||||
},
|
||||
"proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
|
||||
},
|
||||
"smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="
|
||||
},
|
||||
"socks": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
|
||||
"integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
|
||||
"requires": {
|
||||
"ip-address": "^9.0.5",
|
||||
"smart-buffer": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"socks-proxy-agent": {
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
|
||||
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
|
||||
"requires": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "^4.3.4",
|
||||
"socks": "^2.8.3"
|
||||
}
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
|
||||
},
|
||||
"stack-utils": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz",
|
||||
"integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==",
|
||||
"requires": {
|
||||
"escape-string-regexp": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"yaml": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
|
||||
"integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"diff": "^7.0.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"graceful-fs": "4.2.10",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"https-proxy-agent": "5.0.1",
|
||||
"jpeg-js": "0.4.4",
|
||||
"mime": "^3.0.0",
|
||||
"minimatch": "^3.1.2",
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
"proxy-from-env": "1.1.0",
|
||||
"retry": "0.12.0",
|
||||
"signal-exit": "3.0.7",
|
||||
"socks-proxy-agent": "8.0.5",
|
||||
"socks-proxy-agent": "6.1.1",
|
||||
"stack-utils": "2.0.5",
|
||||
"ws": "8.17.1",
|
||||
"yaml": "^2.6.0"
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { Worker } from './worker';
|
|||
import { Events } from './events';
|
||||
import { TimeoutSettings } from '../common/timeoutSettings';
|
||||
import { Waiter } from './waiter';
|
||||
import type { Headers, WaitForEventOptions, BrowserContextOptions, StorageState, LaunchOptions } from './types';
|
||||
import type { Headers, WaitForEventOptions, BrowserContextOptions, LaunchOptions, StorageState } from './types';
|
||||
import { type URLMatch, headersObjectToArray, isRegExp, isString, urlMatchesEqual, mkdirIfNeeded } from '../utils';
|
||||
import type * as api from '../../types/types';
|
||||
import type * as structs from '../../types/structs';
|
||||
|
|
@ -536,6 +536,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
|
|||
colorScheme: options.colorScheme === null ? 'no-override' : options.colorScheme,
|
||||
reducedMotion: options.reducedMotion === null ? 'no-override' : options.reducedMotion,
|
||||
forcedColors: options.forcedColors === null ? 'no-override' : options.forcedColors,
|
||||
contrast: options.contrast === null ? 'no-override' : options.contrast,
|
||||
acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads),
|
||||
clientCertificates: await toClientCertificatesProtocol(options.clientCertificates),
|
||||
mockingProxyBaseURL: type?._playwright._mockingProxy?.baseURL(),
|
||||
|
|
|
|||
|
|
@ -486,12 +486,13 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
await this._channel.requestGC();
|
||||
}
|
||||
|
||||
async emulateMedia(options: { media?: 'screen' | 'print' | null, colorScheme?: 'dark' | 'light' | 'no-preference' | null, reducedMotion?: 'reduce' | 'no-preference' | null, forcedColors?: 'active' | 'none' | null } = {}) {
|
||||
async emulateMedia(options: { media?: 'screen' | 'print' | null, colorScheme?: 'dark' | 'light' | 'no-preference' | null, reducedMotion?: 'reduce' | 'no-preference' | null, forcedColors?: 'active' | 'none' | null, contrast?: 'no-preference' | 'more' | null } = {}) {
|
||||
await this._channel.emulateMedia({
|
||||
media: options.media === null ? 'no-override' : options.media,
|
||||
colorScheme: options.colorScheme === null ? 'no-override' : options.colorScheme,
|
||||
reducedMotion: options.reducedMotion === null ? 'no-override' : options.reducedMotion,
|
||||
forcedColors: options.forcedColors === null ? 'no-override' : options.forcedColors,
|
||||
contrast: options.contrast === null ? 'no-override' : options.contrast,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export type StorageState = {
|
|||
};
|
||||
export type SetStorageState = {
|
||||
cookies?: channels.SetNetworkCookie[],
|
||||
origins?: channels.OriginStorage[]
|
||||
origins?: channels.SetOriginStorage[]
|
||||
};
|
||||
|
||||
export type LifecycleEvent = channels.LifecycleEvent;
|
||||
|
|
@ -58,7 +58,7 @@ export type ClientCertificate = {
|
|||
passphrase?: string;
|
||||
};
|
||||
|
||||
export type BrowserContextOptions = Omit<channels.BrowserNewContextOptions, 'viewport' | 'noDefaultViewport' | 'extraHTTPHeaders' | 'clientCertificates' | 'storageState' | 'recordHar' | 'colorScheme' | 'reducedMotion' | 'forcedColors' | 'acceptDownloads'> & {
|
||||
export type BrowserContextOptions = Omit<channels.BrowserNewContextOptions, 'viewport' | 'noDefaultViewport' | 'extraHTTPHeaders' | 'clientCertificates' | 'storageState' | 'recordHar' | 'colorScheme' | 'reducedMotion' | 'forcedColors' | 'acceptDownloads' | 'contrast'> & {
|
||||
viewport?: Size | null;
|
||||
extraHTTPHeaders?: Headers;
|
||||
logger?: Logger;
|
||||
|
|
@ -80,6 +80,7 @@ export type BrowserContextOptions = Omit<channels.BrowserNewContextOptions, 'vie
|
|||
colorScheme?: 'dark' | 'light' | 'no-preference' | null;
|
||||
reducedMotion?: 'reduce' | 'no-preference' | null;
|
||||
forcedColors?: 'active' | 'none' | null;
|
||||
contrast?: 'more' | 'no-preference' | null;
|
||||
acceptDownloads?: boolean;
|
||||
clientCertificates?: ClientCertificate[];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -142,9 +142,38 @@ scheme.NameValue = tObject({
|
|||
name: tString,
|
||||
value: tString,
|
||||
});
|
||||
scheme.IndexedDBDatabase = tObject({
|
||||
name: tString,
|
||||
version: tNumber,
|
||||
stores: tArray(tObject({
|
||||
name: tString,
|
||||
autoIncrement: tBoolean,
|
||||
keyPath: tOptional(tString),
|
||||
keyPathArray: tOptional(tArray(tString)),
|
||||
records: tArray(tObject({
|
||||
key: tOptional(tAny),
|
||||
keyEncoded: tOptional(tAny),
|
||||
value: tOptional(tAny),
|
||||
valueEncoded: tOptional(tAny),
|
||||
})),
|
||||
indexes: tArray(tObject({
|
||||
name: tString,
|
||||
keyPath: tOptional(tString),
|
||||
keyPathArray: tOptional(tArray(tString)),
|
||||
multiEntry: tBoolean,
|
||||
unique: tBoolean,
|
||||
})),
|
||||
})),
|
||||
});
|
||||
scheme.SetOriginStorage = tObject({
|
||||
origin: tString,
|
||||
localStorage: tArray(tType('NameValue')),
|
||||
indexedDB: tOptional(tArray(tType('IndexedDBDatabase'))),
|
||||
});
|
||||
scheme.OriginStorage = tObject({
|
||||
origin: tString,
|
||||
localStorage: tArray(tType('NameValue')),
|
||||
indexedDB: tArray(tType('IndexedDBDatabase')),
|
||||
});
|
||||
scheme.SerializedError = tObject({
|
||||
error: tOptional(tObject({
|
||||
|
|
@ -363,7 +392,7 @@ scheme.LocalUtilsNewRequestParams = tObject({
|
|||
timeout: tOptional(tNumber),
|
||||
storageState: tOptional(tObject({
|
||||
cookies: tOptional(tArray(tType('NetworkCookie'))),
|
||||
origins: tOptional(tArray(tType('OriginStorage'))),
|
||||
origins: tOptional(tArray(tType('SetOriginStorage'))),
|
||||
})),
|
||||
tracesDir: tOptional(tString),
|
||||
});
|
||||
|
|
@ -447,7 +476,7 @@ scheme.PlaywrightNewRequestParams = tObject({
|
|||
timeout: tOptional(tNumber),
|
||||
storageState: tOptional(tObject({
|
||||
cookies: tOptional(tArray(tType('NetworkCookie'))),
|
||||
origins: tOptional(tArray(tType('OriginStorage'))),
|
||||
origins: tOptional(tArray(tType('SetOriginStorage'))),
|
||||
})),
|
||||
tracesDir: tOptional(tString),
|
||||
});
|
||||
|
|
@ -668,6 +697,7 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({
|
|||
reducedMotion: tOptional(tEnum(['reduce', 'no-preference', 'no-override'])),
|
||||
forcedColors: tOptional(tEnum(['active', 'none', 'no-override'])),
|
||||
acceptDownloads: tOptional(tEnum(['accept', 'deny', 'internal-browser-default'])),
|
||||
contrast: tOptional(tEnum(['no-preference', 'more', 'no-override'])),
|
||||
baseURL: tOptional(tString),
|
||||
recordVideo: tOptional(tObject({
|
||||
dir: tString,
|
||||
|
|
@ -754,6 +784,7 @@ scheme.BrowserNewContextParams = tObject({
|
|||
reducedMotion: tOptional(tEnum(['reduce', 'no-preference', 'no-override'])),
|
||||
forcedColors: tOptional(tEnum(['active', 'none', 'no-override'])),
|
||||
acceptDownloads: tOptional(tEnum(['accept', 'deny', 'internal-browser-default'])),
|
||||
contrast: tOptional(tEnum(['no-preference', 'more', 'no-override'])),
|
||||
baseURL: tOptional(tString),
|
||||
recordVideo: tOptional(tObject({
|
||||
dir: tString,
|
||||
|
|
@ -773,7 +804,7 @@ scheme.BrowserNewContextParams = tObject({
|
|||
})),
|
||||
storageState: tOptional(tObject({
|
||||
cookies: tOptional(tArray(tType('SetNetworkCookie'))),
|
||||
origins: tOptional(tArray(tType('OriginStorage'))),
|
||||
origins: tOptional(tArray(tType('SetOriginStorage'))),
|
||||
})),
|
||||
mockingProxyBaseURL: tOptional(tString),
|
||||
});
|
||||
|
|
@ -824,6 +855,7 @@ scheme.BrowserNewContextForReuseParams = tObject({
|
|||
reducedMotion: tOptional(tEnum(['reduce', 'no-preference', 'no-override'])),
|
||||
forcedColors: tOptional(tEnum(['active', 'none', 'no-override'])),
|
||||
acceptDownloads: tOptional(tEnum(['accept', 'deny', 'internal-browser-default'])),
|
||||
contrast: tOptional(tEnum(['no-preference', 'more', 'no-override'])),
|
||||
baseURL: tOptional(tString),
|
||||
recordVideo: tOptional(tObject({
|
||||
dir: tString,
|
||||
|
|
@ -843,7 +875,7 @@ scheme.BrowserNewContextForReuseParams = tObject({
|
|||
})),
|
||||
storageState: tOptional(tObject({
|
||||
cookies: tOptional(tArray(tType('SetNetworkCookie'))),
|
||||
origins: tOptional(tArray(tType('OriginStorage'))),
|
||||
origins: tOptional(tArray(tType('SetOriginStorage'))),
|
||||
})),
|
||||
});
|
||||
scheme.BrowserNewContextForReuseResult = tObject({
|
||||
|
|
@ -1184,6 +1216,7 @@ scheme.PageEmulateMediaParams = tObject({
|
|||
colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference', 'no-override'])),
|
||||
reducedMotion: tOptional(tEnum(['reduce', 'no-preference', 'no-override'])),
|
||||
forcedColors: tOptional(tEnum(['active', 'none', 'no-override'])),
|
||||
contrast: tOptional(tEnum(['no-preference', 'more', 'no-override'])),
|
||||
});
|
||||
scheme.PageEmulateMediaResult = tOptional(tObject({}));
|
||||
scheme.PageExposeBindingParams = tObject({
|
||||
|
|
@ -2706,6 +2739,7 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({
|
|||
reducedMotion: tOptional(tEnum(['reduce', 'no-preference', 'no-override'])),
|
||||
forcedColors: tOptional(tEnum(['active', 'none', 'no-override'])),
|
||||
acceptDownloads: tOptional(tEnum(['accept', 'deny', 'internal-browser-default'])),
|
||||
contrast: tOptional(tEnum(['no-preference', 'more', 'no-override'])),
|
||||
baseURL: tOptional(tString),
|
||||
recordVideo: tOptional(tObject({
|
||||
dir: tString,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ import type { Artifact } from './artifact';
|
|||
import { Clock } from './clock';
|
||||
import type { ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
|
||||
import { RecorderApp } from './recorder/recorderApp';
|
||||
import * as storageScript from './storageScript';
|
||||
import * as utilityScriptSerializers from './isomorphic/utilityScriptSerializers';
|
||||
|
||||
export abstract class BrowserContext extends SdkObject implements network.RequestContext {
|
||||
static Events = {
|
||||
|
|
@ -513,17 +515,17 @@ export abstract class BrowserContext extends SdkObject implements network.Reques
|
|||
};
|
||||
const originsToSave = new Set(this._origins);
|
||||
|
||||
const collectScript = `(${storageScript.collect})((${utilityScriptSerializers.source})(), ${this._browser.options.name === 'firefox'})`;
|
||||
|
||||
// First try collecting storage stage from existing pages.
|
||||
for (const page of this.pages()) {
|
||||
const origin = page.mainFrame().origin();
|
||||
if (!origin || !originsToSave.has(origin))
|
||||
continue;
|
||||
try {
|
||||
const storage = await page.mainFrame().nonStallingEvaluateInExistingContext(`({
|
||||
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })),
|
||||
})`, 'utility');
|
||||
if (storage.localStorage.length)
|
||||
result.origins.push({ origin, localStorage: storage.localStorage } as channels.OriginStorage);
|
||||
const storage: storageScript.Storage = await page.mainFrame().nonStallingEvaluateInExistingContext(collectScript, 'utility');
|
||||
if (storage.localStorage.length || storage.indexedDB.length)
|
||||
result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB });
|
||||
originsToSave.delete(origin);
|
||||
} catch {
|
||||
// When failed on the live page, we'll retry on the blank page below.
|
||||
|
|
@ -539,15 +541,11 @@ export abstract class BrowserContext extends SdkObject implements network.Reques
|
|||
return true;
|
||||
});
|
||||
for (const origin of originsToSave) {
|
||||
const originStorage: channels.OriginStorage = { origin, localStorage: [] };
|
||||
const frame = page.mainFrame();
|
||||
await frame.goto(internalMetadata, origin);
|
||||
const storage = await frame.evaluateExpression(`({
|
||||
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })),
|
||||
})`, { world: 'utility' });
|
||||
originStorage.localStorage = storage.localStorage;
|
||||
if (storage.localStorage.length)
|
||||
result.origins.push(originStorage);
|
||||
const storage: storageScript.Storage = await frame.evaluateExpression(collectScript, { world: 'utility' });
|
||||
if (storage.localStorage.length || storage.indexedDB.length)
|
||||
result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB });
|
||||
}
|
||||
await page.close(internalMetadata);
|
||||
}
|
||||
|
|
@ -610,11 +608,7 @@ export abstract class BrowserContext extends SdkObject implements network.Reques
|
|||
for (const originState of state.origins) {
|
||||
const frame = page.mainFrame();
|
||||
await frame.goto(metadata, originState.origin);
|
||||
await frame.evaluateExpression(`
|
||||
originState => {
|
||||
for (const { name, value } of (originState.localStorage || []))
|
||||
localStorage.setItem(name, value);
|
||||
}`, { isFunction: true, world: 'utility' }, originState);
|
||||
await frame.evaluateExpression(`(${storageScript.restore})(${JSON.stringify(originState)}, (${utilityScriptSerializers.source})())`, { world: 'utility' });
|
||||
}
|
||||
await page.close(internalMetadata);
|
||||
}
|
||||
|
|
@ -762,6 +756,7 @@ const paramsThatAllowContextReuse: (keyof channels.BrowserNewContextForReusePara
|
|||
'colorScheme',
|
||||
'forcedColors',
|
||||
'reducedMotion',
|
||||
'contrast',
|
||||
'screen',
|
||||
'userAgent',
|
||||
'viewport',
|
||||
|
|
|
|||
|
|
@ -1026,10 +1026,12 @@ class FrameSession {
|
|||
const colorScheme = emulatedMedia.colorScheme === 'no-override' ? '' : emulatedMedia.colorScheme;
|
||||
const reducedMotion = emulatedMedia.reducedMotion === 'no-override' ? '' : emulatedMedia.reducedMotion;
|
||||
const forcedColors = emulatedMedia.forcedColors === 'no-override' ? '' : emulatedMedia.forcedColors;
|
||||
const contrast = emulatedMedia.contrast === 'no-override' ? '' : emulatedMedia.contrast;
|
||||
const features = [
|
||||
{ name: 'prefers-color-scheme', value: colorScheme },
|
||||
{ name: 'prefers-reduced-motion', value: reducedMotion },
|
||||
{ name: 'forced-colors', value: forcedColors },
|
||||
{ name: 'prefers-contrast', value: contrast },
|
||||
];
|
||||
await this._client.send('Emulation.setEmulatedMedia', { media, features });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 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/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 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/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 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/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 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/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 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/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 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/133.0.6943.27 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 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/133.0.6943.27 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 1138,
|
||||
"height": 712
|
||||
|
|
@ -1098,7 +1098,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"LG Optimus L70": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/133.0.6943.27 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/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 384,
|
||||
"height": 640
|
||||
|
|
@ -1109,7 +1109,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"LG Optimus L70 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/133.0.6943.27 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/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 384
|
||||
|
|
@ -1120,18 +1120,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Microsoft Lumia 550": {
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
},
|
||||
"deviceScaleFactor": 2,
|
||||
"isMobile": true,
|
||||
"hasTouch": true,
|
||||
"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/133.0.6943.27 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/133.0.6943.35 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1141,8 +1130,19 @@
|
|||
"hasTouch": true,
|
||||
"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/133.0.6943.35 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
},
|
||||
"deviceScaleFactor": 2,
|
||||
"isMobile": true,
|
||||
"hasTouch": true,
|
||||
"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/133.0.6943.27 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/133.0.6943.35 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1153,7 +1153,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Microsoft Lumia 950 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.35 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1164,7 +1164,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 10": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 800,
|
||||
"height": 1280
|
||||
|
|
@ -1175,7 +1175,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 10 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 1280,
|
||||
"height": 800
|
||||
|
|
@ -1186,7 +1186,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 384,
|
||||
"height": 640
|
||||
|
|
@ -1197,7 +1197,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 384
|
||||
|
|
@ -1208,7 +1208,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1219,7 +1219,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1230,7 +1230,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5X": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1241,7 +1241,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5X landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1252,7 +1252,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1263,7 +1263,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1274,7 +1274,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6P": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1285,7 +1285,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6P landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1296,7 +1296,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 7": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 600,
|
||||
"height": 960
|
||||
|
|
@ -1307,7 +1307,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 7 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 960,
|
||||
"height": 600
|
||||
|
|
@ -1362,7 +1362,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Pixel 2": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 411,
|
||||
"height": 731
|
||||
|
|
@ -1373,7 +1373,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 2 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 731,
|
||||
"height": 411
|
||||
|
|
@ -1384,7 +1384,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 2 XL": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 411,
|
||||
"height": 823
|
||||
|
|
@ -1395,7 +1395,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 2 XL landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 823,
|
||||
"height": 411
|
||||
|
|
@ -1406,7 +1406,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 3": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 393,
|
||||
"height": 786
|
||||
|
|
@ -1417,7 +1417,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 3 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 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/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 786,
|
||||
"height": 393
|
||||
|
|
@ -1428,7 +1428,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 353,
|
||||
"height": 745
|
||||
|
|
@ -1439,7 +1439,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 745,
|
||||
"height": 353
|
||||
|
|
@ -1450,7 +1450,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4a (5G)": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 412,
|
||||
"height": 892
|
||||
|
|
@ -1465,7 +1465,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4a (5G) landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"height": 892,
|
||||
"width": 412
|
||||
|
|
@ -1480,7 +1480,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 393,
|
||||
"height": 851
|
||||
|
|
@ -1495,7 +1495,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 5 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 851,
|
||||
"height": 393
|
||||
|
|
@ -1510,7 +1510,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 7": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 412,
|
||||
"height": 915
|
||||
|
|
@ -1525,7 +1525,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 7 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 915,
|
||||
"height": 412
|
||||
|
|
@ -1540,7 +1540,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Moto G4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1551,7 +1551,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Moto G4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1562,7 +1562,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Chrome HiDPI": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36",
|
||||
"screen": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1577,7 +1577,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Edge HiDPI": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Safari/537.36 Edg/133.0.6943.27",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36 Edg/133.0.6943.35",
|
||||
"screen": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1622,7 +1622,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Desktop Chrome": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36",
|
||||
"screen": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
@ -1637,7 +1637,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Edge": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.27 Safari/537.36 Edg/133.0.6943.27",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36 Edg/133.0.6943.35",
|
||||
"screen": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
|
|||
colorScheme: params.colorScheme,
|
||||
reducedMotion: params.reducedMotion,
|
||||
forcedColors: params.forcedColors,
|
||||
contrast: params.contrast,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import http from 'http';
|
|||
import https from 'https';
|
||||
import type { Readable, TransformCallback } from 'stream';
|
||||
import { pipeline, Transform } from 'stream';
|
||||
import url from 'url';
|
||||
import zlib from 'zlib';
|
||||
import type { HTTPCredentials } from '../../types/types';
|
||||
import { TimeoutSettings } from '../common/timeoutSettings';
|
||||
|
|
@ -499,12 +500,12 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
// happy eyeballs don't emit lookup and connect events, so we use our custom ones
|
||||
const happyEyeBallsTimings = timingForSocket(socket);
|
||||
dnsLookupAt = happyEyeBallsTimings.dnsLookupAt;
|
||||
tcpConnectionAt ??= happyEyeBallsTimings.tcpConnectionAt;
|
||||
tcpConnectionAt = happyEyeBallsTimings.tcpConnectionAt;
|
||||
|
||||
// non-happy-eyeballs sockets
|
||||
listeners.push(
|
||||
eventsHelper.addEventListener(socket, 'lookup', () => { dnsLookupAt = monotonicTime(); }),
|
||||
eventsHelper.addEventListener(socket, 'connect', () => { tcpConnectionAt ??= monotonicTime(); }),
|
||||
eventsHelper.addEventListener(socket, 'connect', () => { tcpConnectionAt = monotonicTime(); }),
|
||||
eventsHelper.addEventListener(socket, 'secureConnect', () => {
|
||||
tlsHandshakeAt = monotonicTime();
|
||||
|
||||
|
|
@ -521,21 +522,11 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
}),
|
||||
);
|
||||
|
||||
// when using socks proxy, having the socket means the connection got established
|
||||
if (agent instanceof SocksProxyAgent)
|
||||
tcpConnectionAt ??= monotonicTime();
|
||||
|
||||
serverIPAddress = socket.remoteAddress;
|
||||
serverPort = socket.remotePort;
|
||||
});
|
||||
request.on('finish', () => { requestFinishAt = monotonicTime(); });
|
||||
|
||||
// http proxy
|
||||
request.on('proxyConnect', () => {
|
||||
tcpConnectionAt ??= monotonicTime();
|
||||
});
|
||||
|
||||
|
||||
progress.log(`→ ${options.method} ${url.toString()}`);
|
||||
if (options.headers) {
|
||||
for (const [name, value] of Object.entries(options.headers))
|
||||
|
|
@ -653,7 +644,7 @@ export class GlobalAPIRequestContext extends APIRequestContext {
|
|||
proxy.server = url;
|
||||
}
|
||||
if (options.storageState) {
|
||||
this._origins = options.storageState.origins;
|
||||
this._origins = options.storageState.origins?.map(origin => ({ indexedDB: [], ...origin }));
|
||||
this._cookieStore.addCookies(options.storageState.cookies || []);
|
||||
}
|
||||
verifyClientCertificates(options.clientCertificates);
|
||||
|
|
@ -702,16 +693,17 @@ export class GlobalAPIRequestContext extends APIRequestContext {
|
|||
}
|
||||
|
||||
export function createProxyAgent(proxy: types.ProxySettings) {
|
||||
const proxyURL = new URL(proxy.server);
|
||||
if (proxyURL.protocol?.startsWith('socks'))
|
||||
return new SocksProxyAgent(proxyURL);
|
||||
|
||||
const proxyOpts = url.parse(proxy.server);
|
||||
if (proxyOpts.protocol?.startsWith('socks')) {
|
||||
return new SocksProxyAgent({
|
||||
host: proxyOpts.hostname,
|
||||
port: proxyOpts.port || undefined,
|
||||
});
|
||||
}
|
||||
if (proxy.username)
|
||||
proxyURL.username = proxy.username;
|
||||
if (proxy.password)
|
||||
proxyURL.password = proxy.password;
|
||||
// TODO: We should use HttpProxyAgent conditional on proxyURL.protocol instead of always using CONNECT method.
|
||||
return new HttpsProxyAgent(proxyURL);
|
||||
proxyOpts.auth = `${proxy.username}:${proxy.password || ''}`;
|
||||
// TODO: We should use HttpProxyAgent conditional on proxyOpts.protocol instead of always using CONNECT method.
|
||||
return new HttpsProxyAgent(proxyOpts);
|
||||
}
|
||||
|
||||
function toHeadersArray(rawHeaders: string[]): types.HeadersArray {
|
||||
|
|
|
|||
|
|
@ -240,6 +240,12 @@ export class FFBrowserContext extends BrowserContext {
|
|||
forcedColors: this._options.forcedColors !== undefined ? this._options.forcedColors : 'none',
|
||||
}));
|
||||
}
|
||||
if (this._options.contrast !== 'no-override') {
|
||||
promises.push(this._browser.session.send('Browser.setContrast', {
|
||||
browserContextId,
|
||||
contrast: this._options.contrast !== undefined ? this._options.contrast : 'no-preference',
|
||||
}));
|
||||
}
|
||||
if (this._options.recordVideo) {
|
||||
promises.push(this._ensureVideosPath().then(() => {
|
||||
return this._browser.session.send('Browser.setVideoRecordingOptions', {
|
||||
|
|
@ -435,4 +441,6 @@ function toJugglerProxyOptions(proxy: types.ProxySettings) {
|
|||
|
||||
// Prefs for quick fixes that didn't make it to the build.
|
||||
// Should all be moved to `playwright.cfg`.
|
||||
const kBandaidFirefoxUserPrefs = {};
|
||||
const kBandaidFirefoxUserPrefs = {
|
||||
'dom.fetchKeepalive.enabled': false,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -347,12 +347,14 @@ export class FFPage implements PageDelegate {
|
|||
const colorScheme = emulatedMedia.colorScheme === 'no-override' ? undefined : emulatedMedia.colorScheme;
|
||||
const reducedMotion = emulatedMedia.reducedMotion === 'no-override' ? undefined : emulatedMedia.reducedMotion;
|
||||
const forcedColors = emulatedMedia.forcedColors === 'no-override' ? undefined : emulatedMedia.forcedColors;
|
||||
const contrast = emulatedMedia.contrast === 'no-override' ? undefined : emulatedMedia.contrast;
|
||||
await this._session.send('Page.setEmulatedMedia', {
|
||||
// Empty string means reset.
|
||||
type: emulatedMedia.media === 'no-override' ? '' : emulatedMedia.media,
|
||||
colorScheme,
|
||||
reducedMotion,
|
||||
forcedColors,
|
||||
contrast,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1717,7 +1717,7 @@ export class Frame extends SdkObject {
|
|||
}, { source, arg });
|
||||
}
|
||||
|
||||
async resetStorageForCurrentOriginBestEffort(newStorage: channels.OriginStorage | undefined) {
|
||||
async resetStorageForCurrentOriginBestEffort(newStorage: channels.SetOriginStorage | undefined) {
|
||||
const context = await this._utilityContext();
|
||||
await context.evaluate(async ({ ls }) => {
|
||||
// Clean DOMStorage.
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@ export function generateAriaTree(rootElement: Element): AriaSnapshot {
|
|||
|
||||
if (node.nodeType === Node.TEXT_NODE && node.nodeValue) {
|
||||
const text = node.nodeValue;
|
||||
if (text)
|
||||
// <textarea>AAA</textarea> should not report AAA as a child of the textarea.
|
||||
if (ariaNode.role !== 'textbox' && text)
|
||||
ariaNode.children.push(node.nodeValue || '');
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export class PollingRecorder implements RecorderDelegate {
|
|||
private _recorder: Recorder;
|
||||
private _embedder: Embedder;
|
||||
private _pollRecorderModeTimer: number | undefined;
|
||||
private _lastStateJSON: string | undefined;
|
||||
|
||||
constructor(injectedScript: InjectedScript) {
|
||||
this._recorder = new Recorder(injectedScript);
|
||||
|
|
@ -42,6 +43,7 @@ export class PollingRecorder implements RecorderDelegate {
|
|||
injectedScript.onGlobalListenersRemoved.add(() => this._recorder.installListeners());
|
||||
|
||||
const refreshOverlay = () => {
|
||||
this._lastStateJSON = undefined;
|
||||
this._pollRecorderMode().catch(e => console.log(e)); // eslint-disable-line no-console
|
||||
};
|
||||
this._embedder.__pw_refreshOverlay = refreshOverlay;
|
||||
|
|
@ -57,13 +59,19 @@ export class PollingRecorder implements RecorderDelegate {
|
|||
this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod);
|
||||
return;
|
||||
}
|
||||
const win = this._recorder.document.defaultView!;
|
||||
if (win.top !== win) {
|
||||
// Only show action point in the main frame, since it is relative to the page's viewport.
|
||||
// Otherwise we'll see multiple action points at different locations.
|
||||
state.actionPoint = undefined;
|
||||
|
||||
const stringifiedState = JSON.stringify(state);
|
||||
if (this._lastStateJSON !== stringifiedState) {
|
||||
this._lastStateJSON = stringifiedState;
|
||||
const win = this._recorder.document.defaultView!;
|
||||
if (win.top !== win) {
|
||||
// Only show action point in the main frame, since it is relative to the page's viewport.
|
||||
// Otherwise we'll see multiple action points at different locations.
|
||||
state.actionPoint = undefined;
|
||||
}
|
||||
this._recorder.setUIState(state, this);
|
||||
}
|
||||
this._recorder.setUIState(state, this);
|
||||
|
||||
this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -354,27 +354,34 @@ export function getPseudoContent(element: Element, pseudo: '::before' | '::after
|
|||
if (cache?.has(element))
|
||||
return cache?.get(element) || '';
|
||||
const pseudoStyle = getElementComputedStyle(element, pseudo);
|
||||
const content = getPseudoContentImpl(pseudoStyle);
|
||||
const content = getPseudoContentImpl(element, pseudoStyle);
|
||||
if (cache)
|
||||
cache.set(element, content);
|
||||
return content;
|
||||
}
|
||||
|
||||
function getPseudoContentImpl(pseudoStyle: CSSStyleDeclaration | undefined) {
|
||||
function getPseudoContentImpl(element: Element, pseudoStyle: CSSStyleDeclaration | undefined) {
|
||||
// Note: all browsers ignore display:none and visibility:hidden pseudos.
|
||||
if (!pseudoStyle || pseudoStyle.display === 'none' || pseudoStyle.visibility === 'hidden')
|
||||
return '';
|
||||
const content = pseudoStyle.content;
|
||||
let resolvedContent: string | undefined;
|
||||
if ((content[0] === '\'' && content[content.length - 1] === '\'') ||
|
||||
(content[0] === '"' && content[content.length - 1] === '"')) {
|
||||
const unquoted = content.substring(1, content.length - 1);
|
||||
resolvedContent = content.substring(1, content.length - 1);
|
||||
} else if (content.startsWith('attr(') && content.endsWith(')')) {
|
||||
// Firefox does not resolve attribute accessors in content.
|
||||
const attrName = content.substring('attr('.length, content.length - 1).trim();
|
||||
resolvedContent = element.getAttribute(attrName) || '';
|
||||
}
|
||||
if (resolvedContent !== undefined) {
|
||||
// SPEC DIFFERENCE.
|
||||
// Spec says "CSS textual content, without a space", but we account for display
|
||||
// to pass "name_file-label-inline-block-styles-manual.html"
|
||||
const display = pseudoStyle.display || 'inline';
|
||||
if (display !== 'inline')
|
||||
return ' ' + unquoted + ' ';
|
||||
return unquoted;
|
||||
return ' ' + resolvedContent + ' ';
|
||||
return resolvedContent;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ type EmulatedMedia = {
|
|||
colorScheme: types.ColorScheme;
|
||||
reducedMotion: types.ReducedMotion;
|
||||
forcedColors: types.ForcedColors;
|
||||
contrast: types.Contrast;
|
||||
};
|
||||
|
||||
type ExpectScreenshotOptions = ImageComparatorOptions & ScreenshotOptions & {
|
||||
|
|
@ -542,6 +543,8 @@ export class Page extends SdkObject {
|
|||
this._emulatedMedia.reducedMotion = options.reducedMotion;
|
||||
if (options.forcedColors !== undefined)
|
||||
this._emulatedMedia.forcedColors = options.forcedColors;
|
||||
if (options.contrast !== undefined)
|
||||
this._emulatedMedia.contrast = options.contrast;
|
||||
|
||||
await this._delegate.updateEmulateMedia();
|
||||
}
|
||||
|
|
@ -553,6 +556,7 @@ export class Page extends SdkObject {
|
|||
colorScheme: this._emulatedMedia.colorScheme !== undefined ? this._emulatedMedia.colorScheme : contextOptions.colorScheme ?? 'light',
|
||||
reducedMotion: this._emulatedMedia.reducedMotion !== undefined ? this._emulatedMedia.reducedMotion : contextOptions.reducedMotion ?? 'no-preference',
|
||||
forcedColors: this._emulatedMedia.forcedColors !== undefined ? this._emulatedMedia.forcedColors : contextOptions.forcedColors ?? 'none',
|
||||
contrast: this._emulatedMedia.contrast !== undefined ? this._emulatedMedia.contrast : contextOptions.contrast ?? 'no-preference',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export class RecorderCollection extends EventEmitter {
|
|||
let generateGoto = false;
|
||||
if (!lastAction)
|
||||
generateGoto = true;
|
||||
else if (lastAction.action.name !== 'click' && lastAction.action.name !== 'press')
|
||||
else if (lastAction.action.name !== 'click' && lastAction.action.name !== 'press' && lastAction.action.name !== 'fill')
|
||||
generateGoto = true;
|
||||
else if (timestamp - lastAction.startTime > signalThreshold)
|
||||
generateGoto = true;
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ class SocksProxyConnection {
|
|||
|
||||
async connect() {
|
||||
if (this.socksProxy.proxyAgentFromOptions)
|
||||
this.target = await this.socksProxy.proxyAgentFromOptions.connect(new EventEmitter() as any, { host: rewriteToLocalhostIfNeeded(this.host), port: this.port, secureEndpoint: false });
|
||||
this.target = await this.socksProxy.proxyAgentFromOptions.callback(new EventEmitter() as any, { host: rewriteToLocalhostIfNeeded(this.host), port: this.port, secureEndpoint: false });
|
||||
else
|
||||
this.target = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port);
|
||||
|
||||
|
|
|
|||
171
packages/playwright-core/src/server/storageScript.ts
Normal file
171
packages/playwright-core/src/server/storageScript.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type * as channels from '@protocol/channels';
|
||||
import type { source } from './isomorphic/utilityScriptSerializers';
|
||||
|
||||
export type Storage = Omit<channels.OriginStorage, 'origin'>;
|
||||
|
||||
export async function collect(serializers: ReturnType<typeof source>, isFirefox: boolean): Promise<Storage> {
|
||||
const idbResult = await Promise.all((await indexedDB.databases()).map(async dbInfo => {
|
||||
if (!dbInfo.name)
|
||||
throw new Error('Database name is empty');
|
||||
if (!dbInfo.version)
|
||||
throw new Error('Database version is unset');
|
||||
|
||||
function idbRequestToPromise<T extends IDBOpenDBRequest | IDBRequest>(request: T) {
|
||||
return new Promise<T['result']>((resolve, reject) => {
|
||||
request.addEventListener('success', () => resolve(request.result));
|
||||
request.addEventListener('error', () => reject(request.error));
|
||||
});
|
||||
}
|
||||
|
||||
function isPlainObject(v: any) {
|
||||
const ctor = v?.constructor;
|
||||
if (isFirefox) {
|
||||
const constructorImpl = ctor?.toString();
|
||||
if (constructorImpl.startsWith('function Object() {') && constructorImpl.includes('[native code]'))
|
||||
return true;
|
||||
}
|
||||
|
||||
return ctor === Object;
|
||||
}
|
||||
|
||||
function trySerialize(value: any): { trivial?: any, encoded?: any } {
|
||||
let trivial = true;
|
||||
const encoded = serializers.serializeAsCallArgument(value, v => {
|
||||
const isTrivial = (
|
||||
isPlainObject(v)
|
||||
|| Array.isArray(v)
|
||||
|| typeof v === 'string'
|
||||
|| typeof v === 'number'
|
||||
|| typeof v === 'boolean'
|
||||
|| Object.is(v, null)
|
||||
);
|
||||
|
||||
if (!isTrivial)
|
||||
trivial = false;
|
||||
|
||||
return { fallThrough: v };
|
||||
});
|
||||
if (trivial)
|
||||
return { trivial: value };
|
||||
return { encoded };
|
||||
}
|
||||
|
||||
const db = await idbRequestToPromise(indexedDB.open(dbInfo.name));
|
||||
const transaction = db.transaction(db.objectStoreNames, 'readonly');
|
||||
const stores = await Promise.all([...db.objectStoreNames].map(async storeName => {
|
||||
const objectStore = transaction.objectStore(storeName);
|
||||
|
||||
const keys = await idbRequestToPromise(objectStore.getAllKeys());
|
||||
const records = await Promise.all(keys.map(async key => {
|
||||
const record: channels.OriginStorage['indexedDB'][0]['stores'][0]['records'][0] = {};
|
||||
|
||||
if (objectStore.keyPath === null) {
|
||||
const { encoded, trivial } = trySerialize(key);
|
||||
if (trivial)
|
||||
record.key = trivial;
|
||||
else
|
||||
record.keyEncoded = encoded;
|
||||
}
|
||||
|
||||
const value = await idbRequestToPromise(objectStore.get(key));
|
||||
const { encoded, trivial } = trySerialize(value);
|
||||
if (trivial)
|
||||
record.value = trivial;
|
||||
else
|
||||
record.valueEncoded = encoded;
|
||||
|
||||
return record;
|
||||
}));
|
||||
|
||||
const indexes = [...objectStore.indexNames].map(indexName => {
|
||||
const index = objectStore.index(indexName);
|
||||
return {
|
||||
name: index.name,
|
||||
keyPath: typeof index.keyPath === 'string' ? index.keyPath : undefined,
|
||||
keyPathArray: Array.isArray(index.keyPath) ? index.keyPath : undefined,
|
||||
multiEntry: index.multiEntry,
|
||||
unique: index.unique,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
name: storeName,
|
||||
records: records,
|
||||
indexes,
|
||||
autoIncrement: objectStore.autoIncrement,
|
||||
keyPath: typeof objectStore.keyPath === 'string' ? objectStore.keyPath : undefined,
|
||||
keyPathArray: Array.isArray(objectStore.keyPath) ? objectStore.keyPath : undefined,
|
||||
};
|
||||
}));
|
||||
|
||||
return {
|
||||
name: dbInfo.name,
|
||||
version: dbInfo.version,
|
||||
stores,
|
||||
};
|
||||
})).catch(e => {
|
||||
throw new Error('Unable to serialize IndexedDB: ' + e.message);
|
||||
});
|
||||
|
||||
return {
|
||||
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name)! })),
|
||||
indexedDB: idbResult,
|
||||
};
|
||||
}
|
||||
|
||||
export async function restore(originState: channels.SetOriginStorage, serializers: ReturnType<typeof source>) {
|
||||
for (const { name, value } of (originState.localStorage || []))
|
||||
localStorage.setItem(name, value);
|
||||
|
||||
await Promise.all((originState.indexedDB ?? []).map(async dbInfo => {
|
||||
const openRequest = indexedDB.open(dbInfo.name, dbInfo.version);
|
||||
openRequest.addEventListener('upgradeneeded', () => {
|
||||
const db = openRequest.result;
|
||||
for (const store of dbInfo.stores) {
|
||||
const objectStore = db.createObjectStore(store.name, { autoIncrement: store.autoIncrement, keyPath: store.keyPathArray ?? store.keyPath });
|
||||
for (const index of store.indexes)
|
||||
objectStore.createIndex(index.name, index.keyPathArray ?? index.keyPath!, { unique: index.unique, multiEntry: index.multiEntry });
|
||||
}
|
||||
});
|
||||
|
||||
function idbRequestToPromise<T extends IDBOpenDBRequest | IDBRequest>(request: T) {
|
||||
return new Promise<T['result']>((resolve, reject) => {
|
||||
request.addEventListener('success', () => resolve(request.result));
|
||||
request.addEventListener('error', () => reject(request.error));
|
||||
});
|
||||
}
|
||||
|
||||
// after `upgradeneeded` finishes, `success` event is fired.
|
||||
const db = await idbRequestToPromise(openRequest);
|
||||
const transaction = db.transaction(db.objectStoreNames, 'readwrite');
|
||||
await Promise.all(dbInfo.stores.map(async store => {
|
||||
const objectStore = transaction.objectStore(store.name);
|
||||
await Promise.all(store.records.map(async record => {
|
||||
await idbRequestToPromise(
|
||||
objectStore.add(
|
||||
record.value ?? serializers.parseEvaluationResultValue(record.valueEncoded),
|
||||
record.key ?? serializers.parseEvaluationResultValue(record.keyEncoded),
|
||||
)
|
||||
);
|
||||
}));
|
||||
}));
|
||||
})).catch(e => {
|
||||
throw new Error('Unable to restore IndexedDB: ' + e.message);
|
||||
});
|
||||
}
|
||||
|
|
@ -170,10 +170,16 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
|||
this._state.recording = true;
|
||||
this._state.callIds.clear();
|
||||
|
||||
// - Browser context network trace is shared across chunks as it contains resources
|
||||
// used to serve page snapshots, so make a copy with the new name.
|
||||
// - APIRequestContext network traces are chunk-specific, always start from scratch.
|
||||
const preserveNetworkResources = this._context instanceof BrowserContext;
|
||||
if (options.name && options.name !== this._state.traceName)
|
||||
this._changeTraceName(this._state, options.name);
|
||||
this._changeTraceName(this._state, options.name, preserveNetworkResources);
|
||||
else
|
||||
this._allocateNewTraceFile(this._state);
|
||||
if (!preserveNetworkResources)
|
||||
this._fs.writeFile(this._state.networkFile, '');
|
||||
|
||||
this._fs.mkdir(path.dirname(this._state.traceFile));
|
||||
const event: trace.TraceEvent = {
|
||||
|
|
@ -267,14 +273,14 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
|||
state.traceFile = path.join(state.tracesDir, `${state.traceName}${suffix}.trace`);
|
||||
}
|
||||
|
||||
private _changeTraceName(state: RecordingState, name: string) {
|
||||
private _changeTraceName(state: RecordingState, name: string, preserveNetworkResources: boolean) {
|
||||
state.traceName = name;
|
||||
state.chunkOrdinal = 0; // Reset ordinal for the new name.
|
||||
this._allocateNewTraceFile(state);
|
||||
|
||||
// Network file survives across chunks, so make a copy with the new name.
|
||||
const newNetworkFile = path.join(state.tracesDir, name + '.network');
|
||||
this._fs.copyFile(state.networkFile, newNetworkFile);
|
||||
if (preserveNetworkResources)
|
||||
this._fs.copyFile(state.networkFile, newNetworkFile);
|
||||
state.networkFile = newNetworkFile;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ export async function startTraceViewerServer(options?: TraceViewerServerOptions)
|
|||
server.routePrefix('/trace', (request, response) => {
|
||||
const url = new URL('http://localhost' + request.url!);
|
||||
const relativePath = url.pathname.slice('/trace'.length);
|
||||
if (process.env.PW_HMR) {
|
||||
// When running in Vite HMR mode, port is hardcoded in build.js
|
||||
response.appendHeader('Access-Control-Allow-Origin', 'http://localhost:44223');
|
||||
}
|
||||
if (relativePath.endsWith('/stall.js'))
|
||||
return true;
|
||||
if (relativePath.startsWith('/file')) {
|
||||
|
|
|
|||
|
|
@ -84,6 +84,8 @@ export type ReducedMotion = 'no-preference' | 'reduce' | 'no-override';
|
|||
|
||||
export type ForcedColors = 'active' | 'none' | 'no-override';
|
||||
|
||||
export type Contrast = 'no-preference' | 'more' | 'no-override';
|
||||
|
||||
export type DeviceDescriptor = {
|
||||
userAgent: string,
|
||||
viewport: Size,
|
||||
|
|
|
|||
|
|
@ -191,8 +191,8 @@ export class WKPage implements PageDelegate {
|
|||
if (contextOptions.userAgent)
|
||||
promises.push(this.updateUserAgent());
|
||||
const emulatedMedia = this._page.emulatedMedia();
|
||||
if (emulatedMedia.media || emulatedMedia.colorScheme || emulatedMedia.reducedMotion || emulatedMedia.forcedColors)
|
||||
promises.push(WKPage._setEmulateMedia(session, emulatedMedia.media, emulatedMedia.colorScheme, emulatedMedia.reducedMotion, emulatedMedia.forcedColors));
|
||||
if (emulatedMedia.media || emulatedMedia.colorScheme || emulatedMedia.reducedMotion || emulatedMedia.forcedColors || emulatedMedia.contrast)
|
||||
promises.push(WKPage._setEmulateMedia(session, emulatedMedia.media, emulatedMedia.colorScheme, emulatedMedia.reducedMotion, emulatedMedia.forcedColors, emulatedMedia.contrast));
|
||||
const bootstrapScript = this._calculateBootstrapScript();
|
||||
if (bootstrapScript.length)
|
||||
promises.push(session.send('Page.setBootstrapScript', { source: bootstrapScript }));
|
||||
|
|
@ -615,7 +615,7 @@ export class WKPage implements PageDelegate {
|
|||
await this._page._onFileChooserOpened(handle);
|
||||
}
|
||||
|
||||
private static async _setEmulateMedia(session: WKSession, mediaType: types.MediaType, colorScheme: types.ColorScheme, reducedMotion: types.ReducedMotion, forcedColors: types.ForcedColors): Promise<void> {
|
||||
private static async _setEmulateMedia(session: WKSession, mediaType: types.MediaType, colorScheme: types.ColorScheme, reducedMotion: types.ReducedMotion, forcedColors: types.ForcedColors, contrast: types.Contrast): Promise<void> {
|
||||
const promises = [];
|
||||
promises.push(session.send('Page.setEmulatedMedia', { media: mediaType === 'no-override' ? '' : mediaType }));
|
||||
let appearance: any = undefined;
|
||||
|
|
@ -639,6 +639,13 @@ export class WKPage implements PageDelegate {
|
|||
case 'no-override': forcedColorsWk = undefined; break;
|
||||
}
|
||||
promises.push(session.send('Page.setForcedColors', { forcedColors: forcedColorsWk }));
|
||||
let contrastWk: any = undefined;
|
||||
switch (contrast) {
|
||||
case 'more': contrastWk = 'More'; break;
|
||||
case 'no-preference': contrastWk = 'NoPreference'; break;
|
||||
case 'no-override': contrastWk = undefined; break;
|
||||
}
|
||||
promises.push(session.send('Page.overrideUserPreference', { name: 'PrefersContrast', value: contrastWk }));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
|
|
@ -661,7 +668,8 @@ export class WKPage implements PageDelegate {
|
|||
const colorScheme = emulatedMedia.colorScheme;
|
||||
const reducedMotion = emulatedMedia.reducedMotion;
|
||||
const forcedColors = emulatedMedia.forcedColors;
|
||||
await this._forAllSessions(session => WKPage._setEmulateMedia(session, emulatedMedia.media, colorScheme, reducedMotion, forcedColors));
|
||||
const contrast = emulatedMedia.contrast;
|
||||
await this._forAllSessions(session => WKPage._setEmulateMedia(session, emulatedMedia.media, colorScheme, reducedMotion, forcedColors, contrast));
|
||||
}
|
||||
|
||||
async updateEmulatedViewportSize(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export function httpRequest(params: HTTPRequestParams, onResponse: (r: http.Inco
|
|||
|
||||
const proxyURL = getProxyForUrl(params.url);
|
||||
if (proxyURL) {
|
||||
const parsedProxyURL = new URL(proxyURL);
|
||||
const parsedProxyURL = url.parse(proxyURL);
|
||||
if (params.url.startsWith('http:')) {
|
||||
options = {
|
||||
path: parsedUrl.href,
|
||||
|
|
|
|||
335
packages/playwright-core/types/types.d.ts
vendored
335
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -2565,6 +2565,12 @@ export interface Page {
|
|||
*/
|
||||
colorScheme?: null|"light"|"dark"|"no-preference";
|
||||
|
||||
/**
|
||||
* Emulates `'prefers-contrast'` media feature, supported values are `'no-preference'`, `'more'`. Passing `null`
|
||||
* disables contrast emulation.
|
||||
*/
|
||||
contrast?: null|"no-preference"|"more";
|
||||
|
||||
/**
|
||||
* Emulates `'forced-colors'` media feature, supported values are `'active'` and `'none'`. Passing `null` disables
|
||||
* forced colors emulation.
|
||||
|
|
@ -9260,7 +9266,8 @@ export interface BrowserContext {
|
|||
setOffline(offline: boolean): Promise<void>;
|
||||
|
||||
/**
|
||||
* Returns storage state for this browser context, contains current cookies and local storage snapshot.
|
||||
* Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB
|
||||
* snapshot.
|
||||
* @param options
|
||||
*/
|
||||
storageState(options?: {
|
||||
|
|
@ -9301,6 +9308,50 @@ export interface BrowserContext {
|
|||
|
||||
value: string;
|
||||
}>;
|
||||
|
||||
indexedDB: Array<{
|
||||
name: string;
|
||||
|
||||
version: number;
|
||||
|
||||
stores: Array<{
|
||||
name: string;
|
||||
|
||||
keyPath?: string;
|
||||
|
||||
keyPathArray?: Array<string>;
|
||||
|
||||
autoIncrement: boolean;
|
||||
|
||||
indexes: Array<{
|
||||
name: string;
|
||||
|
||||
keyPath?: string;
|
||||
|
||||
keyPathArray?: Array<string>;
|
||||
|
||||
unique: boolean;
|
||||
|
||||
multiEntry: boolean;
|
||||
}>;
|
||||
|
||||
records: Array<{
|
||||
key?: Object;
|
||||
|
||||
/**
|
||||
* if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
*/
|
||||
keyEncoded?: Object;
|
||||
|
||||
value: Object;
|
||||
|
||||
/**
|
||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
*/
|
||||
valueEncoded?: Object;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
|
|
@ -9770,6 +9821,13 @@ export interface Browser {
|
|||
*/
|
||||
colorScheme?: null|"light"|"dark"|"no-preference";
|
||||
|
||||
/**
|
||||
* Emulates `'prefers-contrast'` media feature, supported values are `'no-preference'`, `'more'`. See
|
||||
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details.
|
||||
* Passing `null` resets emulation to system defaults. Defaults to `'no-preference'`.
|
||||
*/
|
||||
contrast?: null|"no-preference"|"more";
|
||||
|
||||
/**
|
||||
* Specify device scale factor (can be thought of as dpr). Defaults to `1`. Learn more about
|
||||
* [emulating devices with device scale factor](https://playwright.dev/docs/emulation#devices).
|
||||
|
|
@ -10049,17 +10107,70 @@ export interface Browser {
|
|||
sameSite: "Strict"|"Lax"|"None";
|
||||
}>;
|
||||
|
||||
/**
|
||||
* localStorage to set for context
|
||||
*/
|
||||
origins: Array<{
|
||||
origin: string;
|
||||
|
||||
/**
|
||||
* localStorage to set for context
|
||||
*/
|
||||
localStorage: Array<{
|
||||
name: string;
|
||||
|
||||
value: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* indexedDB to set for context
|
||||
*/
|
||||
indexedDB?: Array<{
|
||||
/**
|
||||
* database name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* database version
|
||||
*/
|
||||
version: number;
|
||||
|
||||
stores: Array<{
|
||||
name: string;
|
||||
|
||||
keyPath?: string;
|
||||
|
||||
keyPathArray?: Array<string>;
|
||||
|
||||
autoIncrement: boolean;
|
||||
|
||||
indexes: Array<{
|
||||
name: string;
|
||||
|
||||
keyPath?: string;
|
||||
|
||||
keyPathArray?: Array<string>;
|
||||
|
||||
unique: boolean;
|
||||
|
||||
multiEntry: boolean;
|
||||
}>;
|
||||
|
||||
records: Array<{
|
||||
key?: Object;
|
||||
|
||||
/**
|
||||
* if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
*/
|
||||
keyEncoded?: Object;
|
||||
|
||||
value: Object;
|
||||
|
||||
/**
|
||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
*/
|
||||
valueEncoded?: Object;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
|
@ -12417,7 +12528,7 @@ export interface Locator {
|
|||
|
||||
/**
|
||||
* Captures the aria snapshot of the given element. Read more about [aria snapshots](https://playwright.dev/docs/aria-snapshots) and
|
||||
* [expect(locator).toMatchAriaSnapshot(expected[, options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-1)
|
||||
* [expect(locator).toMatchAriaSnapshot(expected[, options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot)
|
||||
* for the corresponding assertion.
|
||||
*
|
||||
* **Usage**
|
||||
|
|
@ -14554,6 +14665,11 @@ export interface BrowserType<Unused = {}> {
|
|||
*
|
||||
* **NOTE** Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
|
||||
*
|
||||
* **NOTE** This connection is significantly lower fidelity than the Playwright protocol connection via
|
||||
* [browserType.connect(wsEndpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect).
|
||||
* If you are experiencing issues or attempting to use advanced functionality, you probably want to use
|
||||
* [browserType.connect(wsEndpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect).
|
||||
*
|
||||
* **Usage**
|
||||
*
|
||||
* ```js
|
||||
|
|
@ -14579,6 +14695,11 @@ export interface BrowserType<Unused = {}> {
|
|||
*
|
||||
* **NOTE** Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
|
||||
*
|
||||
* **NOTE** This connection is significantly lower fidelity than the Playwright protocol connection via
|
||||
* [browserType.connect(wsEndpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect).
|
||||
* If you are experiencing issues or attempting to use advanced functionality, you probably want to use
|
||||
* [browserType.connect(wsEndpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect).
|
||||
*
|
||||
* **Usage**
|
||||
*
|
||||
* ```js
|
||||
|
|
@ -14593,10 +14714,12 @@ export interface BrowserType<Unused = {}> {
|
|||
*/
|
||||
connectOverCDP(options: ConnectOverCDPOptions & { wsEndpoint?: string }): Promise<Browser>;
|
||||
/**
|
||||
* This method attaches Playwright to an existing browser instance. When connecting to another browser launched via
|
||||
* `BrowserType.launchServer` in Node.js, the major and minor version needs to match the client version (1.2.3 → is
|
||||
* compatible with 1.2.x).
|
||||
* @param wsEndpoint A browser websocket endpoint to connect to.
|
||||
* This method attaches Playwright to an existing browser instance created via `BrowserType.launchServer` in Node.js.
|
||||
*
|
||||
* **NOTE** The major and minor version of the Playwright instance that connects needs to match the version of
|
||||
* Playwright that launches the browser (1.2.3 → is compatible with 1.2.x).
|
||||
*
|
||||
* @param wsEndpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via `BrowserServer.wsEndpoint`.
|
||||
* @param options
|
||||
*/
|
||||
connect(wsEndpoint: string, options?: ConnectOptions): Promise<Browser>;
|
||||
|
|
@ -14607,10 +14730,12 @@ export interface BrowserType<Unused = {}> {
|
|||
* @deprecated
|
||||
*/
|
||||
/**
|
||||
* This method attaches Playwright to an existing browser instance. When connecting to another browser launched via
|
||||
* `BrowserType.launchServer` in Node.js, the major and minor version needs to match the client version (1.2.3 → is
|
||||
* compatible with 1.2.x).
|
||||
* @param wsEndpoint A browser websocket endpoint to connect to.
|
||||
* This method attaches Playwright to an existing browser instance created via `BrowserType.launchServer` in Node.js.
|
||||
*
|
||||
* **NOTE** The major and minor version of the Playwright instance that connects needs to match the version of
|
||||
* Playwright that launches the browser (1.2.3 → is compatible with 1.2.x).
|
||||
*
|
||||
* @param wsEndpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via `BrowserServer.wsEndpoint`.
|
||||
* @param options
|
||||
*/
|
||||
connect(options: ConnectOptions & { wsEndpoint?: string }): Promise<Browser>;
|
||||
|
|
@ -14783,6 +14908,13 @@ export interface BrowserType<Unused = {}> {
|
|||
*/
|
||||
colorScheme?: null|"light"|"dark"|"no-preference";
|
||||
|
||||
/**
|
||||
* Emulates `'prefers-contrast'` media feature, supported values are `'no-preference'`, `'more'`. See
|
||||
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details.
|
||||
* Passing `null` resets emulation to system defaults. Defaults to `'no-preference'`.
|
||||
*/
|
||||
contrast?: null|"no-preference"|"more";
|
||||
|
||||
/**
|
||||
* Specify device scale factor (can be thought of as dpr). Defaults to `1`. Learn more about
|
||||
* [emulating devices with device scale factor](https://playwright.dev/docs/emulation#devices).
|
||||
|
|
@ -16595,6 +16727,13 @@ export interface AndroidDevice {
|
|||
*/
|
||||
colorScheme?: null|"light"|"dark"|"no-preference";
|
||||
|
||||
/**
|
||||
* Emulates `'prefers-contrast'` media feature, supported values are `'no-preference'`, `'more'`. See
|
||||
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details.
|
||||
* Passing `null` resets emulation to system defaults. Defaults to `'no-preference'`.
|
||||
*/
|
||||
contrast?: null|"no-preference"|"more";
|
||||
|
||||
/**
|
||||
* Specify device scale factor (can be thought of as dpr). Defaults to `1`. Learn more about
|
||||
* [emulating devices with device scale factor](https://playwright.dev/docs/emulation#devices).
|
||||
|
|
@ -17567,6 +17706,59 @@ export interface APIRequest {
|
|||
|
||||
value: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* indexedDB to set for context
|
||||
*/
|
||||
indexedDB?: Array<{
|
||||
/**
|
||||
* database name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* database version
|
||||
*/
|
||||
version: number;
|
||||
|
||||
stores: Array<{
|
||||
name: string;
|
||||
|
||||
keyPath?: string;
|
||||
|
||||
keyPathArray?: Array<string>;
|
||||
|
||||
autoIncrement: boolean;
|
||||
|
||||
indexes: Array<{
|
||||
name: string;
|
||||
|
||||
keyPath?: string;
|
||||
|
||||
keyPathArray?: Array<string>;
|
||||
|
||||
unique: boolean;
|
||||
|
||||
multiEntry: boolean;
|
||||
}>;
|
||||
|
||||
records: Array<{
|
||||
key?: Object;
|
||||
|
||||
/**
|
||||
* if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
*/
|
||||
keyEncoded?: Object;
|
||||
|
||||
value: Object;
|
||||
|
||||
/**
|
||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
*/
|
||||
valueEncoded?: Object;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
|
@ -18376,6 +18568,50 @@ export interface APIRequestContext {
|
|||
|
||||
value: string;
|
||||
}>;
|
||||
|
||||
indexedDB: Array<{
|
||||
name: string;
|
||||
|
||||
version: number;
|
||||
|
||||
stores: Array<{
|
||||
name: string;
|
||||
|
||||
keyPath?: string;
|
||||
|
||||
keyPathArray?: Array<string>;
|
||||
|
||||
autoIncrement: boolean;
|
||||
|
||||
indexes: Array<{
|
||||
name: string;
|
||||
|
||||
keyPath?: string;
|
||||
|
||||
keyPathArray?: Array<string>;
|
||||
|
||||
unique: boolean;
|
||||
|
||||
multiEntry: boolean;
|
||||
}>;
|
||||
|
||||
records: Array<{
|
||||
key?: Object;
|
||||
|
||||
/**
|
||||
* if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
*/
|
||||
keyEncoded?: Object;
|
||||
|
||||
value: Object;
|
||||
|
||||
/**
|
||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
*/
|
||||
valueEncoded?: Object;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
|
|
@ -21287,8 +21523,11 @@ export interface WebError {
|
|||
}
|
||||
|
||||
/**
|
||||
* The [WebSocket](https://playwright.dev/docs/api/class-websocket) class represents websocket connections in the
|
||||
* page.
|
||||
* The [WebSocket](https://playwright.dev/docs/api/class-websocket) class represents WebSocket connections within a
|
||||
* page. It provides the ability to inspect and manipulate the data being transmitted and received.
|
||||
*
|
||||
* If you want to intercept or modify WebSocket frames, consider using
|
||||
* [WebSocketRoute](https://playwright.dev/docs/api/class-websocketroute).
|
||||
*/
|
||||
export interface WebSocket {
|
||||
/**
|
||||
|
|
@ -21952,6 +22191,13 @@ export interface BrowserContextOptions {
|
|||
*/
|
||||
colorScheme?: null|"light"|"dark"|"no-preference";
|
||||
|
||||
/**
|
||||
* Emulates `'prefers-contrast'` media feature, supported values are `'no-preference'`, `'more'`. See
|
||||
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details.
|
||||
* Passing `null` resets emulation to system defaults. Defaults to `'no-preference'`.
|
||||
*/
|
||||
contrast?: null|"no-preference"|"more";
|
||||
|
||||
/**
|
||||
* Specify device scale factor (can be thought of as dpr). Defaults to `1`. Learn more about
|
||||
* [emulating devices with device scale factor](https://playwright.dev/docs/emulation#devices).
|
||||
|
|
@ -22198,17 +22444,70 @@ export interface BrowserContextOptions {
|
|||
sameSite: "Strict"|"Lax"|"None";
|
||||
}>;
|
||||
|
||||
/**
|
||||
* localStorage to set for context
|
||||
*/
|
||||
origins: Array<{
|
||||
origin: string;
|
||||
|
||||
/**
|
||||
* localStorage to set for context
|
||||
*/
|
||||
localStorage: Array<{
|
||||
name: string;
|
||||
|
||||
value: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* indexedDB to set for context
|
||||
*/
|
||||
indexedDB?: Array<{
|
||||
/**
|
||||
* database name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* database version
|
||||
*/
|
||||
version: number;
|
||||
|
||||
stores: Array<{
|
||||
name: string;
|
||||
|
||||
keyPath?: string;
|
||||
|
||||
keyPathArray?: Array<string>;
|
||||
|
||||
autoIncrement: boolean;
|
||||
|
||||
indexes: Array<{
|
||||
name: string;
|
||||
|
||||
keyPath?: string;
|
||||
|
||||
keyPathArray?: Array<string>;
|
||||
|
||||
unique: boolean;
|
||||
|
||||
multiEntry: boolean;
|
||||
}>;
|
||||
|
||||
records: Array<{
|
||||
key?: Object;
|
||||
|
||||
/**
|
||||
* if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
*/
|
||||
keyEncoded?: Object;
|
||||
|
||||
value: Object;
|
||||
|
||||
/**
|
||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
*/
|
||||
valueEncoded?: Object;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -118,15 +118,15 @@ function isTypeScript(filename: string) {
|
|||
return filename.endsWith('.ts') || filename.endsWith('.tsx') || filename.endsWith('.mts') || filename.endsWith('.cts');
|
||||
}
|
||||
|
||||
export function babelTransform(code: string, filename: string, isModule: boolean, pluginsPrologue: [string, any?][], pluginsEpilogue: [string, any?][]): BabelFileResult {
|
||||
export function babelTransform(code: string, filename: string, isModule: boolean, pluginsPrologue: [string, any?][], pluginsEpilogue: [string, any?][]): BabelFileResult | null {
|
||||
if (isTransforming)
|
||||
return {};
|
||||
return null;
|
||||
|
||||
// Prevent reentry while requiring plugins lazily.
|
||||
isTransforming = true;
|
||||
try {
|
||||
const options = babelTransformOptions(isTypeScript(filename), isModule, pluginsPrologue, pluginsEpilogue);
|
||||
return babel.transform(code, { filename, ...options })!;
|
||||
return babel.transform(code, { filename, ...options });
|
||||
} finally {
|
||||
isTransforming = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ export class FullProjectInternal {
|
|||
readonly fullyParallel: boolean;
|
||||
readonly expect: Project['expect'];
|
||||
readonly respectGitIgnore: boolean;
|
||||
readonly snapshotPathTemplate: string;
|
||||
readonly snapshotPathTemplate: string | undefined;
|
||||
readonly ignoreSnapshots: boolean;
|
||||
id = '';
|
||||
deps: FullProjectInternal[] = [];
|
||||
|
|
@ -179,8 +179,7 @@ export class FullProjectInternal {
|
|||
constructor(configDir: string, config: Config, fullConfig: FullConfigInternal, projectConfig: Project, configCLIOverrides: ConfigCLIOverrides, packageJsonDir: string) {
|
||||
this.fullConfig = fullConfig;
|
||||
const testDir = takeFirst(pathResolve(configDir, projectConfig.testDir), pathResolve(configDir, config.testDir), fullConfig.configDir);
|
||||
const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
|
||||
this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate);
|
||||
this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate);
|
||||
|
||||
this.project = {
|
||||
grep: takeFirst(projectConfig.grep, config.grep, defaultGrep),
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ export type StepEndPayload = {
|
|||
wallTime: number; // milliseconds since unix epoch
|
||||
error?: TestInfoErrorImpl;
|
||||
suggestedRebaseline?: string;
|
||||
annotations: { type: string, description?: string }[];
|
||||
};
|
||||
|
||||
export type TestEntry = {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { currentlyLoadingFileSuite, currentTestInfo, setCurrentlyLoadingFileSuit
|
|||
import { TestCase, Suite } from './test';
|
||||
import { wrapFunctionWithLocation } from '../transform/transform';
|
||||
import type { FixturesWithLocation } from './config';
|
||||
import type { Fixtures, TestType, TestDetails } from '../../types/test';
|
||||
import type { Fixtures, TestType, TestDetails, TestStepInfo } from '../../types/test';
|
||||
import type { Location } from '../../types/testReporter';
|
||||
import { getPackageManagerExecCommand, monotonicTime, raceAgainstDeadline, zones } from 'playwright-core/lib/utils';
|
||||
import { errors } from 'playwright-core';
|
||||
|
|
@ -258,22 +258,17 @@ export class TestTypeImpl {
|
|||
suite._use.push({ fixtures, location });
|
||||
}
|
||||
|
||||
async _step<T>(expectation: 'pass'|'skip', title: string, body: () => T | Promise<T>, options: {box?: boolean, location?: Location, timeout?: number } = {}): Promise<T> {
|
||||
async _step<T>(expectation: 'pass'|'skip', title: string, body: (step: TestStepInfo) => T | Promise<T>, options: {box?: boolean, location?: Location, timeout?: number } = {}): Promise<T> {
|
||||
const testInfo = currentTestInfo();
|
||||
if (!testInfo)
|
||||
throw new Error(`test.step() can only be called from a test`);
|
||||
if (expectation === 'skip') {
|
||||
const step = testInfo._addStep({ category: 'test.step.skip', title, location: options.location, box: options.box });
|
||||
step.complete({});
|
||||
return undefined as T;
|
||||
}
|
||||
const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box });
|
||||
return await zones.run('stepZone', step, async () => {
|
||||
try {
|
||||
let result: Awaited<ReturnType<typeof raceAgainstDeadline<T>>> | undefined = undefined;
|
||||
result = await raceAgainstDeadline(async () => {
|
||||
try {
|
||||
return await body();
|
||||
return await step.info._runStepBody(expectation === 'skip', body);
|
||||
} catch (e) {
|
||||
// If the step timed out, the test fixtures will tear down, which in turn
|
||||
// will abort unfinished actions in the step body. Record such errors here.
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ export type JsonTestStepEnd = {
|
|||
duration: number;
|
||||
error?: reporterTypes.TestError;
|
||||
attachments?: number[]; // index of JsonTestResultEnd.attachments
|
||||
annotations?: Annotation[];
|
||||
};
|
||||
|
||||
export type JsonFullResult = {
|
||||
|
|
@ -546,6 +547,10 @@ class TeleTestStep implements reporterTypes.TestStep {
|
|||
get attachments() {
|
||||
return this._endPayload?.attachments?.map(index => this._result.attachments[index]) ?? [];
|
||||
}
|
||||
|
||||
get annotations() {
|
||||
return this._endPayload?.annotations ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
export class TeleTestResult implements reporterTypes.TestResult {
|
||||
|
|
|
|||
25
packages/playwright/src/isomorphic/types.d.ts
vendored
Normal file
25
packages/playwright/src/isomorphic/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface GitCommitInfo {
|
||||
'revision.id'?: string;
|
||||
'revision.author'?: string;
|
||||
'revision.email'?: string;
|
||||
'revision.subject'?: string;
|
||||
'revision.timestamp'?: number | Date;
|
||||
'revision.link'?: string;
|
||||
'ci.link'?: string;
|
||||
}
|
||||
|
|
@ -49,8 +49,9 @@ import {
|
|||
toHaveValues,
|
||||
toPass
|
||||
} from './matchers';
|
||||
import type { ExpectMatcherStateInternal } from './matchers';
|
||||
import { toMatchSnapshot, toHaveScreenshot, toHaveScreenshotStepTitle } from './toMatchSnapshot';
|
||||
import type { Expect, ExpectMatcherState } from '../../types/test';
|
||||
import type { Expect } from '../../types/test';
|
||||
import { currentTestInfo } from '../common/globals';
|
||||
import { filteredStackTrace, trimLongString } from '../util';
|
||||
import {
|
||||
|
|
@ -61,6 +62,7 @@ import {
|
|||
} from '../common/expectBundle';
|
||||
import { zones } from 'playwright-core/lib/utils';
|
||||
import { TestInfoImpl } from '../worker/testInfo';
|
||||
import type { TestStepInfoImpl } from '../worker/testInfo';
|
||||
import { ExpectError, isJestError } from './matcherHint';
|
||||
import { toMatchAriaSnapshot } from './toMatchAriaSnapshot';
|
||||
|
||||
|
|
@ -110,13 +112,13 @@ function createMatchers(actual: unknown, info: ExpectMetaInfo, prefix: string[])
|
|||
return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(info, prefix));
|
||||
}
|
||||
|
||||
const getCustomMatchersSymbol = Symbol('get custom matchers');
|
||||
const userMatchersSymbol = Symbol('userMatchers');
|
||||
|
||||
function qualifiedMatcherName(qualifier: string[], matcherName: string) {
|
||||
return qualifier.join(':') + '$' + matcherName;
|
||||
}
|
||||
|
||||
function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Record<string, Function>) {
|
||||
function createExpect(info: ExpectMetaInfo, prefix: string[], userMatchers: Record<string, Function>) {
|
||||
const expectInstance: Expect<{}> = new Proxy(expectLibrary, {
|
||||
apply: function(target: any, thisArg: any, argumentsList: [unknown, ExpectMessage?]) {
|
||||
const [actual, messageOrOptions] = argumentsList;
|
||||
|
|
@ -130,7 +132,7 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
|
|||
return createMatchers(actual, newInfo, prefix);
|
||||
},
|
||||
|
||||
get: function(target: any, property: string | typeof getCustomMatchersSymbol) {
|
||||
get: function(target: any, property: string | typeof userMatchersSymbol) {
|
||||
if (property === 'configure')
|
||||
return configure;
|
||||
|
||||
|
|
@ -139,27 +141,14 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
|
|||
const qualifier = [...prefix, createGuid()];
|
||||
|
||||
const wrappedMatchers: any = {};
|
||||
const extendedMatchers: any = { ...customMatchers };
|
||||
for (const [name, matcher] of Object.entries(matchers)) {
|
||||
wrappedMatchers[name] = function(...args: any[]) {
|
||||
const { isNot, promise, utils } = this;
|
||||
const newThis: ExpectMatcherState = {
|
||||
isNot,
|
||||
promise,
|
||||
utils,
|
||||
timeout: currentExpectTimeout()
|
||||
};
|
||||
(newThis as any).equals = throwUnsupportedExpectMatcherError;
|
||||
return (matcher as any).call(newThis, ...args);
|
||||
};
|
||||
wrappedMatchers[name] = wrapPlaywrightMatcherToPassNiceThis(matcher);
|
||||
const key = qualifiedMatcherName(qualifier, name);
|
||||
wrappedMatchers[key] = wrappedMatchers[name];
|
||||
Object.defineProperty(wrappedMatchers[key], 'name', { value: name });
|
||||
extendedMatchers[name] = wrappedMatchers[key];
|
||||
}
|
||||
expectLibrary.extend(wrappedMatchers);
|
||||
|
||||
return createExpect(info, qualifier, extendedMatchers);
|
||||
return createExpect(info, qualifier, { ...userMatchers, ...matchers });
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -169,8 +158,8 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
|
|||
};
|
||||
}
|
||||
|
||||
if (property === getCustomMatchersSymbol)
|
||||
return customMatchers;
|
||||
if (property === userMatchersSymbol)
|
||||
return userMatchers;
|
||||
|
||||
if (property === 'poll') {
|
||||
return (actual: unknown, messageOrOptions?: ExpectMessage & { timeout?: number, intervals?: number[] }) => {
|
||||
|
|
@ -197,12 +186,53 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
|
|||
newInfo.poll!.intervals = configuration._poll.intervals ?? newInfo.poll!.intervals;
|
||||
}
|
||||
}
|
||||
return createExpect(newInfo, prefix, customMatchers);
|
||||
return createExpect(newInfo, prefix, userMatchers);
|
||||
};
|
||||
|
||||
return expectInstance;
|
||||
}
|
||||
|
||||
// Expect wraps matchers, so there is no way to pass this information to the raw Playwright matcher.
|
||||
// Rely on sync call sequence to seed each matcher call with the context.
|
||||
type MatcherCallContext = {
|
||||
expectInfo: ExpectMetaInfo;
|
||||
testInfo: TestInfoImpl | null;
|
||||
step?: TestStepInfoImpl;
|
||||
};
|
||||
|
||||
let matcherCallContext: MatcherCallContext | undefined;
|
||||
|
||||
function setMatcherCallContext(context: MatcherCallContext) {
|
||||
matcherCallContext = context;
|
||||
}
|
||||
|
||||
function takeMatcherCallContext(): MatcherCallContext {
|
||||
try {
|
||||
return matcherCallContext!;
|
||||
} finally {
|
||||
matcherCallContext = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const defaultExpectTimeout = 5000;
|
||||
|
||||
function wrapPlaywrightMatcherToPassNiceThis(matcher: any) {
|
||||
return function(this: any, ...args: any[]) {
|
||||
const { isNot, promise, utils } = this;
|
||||
const context = takeMatcherCallContext();
|
||||
const timeout = context.expectInfo.timeout ?? context.testInfo?._projectInternal?.expect?.timeout ?? defaultExpectTimeout;
|
||||
const newThis: ExpectMatcherStateInternal = {
|
||||
isNot,
|
||||
promise,
|
||||
utils,
|
||||
timeout,
|
||||
_stepInfo: context.step,
|
||||
};
|
||||
(newThis as any).equals = throwUnsupportedExpectMatcherError;
|
||||
return matcher.call(newThis, ...args);
|
||||
};
|
||||
}
|
||||
|
||||
function throwUnsupportedExpectMatcherError() {
|
||||
throw new Error('It looks like you are using custom expect matchers that are not compatible with Playwright. See https://aka.ms/playwright/expect-compatibility');
|
||||
}
|
||||
|
|
@ -299,8 +329,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
|||
}
|
||||
return (...args: any[]) => {
|
||||
const testInfo = currentTestInfo();
|
||||
// We assume that the matcher will read the current expect timeout the first thing.
|
||||
setCurrentExpectConfigureTimeout(this._info.timeout);
|
||||
setMatcherCallContext({ expectInfo: this._info, testInfo });
|
||||
if (!testInfo)
|
||||
return matcher.call(target, ...args);
|
||||
|
||||
|
|
@ -329,6 +358,8 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
|||
const jestError = isJestError(e) ? e : null;
|
||||
const error = jestError ? new ExpectError(jestError, customMessage, stackFrames) : e;
|
||||
if (jestError?.matcherResult.suggestedRebaseline) {
|
||||
// NOTE: this is a workaround for the fact that we can't pass the suggested rebaseline
|
||||
// for passing matchers. See toMatchAriaSnapshot for a counterpart.
|
||||
step.complete({ suggestedRebaseline: jestError?.matcherResult.suggestedRebaseline });
|
||||
return;
|
||||
}
|
||||
|
|
@ -344,6 +375,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
|||
};
|
||||
|
||||
try {
|
||||
setMatcherCallContext({ expectInfo: this._info, testInfo, step: step.info });
|
||||
const callback = () => matcher.call(target, ...args);
|
||||
const result = zones.run('stepZone', step, callback);
|
||||
if (result instanceof Promise)
|
||||
|
|
@ -360,7 +392,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
|||
async function pollMatcher(qualifiedMatcherName: string, info: ExpectMetaInfo, prefix: string[], ...args: any[]) {
|
||||
const testInfo = currentTestInfo();
|
||||
const poll = info.poll!;
|
||||
const timeout = poll.timeout ?? currentExpectTimeout();
|
||||
const timeout = poll.timeout ?? info.timeout ?? testInfo?._projectInternal?.expect?.timeout ?? defaultExpectTimeout;
|
||||
const { deadline, timeoutMessage } = testInfo ? testInfo._deadlineForMatcher(timeout) : TestInfoImpl._defaultDeadlineForMatcher(timeout);
|
||||
|
||||
const result = await pollAgainstDeadline<Error|undefined>(async () => {
|
||||
|
|
@ -396,22 +428,6 @@ async function pollMatcher(qualifiedMatcherName: string, info: ExpectMetaInfo, p
|
|||
}
|
||||
}
|
||||
|
||||
let currentExpectConfigureTimeout: number | undefined;
|
||||
|
||||
function setCurrentExpectConfigureTimeout(timeout: number | undefined) {
|
||||
currentExpectConfigureTimeout = timeout;
|
||||
}
|
||||
|
||||
function currentExpectTimeout() {
|
||||
if (currentExpectConfigureTimeout !== undefined)
|
||||
return currentExpectConfigureTimeout;
|
||||
const testInfo = currentTestInfo();
|
||||
let defaultExpectTimeout = testInfo?._projectInternal?.expect?.timeout;
|
||||
if (typeof defaultExpectTimeout === 'undefined')
|
||||
defaultExpectTimeout = 5000;
|
||||
return defaultExpectTimeout;
|
||||
}
|
||||
|
||||
function computeArgsSuffix(matcherName: string, args: any[]) {
|
||||
let value = '';
|
||||
if (matcherName === 'toHaveScreenshot')
|
||||
|
|
@ -424,7 +440,7 @@ export const expect: Expect<{}> = createExpect({}, [], {}).extend(customMatchers
|
|||
export function mergeExpects(...expects: any[]) {
|
||||
let merged = expect;
|
||||
for (const e of expects) {
|
||||
const internals = e[getCustomMatchersSymbol];
|
||||
const internals = e[userMatchersSymbol];
|
||||
if (!internals) // non-playwright expects mutate the global expect, so we don't need to do anything special
|
||||
continue;
|
||||
merged = merged.extend(internals);
|
||||
|
|
|
|||
|
|
@ -24,10 +24,13 @@ import { toMatchText } from './toMatchText';
|
|||
import { isRegExp, isString, isTextualMimeType, pollAgainstDeadline, serializeExpectedTextValues } from 'playwright-core/lib/utils';
|
||||
import { currentTestInfo } from '../common/globals';
|
||||
import { TestInfoImpl } from '../worker/testInfo';
|
||||
import type { TestStepInfoImpl } from '../worker/testInfo';
|
||||
import type { ExpectMatcherState } from '../../types/test';
|
||||
import { takeFirst } from '../common/config';
|
||||
import { toHaveURL as toHaveURLExternal } from './toHaveURL';
|
||||
|
||||
export type ExpectMatcherStateInternal = ExpectMatcherState & { _stepInfo?: TestStepInfoImpl };
|
||||
|
||||
export interface LocatorEx extends Locator {
|
||||
_expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ export async function toMatchAriaSnapshot(
|
|||
return { pass: !this.isNot, message: () => '', name: 'toMatchAriaSnapshot', expected: '' };
|
||||
|
||||
const updateSnapshots = testInfo.config.updateSnapshots;
|
||||
const pathTemplate = testInfo._projectInternal.expect?.toMatchAriaSnapshot?.pathTemplate;
|
||||
const defaultTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{ext}';
|
||||
|
||||
const matcherOptions = {
|
||||
isNot: this.isNot,
|
||||
|
|
@ -63,7 +65,7 @@ export async function toMatchAriaSnapshot(
|
|||
timeout = options.timeout ?? this.timeout;
|
||||
} else {
|
||||
if (expectedParam?.name) {
|
||||
expectedPath = testInfo.snapshotPath(sanitizeFilePathBeforeExtension(expectedParam.name));
|
||||
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [sanitizeFilePathBeforeExtension(expectedParam.name)]);
|
||||
} else {
|
||||
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
|
||||
if (!snapshotNames) {
|
||||
|
|
@ -71,7 +73,7 @@ export async function toMatchAriaSnapshot(
|
|||
(testInfo as any)[snapshotNamesSymbol] = snapshotNames;
|
||||
}
|
||||
const fullTitleWithoutSpec = [...testInfo.titlePath.slice(1), ++snapshotNames.anonymousSnapshotIndex].join(' ');
|
||||
expectedPath = testInfo.snapshotPath(sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.yml');
|
||||
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.yml']);
|
||||
}
|
||||
expected = await fs.promises.readFile(expectedPath, 'utf8').catch(() => '');
|
||||
timeout = expectedParam?.timeout ?? this.timeout;
|
||||
|
|
@ -138,6 +140,14 @@ export async function toMatchAriaSnapshot(
|
|||
return { pass: true, message: () => '', name: 'toMatchAriaSnapshot' };
|
||||
} else {
|
||||
const suggestedRebaseline = `\`\n${escapeTemplateString(indent(typedReceived.regex, '{indent} '))}\n{indent}\``;
|
||||
if (updateSnapshots === 'missing') {
|
||||
const message = 'A snapshot is not provided, generating new baseline.';
|
||||
testInfo._hasNonRetriableError = true;
|
||||
testInfo._failWithError(new Error(message));
|
||||
}
|
||||
// TODO: ideally, we should return "pass: true" here because this matcher passes
|
||||
// when regenerating baselines. However, we can only access suggestedRebaseline in case
|
||||
// of an error, so we fail here and workaround it in the expect implementation.
|
||||
return { pass: false, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ import { colors } from 'playwright-core/lib/utilsBundle';
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { mime } from 'playwright-core/lib/utilsBundle';
|
||||
import type { TestInfoImpl } from '../worker/testInfo';
|
||||
import type { ExpectMatcherState } from '../../types/test';
|
||||
import type { TestInfoImpl, TestStepInfoImpl } from '../worker/testInfo';
|
||||
import type { ExpectMatcherStateInternal } from './matchers';
|
||||
import { matcherHint, type MatcherResult } from './matcherHint';
|
||||
import type { FullProjectInternal } from '../common/config';
|
||||
|
||||
|
|
@ -148,7 +148,8 @@ class SnapshotHelper {
|
|||
outputBasePath = testInfo._getOutputPath(sanitizedName);
|
||||
this.attachmentBaseName = sanitizedName;
|
||||
}
|
||||
this.expectedPath = testInfo.snapshotPath(...expectedPathSegments);
|
||||
const defaultTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
|
||||
this.expectedPath = testInfo._resolveSnapshotPath(configOptions.pathTemplate, defaultTemplate, expectedPathSegments);
|
||||
this.legacyExpectedPath = addSuffixToFilePath(outputBasePath, '-expected');
|
||||
this.previousPath = addSuffixToFilePath(outputBasePath, '-previous');
|
||||
this.actualPath = addSuffixToFilePath(outputBasePath, '-actual');
|
||||
|
|
@ -220,13 +221,13 @@ class SnapshotHelper {
|
|||
return this.createMatcherResult(message, true);
|
||||
}
|
||||
|
||||
handleMissing(actual: Buffer | string): ImageMatcherResult {
|
||||
handleMissing(actual: Buffer | string, step: TestStepInfoImpl | undefined): ImageMatcherResult {
|
||||
const isWriteMissingMode = this.updateSnapshots !== 'none';
|
||||
if (isWriteMissingMode)
|
||||
writeFileSync(this.expectedPath, actual);
|
||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath });
|
||||
step?._attachToStep({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath });
|
||||
writeFileSync(this.actualPath, actual);
|
||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
|
||||
step?._attachToStep({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
|
||||
const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ', writing actual.' : '.'}`;
|
||||
if (this.updateSnapshots === 'all' || this.updateSnapshots === 'changed') {
|
||||
/* eslint-disable no-console */
|
||||
|
|
@ -248,28 +249,29 @@ class SnapshotHelper {
|
|||
diff: Buffer | string | undefined,
|
||||
header: string,
|
||||
diffError: string,
|
||||
log: string[] | undefined): ImageMatcherResult {
|
||||
log: string[] | undefined,
|
||||
step: TestStepInfoImpl | undefined): ImageMatcherResult {
|
||||
const output = [`${header}${indent(diffError, ' ')}`];
|
||||
if (expected !== undefined) {
|
||||
// Copy the expectation inside the `test-results/` folder for backwards compatibility,
|
||||
// so that one can upload `test-results/` directory and have all the data inside.
|
||||
writeFileSync(this.legacyExpectedPath, expected);
|
||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath });
|
||||
step?._attachToStep({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath });
|
||||
output.push(`\nExpected: ${colors.yellow(this.expectedPath)}`);
|
||||
}
|
||||
if (previous !== undefined) {
|
||||
writeFileSync(this.previousPath, previous);
|
||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-previous'), contentType: this.mimeType, path: this.previousPath });
|
||||
step?._attachToStep({ name: addSuffixToFilePath(this.attachmentBaseName, '-previous'), contentType: this.mimeType, path: this.previousPath });
|
||||
output.push(`Previous: ${colors.yellow(this.previousPath)}`);
|
||||
}
|
||||
if (actual !== undefined) {
|
||||
writeFileSync(this.actualPath, actual);
|
||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
|
||||
step?._attachToStep({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
|
||||
output.push(`Received: ${colors.yellow(this.actualPath)}`);
|
||||
}
|
||||
if (diff !== undefined) {
|
||||
writeFileSync(this.diffPath, diff);
|
||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-diff'), contentType: this.mimeType, path: this.diffPath });
|
||||
step?._attachToStep({ name: addSuffixToFilePath(this.attachmentBaseName, '-diff'), contentType: this.mimeType, path: this.diffPath });
|
||||
output.push(` Diff: ${colors.yellow(this.diffPath)}`);
|
||||
}
|
||||
|
||||
|
|
@ -287,7 +289,7 @@ class SnapshotHelper {
|
|||
}
|
||||
|
||||
export function toMatchSnapshot(
|
||||
this: ExpectMatcherState,
|
||||
this: ExpectMatcherStateInternal,
|
||||
received: Buffer | string,
|
||||
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & ImageComparatorOptions = {},
|
||||
optOptions: ImageComparatorOptions = {}
|
||||
|
|
@ -314,7 +316,7 @@ export function toMatchSnapshot(
|
|||
}
|
||||
|
||||
if (!fs.existsSync(helper.expectedPath))
|
||||
return helper.handleMissing(received);
|
||||
return helper.handleMissing(received, this._stepInfo);
|
||||
|
||||
const expected = fs.readFileSync(helper.expectedPath);
|
||||
|
||||
|
|
@ -343,7 +345,7 @@ export function toMatchSnapshot(
|
|||
|
||||
const receiver = isString(received) ? 'string' : 'Buffer';
|
||||
const header = matcherHint(this, undefined, 'toMatchSnapshot', receiver, undefined, undefined);
|
||||
return helper.handleDifferent(received, expected, undefined, result.diff, header, result.errorMessage, undefined);
|
||||
return helper.handleDifferent(received, expected, undefined, result.diff, header, result.errorMessage, undefined, this._stepInfo);
|
||||
}
|
||||
|
||||
export function toHaveScreenshotStepTitle(
|
||||
|
|
@ -359,7 +361,7 @@ export function toHaveScreenshotStepTitle(
|
|||
}
|
||||
|
||||
export async function toHaveScreenshot(
|
||||
this: ExpectMatcherState,
|
||||
this: ExpectMatcherStateInternal,
|
||||
pageOrLocator: Page | Locator,
|
||||
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & ToHaveScreenshotOptions = {},
|
||||
optOptions: ToHaveScreenshotOptions = {}
|
||||
|
|
@ -424,11 +426,11 @@ export async function toHaveScreenshot(
|
|||
// This can be due to e.g. spinning animation, so we want to show it as a diff.
|
||||
if (errorMessage) {
|
||||
const header = matcherHint(this, locator, 'toHaveScreenshot', receiver, undefined, undefined, timedOut ? timeout : undefined);
|
||||
return helper.handleDifferent(actual, undefined, previous, diff, header, errorMessage, log);
|
||||
return helper.handleDifferent(actual, undefined, previous, diff, header, errorMessage, log, this._stepInfo);
|
||||
}
|
||||
|
||||
// We successfully generated new screenshot.
|
||||
return helper.handleMissing(actual!);
|
||||
return helper.handleMissing(actual!, this._stepInfo);
|
||||
}
|
||||
|
||||
// General case:
|
||||
|
|
@ -459,7 +461,7 @@ export async function toHaveScreenshot(
|
|||
return writeFiles();
|
||||
|
||||
const header = matcherHint(this, undefined, 'toHaveScreenshot', receiver, undefined, undefined, timedOut ? timeout : undefined);
|
||||
return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header, errorMessage, log);
|
||||
return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header, errorMessage, log, this._stepInfo);
|
||||
}
|
||||
|
||||
function writeFileSync(aPath: string, content: Buffer | string) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { createGuid, spawnAsync } from 'playwright-core/lib/utils';
|
|||
import type { TestRunnerPlugin } from './';
|
||||
import type { FullConfig } from '../../types/testReporter';
|
||||
import type { FullConfigInternal } from '../common/config';
|
||||
import type { GitCommitInfo } from '../isomorphic/types';
|
||||
|
||||
const GIT_OPERATIONS_TIMEOUT_MS = 1500;
|
||||
|
||||
|
|
@ -31,38 +32,23 @@ export const gitCommitInfo = (options?: GitCommitInfoPluginOptions): TestRunnerP
|
|||
name: 'playwright:git-commit-info',
|
||||
|
||||
setup: async (config: FullConfig, configDir: string) => {
|
||||
const info = {
|
||||
...linksFromEnv(),
|
||||
...options?.info ? options.info : await gitStatusFromCLI(options?.directory || configDir),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
// Normalize dates
|
||||
const timestamp = info['revision.timestamp'];
|
||||
if (timestamp instanceof Date)
|
||||
info['revision.timestamp'] = timestamp.getTime();
|
||||
const fromEnv = linksFromEnv();
|
||||
const fromCLI = await gitStatusFromCLI(options?.directory || configDir);
|
||||
const info = { ...fromEnv, ...fromCLI };
|
||||
if (info['revision.timestamp'] instanceof Date)
|
||||
info['revision.timestamp'] = info['revision.timestamp'].getTime();
|
||||
|
||||
config.metadata = config.metadata || {};
|
||||
Object.assign(config.metadata, info);
|
||||
config.metadata['git.commit.info'] = info;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export interface GitCommitInfoPluginOptions {
|
||||
directory?: string;
|
||||
info?: Info;
|
||||
interface GitCommitInfoPluginOptions {
|
||||
directory?: string;
|
||||
}
|
||||
|
||||
export interface Info {
|
||||
'revision.id'?: string;
|
||||
'revision.author'?: string;
|
||||
'revision.email'?: string;
|
||||
'revision.subject'?: string;
|
||||
'revision.timestamp'?: number | Date;
|
||||
'revision.link'?: string;
|
||||
'ci.link'?: string;
|
||||
}
|
||||
|
||||
const linksFromEnv = (): Pick<Info, 'revision.link' | 'ci.link'> => {
|
||||
function linksFromEnv(): Pick<GitCommitInfo, 'revision.link' | 'ci.link'> {
|
||||
const out: { 'revision.link'?: string; 'ci.link'?: string; } = {};
|
||||
// Jenkins: https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
|
||||
if (process.env.BUILD_URL)
|
||||
|
|
@ -78,9 +64,9 @@ const linksFromEnv = (): Pick<Info, 'revision.link' | 'ci.link'> => {
|
|||
if (process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID)
|
||||
out['ci.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
||||
return out;
|
||||
};
|
||||
}
|
||||
|
||||
export const gitStatusFromCLI = async (gitDir: string): Promise<Info | undefined> => {
|
||||
async function gitStatusFromCLI(gitDir: string): Promise<GitCommitInfo | undefined> {
|
||||
const separator = `:${createGuid().slice(0, 4)}:`;
|
||||
const { code, stdout } = await spawnAsync(
|
||||
'git',
|
||||
|
|
@ -101,4 +87,4 @@ export const gitStatusFromCLI = async (gitDir: string): Promise<Info | undefined
|
|||
'revision.subject': subject,
|
||||
'revision.timestamp': timestamp,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -411,7 +411,7 @@ const testOptions: [string, string][] = [
|
|||
['--ui', `Run tests in interactive UI mode`],
|
||||
['--ui-host <host>', 'Host to serve UI on; specifying this option opens UI in a browser tab'],
|
||||
['--ui-port <port>', 'Port to serve UI on, 0 for any free port; specifying this option opens UI in a browser tab'],
|
||||
['-u, --update-snapshots [mode]', `Update snapshots with actual results. Possible values are 'all', 'changed', 'missing' and 'none'. Not passing defaults to 'missing', passing without value defaults to 'changed'`],
|
||||
['-u, --update-snapshots [mode]', `Update snapshots with actual results. Possible values are "all", "changed", "missing", and "none". Running tests without the flag defaults to "missing"; running tests with the flag but without a value defaults to "changed".`],
|
||||
['--update-source-method <method>', `Chooses the way source is updated. Possible values are 'overwrite', '3way' and 'patch'. Defaults to 'patch'`],
|
||||
['-j, --workers <workers>', `Number of concurrent workers or percentage of logical CPU cores, use 1 to run in a single worker (default: 50%)`],
|
||||
['-x', `Stop after the first failure`],
|
||||
|
|
|
|||
|
|
@ -517,9 +517,12 @@ class HtmlBuilder {
|
|||
|
||||
private _createTestStep(dedupedStep: DedupedStep, result: api.TestResult): TestStep {
|
||||
const { step, duration, count } = dedupedStep;
|
||||
const skipped = dedupedStep.step.category === 'test.step.skip';
|
||||
const skipped = dedupedStep.step.annotations?.find(a => a.type === 'skip');
|
||||
let title = step.title;
|
||||
if (skipped)
|
||||
title = `${title} (skipped${skipped.description ? ': ' + skipped.description : ''})`;
|
||||
const testStep: TestStep = {
|
||||
title: step.title,
|
||||
title,
|
||||
startTime: step.startTime.toISOString(),
|
||||
duration,
|
||||
steps: dedupeSteps(step.steps).map(s => this._createTestStep(s, result)),
|
||||
|
|
@ -532,7 +535,7 @@ class HtmlBuilder {
|
|||
location: this._relativeLocation(step.location),
|
||||
error: step.error?.message,
|
||||
count,
|
||||
skipped
|
||||
skipped: !!skipped,
|
||||
};
|
||||
if (step.location)
|
||||
this._stepsInFile.set(step.location.file, testStep);
|
||||
|
|
|
|||
|
|
@ -68,12 +68,12 @@ class LineReporter extends TerminalReporter {
|
|||
}
|
||||
|
||||
onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
|
||||
if (step.category === 'test.step')
|
||||
if (this.screen.isTTY && step.category === 'test.step')
|
||||
this._updateLine(test, result, step);
|
||||
}
|
||||
|
||||
onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
|
||||
if (step.category === 'test.step')
|
||||
if (this.screen.isTTY && step.category === 'test.step')
|
||||
this._updateLine(test, result, step.parent);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -256,7 +256,8 @@ export class TeleReporterEmitter implements ReporterV2 {
|
|||
id: (step as any)[this._idSymbol],
|
||||
duration: step.duration,
|
||||
error: step.error,
|
||||
attachments: step.attachments.map(a => result.attachments.indexOf(a)),
|
||||
attachments: step.attachments.length ? step.attachments.map(a => result.attachments.indexOf(a)) : undefined,
|
||||
annotations: step.annotations.length ? step.annotations : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -321,6 +321,7 @@ class JobDispatcher {
|
|||
duration: -1,
|
||||
steps: [],
|
||||
attachments: [],
|
||||
annotations: [],
|
||||
location: params.location,
|
||||
};
|
||||
steps.set(params.stepId, step);
|
||||
|
|
@ -345,6 +346,7 @@ class JobDispatcher {
|
|||
step.error = params.error;
|
||||
if (params.suggestedRebaseline)
|
||||
addSuggestedRebaseline(step.location!, params.suggestedRebaseline);
|
||||
step.annotations = params.annotations;
|
||||
steps.delete(params.stepId);
|
||||
this._reporter.onStepEnd?.(test, result, step);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ export function addSuggestedRebaseline(location: Location, suggestedRebaseline:
|
|||
suggestedRebaselines.set(location.file, { location, code: suggestedRebaseline });
|
||||
}
|
||||
|
||||
export function clearSuggestedRebaselines() {
|
||||
suggestedRebaselines.clear();
|
||||
}
|
||||
|
||||
export async function applySuggestedRebaselines(config: FullConfigInternal, reporter: InternalReporter) {
|
||||
if (config.config.updateSnapshots === 'none')
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import { detectChangedTestFiles } from './vcs';
|
|||
import type { InternalReporter } from '../reporters/internalReporter';
|
||||
import { cacheDir } from '../transform/compilationCache';
|
||||
import type { FullResult } from '../../types/testReporter';
|
||||
import { applySuggestedRebaselines } from './rebase';
|
||||
import { applySuggestedRebaselines, clearSuggestedRebaselines } from './rebase';
|
||||
|
||||
const readDirAsync = promisify(fs.readdir);
|
||||
|
||||
|
|
@ -284,6 +284,9 @@ export function createLoadTask(mode: 'out-of-process' | 'in-process', options: {
|
|||
export function createApplyRebaselinesTask(): Task<TestRun> {
|
||||
return {
|
||||
title: 'apply rebaselines',
|
||||
setup: async () => {
|
||||
clearSuggestedRebaselines();
|
||||
},
|
||||
teardown: async ({ config, reporter }) => {
|
||||
await applySuggestedRebaselines(config, reporter);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export const declare: typeof import('../../bundles/babel/node_modules/@types/bab
|
|||
export const types: typeof import('../../bundles/babel/node_modules/@types/babel__core').types = require('./babelBundleImpl').types;
|
||||
export const traverse: typeof import('../../bundles/babel/node_modules/@types/babel__traverse').default = require('./babelBundleImpl').traverse;
|
||||
export type BabelPlugin = [string, any?];
|
||||
export type BabelTransformFunction = (code: string, filename: string, isModule: boolean, pluginsPrefix: BabelPlugin[], pluginsSuffix: BabelPlugin[]) => BabelFileResult;
|
||||
export type BabelTransformFunction = (code: string, filename: string, isModule: boolean, pluginsPrefix: BabelPlugin[], pluginsSuffix: BabelPlugin[]) => BabelFileResult | null;
|
||||
export const babelTransform: BabelTransformFunction = require('./babelBundleImpl').babelTransform;
|
||||
export type BabelParseFunction = (code: string, filename: string, isModule: boolean) => ParseResult;
|
||||
export const babelParse: BabelParseFunction = require('./babelBundleImpl').babelParse;
|
||||
|
|
|
|||
|
|
@ -232,9 +232,10 @@ export function transformHook(originalCode: string, filename: string, moduleUrl?
|
|||
|
||||
const { babelTransform }: { babelTransform: BabelTransformFunction } = require('./babelBundle');
|
||||
transformData = new Map<string, any>();
|
||||
const { code, map } = babelTransform(originalCode, filename, !!moduleUrl, pluginsPrologue, pluginsEpilogue);
|
||||
if (!code)
|
||||
return { code: '', serializedCache };
|
||||
const babelResult = babelTransform(originalCode, filename, !!moduleUrl, pluginsPrologue, pluginsEpilogue);
|
||||
if (!babelResult?.code)
|
||||
return { code: originalCode, serializedCache };
|
||||
const { code, map } = babelResult;
|
||||
const added = addToCache!(code, map, transformData);
|
||||
return { code, serializedCache: added.serializedCache };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { captureRawStack, monotonicTime, zones, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils';
|
||||
import type { TestInfo, TestStatus, FullProject } from '../../types/test';
|
||||
import type { TestInfo, TestStatus, FullProject, TestStepInfo } from '../../types/test';
|
||||
import type { AttachmentPayload, StepBeginPayload, StepEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc';
|
||||
import type { TestCase } from '../common/test';
|
||||
import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager';
|
||||
|
|
@ -31,6 +31,7 @@ import { testInfoError } from './util';
|
|||
|
||||
export interface TestStepInternal {
|
||||
complete(result: { error?: Error | unknown, suggestedRebaseline?: string }): void;
|
||||
info: TestStepInfoImpl
|
||||
attachmentIndices: number[];
|
||||
stepId: string;
|
||||
title: string;
|
||||
|
|
@ -244,7 +245,7 @@ export class TestInfoImpl implements TestInfo {
|
|||
?? this._findLastStageStep(this._steps); // If no parent step on stack, assume the current stage as parent.
|
||||
}
|
||||
|
||||
_addStep(data: Omit<TestStepInternal, 'complete' | 'stepId' | 'steps' | 'attachmentIndices'>, parentStep?: TestStepInternal): TestStepInternal {
|
||||
_addStep(data: Omit<TestStepInternal, 'complete' | 'stepId' | 'steps' | 'attachmentIndices' | 'info'>, parentStep?: TestStepInternal): TestStepInternal {
|
||||
const stepId = `${data.category}@${++this._lastStepId}`;
|
||||
|
||||
if (data.isStage) {
|
||||
|
|
@ -269,6 +270,7 @@ export class TestInfoImpl implements TestInfo {
|
|||
...data,
|
||||
steps: [],
|
||||
attachmentIndices,
|
||||
info: new TestStepInfoImpl(this, stepId),
|
||||
complete: result => {
|
||||
if (step.endWallTime)
|
||||
return;
|
||||
|
|
@ -302,11 +304,12 @@ export class TestInfoImpl implements TestInfo {
|
|||
wallTime: step.endWallTime,
|
||||
error: step.error,
|
||||
suggestedRebaseline: result.suggestedRebaseline,
|
||||
annotations: step.info.annotations,
|
||||
};
|
||||
this._onStepEnd(payload);
|
||||
const errorForTrace = step.error ? { name: '', message: step.error.message || '', stack: step.error.stack } : undefined;
|
||||
const attachments = attachmentIndices.map(i => this.attachments[i]);
|
||||
this._tracing.appendAfterActionForStep(stepId, errorForTrace, attachments);
|
||||
this._tracing.appendAfterActionForStep(stepId, errorForTrace, attachments, step.info.annotations);
|
||||
}
|
||||
};
|
||||
const parentStepList = parentStep ? parentStep.steps : this._steps;
|
||||
|
|
@ -414,7 +417,7 @@ export class TestInfoImpl implements TestInfo {
|
|||
step.complete({});
|
||||
}
|
||||
|
||||
private _attach(attachment: TestInfo['attachments'][0], stepId: string | undefined) {
|
||||
_attach(attachment: TestInfo['attachments'][0], stepId: string | undefined) {
|
||||
const index = this._attachmentsPush(attachment) - 1;
|
||||
if (stepId) {
|
||||
this._stepMap.get(stepId)!.attachmentIndices.push(index);
|
||||
|
|
@ -454,14 +457,15 @@ export class TestInfoImpl implements TestInfo {
|
|||
return sanitizeForFilePath(trimLongString(fullTitleWithoutSpec));
|
||||
}
|
||||
|
||||
snapshotPath(...pathSegments: string[]) {
|
||||
_resolveSnapshotPath(template: string | undefined, defaultTemplate: string, pathSegments: string[]) {
|
||||
const subPath = path.join(...pathSegments);
|
||||
const parsedSubPath = path.parse(subPath);
|
||||
const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile);
|
||||
const parsedRelativeTestFilePath = path.parse(relativeTestFilePath);
|
||||
const projectNamePathSegment = sanitizeForFilePath(this.project.name);
|
||||
|
||||
const snapshotPath = (this._projectInternal.snapshotPathTemplate || '')
|
||||
const actualTemplate = (template || this._projectInternal.snapshotPathTemplate || defaultTemplate);
|
||||
const snapshotPath = actualTemplate
|
||||
.replace(/\{(.)?testDir\}/g, '$1' + this.project.testDir)
|
||||
.replace(/\{(.)?snapshotDir\}/g, '$1' + this.project.snapshotDir)
|
||||
.replace(/\{(.)?snapshotSuffix\}/g, this.snapshotSuffix ? '$1' + this.snapshotSuffix : '')
|
||||
|
|
@ -477,6 +481,11 @@ export class TestInfoImpl implements TestInfo {
|
|||
return path.normalize(path.resolve(this._configInternal.configDir, snapshotPath));
|
||||
}
|
||||
|
||||
snapshotPath(...pathSegments: string[]) {
|
||||
const legacyTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
|
||||
return this._resolveSnapshotPath(undefined, legacyTemplate, pathSegments);
|
||||
}
|
||||
|
||||
skip(...args: [arg?: any, description?: string]) {
|
||||
this._modifier('skip', args);
|
||||
}
|
||||
|
|
@ -498,6 +507,50 @@ export class TestInfoImpl implements TestInfo {
|
|||
}
|
||||
}
|
||||
|
||||
export class TestStepInfoImpl implements TestStepInfo {
|
||||
annotations: Annotation[] = [];
|
||||
|
||||
private _testInfo: TestInfoImpl;
|
||||
private _stepId: string;
|
||||
|
||||
constructor(testInfo: TestInfoImpl, stepId: string) {
|
||||
this._testInfo = testInfo;
|
||||
this._stepId = stepId;
|
||||
}
|
||||
|
||||
async _runStepBody<T>(skip: boolean, body: (step: TestStepInfo) => T | Promise<T>) {
|
||||
if (skip) {
|
||||
this.annotations.push({ type: 'skip' });
|
||||
return undefined as T;
|
||||
}
|
||||
try {
|
||||
return await body(this);
|
||||
} catch (e) {
|
||||
if (e instanceof SkipError)
|
||||
return undefined as T;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
_attachToStep(attachment: TestInfo['attachments'][0]): void {
|
||||
this._testInfo._attach(attachment, this._stepId);
|
||||
}
|
||||
|
||||
async attach(name: string, options?: { body?: string | Buffer; contentType?: string; path?: string; }): Promise<void> {
|
||||
this._attachToStep(await normalizeAndSaveAttachment(this._testInfo.outputPath(), name, options));
|
||||
}
|
||||
|
||||
skip(...args: unknown[]) {
|
||||
// skip();
|
||||
// skip(condition: boolean, description: string);
|
||||
if (args.length > 0 && !args[0])
|
||||
return;
|
||||
const description = args[1] as (string|undefined);
|
||||
this.annotations.push({ type: 'skip', description });
|
||||
throw new SkipError(description);
|
||||
}
|
||||
}
|
||||
|
||||
export class SkipError extends Error {
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -252,19 +252,20 @@ export class TestTracing {
|
|||
parentId,
|
||||
startTime: monotonicTime(),
|
||||
class: 'Test',
|
||||
method: category,
|
||||
method: 'step',
|
||||
apiName,
|
||||
params: Object.fromEntries(Object.entries(params || {}).map(([name, value]) => [name, generatePreview(value)])),
|
||||
stack,
|
||||
});
|
||||
}
|
||||
|
||||
appendAfterActionForStep(callId: string, error?: SerializedError['error'], attachments: Attachment[] = []) {
|
||||
appendAfterActionForStep(callId: string, error?: SerializedError['error'], attachments: Attachment[] = [], annotations?: trace.AfterActionTraceEventAnnotation[]) {
|
||||
this._appendTraceEvent({
|
||||
type: 'after',
|
||||
callId,
|
||||
endTime: monotonicTime(),
|
||||
attachments: serializeAttachments(attachments),
|
||||
annotations,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
|
@ -277,6 +278,8 @@ export class TestTracing {
|
|||
}
|
||||
|
||||
function serializeAttachments(attachments: Attachment[]): trace.AfterActionTraceEvent['attachments'] {
|
||||
if (attachments.length === 0)
|
||||
return undefined;
|
||||
return attachments.filter(a => a.name !== 'trace').map(a => {
|
||||
return {
|
||||
name: a.name,
|
||||
|
|
@ -335,7 +338,7 @@ async function mergeTraceFiles(fileName: string, temporaryTraceFiles: string[])
|
|||
// Keep the name for test traces so that the last test trace
|
||||
// that contains most of the information is kept in the trace.
|
||||
// Note the reverse order of the iteration (from new traces to old).
|
||||
} else if (entry.fileName.match(/[\d-]*trace\./)) {
|
||||
} else if (entry.fileName.match(/trace\.[a-z]*$/)) {
|
||||
entryName = i + '-' + entry.fileName;
|
||||
}
|
||||
if (entryNames.has(entryName)) {
|
||||
|
|
|
|||
|
|
@ -105,6 +105,10 @@ export class WorkerMain extends ProcessRunner {
|
|||
override async gracefullyClose() {
|
||||
try {
|
||||
await this._stop();
|
||||
if (!this._config) {
|
||||
// We never set anything up and we can crash on attempting cleanup
|
||||
return;
|
||||
}
|
||||
// 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;
|
||||
|
|
@ -190,15 +194,19 @@ export class WorkerMain extends ProcessRunner {
|
|||
if (this._config)
|
||||
return;
|
||||
|
||||
this._config = await deserializeConfig(this._params.config);
|
||||
this._project = this._config.projects.find(p => p.id === this._params.projectId)!;
|
||||
const config = await deserializeConfig(this._params.config);
|
||||
const project = config.projects.find(p => p.id === this._params.projectId);
|
||||
if (!project)
|
||||
throw new Error(`Project "${this._params.projectId}" not found in the worker process. Make sure project name does not change.`);
|
||||
this._config = config;
|
||||
this._project = project;
|
||||
this._poolBuilder = PoolBuilder.createForWorker(this._project);
|
||||
}
|
||||
|
||||
async runTestGroup(runPayload: RunPayload) {
|
||||
this._runFinished = new ManualPromise<void>();
|
||||
const entries = new Map(runPayload.entries.map(e => [e.testId, e]));
|
||||
let fatalUnknownTestIds;
|
||||
let fatalUnknownTestIds: string[] | undefined;
|
||||
try {
|
||||
await this._loadIfNeeded();
|
||||
const fileSuite = await loadTestFile(runPayload.file, this._config.config.rootDir);
|
||||
|
|
|
|||
242
packages/playwright/types/test.d.ts
vendored
242
packages/playwright/types/test.d.ts
vendored
|
|
@ -214,6 +214,27 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
|
|||
* [page.screenshot([options])](https://playwright.dev/docs/api/class-page#page-screenshot).
|
||||
*/
|
||||
stylePath?: string|Array<string>;
|
||||
|
||||
/**
|
||||
* A template controlling location of the screenshots. See
|
||||
* [testProject.snapshotPathTemplate](https://playwright.dev/docs/api/class-testproject#test-project-snapshot-path-template)
|
||||
* for details.
|
||||
*/
|
||||
pathTemplate?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuration for the
|
||||
* [expect(locator).toMatchAriaSnapshot([options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
|
||||
* method.
|
||||
*/
|
||||
toMatchAriaSnapshot?: {
|
||||
/**
|
||||
* A template controlling location of the aria snapshots. See
|
||||
* [testProject.snapshotPathTemplate](https://playwright.dev/docs/api/class-testproject#test-project-snapshot-path-template)
|
||||
* for details.
|
||||
*/
|
||||
pathTemplate?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -328,6 +349,10 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
|
|||
|
||||
/**
|
||||
* Project name is visible in the report and during test execution.
|
||||
*
|
||||
* **NOTE** Playwright executes the configuration file multiple times. Do not dynamically produce non-stable values in
|
||||
* your configuration.
|
||||
*
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
|
|
@ -404,10 +429,14 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
|
|||
|
||||
/**
|
||||
* This option configures a template controlling location of snapshots generated by
|
||||
* [expect(page).toHaveScreenshot(name[, options])](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-1)
|
||||
* [expect(page).toHaveScreenshot(name[, options])](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-1),
|
||||
* [expect(locator).toMatchAriaSnapshot([options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
|
||||
* and
|
||||
* [expect(value).toMatchSnapshot(name[, options])](https://playwright.dev/docs/api/class-snapshotassertions#snapshot-assertions-to-match-snapshot-1).
|
||||
*
|
||||
* You can configure templates for each assertion separately in
|
||||
* [testConfig.expect](https://playwright.dev/docs/api/class-testconfig#test-config-expect).
|
||||
*
|
||||
* **Usage**
|
||||
*
|
||||
* ```js
|
||||
|
|
@ -416,7 +445,19 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
|
|||
*
|
||||
* export default defineConfig({
|
||||
* testDir: './tests',
|
||||
*
|
||||
* // Single template for all assertions
|
||||
* snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
|
||||
*
|
||||
* // Assertion-specific templates
|
||||
* expect: {
|
||||
* toHaveScreenshot: {
|
||||
* pathTemplate: '{testDir}/__screenshots__{/projectName}/{testFilePath}/{arg}{ext}',
|
||||
* },
|
||||
* toMatchAriaSnapshot: {
|
||||
* pathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',
|
||||
* },
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
|
|
@ -447,27 +488,27 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
|
|||
* ```
|
||||
*
|
||||
* The list of supported tokens:
|
||||
* - `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the
|
||||
* `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated
|
||||
* snapshot name.
|
||||
* - `{arg}` - Relative snapshot path **without extension**. This comes from the arguments passed to
|
||||
* `toHaveScreenshot()`, `toMatchAriaSnapshot()` or `toMatchSnapshot()`; if called without arguments, this will be
|
||||
* an auto-generated snapshot name.
|
||||
* - Value: `foo/bar/baz`
|
||||
* - `{ext}` - snapshot extension (with dots)
|
||||
* - `{ext}` - Snapshot extension (with the leading dot).
|
||||
* - Value: `.png`
|
||||
* - `{platform}` - The value of `process.platform`.
|
||||
* - `{projectName}` - Project's file-system-sanitized name, if any.
|
||||
* - Value: `''` (empty string).
|
||||
* - `{snapshotDir}` - Project's
|
||||
* [testConfig.snapshotDir](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-dir).
|
||||
* [testProject.snapshotDir](https://playwright.dev/docs/api/class-testproject#test-project-snapshot-dir).
|
||||
* - Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
|
||||
* - `{testDir}` - Project's
|
||||
* [testConfig.testDir](https://playwright.dev/docs/api/class-testconfig#test-config-test-dir).
|
||||
* - Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with
|
||||
* [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir).
|
||||
* - Value: `/home/playwright/tests` (absolute path since `testDir` is resolved relative to directory with
|
||||
* config)
|
||||
* - `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
|
||||
* - Value: `page`
|
||||
* - `{testFileName}` - Test file name with extension.
|
||||
* - Value: `page-click.spec.ts`
|
||||
* - `{testFilePath}` - Relative path from `testDir` to **test file**
|
||||
* - `{testFilePath}` - Relative path from `testDir` to **test file**.
|
||||
* - Value: `page/page-click.spec.ts`
|
||||
* - `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
|
||||
* - Value: `suite-test-should-work`
|
||||
|
|
@ -991,6 +1032,27 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
|||
* [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
|
||||
*/
|
||||
threshold?: number;
|
||||
|
||||
/**
|
||||
* A template controlling location of the screenshots. See
|
||||
* [testConfig.snapshotPathTemplate](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-path-template)
|
||||
* for details.
|
||||
*/
|
||||
pathTemplate?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuration for the
|
||||
* [expect(locator).toMatchAriaSnapshot([options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
|
||||
* method.
|
||||
*/
|
||||
toMatchAriaSnapshot?: {
|
||||
/**
|
||||
* A template controlling location of the aria snapshots. See
|
||||
* [testConfig.snapshotPathTemplate](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-path-template)
|
||||
* for details.
|
||||
*/
|
||||
pathTemplate?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -1220,7 +1282,12 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
|||
maxFailures?: number;
|
||||
|
||||
/**
|
||||
* Metadata that will be put directly to the test report serialized as JSON.
|
||||
* Metadata contains key-value pairs to be included in the report. For example, HTML report will display it as
|
||||
* key-value pairs, and JSON report will include metadata serialized as json.
|
||||
*
|
||||
* See also
|
||||
* [testConfig.populateGitInfo](https://playwright.dev/docs/api/class-testconfig#test-config-populate-git-info) that
|
||||
* populates metadata.
|
||||
*
|
||||
* **Usage**
|
||||
*
|
||||
|
|
@ -1229,7 +1296,7 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
|||
* import { defineConfig } from '@playwright/test';
|
||||
*
|
||||
* export default defineConfig({
|
||||
* metadata: 'acceptance tests',
|
||||
* metadata: { title: 'acceptance tests' },
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
|
|
@ -1294,8 +1361,11 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
|||
outputDir?: string;
|
||||
|
||||
/**
|
||||
* Whether to populate [testConfig.metadata](https://playwright.dev/docs/api/class-testconfig#test-config-metadata)
|
||||
* with Git info. The metadata will automatically appear in the HTML report and is available in Reporter API.
|
||||
* Whether to populate `'git.commit.info'` field of the
|
||||
* [testConfig.metadata](https://playwright.dev/docs/api/class-testconfig#test-config-metadata) with Git commit info
|
||||
* and CI/CD information.
|
||||
*
|
||||
* This information will appear in the HTML and JSON reports and is available in the Reporter API.
|
||||
*
|
||||
* **Usage**
|
||||
*
|
||||
|
|
@ -1486,10 +1556,14 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
|||
|
||||
/**
|
||||
* This option configures a template controlling location of snapshots generated by
|
||||
* [expect(page).toHaveScreenshot(name[, options])](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-1)
|
||||
* [expect(page).toHaveScreenshot(name[, options])](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-1),
|
||||
* [expect(locator).toMatchAriaSnapshot([options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
|
||||
* and
|
||||
* [expect(value).toMatchSnapshot(name[, options])](https://playwright.dev/docs/api/class-snapshotassertions#snapshot-assertions-to-match-snapshot-1).
|
||||
*
|
||||
* You can configure templates for each assertion separately in
|
||||
* [testConfig.expect](https://playwright.dev/docs/api/class-testconfig#test-config-expect).
|
||||
*
|
||||
* **Usage**
|
||||
*
|
||||
* ```js
|
||||
|
|
@ -1498,7 +1572,19 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
|||
*
|
||||
* export default defineConfig({
|
||||
* testDir: './tests',
|
||||
*
|
||||
* // Single template for all assertions
|
||||
* snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
|
||||
*
|
||||
* // Assertion-specific templates
|
||||
* expect: {
|
||||
* toHaveScreenshot: {
|
||||
* pathTemplate: '{testDir}/__screenshots__{/projectName}/{testFilePath}/{arg}{ext}',
|
||||
* },
|
||||
* toMatchAriaSnapshot: {
|
||||
* pathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',
|
||||
* },
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
|
|
@ -1529,27 +1615,27 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
|||
* ```
|
||||
*
|
||||
* The list of supported tokens:
|
||||
* - `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the
|
||||
* `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated
|
||||
* snapshot name.
|
||||
* - `{arg}` - Relative snapshot path **without extension**. This comes from the arguments passed to
|
||||
* `toHaveScreenshot()`, `toMatchAriaSnapshot()` or `toMatchSnapshot()`; if called without arguments, this will be
|
||||
* an auto-generated snapshot name.
|
||||
* - Value: `foo/bar/baz`
|
||||
* - `{ext}` - snapshot extension (with dots)
|
||||
* - `{ext}` - Snapshot extension (with the leading dot).
|
||||
* - Value: `.png`
|
||||
* - `{platform}` - The value of `process.platform`.
|
||||
* - `{projectName}` - Project's file-system-sanitized name, if any.
|
||||
* - Value: `''` (empty string).
|
||||
* - `{snapshotDir}` - Project's
|
||||
* [testConfig.snapshotDir](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-dir).
|
||||
* [testProject.snapshotDir](https://playwright.dev/docs/api/class-testproject#test-project-snapshot-dir).
|
||||
* - Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
|
||||
* - `{testDir}` - Project's
|
||||
* [testConfig.testDir](https://playwright.dev/docs/api/class-testconfig#test-config-test-dir).
|
||||
* - Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with
|
||||
* [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir).
|
||||
* - Value: `/home/playwright/tests` (absolute path since `testDir` is resolved relative to directory with
|
||||
* config)
|
||||
* - `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
|
||||
* - Value: `page`
|
||||
* - `{testFileName}` - Test file name with extension.
|
||||
* - Value: `page-click.spec.ts`
|
||||
* - `{testFilePath}` - Relative path from `testDir` to **test file**
|
||||
* - `{testFilePath}` - Relative path from `testDir` to **test file**.
|
||||
* - Value: `page/page-click.spec.ts`
|
||||
* - `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
|
||||
* - Value: `suite-test-should-work`
|
||||
|
|
@ -5729,7 +5815,7 @@ export interface TestType<TestArgs extends {}, WorkerArgs extends {}> {
|
|||
* @param body Step body.
|
||||
* @param options
|
||||
*/
|
||||
<T>(title: string, body: () => T | Promise<T>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<T>;
|
||||
<T>(title: string, body: (step: TestStepInfo) => T | Promise<T>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<T>;
|
||||
/**
|
||||
* Mark a test step as "skip" to temporarily disable its execution, useful for steps that are currently failing and
|
||||
* planned for a near-term fix. Playwright will not run the step.
|
||||
|
|
@ -5753,7 +5839,7 @@ export interface TestType<TestArgs extends {}, WorkerArgs extends {}> {
|
|||
* @param body Step body.
|
||||
* @param options
|
||||
*/
|
||||
skip(title: string, body: () => any | Promise<any>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<void>;
|
||||
skip(title: string, body: (step: TestStepInfo) => any | Promise<any>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<void>;
|
||||
}
|
||||
/**
|
||||
* `expect` function can be used to create test assertions. Read more about [test assertions](https://playwright.dev/docs/test-assertions).
|
||||
|
|
@ -8720,19 +8806,22 @@ interface LocatorAssertions {
|
|||
/**
|
||||
* Asserts that the target element matches the given [accessibility snapshot](https://playwright.dev/docs/aria-snapshots).
|
||||
*
|
||||
* Snapshot is stored in a separate `.yml` file in a location configured by `expect.toMatchAriaSnapshot.pathTemplate`
|
||||
* and/or `snapshotPathTemplate` properties in the configuration file.
|
||||
*
|
||||
* **Usage**
|
||||
*
|
||||
* ```js
|
||||
* await expect(page.locator('body')).toMatchAriaSnapshot();
|
||||
* await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
|
||||
* await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'body.yml' });
|
||||
* ```
|
||||
*
|
||||
* @param options
|
||||
*/
|
||||
toMatchAriaSnapshot(options?: {
|
||||
/**
|
||||
* Name of the snapshot to store in the snapshot (screenshot) folder corresponding to this test. Generates sequential
|
||||
* names if not specified.
|
||||
* Name of the snapshot to store in the snapshot folder corresponding to this test. Generates sequential names if not
|
||||
* specified.
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
|
|
@ -9485,6 +9574,105 @@ export interface TestInfoError {
|
|||
value?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* `TestStepInfo` contains information about currently running test step. It is passed as an argument to the step
|
||||
* function. `TestStepInfo` provides utilities to control test step execution.
|
||||
*
|
||||
* ```js
|
||||
* import { test, expect } from '@playwright/test';
|
||||
*
|
||||
* test('basic test', async ({ page, browserName }, TestStepInfo) => {
|
||||
* await test.step('check some behavior', async step => {
|
||||
* await step.skip(browserName === 'webkit', 'The feature is not available in WebKit');
|
||||
* // ... rest of the step code
|
||||
* await page.check('input');
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
export interface TestStepInfo {
|
||||
/**
|
||||
* Attach a value or a file from disk to the current test step. Some reporters show test step attachments. Either
|
||||
* [`path`](https://playwright.dev/docs/api/class-teststepinfo#test-step-info-attach-option-path) or
|
||||
* [`body`](https://playwright.dev/docs/api/class-teststepinfo#test-step-info-attach-option-body) must be specified,
|
||||
* but not both. Calling this method will attribute the attachment to the step, as opposed to
|
||||
* [testInfo.attach(name[, options])](https://playwright.dev/docs/api/class-testinfo#test-info-attach) which stores
|
||||
* all attachments at the test level.
|
||||
*
|
||||
* For example, you can attach a screenshot to the test step:
|
||||
*
|
||||
* ```js
|
||||
* import { test, expect } from '@playwright/test';
|
||||
*
|
||||
* test('basic test', async ({ page }) => {
|
||||
* await page.goto('https://playwright.dev');
|
||||
* await test.step('check page rendering', async step => {
|
||||
* const screenshot = await page.screenshot();
|
||||
* await step.attach('screenshot', { body: screenshot, contentType: 'image/png' });
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Or you can attach files returned by your APIs:
|
||||
*
|
||||
* ```js
|
||||
* import { test, expect } from '@playwright/test';
|
||||
* import { download } from './my-custom-helpers';
|
||||
*
|
||||
* test('basic test', async ({}) => {
|
||||
* await test.step('check download behavior', async step => {
|
||||
* const tmpPath = await download('a');
|
||||
* await step.attach('downloaded', { path: tmpPath });
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* **NOTE**
|
||||
* [testStepInfo.attach(name[, options])](https://playwright.dev/docs/api/class-teststepinfo#test-step-info-attach)
|
||||
* automatically takes care of copying attached files to a location that is accessible to reporters. You can safely
|
||||
* remove the attachment after awaiting the attach call.
|
||||
*
|
||||
* @param name Attachment name. The name will also be sanitized and used as the prefix of file name when saving to disk.
|
||||
* @param options
|
||||
*/
|
||||
attach(name: string, options?: {
|
||||
/**
|
||||
* Attachment body. Mutually exclusive with
|
||||
* [`path`](https://playwright.dev/docs/api/class-teststepinfo#test-step-info-attach-option-path).
|
||||
*/
|
||||
body?: string|Buffer;
|
||||
|
||||
/**
|
||||
* Content type of this attachment to properly present in the report, for example `'application/json'` or
|
||||
* `'image/png'`. If omitted, content type is inferred based on the
|
||||
* [`path`](https://playwright.dev/docs/api/class-teststepinfo#test-step-info-attach-option-path), or defaults to
|
||||
* `text/plain` for [string] attachments and `application/octet-stream` for [Buffer] attachments.
|
||||
*/
|
||||
contentType?: string;
|
||||
|
||||
/**
|
||||
* Path on the filesystem to the attached file. Mutually exclusive with
|
||||
* [`body`](https://playwright.dev/docs/api/class-teststepinfo#test-step-info-attach-option-body).
|
||||
*/
|
||||
path?: string;
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* Unconditionally skip the currently running step. Test step is immediately aborted. This is similar to
|
||||
* [test.step.skip(title, body[, options])](https://playwright.dev/docs/api/class-test#test-step-skip).
|
||||
*/
|
||||
skip(): void;
|
||||
|
||||
/**
|
||||
* Conditionally skips the currently running step with an optional description. This is similar to
|
||||
* [test.step.skip(title, body[, options])](https://playwright.dev/docs/api/class-test#test-step-skip).
|
||||
* @param condition A skip condition. Test step is skipped when the condition is `true`.
|
||||
* @param description Optional description that will be reflected in a test report.
|
||||
*/
|
||||
skip(condition: boolean, description?: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* `WorkerInfo` contains information about the worker that is running tests and is available to worker-scoped
|
||||
* fixtures. `WorkerInfo` is a subset of [TestInfo](https://playwright.dev/docs/api/class-testinfo) that is available
|
||||
|
|
|
|||
15
packages/playwright/types/testReporter.d.ts
vendored
15
packages/playwright/types/testReporter.d.ts
vendored
|
|
@ -691,6 +691,21 @@ export interface TestStep {
|
|||
*/
|
||||
titlePath(): Array<string>;
|
||||
|
||||
/**
|
||||
* The list of annotations applicable to the current test step.
|
||||
*/
|
||||
annotations: Array<{
|
||||
/**
|
||||
* Annotation type, for example `'skip'`.
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Optional description.
|
||||
*/
|
||||
description?: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* The list of files or buffers attached in the step execution through
|
||||
* [testInfo.attach(name[, options])](https://playwright.dev/docs/api/class-testinfo#test-info-attach).
|
||||
|
|
|
|||
57
packages/protocol/src/channels.d.ts
vendored
57
packages/protocol/src/channels.d.ts
vendored
|
|
@ -274,9 +274,40 @@ export type NameValue = {
|
|||
value: string,
|
||||
};
|
||||
|
||||
export type IndexedDBDatabase = {
|
||||
name: string,
|
||||
version: number,
|
||||
stores: {
|
||||
name: string,
|
||||
autoIncrement: boolean,
|
||||
keyPath?: string,
|
||||
keyPathArray?: string[],
|
||||
records: {
|
||||
key?: any,
|
||||
keyEncoded?: any,
|
||||
value?: any,
|
||||
valueEncoded?: any,
|
||||
}[],
|
||||
indexes: {
|
||||
name: string,
|
||||
keyPath?: string,
|
||||
keyPathArray?: string[],
|
||||
multiEntry: boolean,
|
||||
unique: boolean,
|
||||
}[],
|
||||
}[],
|
||||
};
|
||||
|
||||
export type SetOriginStorage = {
|
||||
origin: string,
|
||||
localStorage: NameValue[],
|
||||
indexedDB?: IndexedDBDatabase[],
|
||||
};
|
||||
|
||||
export type OriginStorage = {
|
||||
origin: string,
|
||||
localStorage: NameValue[],
|
||||
indexedDB: IndexedDBDatabase[],
|
||||
};
|
||||
|
||||
export type SerializedError = {
|
||||
|
|
@ -594,7 +625,7 @@ export type LocalUtilsNewRequestParams = {
|
|||
timeout?: number,
|
||||
storageState?: {
|
||||
cookies?: NetworkCookie[],
|
||||
origins?: OriginStorage[],
|
||||
origins?: SetOriginStorage[],
|
||||
},
|
||||
tracesDir?: string,
|
||||
};
|
||||
|
|
@ -625,7 +656,7 @@ export type LocalUtilsNewRequestOptions = {
|
|||
timeout?: number,
|
||||
storageState?: {
|
||||
cookies?: NetworkCookie[],
|
||||
origins?: OriginStorage[],
|
||||
origins?: SetOriginStorage[],
|
||||
},
|
||||
tracesDir?: string,
|
||||
};
|
||||
|
|
@ -757,7 +788,7 @@ export type PlaywrightNewRequestParams = {
|
|||
timeout?: number,
|
||||
storageState?: {
|
||||
cookies?: NetworkCookie[],
|
||||
origins?: OriginStorage[],
|
||||
origins?: SetOriginStorage[],
|
||||
},
|
||||
tracesDir?: string,
|
||||
};
|
||||
|
|
@ -788,7 +819,7 @@ export type PlaywrightNewRequestOptions = {
|
|||
timeout?: number,
|
||||
storageState?: {
|
||||
cookies?: NetworkCookie[],
|
||||
origins?: OriginStorage[],
|
||||
origins?: SetOriginStorage[],
|
||||
},
|
||||
tracesDir?: string,
|
||||
};
|
||||
|
|
@ -1153,6 +1184,7 @@ export type BrowserTypeLaunchPersistentContextParams = {
|
|||
reducedMotion?: 'reduce' | 'no-preference' | 'no-override',
|
||||
forcedColors?: 'active' | 'none' | 'no-override',
|
||||
acceptDownloads?: 'accept' | 'deny' | 'internal-browser-default',
|
||||
contrast?: 'no-preference' | 'more' | 'no-override',
|
||||
baseURL?: string,
|
||||
recordVideo?: {
|
||||
dir: string,
|
||||
|
|
@ -1233,6 +1265,7 @@ export type BrowserTypeLaunchPersistentContextOptions = {
|
|||
reducedMotion?: 'reduce' | 'no-preference' | 'no-override',
|
||||
forcedColors?: 'active' | 'none' | 'no-override',
|
||||
acceptDownloads?: 'accept' | 'deny' | 'internal-browser-default',
|
||||
contrast?: 'no-preference' | 'more' | 'no-override',
|
||||
baseURL?: string,
|
||||
recordVideo?: {
|
||||
dir: string,
|
||||
|
|
@ -1348,6 +1381,7 @@ export type BrowserNewContextParams = {
|
|||
reducedMotion?: 'reduce' | 'no-preference' | 'no-override',
|
||||
forcedColors?: 'active' | 'none' | 'no-override',
|
||||
acceptDownloads?: 'accept' | 'deny' | 'internal-browser-default',
|
||||
contrast?: 'no-preference' | 'more' | 'no-override',
|
||||
baseURL?: string,
|
||||
recordVideo?: {
|
||||
dir: string,
|
||||
|
|
@ -1367,7 +1401,7 @@ export type BrowserNewContextParams = {
|
|||
},
|
||||
storageState?: {
|
||||
cookies?: SetNetworkCookie[],
|
||||
origins?: OriginStorage[],
|
||||
origins?: SetOriginStorage[],
|
||||
},
|
||||
mockingProxyBaseURL?: string,
|
||||
};
|
||||
|
|
@ -1415,6 +1449,7 @@ export type BrowserNewContextOptions = {
|
|||
reducedMotion?: 'reduce' | 'no-preference' | 'no-override',
|
||||
forcedColors?: 'active' | 'none' | 'no-override',
|
||||
acceptDownloads?: 'accept' | 'deny' | 'internal-browser-default',
|
||||
contrast?: 'no-preference' | 'more' | 'no-override',
|
||||
baseURL?: string,
|
||||
recordVideo?: {
|
||||
dir: string,
|
||||
|
|
@ -1434,7 +1469,7 @@ export type BrowserNewContextOptions = {
|
|||
},
|
||||
storageState?: {
|
||||
cookies?: SetNetworkCookie[],
|
||||
origins?: OriginStorage[],
|
||||
origins?: SetOriginStorage[],
|
||||
},
|
||||
mockingProxyBaseURL?: string,
|
||||
};
|
||||
|
|
@ -1485,6 +1520,7 @@ export type BrowserNewContextForReuseParams = {
|
|||
reducedMotion?: 'reduce' | 'no-preference' | 'no-override',
|
||||
forcedColors?: 'active' | 'none' | 'no-override',
|
||||
acceptDownloads?: 'accept' | 'deny' | 'internal-browser-default',
|
||||
contrast?: 'no-preference' | 'more' | 'no-override',
|
||||
baseURL?: string,
|
||||
recordVideo?: {
|
||||
dir: string,
|
||||
|
|
@ -1504,7 +1540,7 @@ export type BrowserNewContextForReuseParams = {
|
|||
},
|
||||
storageState?: {
|
||||
cookies?: SetNetworkCookie[],
|
||||
origins?: OriginStorage[],
|
||||
origins?: SetOriginStorage[],
|
||||
},
|
||||
};
|
||||
export type BrowserNewContextForReuseOptions = {
|
||||
|
|
@ -1551,6 +1587,7 @@ export type BrowserNewContextForReuseOptions = {
|
|||
reducedMotion?: 'reduce' | 'no-preference' | 'no-override',
|
||||
forcedColors?: 'active' | 'none' | 'no-override',
|
||||
acceptDownloads?: 'accept' | 'deny' | 'internal-browser-default',
|
||||
contrast?: 'no-preference' | 'more' | 'no-override',
|
||||
baseURL?: string,
|
||||
recordVideo?: {
|
||||
dir: string,
|
||||
|
|
@ -1570,7 +1607,7 @@ export type BrowserNewContextForReuseOptions = {
|
|||
},
|
||||
storageState?: {
|
||||
cookies?: SetNetworkCookie[],
|
||||
origins?: OriginStorage[],
|
||||
origins?: SetOriginStorage[],
|
||||
},
|
||||
};
|
||||
export type BrowserNewContextForReuseResult = {
|
||||
|
|
@ -2187,12 +2224,14 @@ export type PageEmulateMediaParams = {
|
|||
colorScheme?: 'dark' | 'light' | 'no-preference' | 'no-override',
|
||||
reducedMotion?: 'reduce' | 'no-preference' | 'no-override',
|
||||
forcedColors?: 'active' | 'none' | 'no-override',
|
||||
contrast?: 'no-preference' | 'more' | 'no-override',
|
||||
};
|
||||
export type PageEmulateMediaOptions = {
|
||||
media?: 'screen' | 'print' | 'no-override',
|
||||
colorScheme?: 'dark' | 'light' | 'no-preference' | 'no-override',
|
||||
reducedMotion?: 'reduce' | 'no-preference' | 'no-override',
|
||||
forcedColors?: 'active' | 'none' | 'no-override',
|
||||
contrast?: 'no-preference' | 'more' | 'no-override',
|
||||
};
|
||||
export type PageEmulateMediaResult = void;
|
||||
export type PageExposeBindingParams = {
|
||||
|
|
@ -4885,6 +4924,7 @@ export type AndroidDeviceLaunchBrowserParams = {
|
|||
reducedMotion?: 'reduce' | 'no-preference' | 'no-override',
|
||||
forcedColors?: 'active' | 'none' | 'no-override',
|
||||
acceptDownloads?: 'accept' | 'deny' | 'internal-browser-default',
|
||||
contrast?: 'no-preference' | 'more' | 'no-override',
|
||||
baseURL?: string,
|
||||
recordVideo?: {
|
||||
dir: string,
|
||||
|
|
@ -4949,6 +4989,7 @@ export type AndroidDeviceLaunchBrowserOptions = {
|
|||
reducedMotion?: 'reduce' | 'no-preference' | 'no-override',
|
||||
forcedColors?: 'active' | 'none' | 'no-override',
|
||||
acceptDownloads?: 'accept' | 'deny' | 'internal-browser-default',
|
||||
contrast?: 'no-preference' | 'more' | 'no-override',
|
||||
baseURL?: string,
|
||||
recordVideo?: {
|
||||
dir: string,
|
||||
|
|
|
|||
|
|
@ -222,6 +222,54 @@ NameValue:
|
|||
name: string
|
||||
value: string
|
||||
|
||||
IndexedDBDatabase:
|
||||
type: object
|
||||
properties:
|
||||
name: string
|
||||
version: number
|
||||
stores:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name: string
|
||||
autoIncrement: boolean
|
||||
keyPath: string?
|
||||
keyPathArray:
|
||||
type: array?
|
||||
items: string
|
||||
records:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
key: json?
|
||||
keyEncoded: json?
|
||||
value: json?
|
||||
valueEncoded: json?
|
||||
indexes:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name: string
|
||||
keyPath: string?
|
||||
keyPathArray:
|
||||
type: array?
|
||||
items: string
|
||||
multiEntry: boolean
|
||||
unique: boolean
|
||||
|
||||
SetOriginStorage:
|
||||
type: object
|
||||
properties:
|
||||
origin: string
|
||||
localStorage:
|
||||
type: array
|
||||
items: NameValue
|
||||
indexedDB:
|
||||
type: array?
|
||||
items: IndexedDBDatabase
|
||||
|
||||
OriginStorage:
|
||||
type: object
|
||||
|
|
@ -230,7 +278,9 @@ OriginStorage:
|
|||
localStorage:
|
||||
type: array
|
||||
items: NameValue
|
||||
|
||||
indexedDB:
|
||||
type: array
|
||||
items: IndexedDBDatabase
|
||||
|
||||
SerializedError:
|
||||
type: object
|
||||
|
|
@ -508,6 +558,12 @@ ContextOptions:
|
|||
- accept
|
||||
- deny
|
||||
- internal-browser-default
|
||||
contrast:
|
||||
type: enum?
|
||||
literals:
|
||||
- no-preference
|
||||
- more
|
||||
- no-override
|
||||
baseURL: string?
|
||||
recordVideo:
|
||||
type: object?
|
||||
|
|
@ -572,7 +628,7 @@ NewRequestParameters:
|
|||
items: NetworkCookie
|
||||
origins:
|
||||
type: array?
|
||||
items: OriginStorage
|
||||
items: SetOriginStorage
|
||||
tracesDir: string?
|
||||
|
||||
|
||||
|
|
@ -1037,7 +1093,7 @@ Browser:
|
|||
items: SetNetworkCookie
|
||||
origins:
|
||||
type: array?
|
||||
items: OriginStorage
|
||||
items: SetOriginStorage
|
||||
mockingProxyBaseURL: string?
|
||||
returns:
|
||||
context: BrowserContext
|
||||
|
|
@ -1060,7 +1116,7 @@ Browser:
|
|||
items: SetNetworkCookie
|
||||
origins:
|
||||
type: array?
|
||||
items: OriginStorage
|
||||
items: SetOriginStorage
|
||||
returns:
|
||||
context: BrowserContext
|
||||
|
||||
|
|
@ -1471,6 +1527,12 @@ Page:
|
|||
- active
|
||||
- none
|
||||
- no-override
|
||||
contrast:
|
||||
type: enum?
|
||||
literals:
|
||||
- no-preference
|
||||
- more
|
||||
- no-override
|
||||
flags:
|
||||
snapshot: true
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export class TraceModel {
|
|||
const ordinals: string[] = [];
|
||||
let hasSource = false;
|
||||
for (const entryName of await this._backend.entryNames()) {
|
||||
const match = entryName.match(/(.+)\.trace/);
|
||||
const match = entryName.match(/(.+)\.trace$/);
|
||||
if (match)
|
||||
ordinals.push(match[1] || '');
|
||||
if (entryName.includes('src@'))
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ export class TraceModernizer {
|
|||
existing!.result = event.result;
|
||||
existing!.error = event.error;
|
||||
existing!.attachments = event.attachments;
|
||||
existing!.annotations = event.annotations;
|
||||
if (event.point)
|
||||
existing!.point = event.point;
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ export const renderAction = (
|
|||
|
||||
const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript');
|
||||
|
||||
const isSkipped = action.class === 'Test' && action.method === 'test.step.skip';
|
||||
const isSkipped = action.class === 'Test' && action.method === 'step' && action.annotations?.some(a => a.type === 'skip');
|
||||
let time: string = '';
|
||||
if (action.endTime)
|
||||
time = msToString(action.endTime - action.startTime);
|
||||
|
|
|
|||
|
|
@ -40,18 +40,12 @@ export const CallTab: React.FunctionComponent<{
|
|||
const startTimeMillis = action.startTime - startTimeOffset;
|
||||
const startTime = msToString(startTimeMillis);
|
||||
|
||||
const duration = action.endTime ? msToString(action.endTime - action.startTime) : 'Timed Out';
|
||||
|
||||
return (
|
||||
<div className='call-tab'>
|
||||
<div className='call-line'>{action.apiName}</div>
|
||||
{
|
||||
<>
|
||||
<div className='call-section'>Time</div>
|
||||
<DateTimeCallLine name='start:' value={startTime} />
|
||||
<DateTimeCallLine name='duration:' value={duration} />
|
||||
</>
|
||||
}
|
||||
<div className='call-section'>Time</div>
|
||||
<DateTimeCallLine name='start:' value={startTime} />
|
||||
<DateTimeCallLine name='duration:' value={renderDuration(action)} />
|
||||
{
|
||||
!!paramKeys.length && <>
|
||||
<div className='call-section'>Parameters</div>
|
||||
|
|
@ -78,6 +72,15 @@ type Property = {
|
|||
text: string;
|
||||
};
|
||||
|
||||
function renderDuration(action: ActionTraceEventInContext): string {
|
||||
if (action.endTime)
|
||||
return msToString(action.endTime - action.startTime);
|
||||
else if (!!action.error)
|
||||
return 'Timed Out';
|
||||
else
|
||||
return 'Running';
|
||||
}
|
||||
|
||||
function renderProperty(property: Property) {
|
||||
let text = property.text.replace(/\n/g, '↵');
|
||||
if (property.type === 'string')
|
||||
|
|
|
|||
|
|
@ -258,6 +258,8 @@ function mergeActionsAndUpdateTimingSameTrace(contexts: ContextEntry[]): ActionT
|
|||
existing.error = action.error;
|
||||
if (action.attachments)
|
||||
existing.attachments = action.attachments;
|
||||
if (action.annotations)
|
||||
existing.annotations = action.annotations;
|
||||
if (action.parentId)
|
||||
existing.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId;
|
||||
// For the events that are present in the test runner context, always take
|
||||
|
|
|
|||
|
|
@ -86,6 +86,11 @@ export type AfterActionTraceEventAttachment = {
|
|||
base64?: string;
|
||||
};
|
||||
|
||||
export type AfterActionTraceEventAnnotation = {
|
||||
type: string,
|
||||
description?: string
|
||||
};
|
||||
|
||||
export type AfterActionTraceEvent = {
|
||||
type: 'after',
|
||||
callId: string;
|
||||
|
|
@ -93,6 +98,7 @@ export type AfterActionTraceEvent = {
|
|||
afterSnapshot?: string;
|
||||
error?: SerializedError['error'];
|
||||
attachments?: AfterActionTraceEventAttachment[];
|
||||
annotations?: AfterActionTraceEventAnnotation[];
|
||||
result?: any;
|
||||
point?: Point;
|
||||
};
|
||||
|
|
|
|||
116
tests/assets/to-do-notifications/LICENSE
Normal file
116
tests/assets/to-do-notifications/LICENSE
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
CC0 1.0 Universal
|
||||
|
||||
Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator and
|
||||
subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for the
|
||||
purpose of contributing to a commons of creative, cultural and scientific
|
||||
works ("Commons") that the public can reliably and without fear of later
|
||||
claims of infringement build upon, modify, incorporate in other works, reuse
|
||||
and redistribute as freely as possible in any form whatsoever and for any
|
||||
purposes, including without limitation commercial purposes. These owners may
|
||||
contribute to the Commons to promote the ideal of a free culture and the
|
||||
further production of creative, cultural and scientific works, or to gain
|
||||
reputation or greater distribution for their Work in part through the use and
|
||||
efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any expectation
|
||||
of additional consideration or compensation, the person associating CC0 with a
|
||||
Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
|
||||
and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
|
||||
and publicly distribute the Work under its terms, with knowledge of his or her
|
||||
Copyright and Related Rights in the Work and the meaning and intended legal
|
||||
effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not limited
|
||||
to, the following:
|
||||
|
||||
i. the right to reproduce, adapt, distribute, perform, display, communicate,
|
||||
and translate a Work;
|
||||
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
|
||||
iii. publicity and privacy rights pertaining to a person's image or likeness
|
||||
depicted in a Work;
|
||||
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data in
|
||||
a Work;
|
||||
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation thereof,
|
||||
including any amended or successor version of such directive); and
|
||||
|
||||
vii. other similar, equivalent or corresponding rights throughout the world
|
||||
based on applicable law or treaty, and any national implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention of,
|
||||
applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
|
||||
unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
|
||||
and Related Rights and associated claims and causes of action, whether now
|
||||
known or unknown (including existing as well as future claims and causes of
|
||||
action), in the Work (i) in all territories worldwide, (ii) for the maximum
|
||||
duration provided by applicable law or treaty (including future time
|
||||
extensions), (iii) in any current or future medium and for any number of
|
||||
copies, and (iv) for any purpose whatsoever, including without limitation
|
||||
commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
|
||||
the Waiver for the benefit of each member of the public at large and to the
|
||||
detriment of Affirmer's heirs and successors, fully intending that such Waiver
|
||||
shall not be subject to revocation, rescission, cancellation, termination, or
|
||||
any other legal or equitable action to disrupt the quiet enjoyment of the Work
|
||||
by the public as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason be
|
||||
judged legally invalid or ineffective under applicable law, then the Waiver
|
||||
shall be preserved to the maximum extent permitted taking into account
|
||||
Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
|
||||
is so judged Affirmer hereby grants to each affected person a royalty-free,
|
||||
non transferable, non sublicensable, non exclusive, irrevocable and
|
||||
unconditional license to exercise Affirmer's Copyright and Related Rights in
|
||||
the Work (i) in all territories worldwide, (ii) for the maximum duration
|
||||
provided by applicable law or treaty (including future time extensions), (iii)
|
||||
in any current or future medium and for any number of copies, and (iv) for any
|
||||
purpose whatsoever, including without limitation commercial, advertising or
|
||||
promotional purposes (the "License"). The License shall be deemed effective as
|
||||
of the date CC0 was applied by Affirmer to the Work. Should any part of the
|
||||
License for any reason be judged legally invalid or ineffective under
|
||||
applicable law, such partial invalidity or ineffectiveness shall not
|
||||
invalidate the remainder of the License, and in such case Affirmer hereby
|
||||
affirms that he or she will not (i) exercise any of his or her remaining
|
||||
Copyright and Related Rights in the Work or (ii) assert any associated claims
|
||||
and causes of action with respect to the Work, in either case contrary to
|
||||
Affirmer's express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
|
||||
b. Affirmer offers the Work as-is and makes no representations or warranties
|
||||
of any kind concerning the Work, express, implied, statutory or otherwise,
|
||||
including without limitation warranties of title, merchantability, fitness
|
||||
for a particular purpose, non infringement, or the absence of latent or
|
||||
other defects, accuracy, or the present or absence of errors, whether or not
|
||||
discoverable, all to the greatest extent permissible under applicable law.
|
||||
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without limitation
|
||||
any person's Copyright and Related Rights in the Work. Further, Affirmer
|
||||
disclaims responsibility for obtaining any necessary consents, permissions
|
||||
or other rights required for any use of the Work.
|
||||
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to this
|
||||
CC0 or use of the Work.
|
||||
|
||||
For more information, please see
|
||||
<http://creativecommons.org/publicdomain/zero/1.0/>
|
||||
1
tests/assets/to-do-notifications/README.md
Normal file
1
tests/assets/to-do-notifications/README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Source: https://github.com/mdn/dom-examples/tree/main/to-do-notifications
|
||||
108
tests/assets/to-do-notifications/index.html
Normal file
108
tests/assets/to-do-notifications/index.html
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=380">
|
||||
<script src="scripts/todo.js"></script>
|
||||
<title>To-do list with Notifications</title>
|
||||
<link href="style/style.css" type="text/css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>To-do list</h1>
|
||||
|
||||
<div class="task-box">
|
||||
|
||||
<ul id="task-list">
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-box">
|
||||
<h2>Add new to-do item.</h2>
|
||||
|
||||
<form id="task-form" action="index.html">
|
||||
<div class="full-width"><label for="title">Task title:</label><input type="text" id="title" required></div>
|
||||
<div class="half-width"><label for="deadline-hours">Hours (hh):</label><input type="number" id="deadline-hours" required></div>
|
||||
<div class="half-width"><label for="deadline-minutes">Mins (mm):</label><input type="number" id="deadline-minutes" required></div>
|
||||
<div class="third-width"><label for="deadline-day">Day:</label>
|
||||
<select id="deadline-day" required>
|
||||
<option value="01">01</option>
|
||||
<option value="02">02</option>
|
||||
<option value="03">03</option>
|
||||
<option value="04">04</option>
|
||||
<option value="05">05</option>
|
||||
<option value="06">06</option>
|
||||
<option value="07">07</option>
|
||||
<option value="08">08</option>
|
||||
<option value="09">09</option>
|
||||
<option value="10">10</option>
|
||||
<option value="11">11</option>
|
||||
<option value="12">12</option>
|
||||
<option value="13">13</option>
|
||||
<option value="14">14</option>
|
||||
<option value="15">15</option>
|
||||
<option value="16">16</option>
|
||||
<option value="17">17</option>
|
||||
<option value="18">18</option>
|
||||
<option value="19">19</option>
|
||||
<option value="20">20</option>
|
||||
<option value="21">21</option>
|
||||
<option value="22">22</option>
|
||||
<option value="23">23</option>
|
||||
<option value="24">24</option>
|
||||
<option value="25">25</option>
|
||||
<option value="26">26</option>
|
||||
<option value="27">27</option>
|
||||
<option value="28">28</option>
|
||||
<option value="29">29</option>
|
||||
<option value="30">30</option>
|
||||
<option value="31">31</option>
|
||||
</select></div>
|
||||
|
||||
<div class="third-width"><label for="deadline-month">Month:</label>
|
||||
<select id="deadline-month" required>
|
||||
<option value="January">January</option>
|
||||
<option value="February">February</option>
|
||||
<option value="March">March</option>
|
||||
<option value="April">April</option>
|
||||
<option value="May">May</option>
|
||||
<option value="June">June</option>
|
||||
<option value="July">July</option>
|
||||
<option value="August">August</option>
|
||||
<option value="September">September</option>
|
||||
<option value="October">October</option>
|
||||
<option value="November">November</option>
|
||||
<option value="December">December</option>
|
||||
</select></div>
|
||||
|
||||
<div class="third-width"><label for="deadline-year">Year:</label>
|
||||
<select id="deadline-year" required>
|
||||
<option value="2025">2025</option>
|
||||
<option value="2024">2024</option>
|
||||
<option value="2023">2023</option>
|
||||
<option value="2022">2022</option>
|
||||
<option value="2021">2021</option>
|
||||
<option value="2020">2020</option>
|
||||
<option value="2019">2019</option>
|
||||
<option value="2018">2018</option>
|
||||
</select></div>
|
||||
|
||||
<div><input type="submit" id="submit" value="Add Task"></div>
|
||||
<div></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="toolbar">
|
||||
<ul id="notifications">
|
||||
|
||||
</ul>
|
||||
|
||||
<button id="enable">
|
||||
Enable notifications
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
18
tests/assets/to-do-notifications/manifest.webapp
Normal file
18
tests/assets/to-do-notifications/manifest.webapp
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"version": "0.1",
|
||||
"name": "To-do list",
|
||||
"description": "Store to-do items on your device, and be notified when the deadlines are up.",
|
||||
"launch_path": "/to-do-notifications/index.html",
|
||||
"icons": {
|
||||
"128": "/to-do-notifications/img/icon-128.png"
|
||||
},
|
||||
"developer": {
|
||||
"name": "Chris Mills",
|
||||
"url": "http://chrisdavidmills.github.io/to-do-notifications/"
|
||||
},
|
||||
"permissions": {
|
||||
"desktop-notification": {
|
||||
"description": "Needed for creating system notifications."
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue