Compare commits
31 commits
main
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aac1613b01 | ||
|
|
0e99d48e3f | ||
|
|
9f60397d24 | ||
|
|
dbc685ca98 | ||
|
|
13d80f184e | ||
|
|
159210da82 | ||
|
|
fbad9f7ff7 | ||
|
|
67313faaa7 | ||
|
|
4b7794b37a | ||
|
|
1efbedd3b3 | ||
|
|
1e258e0894 | ||
|
|
7be4ef58de | ||
|
|
7b3e590289 | ||
|
|
19fe83f9a1 | ||
|
|
99ca8c0d43 | ||
|
|
50a58f992b | ||
|
|
0213cf57bd | ||
|
|
92916b8e91 | ||
|
|
2c85ade384 | ||
|
|
b7b0f9571d | ||
|
|
9d22178533 | ||
|
|
5d6ac9622d | ||
|
|
97b76b46af | ||
|
|
7bbcc3c624 | ||
|
|
2811a1d4f5 | ||
|
|
2b8d1ce260 | ||
|
|
715eb250e7 | ||
|
|
6106ef020f | ||
|
|
09cd74f7b0 | ||
|
|
cc6eb090ec | ||
|
|
cd02be37ae |
|
|
@ -155,7 +155,7 @@ Additional locator to match.
|
||||||
- returns: <[string]>
|
- returns: <[string]>
|
||||||
|
|
||||||
Captures the aria snapshot of the given element.
|
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**
|
**Usage**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -240,6 +240,24 @@ Expected accessible description.
|
||||||
### option: LocatorAssertions.NotToHaveAccessibleDescription.timeout = %%-csharp-java-python-assertions-timeout-%%
|
### option: LocatorAssertions.NotToHaveAccessibleDescription.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||||
* since: v1.44
|
* 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
|
## async method: LocatorAssertions.NotToHaveAccessibleName
|
||||||
* since: v1.44
|
* since: v1.44
|
||||||
|
|
@ -446,7 +464,7 @@ Expected options currently selected.
|
||||||
* since: v1.49
|
* since: v1.49
|
||||||
* langs: python
|
* langs: python
|
||||||
|
|
||||||
The opposite of [`method: LocatorAssertions.toMatchAriaSnapshot#1`].
|
The opposite of [`method: LocatorAssertions.toMatchAriaSnapshot`].
|
||||||
|
|
||||||
### param: LocatorAssertions.NotToMatchAriaSnapshot.expected
|
### param: LocatorAssertions.NotToMatchAriaSnapshot.expected
|
||||||
* since: v1.49
|
* since: v1.49
|
||||||
|
|
@ -2180,7 +2198,7 @@ Expected options currently selected.
|
||||||
* since: v1.23
|
* since: v1.23
|
||||||
|
|
||||||
|
|
||||||
## async method: LocatorAssertions.toMatchAriaSnapshot#1
|
## async method: LocatorAssertions.toMatchAriaSnapshot
|
||||||
* since: v1.49
|
* since: v1.49
|
||||||
* langs:
|
* langs:
|
||||||
- alias-java: matchesAriaSnapshot
|
- alias-java: matchesAriaSnapshot
|
||||||
|
|
@ -2229,14 +2247,14 @@ assertThat(page.locator("body")).matchesAriaSnapshot("""
|
||||||
""");
|
""");
|
||||||
```
|
```
|
||||||
|
|
||||||
### param: LocatorAssertions.toMatchAriaSnapshot#1.expected
|
### param: LocatorAssertions.toMatchAriaSnapshot.expected
|
||||||
* since: v1.49
|
* since: v1.49
|
||||||
- `expected` <string>
|
- `expected` <string>
|
||||||
|
|
||||||
### option: LocatorAssertions.toMatchAriaSnapshot#1.timeout = %%-js-assertions-timeout-%%
|
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%%
|
||||||
* since: v1.49
|
* 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
|
* since: v1.49
|
||||||
|
|
||||||
## async method: LocatorAssertions.toMatchAriaSnapshot#2
|
## async method: LocatorAssertions.toMatchAriaSnapshot#2
|
||||||
|
|
@ -2245,28 +2263,13 @@ assertThat(page.locator("body")).matchesAriaSnapshot("""
|
||||||
|
|
||||||
Asserts that the target element matches the given [accessibility snapshot](../aria-snapshots.md).
|
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**
|
**Usage**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
await expect(page.locator('body')).toMatchAriaSnapshot();
|
await expect(page.locator('body')).toMatchAriaSnapshot();
|
||||||
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
|
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'body.yml' });
|
||||||
await expect(page.locator('body')).toMatchAriaSnapshot({ path: '/path/to/snapshot.yml' });
|
|
||||||
```
|
|
||||||
|
|
||||||
```python async
|
|
||||||
await expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml')
|
|
||||||
```
|
|
||||||
|
|
||||||
```python sync
|
|
||||||
expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml')
|
|
||||||
```
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(new { Path = "/path/to/snapshot.yml" });
|
|
||||||
```
|
|
||||||
|
|
||||||
```java
|
|
||||||
assertThat(page.locator("body")).matchesAriaSnapshot(new LocatorAssertions.MatchesAriaSnapshotOptions().setPath("/path/to/snapshot.yml"));
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### option: LocatorAssertions.toMatchAriaSnapshot#2.name
|
### option: LocatorAssertions.toMatchAriaSnapshot#2.name
|
||||||
|
|
@ -2274,13 +2277,8 @@ assertThat(page.locator("body")).matchesAriaSnapshot(new LocatorAssertions.Match
|
||||||
* langs: js
|
* langs: js
|
||||||
- `name` <[string]>
|
- `name` <[string]>
|
||||||
|
|
||||||
Name of the snapshot to store in the snapshot folder corresponding to this test. Generates ordinal name if not specified.
|
Name of the snapshot to store in the snapshot folder corresponding to this test.
|
||||||
|
Generates sequential names if not specified.
|
||||||
### option: LocatorAssertions.toMatchAriaSnapshot#2.path
|
|
||||||
* since: v1.50
|
|
||||||
- `path` <[string]>
|
|
||||||
|
|
||||||
Path to the YAML snapshot file.
|
|
||||||
|
|
||||||
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-js-assertions-timeout-%%
|
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-js-assertions-timeout-%%
|
||||||
* since: v1.50
|
* since: v1.50
|
||||||
|
|
|
||||||
|
|
@ -1758,7 +1758,9 @@ await Expect(Page.GetByTitle("Issues count")).toHaveText("25 issues");
|
||||||
- `type` ?<[string]>
|
- `type` ?<[string]>
|
||||||
* langs: js
|
* 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**
|
**Usage**
|
||||||
|
|
||||||
|
|
@ -1767,7 +1769,19 @@ import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './tests',
|
testDir: './tests',
|
||||||
|
|
||||||
|
// Single template for all assertions
|
||||||
snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
|
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 +1812,22 @@ test.describe('suite', () => {
|
||||||
|
|
||||||
The list of supported tokens:
|
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`
|
* Value: `foo/bar/baz`
|
||||||
* `{ext}` - snapshot extension (with dots)
|
* `{ext}` - Snapshot extension (with the leading dot).
|
||||||
* Value: `.png`
|
* Value: `.png`
|
||||||
* `{platform}` - The value of `process.platform`.
|
* `{platform}` - The value of `process.platform`.
|
||||||
* `{projectName}` - Project's file-system-sanitized name, if any.
|
* `{projectName}` - Project's file-system-sanitized name, if any.
|
||||||
* Value: `''` (empty string).
|
* 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`)
|
* Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
|
||||||
* `{testDir}` - Project's [`property: TestConfig.testDir`].
|
* `{testDir}` - Project's [`property: TestProject.testDir`].
|
||||||
* Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with config)
|
* 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**.
|
* `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
|
||||||
* Value: `page`
|
* Value: `page`
|
||||||
* `{testFileName}` - Test file name with extension.
|
* `{testFileName}` - Test file name with extension.
|
||||||
* Value: `page-click.spec.ts`
|
* 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`
|
* Value: `page/page-click.spec.ts`
|
||||||
* `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
|
* `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
|
||||||
* Value: `suite-test-should-work`
|
* Value: `suite-test-should-work`
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ structure of a page, use the [Chrome DevTools Accessibility Pane](https://develo
|
||||||
|
|
||||||
## Snapshot matching
|
## 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
|
structure of the locator scope with a predefined aria snapshot template, helping validate the page's state against
|
||||||
testing requirements.
|
testing requirements.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,43 @@ title: "Release notes"
|
||||||
toc_max_heading_level: 2
|
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
|
## Version 1.49
|
||||||
|
|
||||||
### Aria snapshots
|
### 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
|
```csharp
|
||||||
await page.GotoAsync("https://playwright.dev");
|
await page.GotoAsync("https://playwright.dev");
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,39 @@ title: "Release notes"
|
||||||
toc_max_heading_level: 2
|
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
|
## Version 1.49
|
||||||
|
|
||||||
### Aria snapshots
|
### 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
|
```java
|
||||||
page.navigate("https://playwright.dev");
|
page.navigate("https://playwright.dev");
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,75 @@ toc_max_heading_level: 2
|
||||||
|
|
||||||
import LiteYouTube from '@site/src/components/LiteYouTube';
|
import LiteYouTube from '@site/src/components/LiteYouTube';
|
||||||
|
|
||||||
|
## Version 1.50
|
||||||
|
|
||||||
|
### Test runner
|
||||||
|
|
||||||
|
* New option [`option: Test.step.timeout`] allows specifying a maximum run time for an individual test step. A timed-out step will fail the execution of the test.
|
||||||
|
|
||||||
|
```js
|
||||||
|
test('some test', async ({ page }) => {
|
||||||
|
await test.step('a step', async () => {
|
||||||
|
// This step can time out separately from the test
|
||||||
|
}, { timeout: 1000 });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
* 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 () => {
|
||||||
|
// Normal step
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step.skip('not yet ready', async () => {
|
||||||
|
// This step is skipped
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('after running step', async () => {
|
||||||
|
// This step still runs even though the previous one was skipped
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
* Expanded [`method: LocatorAssertions.toMatchAriaSnapshot#2`] to allow storing of aria snapshots in separate YAML files.
|
||||||
|
* 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).
|
||||||
|
* Option [`property: TestConfig.updateSnapshots`] added the configuration enum `changed`. `changed` updates only the snapshots that have changed, whereas `all` now updates all snapshots, regardless of whether there are any differences.
|
||||||
|
* New option [`property: TestConfig.updateSourceMethod`] defines the way source code is updated when [`property: TestConfig.updateSnapshots`] is configured. Added `overwrite` and `3-way` modes that write the changes into source code, on top of existing `patch` mode that creates a patch file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx playwright test --update-snapshots=changed --update-source-method=3way
|
||||||
|
```
|
||||||
|
|
||||||
|
* 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 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.
|
||||||
|
* Option [`property: TestConfig.updateSnapshots`] now updates all snapshots when set to `all`, rather than only the failed/changed snapshots. Use the new enum `changed` to keep the old functionality of only updating the changed snapshots.
|
||||||
|
|
||||||
|
### 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
|
## Version 1.49
|
||||||
|
|
||||||
<LiteYouTube
|
<LiteYouTube
|
||||||
|
|
@ -15,7 +84,7 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
|
||||||
|
|
||||||
### Aria snapshots
|
### 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
|
```js
|
||||||
await page.goto('https://playwright.dev');
|
await page.goto('https://playwright.dev');
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,43 @@ title: "Release notes"
|
||||||
toc_max_heading_level: 2
|
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
|
## Version 1.49
|
||||||
|
|
||||||
### Aria snapshots
|
### 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
|
```python
|
||||||
page.goto("https://playwright.dev")
|
page.goto("https://playwright.dev")
|
||||||
|
|
|
||||||
|
|
@ -1822,7 +1822,7 @@ Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout
|
||||||
* since: v1.50
|
* since: v1.50
|
||||||
- `timeout` <[float]>
|
- `timeout` <[float]>
|
||||||
|
|
||||||
Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout).
|
The maximum time, in milliseconds, allowed for the step to complete. If the step does not complete within the specified timeout, the [`method: Test.step`] method will throw a [TimeoutError]. Defaults to `0` (no timeout).
|
||||||
|
|
||||||
## method: Test.use
|
## method: Test.use
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,9 @@ export default defineConfig({
|
||||||
- `scale` ?<[ScreenshotScale]<"css"|"device">> See [`option: Page.screenshot.scale`] in [`method: Page.screenshot`]. Defaults to `"css"`.
|
- `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`].
|
- `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`.
|
- `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.
|
- `toMatchSnapshot` ?<[Object]> Configuration for the [`method: SnapshotAssertions.toMatchSnapshot#1`] method.
|
||||||
- `maxDiffPixels` ?<[int]> An acceptable amount of pixels that could be different, unset by default.
|
- `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.
|
- `maxDiffPixelRatio` ?<[float]> An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default.
|
||||||
|
|
@ -629,7 +632,7 @@ export default defineConfig({
|
||||||
- `stdout` ?<["pipe"|"ignore"]> If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`.
|
- `stdout` ?<["pipe"|"ignore"]> If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`.
|
||||||
- `stderr` ?<["pipe"|"ignore"]> Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`.
|
- `stderr` ?<["pipe"|"ignore"]> Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`.
|
||||||
- `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
|
- `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: 'SIGINT', timeout: 500 }`, the process group is sent a `SIGINT` signal, followed by `SIGKILL` if it doesn't exit within 500ms. You can also use `SIGTERM` instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't support `SIGINT` and `SIGTERM` signals, so this option is ignored.
|
- `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"]>
|
- `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.
|
- `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.
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,9 @@ export default defineConfig({
|
||||||
- `caret` ?<[ScreenshotCaret]<"hide"|"initial">> See [`option: Page.screenshot.caret`] in [`method: Page.screenshot`]. Defaults to `"hide"`.
|
- `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"`.
|
- `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`].
|
- `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.
|
- `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`.
|
- `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.
|
- `maxDiffPixels` ?<[int]> an acceptable amount of pixels that could be different, unset by default.
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,7 @@ See the [guides for CI providers](./ci.md) to deploy your tests to CI/CD.
|
||||||
## Async Fixtures
|
## Async Fixtures
|
||||||
|
|
||||||
If you want to use async fixtures, you can use the [`pytest-playwright-asyncio`](https://pypi.org/project/pytest-playwright-asyncio/) plugin.
|
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
|
```python
|
||||||
import pytest
|
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`.
|
- `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
|
## Updating screenshots
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export default defineConfig({
|
||||||
| `stdout` | If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`. |
|
| `stdout` | If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`. |
|
||||||
| `stderr` | Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`. |
|
| `stderr` | Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`. |
|
||||||
| `timeout` | How long to wait for the process to start up and be available in milliseconds. Defaults to 60000. |
|
| `timeout` | How long to wait for the process to start up and be available in milliseconds. Defaults to 60000. |
|
||||||
| `gracefulShutdown` | How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal: 'SIGINT', timeout: 500 }`, the process group is sent a `SIGINT` signal, followed by `SIGKILL` if it doesn't exit within 500ms. You can also use `SIGTERM` instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't support `SIGINT` and `SIGTERM` signals, so this option is ignored. |
|
| `gracefulShutdown` | 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`. |
|
||||||
|
|
||||||
## Adding a server timeout
|
## Adding a server timeout
|
||||||
|
|
||||||
|
|
|
||||||
71
package-lock.json
generated
71
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-internal",
|
"name": "playwright-internal",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "playwright-internal",
|
"name": "playwright-internal",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
@ -61,7 +61,7 @@
|
||||||
"react-dom": "^18.1.0",
|
"react-dom": "^18.1.0",
|
||||||
"ssim.js": "^3.5.0",
|
"ssim.js": "^3.5.0",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vite": "^5.4.6",
|
"vite": "^5.4.14",
|
||||||
"ws": "^8.17.1",
|
"ws": "^8.17.1",
|
||||||
"xml2js": "^0.5.0",
|
"xml2js": "^0.5.0",
|
||||||
"yaml": "^2.6.0"
|
"yaml": "^2.6.0"
|
||||||
|
|
@ -7027,9 +7027,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.6",
|
"version": "5.4.14",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
|
||||||
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
|
"integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
|
|
@ -7751,10 +7752,10 @@
|
||||||
"version": "0.0.0"
|
"version": "0.0.0"
|
||||||
},
|
},
|
||||||
"packages/playwright": {
|
"packages/playwright": {
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -7768,11 +7769,11 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-browser-chromium": {
|
"packages/playwright-browser-chromium": {
|
||||||
"name": "@playwright/browser-chromium",
|
"name": "@playwright/browser-chromium",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|
@ -7780,11 +7781,11 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-browser-firefox": {
|
"packages/playwright-browser-firefox": {
|
||||||
"name": "@playwright/browser-firefox",
|
"name": "@playwright/browser-firefox",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|
@ -7792,22 +7793,22 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-browser-webkit": {
|
"packages/playwright-browser-webkit": {
|
||||||
"name": "@playwright/browser-webkit",
|
"name": "@playwright/browser-webkit",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-chromium": {
|
"packages/playwright-chromium": {
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -7817,7 +7818,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-core": {
|
"packages/playwright-core": {
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright-core": "cli.js"
|
"playwright-core": "cli.js"
|
||||||
|
|
@ -7828,12 +7829,12 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-core": {
|
"packages/playwright-ct-core": {
|
||||||
"name": "@playwright/experimental-ct-core",
|
"name": "@playwright/experimental-ct-core",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.50.0-next",
|
"playwright": "1.50.1",
|
||||||
"playwright-core": "1.50.0-next",
|
"playwright-core": "1.50.1",
|
||||||
"vite": "^5.2.8"
|
"vite": "^5.4.14"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|
@ -7841,10 +7842,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-react": {
|
"packages/playwright-ct-react": {
|
||||||
"name": "@playwright/experimental-ct-react",
|
"name": "@playwright/experimental-ct-react",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
"@playwright/experimental-ct-core": "1.50.1",
|
||||||
"@vitejs/plugin-react": "^4.2.1"
|
"@vitejs/plugin-react": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -7856,10 +7857,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-react17": {
|
"packages/playwright-ct-react17": {
|
||||||
"name": "@playwright/experimental-ct-react17",
|
"name": "@playwright/experimental-ct-react17",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
"@playwright/experimental-ct-core": "1.50.1",
|
||||||
"@vitejs/plugin-react": "^4.2.1"
|
"@vitejs/plugin-react": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -7871,10 +7872,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-svelte": {
|
"packages/playwright-ct-svelte": {
|
||||||
"name": "@playwright/experimental-ct-svelte",
|
"name": "@playwright/experimental-ct-svelte",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
"@playwright/experimental-ct-core": "1.50.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -7889,10 +7890,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-vue": {
|
"packages/playwright-ct-vue": {
|
||||||
"name": "@playwright/experimental-ct-vue",
|
"name": "@playwright/experimental-ct-vue",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
"@playwright/experimental-ct-core": "1.50.1",
|
||||||
"@vitejs/plugin-vue": "^5.2.0"
|
"@vitejs/plugin-vue": "^5.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -7903,11 +7904,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-firefox": {
|
"packages/playwright-firefox": {
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -7918,10 +7919,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-test": {
|
"packages/playwright-test": {
|
||||||
"name": "@playwright/test",
|
"name": "@playwright/test",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.50.0-next"
|
"playwright": "1.50.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -7931,11 +7932,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-webkit": {
|
"packages/playwright-webkit": {
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-internal",
|
"name": "playwright-internal",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"description": "A high-level API to automate web browsers",
|
"description": "A high-level API to automate web browsers",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -100,7 +100,7 @@
|
||||||
"react-dom": "^18.1.0",
|
"react-dom": "^18.1.0",
|
||||||
"ssim.js": "^3.5.0",
|
"ssim.js": "^3.5.0",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vite": "^5.4.6",
|
"vite": "^5.4.14",
|
||||||
"ws": "^8.17.1",
|
"ws": "^8.17.1",
|
||||||
"xml2js": "^0.5.0",
|
"xml2js": "^0.5.0",
|
||||||
"yaml": "^2.6.0"
|
"yaml": "^2.6.0"
|
||||||
|
|
|
||||||
|
|
@ -60,11 +60,6 @@
|
||||||
color: var(--color-scale-orange-6);
|
color: var(--color-scale-orange-6);
|
||||||
border: 1px solid var(--color-scale-orange-4);
|
border: 1px solid var(--color-scale-orange-4);
|
||||||
}
|
}
|
||||||
.label-color-gray {
|
|
||||||
background-color: var(--color-scale-gray-0);
|
|
||||||
color: var(--color-scale-gray-6);
|
|
||||||
border: 1px solid var(--color-scale-gray-4);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(prefers-color-scheme: dark) {
|
@media(prefers-color-scheme: dark) {
|
||||||
|
|
@ -98,11 +93,6 @@
|
||||||
color: var(--color-scale-orange-2);
|
color: var(--color-scale-orange-2);
|
||||||
border: 1px solid var(--color-scale-orange-4);
|
border: 1px solid var(--color-scale-orange-4);
|
||||||
}
|
}
|
||||||
.label-color-gray {
|
|
||||||
background-color: var(--color-scale-gray-9);
|
|
||||||
color: var(--color-scale-gray-2);
|
|
||||||
border: 1px solid var(--color-scale-gray-4);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-body {
|
.attachment-body {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import { TreeItem } from './treeItem';
|
||||||
import { CopyToClipboard } from './copyToClipboard';
|
import { CopyToClipboard } from './copyToClipboard';
|
||||||
import './links.css';
|
import './links.css';
|
||||||
import { linkifyText } from '@web/renderUtils';
|
import { linkifyText } from '@web/renderUtils';
|
||||||
import { clsx } from '@web/uiUtils';
|
import { clsx, useFlash } from '@web/uiUtils';
|
||||||
|
|
||||||
export function navigate(href: string | URL) {
|
export function navigate(href: string | URL) {
|
||||||
window.history.pushState({}, '', href);
|
window.history.pushState({}, '', href);
|
||||||
|
|
@ -73,7 +73,8 @@ export const AttachmentLink: React.FunctionComponent<{
|
||||||
linkName?: string,
|
linkName?: string,
|
||||||
openInNewTab?: boolean,
|
openInNewTab?: boolean,
|
||||||
}> = ({ attachment, result, href, linkName, openInNewTab }) => {
|
}> = ({ attachment, result, href, linkName, openInNewTab }) => {
|
||||||
const isAnchored = useIsAnchored('attachment-' + result.attachments.indexOf(attachment));
|
const [flash, triggerFlash] = useFlash();
|
||||||
|
useAnchor('attachment-' + result.attachments.indexOf(attachment), triggerFlash);
|
||||||
return <TreeItem title={<span>
|
return <TreeItem title={<span>
|
||||||
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
|
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
|
||||||
{attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>}
|
{attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>}
|
||||||
|
|
@ -84,7 +85,7 @@ export const AttachmentLink: React.FunctionComponent<{
|
||||||
)}
|
)}
|
||||||
</span>} loadChildren={attachment.body ? () => {
|
</span>} loadChildren={attachment.body ? () => {
|
||||||
return [<div key={1} className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>];
|
return [<div key={1} className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>];
|
||||||
} : undefined} depth={0} style={{ lineHeight: '32px' }} selected={isAnchored}></TreeItem>;
|
} : undefined} depth={0} style={{ lineHeight: '32px' }} flash={flash}></TreeItem>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SearchParamsContext = React.createContext<URLSearchParams>(new URLSearchParams(window.location.hash.slice(1)));
|
export const SearchParamsContext = React.createContext<URLSearchParams>(new URLSearchParams(window.location.hash.slice(1)));
|
||||||
|
|
@ -118,12 +119,12 @@ const kMissingContentType = 'x-playwright/missing';
|
||||||
|
|
||||||
export type AnchorID = string | string[] | ((id: string) => boolean) | undefined;
|
export type AnchorID = string | string[] | ((id: string) => boolean) | undefined;
|
||||||
|
|
||||||
export function useAnchor(id: AnchorID, onReveal: () => void) {
|
export function useAnchor(id: AnchorID, onReveal: React.EffectCallback) {
|
||||||
const searchParams = React.useContext(SearchParamsContext);
|
const searchParams = React.useContext(SearchParamsContext);
|
||||||
const isAnchored = useIsAnchored(id);
|
const isAnchored = useIsAnchored(id);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isAnchored)
|
if (isAnchored)
|
||||||
onReveal();
|
return onReveal();
|
||||||
}, [isAnchored, onReveal, searchParams]);
|
}, [isAnchored, onReveal, searchParams]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,7 @@ const StepTreeItem: React.FC<{
|
||||||
}> = ({ test, step, result, depth }) => {
|
}> = ({ test, step, result, depth }) => {
|
||||||
return <TreeItem title={<span aria-label={step.title}>
|
return <TreeItem title={<span aria-label={step.title}>
|
||||||
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
|
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
|
||||||
|
{step.attachments.length > 0 && <a style={{ float: 'right' }} title={`reveal attachment`} href={testResultHref({ test, result, anchor: `attachment-${step.attachments[0]}` })} onClick={evt => { evt.stopPropagation(); }}>{icons.attachment()}</a>}
|
||||||
{statusIcon(step.error || step.duration === -1 ? 'failed' : (step.skipped ? 'skipped' : 'passed'))}
|
{statusIcon(step.error || step.duration === -1 ? 'failed' : (step.skipped ? 'skipped' : 'passed'))}
|
||||||
<span>{step.title}</span>
|
<span>{step.title}</span>
|
||||||
{step.count > 1 && <> ✕ <span className='test-result-counter'>{step.count}</span></>}
|
{step.count > 1 && <> ✕ <span className='test-result-counter'>{step.count}</span></>}
|
||||||
|
|
@ -183,20 +184,6 @@ const StepTreeItem: React.FC<{
|
||||||
</span>} loadChildren={step.steps.length || step.snippet ? () => {
|
</span>} loadChildren={step.steps.length || step.snippet ? () => {
|
||||||
const snippet = step.snippet ? [<TestErrorView testId='test-snippet' key='line' error={step.snippet}/>] : [];
|
const snippet = step.snippet ? [<TestErrorView testId='test-snippet' key='line' error={step.snippet}/>] : [];
|
||||||
const steps = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1} result={result} test={test} />);
|
const steps = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1} result={result} test={test} />);
|
||||||
const attachments = step.attachments.map(attachmentIndex => (
|
return snippet.concat(steps);
|
||||||
<a key={'' + attachmentIndex}
|
|
||||||
href={testResultHref({ test, result, anchor: `attachment-${attachmentIndex}` })}
|
|
||||||
style={{ paddingLeft: depth * 22 + 4, textDecoration: 'none' }}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{ margin: '8px 0 0 8px', padding: '2px 10px', cursor: 'pointer' }}
|
|
||||||
className='label label-color-gray'
|
|
||||||
title={`see "${result.attachments[attachmentIndex].name}"`}
|
|
||||||
>
|
|
||||||
{icons.attachment()}{result.attachments[attachmentIndex].name}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
));
|
|
||||||
return snippet.concat(steps, attachments);
|
|
||||||
} : undefined} depth={depth}/>;
|
} : undefined} depth={depth}/>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,14 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-item-title.selected {
|
|
||||||
text-decoration: underline var(--color-underlinenav-icon);
|
|
||||||
text-decoration-thickness: 1.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-item-body {
|
.tree-item-body {
|
||||||
min-height: 18px;
|
min-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.yellow-flash {
|
||||||
|
animation: yellowflash-bg 2s;
|
||||||
|
}
|
||||||
|
@keyframes yellowflash-bg {
|
||||||
|
from { background: var(--color-attention-subtle); }
|
||||||
|
to { background: transparent; }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,12 @@ export const TreeItem: React.FunctionComponent<{
|
||||||
onClick?: () => void,
|
onClick?: () => void,
|
||||||
expandByDefault?: boolean,
|
expandByDefault?: boolean,
|
||||||
depth: number,
|
depth: number,
|
||||||
selected?: boolean,
|
|
||||||
style?: React.CSSProperties,
|
style?: React.CSSProperties,
|
||||||
}> = ({ title, loadChildren, onClick, expandByDefault, depth, selected, style }) => {
|
flash?: boolean
|
||||||
|
}> = ({ title, loadChildren, onClick, expandByDefault, depth, style, flash }) => {
|
||||||
const [expanded, setExpanded] = React.useState(expandByDefault || false);
|
const [expanded, setExpanded] = React.useState(expandByDefault || false);
|
||||||
return <div className={'tree-item'} style={style}>
|
return <div className={clsx('tree-item', flash && 'yellow-flash')} style={style}>
|
||||||
<span className={clsx('tree-item-title', selected && 'selected')} style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
|
<span className='tree-item-title' style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
|
||||||
{loadChildren && !!expanded && icons.downArrow()}
|
{loadChildren && !!expanded && icons.downArrow()}
|
||||||
{loadChildren && !expanded && icons.rightArrow()}
|
{loadChildren && !expanded && icons.rightArrow()}
|
||||||
{!loadChildren && <span style={{ visibility: 'hidden' }}>{icons.rightArrow()}</span>}
|
{!loadChildren && <span style={{ visibility: 'hidden' }}>{icons.rightArrow()}</span>}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/browser-chromium",
|
"name": "@playwright/browser-chromium",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"description": "Playwright package that automatically installs Chromium",
|
"description": "Playwright package that automatically installs Chromium",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -27,6 +27,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/browser-firefox",
|
"name": "@playwright/browser-firefox",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"description": "Playwright package that automatically installs Firefox",
|
"description": "Playwright package that automatically installs Firefox",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -27,6 +27,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/browser-webkit",
|
"name": "@playwright/browser-webkit",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"description": "Playwright package that automatically installs WebKit",
|
"description": "Playwright package that automatically installs WebKit",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -27,6 +27,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-chromium",
|
"name": "playwright-chromium",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"description": "A high-level API to automate Chromium",
|
"description": "A high-level API to automate Chromium",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,6 +30,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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/node@17.0.24 (https://github.com/DefinitelyTyped/DefinitelyTyped)
|
||||||
- @types/yauzl@2.10.0 (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)
|
- balanced-match@1.0.2 (https://github.com/juliangruber/balanced-match)
|
||||||
- brace-expansion@1.1.11 (https://github.com/juliangruber/brace-expansion)
|
- brace-expansion@1.1.11 (https://github.com/juliangruber/brace-expansion)
|
||||||
- buffer-crc32@0.2.13 (https://github.com/brianloveswords/buffer-crc32)
|
- 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)
|
- fd-slicer@1.1.0 (https://github.com/andrewrk/node-fd-slicer)
|
||||||
- get-stream@5.2.0 (https://github.com/sindresorhus/get-stream)
|
- get-stream@5.2.0 (https://github.com/sindresorhus/get-stream)
|
||||||
- graceful-fs@4.2.10 (https://github.com/isaacs/node-graceful-fs)
|
- 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)
|
- ip-address@9.0.5 (https://github.com/beaugunderson/ip-address)
|
||||||
- is-docker@2.2.1 (https://github.com/sindresorhus/is-docker)
|
- is-docker@2.2.1 (https://github.com/sindresorhus/is-docker)
|
||||||
- is-wsl@2.2.0 (https://github.com/sindresorhus/is-wsl)
|
- 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)
|
- retry@0.12.0 (https://github.com/tim-kos/node-retry)
|
||||||
- signal-exit@3.0.7 (https://github.com/tapjs/signal-exit)
|
- signal-exit@3.0.7 (https://github.com/tapjs/signal-exit)
|
||||||
- smart-buffer@4.2.0 (https://github.com/JoshGlazebrook/smart-buffer)
|
- 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)
|
- socks@2.8.3 (https://github.com/JoshGlazebrook/socks)
|
||||||
- sprintf-js@1.1.3 (https://github.com/alexei/sprintf.js)
|
- sprintf-js@1.1.3 (https://github.com/alexei/sprintf.js)
|
||||||
- stack-utils@2.0.5 (https://github.com/tapjs/stack-utils)
|
- 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
|
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)
|
(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
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
a copy of this software and associated documentation files (the
|
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,
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
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
|
%% 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
|
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)
|
(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
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
a copy of this software and associated documentation files (the
|
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,
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
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
|
%% 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
|
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)
|
(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
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
a copy of this software and associated documentation files (the
|
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
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
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
|
%% socks@2.8.3 NOTICES AND INFORMATION BEGIN HERE
|
||||||
=========================================
|
=========================================
|
||||||
|
|
|
||||||
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",
|
"name": "utils-bundle",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
"diff": "^7.0.0",
|
"diff": "^7.0.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"graceful-fs": "4.2.10",
|
"graceful-fs": "4.2.10",
|
||||||
"https-proxy-agent": "7.0.6",
|
"https-proxy-agent": "5.0.1",
|
||||||
"jpeg-js": "0.4.4",
|
"jpeg-js": "0.4.4",
|
||||||
"mime": "^3.0.0",
|
"mime": "^3.0.0",
|
||||||
"minimatch": "^3.1.2",
|
"minimatch": "^3.1.2",
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
"proxy-from-env": "1.1.0",
|
"proxy-from-env": "1.1.0",
|
||||||
"retry": "0.12.0",
|
"retry": "0.12.0",
|
||||||
"signal-exit": "3.0.7",
|
"signal-exit": "3.0.7",
|
||||||
"socks-proxy-agent": "8.0.5",
|
"socks-proxy-agent": "6.1.1",
|
||||||
"stack-utils": "2.0.5",
|
"stack-utils": "2.0.5",
|
||||||
"ws": "8.17.1",
|
"ws": "8.17.1",
|
||||||
"yaml": "^2.6.0"
|
"yaml": "^2.6.0"
|
||||||
|
|
@ -140,12 +140,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
"version": "7.1.3",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
|
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||||
"license": "MIT",
|
"dependencies": {
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"node": ">= 6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
|
|
@ -241,16 +243,15 @@
|
||||||
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
|
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
|
||||||
},
|
},
|
||||||
"node_modules/https-proxy-agent": {
|
"node_modules/https-proxy-agent": {
|
||||||
"version": "7.0.6",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"agent-base": "^7.1.2",
|
"agent-base": "6",
|
||||||
"debug": "4"
|
"debug": "4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ip-address": {
|
"node_modules/ip-address": {
|
||||||
|
|
@ -400,17 +401,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socks-proxy-agent": {
|
"node_modules/socks-proxy-agent": {
|
||||||
"version": "8.0.5",
|
"version": "6.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz",
|
||||||
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
|
"integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"agent-base": "^7.1.2",
|
"agent-base": "^6.0.2",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.1",
|
||||||
"socks": "^2.8.3"
|
"socks": "^2.6.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sprintf-js": {
|
"node_modules/sprintf-js": {
|
||||||
|
|
@ -460,312 +460,5 @@
|
||||||
"node": ">= 14"
|
"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",
|
"diff": "^7.0.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"graceful-fs": "4.2.10",
|
"graceful-fs": "4.2.10",
|
||||||
"https-proxy-agent": "7.0.6",
|
"https-proxy-agent": "5.0.1",
|
||||||
"jpeg-js": "0.4.4",
|
"jpeg-js": "0.4.4",
|
||||||
"mime": "^3.0.0",
|
"mime": "^3.0.0",
|
||||||
"minimatch": "^3.1.2",
|
"minimatch": "^3.1.2",
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
"proxy-from-env": "1.1.0",
|
"proxy-from-env": "1.1.0",
|
||||||
"retry": "0.12.0",
|
"retry": "0.12.0",
|
||||||
"signal-exit": "3.0.7",
|
"signal-exit": "3.0.7",
|
||||||
"socks-proxy-agent": "8.0.5",
|
"socks-proxy-agent": "6.1.1",
|
||||||
"stack-utils": "2.0.5",
|
"stack-utils": "2.0.5",
|
||||||
"ws": "8.17.1",
|
"ws": "8.17.1",
|
||||||
"yaml": "^2.6.0"
|
"yaml": "^2.6.0"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-core",
|
"name": "playwright-core",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"description": "A high-level API to automate web browsers",
|
"description": "A high-level API to automate web browsers",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import http from 'http';
|
||||||
import https from 'https';
|
import https from 'https';
|
||||||
import type { Readable, TransformCallback } from 'stream';
|
import type { Readable, TransformCallback } from 'stream';
|
||||||
import { pipeline, Transform } from 'stream';
|
import { pipeline, Transform } from 'stream';
|
||||||
|
import url from 'url';
|
||||||
import zlib from 'zlib';
|
import zlib from 'zlib';
|
||||||
import type { HTTPCredentials } from '../../types/types';
|
import type { HTTPCredentials } from '../../types/types';
|
||||||
import { TimeoutSettings } from '../common/timeoutSettings';
|
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
|
// happy eyeballs don't emit lookup and connect events, so we use our custom ones
|
||||||
const happyEyeBallsTimings = timingForSocket(socket);
|
const happyEyeBallsTimings = timingForSocket(socket);
|
||||||
dnsLookupAt = happyEyeBallsTimings.dnsLookupAt;
|
dnsLookupAt = happyEyeBallsTimings.dnsLookupAt;
|
||||||
tcpConnectionAt ??= happyEyeBallsTimings.tcpConnectionAt;
|
tcpConnectionAt = happyEyeBallsTimings.tcpConnectionAt;
|
||||||
|
|
||||||
// non-happy-eyeballs sockets
|
// non-happy-eyeballs sockets
|
||||||
listeners.push(
|
listeners.push(
|
||||||
eventsHelper.addEventListener(socket, 'lookup', () => { dnsLookupAt = monotonicTime(); }),
|
eventsHelper.addEventListener(socket, 'lookup', () => { dnsLookupAt = monotonicTime(); }),
|
||||||
eventsHelper.addEventListener(socket, 'connect', () => { tcpConnectionAt ??= monotonicTime(); }),
|
eventsHelper.addEventListener(socket, 'connect', () => { tcpConnectionAt = monotonicTime(); }),
|
||||||
eventsHelper.addEventListener(socket, 'secureConnect', () => {
|
eventsHelper.addEventListener(socket, 'secureConnect', () => {
|
||||||
tlsHandshakeAt = monotonicTime();
|
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;
|
serverIPAddress = socket.remoteAddress;
|
||||||
serverPort = socket.remotePort;
|
serverPort = socket.remotePort;
|
||||||
});
|
});
|
||||||
request.on('finish', () => { requestFinishAt = monotonicTime(); });
|
request.on('finish', () => { requestFinishAt = monotonicTime(); });
|
||||||
|
|
||||||
// http proxy
|
|
||||||
request.on('proxyConnect', () => {
|
|
||||||
tcpConnectionAt ??= monotonicTime();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
progress.log(`→ ${options.method} ${url.toString()}`);
|
progress.log(`→ ${options.method} ${url.toString()}`);
|
||||||
if (options.headers) {
|
if (options.headers) {
|
||||||
for (const [name, value] of Object.entries(options.headers))
|
for (const [name, value] of Object.entries(options.headers))
|
||||||
|
|
@ -702,16 +693,17 @@ export class GlobalAPIRequestContext extends APIRequestContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createProxyAgent(proxy: types.ProxySettings) {
|
export function createProxyAgent(proxy: types.ProxySettings) {
|
||||||
const proxyURL = new URL(proxy.server);
|
const proxyOpts = url.parse(proxy.server);
|
||||||
if (proxyURL.protocol?.startsWith('socks'))
|
if (proxyOpts.protocol?.startsWith('socks')) {
|
||||||
return new SocksProxyAgent(proxyURL);
|
return new SocksProxyAgent({
|
||||||
|
host: proxyOpts.hostname,
|
||||||
|
port: proxyOpts.port || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (proxy.username)
|
if (proxy.username)
|
||||||
proxyURL.username = proxy.username;
|
proxyOpts.auth = `${proxy.username}:${proxy.password || ''}`;
|
||||||
if (proxy.password)
|
// TODO: We should use HttpProxyAgent conditional on proxyOpts.protocol instead of always using CONNECT method.
|
||||||
proxyURL.password = proxy.password;
|
return new HttpsProxyAgent(proxyOpts);
|
||||||
// TODO: We should use HttpProxyAgent conditional on proxyURL.protocol instead of always using CONNECT method.
|
|
||||||
return new HttpsProxyAgent(proxyURL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toHeadersArray(rawHeaders: string[]): types.HeadersArray {
|
function toHeadersArray(rawHeaders: string[]): types.HeadersArray {
|
||||||
|
|
|
||||||
|
|
@ -435,4 +435,6 @@ function toJugglerProxyOptions(proxy: types.ProxySettings) {
|
||||||
|
|
||||||
// Prefs for quick fixes that didn't make it to the build.
|
// Prefs for quick fixes that didn't make it to the build.
|
||||||
// Should all be moved to `playwright.cfg`.
|
// Should all be moved to `playwright.cfg`.
|
||||||
const kBandaidFirefoxUserPrefs = {};
|
const kBandaidFirefoxUserPrefs = {
|
||||||
|
'dom.fetchKeepalive.enabled': false,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@ export function generateAriaTree(rootElement: Element): AriaSnapshot {
|
||||||
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.nodeValue) {
|
if (node.nodeType === Node.TEXT_NODE && node.nodeValue) {
|
||||||
const text = 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 || '');
|
ariaNode.children.push(node.nodeValue || '');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ export class PollingRecorder implements RecorderDelegate {
|
||||||
private _recorder: Recorder;
|
private _recorder: Recorder;
|
||||||
private _embedder: Embedder;
|
private _embedder: Embedder;
|
||||||
private _pollRecorderModeTimer: number | undefined;
|
private _pollRecorderModeTimer: number | undefined;
|
||||||
|
private _lastStateJSON: string | undefined;
|
||||||
|
|
||||||
constructor(injectedScript: InjectedScript) {
|
constructor(injectedScript: InjectedScript) {
|
||||||
this._recorder = new Recorder(injectedScript);
|
this._recorder = new Recorder(injectedScript);
|
||||||
|
|
@ -42,6 +43,7 @@ export class PollingRecorder implements RecorderDelegate {
|
||||||
injectedScript.onGlobalListenersRemoved.add(() => this._recorder.installListeners());
|
injectedScript.onGlobalListenersRemoved.add(() => this._recorder.installListeners());
|
||||||
|
|
||||||
const refreshOverlay = () => {
|
const refreshOverlay = () => {
|
||||||
|
this._lastStateJSON = undefined;
|
||||||
this._pollRecorderMode().catch(e => console.log(e)); // eslint-disable-line no-console
|
this._pollRecorderMode().catch(e => console.log(e)); // eslint-disable-line no-console
|
||||||
};
|
};
|
||||||
this._embedder.__pw_refreshOverlay = refreshOverlay;
|
this._embedder.__pw_refreshOverlay = refreshOverlay;
|
||||||
|
|
@ -57,6 +59,10 @@ export class PollingRecorder implements RecorderDelegate {
|
||||||
this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod);
|
this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stringifiedState = JSON.stringify(state);
|
||||||
|
if (this._lastStateJSON !== stringifiedState) {
|
||||||
|
this._lastStateJSON = stringifiedState;
|
||||||
const win = this._recorder.document.defaultView!;
|
const win = this._recorder.document.defaultView!;
|
||||||
if (win.top !== win) {
|
if (win.top !== win) {
|
||||||
// Only show action point in the main frame, since it is relative to the page's viewport.
|
// Only show action point in the main frame, since it is relative to the page's viewport.
|
||||||
|
|
@ -64,6 +70,8 @@ export class PollingRecorder implements RecorderDelegate {
|
||||||
state.actionPoint = undefined;
|
state.actionPoint = undefined;
|
||||||
}
|
}
|
||||||
this._recorder.setUIState(state, this);
|
this._recorder.setUIState(state, this);
|
||||||
|
}
|
||||||
|
|
||||||
this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod);
|
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))
|
if (cache?.has(element))
|
||||||
return cache?.get(element) || '';
|
return cache?.get(element) || '';
|
||||||
const pseudoStyle = getElementComputedStyle(element, pseudo);
|
const pseudoStyle = getElementComputedStyle(element, pseudo);
|
||||||
const content = getPseudoContentImpl(pseudoStyle);
|
const content = getPseudoContentImpl(element, pseudoStyle);
|
||||||
if (cache)
|
if (cache)
|
||||||
cache.set(element, content);
|
cache.set(element, content);
|
||||||
return 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.
|
// Note: all browsers ignore display:none and visibility:hidden pseudos.
|
||||||
if (!pseudoStyle || pseudoStyle.display === 'none' || pseudoStyle.visibility === 'hidden')
|
if (!pseudoStyle || pseudoStyle.display === 'none' || pseudoStyle.visibility === 'hidden')
|
||||||
return '';
|
return '';
|
||||||
const content = pseudoStyle.content;
|
const content = pseudoStyle.content;
|
||||||
|
let resolvedContent: string | undefined;
|
||||||
if ((content[0] === '\'' && content[content.length - 1] === '\'') ||
|
if ((content[0] === '\'' && content[content.length - 1] === '\'') ||
|
||||||
(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 DIFFERENCE.
|
||||||
// Spec says "CSS textual content, without a space", but we account for display
|
// Spec says "CSS textual content, without a space", but we account for display
|
||||||
// to pass "name_file-label-inline-block-styles-manual.html"
|
// to pass "name_file-label-inline-block-styles-manual.html"
|
||||||
const display = pseudoStyle.display || 'inline';
|
const display = pseudoStyle.display || 'inline';
|
||||||
if (display !== 'inline')
|
if (display !== 'inline')
|
||||||
return ' ' + unquoted + ' ';
|
return ' ' + resolvedContent + ' ';
|
||||||
return unquoted;
|
return resolvedContent;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ export class RecorderCollection extends EventEmitter {
|
||||||
let generateGoto = false;
|
let generateGoto = false;
|
||||||
if (!lastAction)
|
if (!lastAction)
|
||||||
generateGoto = true;
|
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;
|
generateGoto = true;
|
||||||
else if (timestamp - lastAction.startTime > signalThreshold)
|
else if (timestamp - lastAction.startTime > signalThreshold)
|
||||||
generateGoto = true;
|
generateGoto = true;
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ class SocksProxyConnection {
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
if (this.socksProxy.proxyAgentFromOptions)
|
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
|
else
|
||||||
this.target = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port);
|
this.target = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -214,12 +214,6 @@ export class HttpServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onRequest(request: http.IncomingMessage, response: http.ServerResponse) {
|
private _onRequest(request: http.IncomingMessage, response: http.ServerResponse) {
|
||||||
response.setHeader('Access-Control-Allow-Origin', '*');
|
|
||||||
response.setHeader('Access-Control-Request-Method', '*');
|
|
||||||
response.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET');
|
|
||||||
if (request.headers.origin)
|
|
||||||
response.setHeader('Access-Control-Allow-Headers', request.headers.origin);
|
|
||||||
|
|
||||||
if (request.method === 'OPTIONS') {
|
if (request.method === 'OPTIONS') {
|
||||||
response.writeHead(200);
|
response.writeHead(200);
|
||||||
response.end();
|
response.end();
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export function httpRequest(params: HTTPRequestParams, onResponse: (r: http.Inco
|
||||||
|
|
||||||
const proxyURL = getProxyForUrl(params.url);
|
const proxyURL = getProxyForUrl(params.url);
|
||||||
if (proxyURL) {
|
if (proxyURL) {
|
||||||
const parsedProxyURL = new URL(proxyURL);
|
const parsedProxyURL = url.parse(proxyURL);
|
||||||
if (params.url.startsWith('http:')) {
|
if (params.url.startsWith('http:')) {
|
||||||
options = {
|
options = {
|
||||||
path: parsedUrl.href,
|
path: parsedUrl.href,
|
||||||
|
|
|
||||||
2
packages/playwright-core/types/types.d.ts
vendored
2
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -12429,7 +12429,7 @@ export interface Locator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Captures the aria snapshot of the given element. Read more about [aria snapshots](https://playwright.dev/docs/aria-snapshots) and
|
* 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.
|
* for the corresponding assertion.
|
||||||
*
|
*
|
||||||
* **Usage**
|
* **Usage**
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/experimental-ct-core",
|
"name": "@playwright/experimental-ct-core",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"description": "Playwright Component Testing Helpers",
|
"description": "Playwright Component Testing Helpers",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -26,8 +26,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next",
|
"playwright-core": "1.50.1",
|
||||||
"vite": "^5.2.8",
|
"vite": "^5.4.14",
|
||||||
"playwright": "1.50.0-next"
|
"playwright": "1.50.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/experimental-ct-react",
|
"name": "@playwright/experimental-ct-react",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"description": "Playwright Component Testing for React",
|
"description": "Playwright Component Testing for React",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
"@playwright/experimental-ct-core": "1.50.1",
|
||||||
"@vitejs/plugin-react": "^4.2.1"
|
"@vitejs/plugin-react": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/experimental-ct-react17",
|
"name": "@playwright/experimental-ct-react17",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"description": "Playwright Component Testing for React",
|
"description": "Playwright Component Testing for React",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
"@playwright/experimental-ct-core": "1.50.1",
|
||||||
"@vitejs/plugin-react": "^4.2.1"
|
"@vitejs/plugin-react": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/experimental-ct-svelte",
|
"name": "@playwright/experimental-ct-svelte",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"description": "Playwright Component Testing for Svelte",
|
"description": "Playwright Component Testing for Svelte",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
"@playwright/experimental-ct-core": "1.50.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/experimental-ct-vue",
|
"name": "@playwright/experimental-ct-vue",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"description": "Playwright Component Testing for Vue",
|
"description": "Playwright Component Testing for Vue",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
"@playwright/experimental-ct-core": "1.50.1",
|
||||||
"@vitejs/plugin-vue": "^5.2.0"
|
"@vitejs/plugin-vue": "^5.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-firefox",
|
"name": "playwright-firefox",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"description": "A high-level API to automate Firefox",
|
"description": "A high-level API to automate Firefox",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,6 +30,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/test",
|
"name": "@playwright/test",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"description": "A high-level API to automate web browsers",
|
"description": "A high-level API to automate web browsers",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,6 +30,6 @@
|
||||||
},
|
},
|
||||||
"scripts": {},
|
"scripts": {},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.50.0-next"
|
"playwright": "1.50.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-webkit",
|
"name": "playwright-webkit",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"description": "A high-level API to automate WebKit",
|
"description": "A high-level API to automate WebKit",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,6 +30,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright",
|
"name": "playwright",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.1",
|
||||||
"description": "A high-level API to automate web browsers",
|
"description": "A high-level API to automate web browsers",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -56,7 +56,7 @@
|
||||||
},
|
},
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.1"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"fsevents": "2.3.2"
|
"fsevents": "2.3.2"
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ export class FullProjectInternal {
|
||||||
readonly fullyParallel: boolean;
|
readonly fullyParallel: boolean;
|
||||||
readonly expect: Project['expect'];
|
readonly expect: Project['expect'];
|
||||||
readonly respectGitIgnore: boolean;
|
readonly respectGitIgnore: boolean;
|
||||||
readonly snapshotPathTemplate: string;
|
readonly snapshotPathTemplate: string | undefined;
|
||||||
readonly ignoreSnapshots: boolean;
|
readonly ignoreSnapshots: boolean;
|
||||||
id = '';
|
id = '';
|
||||||
deps: FullProjectInternal[] = [];
|
deps: FullProjectInternal[] = [];
|
||||||
|
|
@ -173,8 +173,7 @@ export class FullProjectInternal {
|
||||||
constructor(configDir: string, config: Config, fullConfig: FullConfigInternal, projectConfig: Project, configCLIOverrides: ConfigCLIOverrides, packageJsonDir: string) {
|
constructor(configDir: string, config: Config, fullConfig: FullConfigInternal, projectConfig: Project, configCLIOverrides: ConfigCLIOverrides, packageJsonDir: string) {
|
||||||
this.fullConfig = fullConfig;
|
this.fullConfig = fullConfig;
|
||||||
const testDir = takeFirst(pathResolve(configDir, projectConfig.testDir), pathResolve(configDir, config.testDir), fullConfig.configDir);
|
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);
|
||||||
this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate);
|
|
||||||
|
|
||||||
this.project = {
|
this.project = {
|
||||||
grep: takeFirst(projectConfig.grep, config.grep, defaultGrep),
|
grep: takeFirst(projectConfig.grep, config.grep, defaultGrep),
|
||||||
|
|
|
||||||
|
|
@ -240,8 +240,8 @@ function validateConfig(file: string, config: Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('updateSnapshots' in config && config.updateSnapshots !== undefined) {
|
if ('updateSnapshots' in config && config.updateSnapshots !== undefined) {
|
||||||
if (typeof config.updateSnapshots !== 'string' || !['all', 'none', 'missing'].includes(config.updateSnapshots))
|
if (typeof config.updateSnapshots !== 'string' || !['all', 'changed', 'missing', 'none'].includes(config.updateSnapshots))
|
||||||
throw errorWithFile(file, `config.updateSnapshots must be one of "all", "none" or "missing"`);
|
throw errorWithFile(file, `config.updateSnapshots must be one of "all", "changed", "missing" or "none"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('workers' in config && config.workers !== undefined) {
|
if ('workers' in config && config.workers !== undefined) {
|
||||||
|
|
|
||||||
|
|
@ -270,9 +270,20 @@ export class TestTypeImpl {
|
||||||
const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box });
|
const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box });
|
||||||
return await zones.run('stepZone', step, async () => {
|
return await zones.run('stepZone', step, async () => {
|
||||||
try {
|
try {
|
||||||
const result = await raceAgainstDeadline(async () => body(), options.timeout ? monotonicTime() + options.timeout : 0);
|
let result: Awaited<ReturnType<typeof raceAgainstDeadline<T>>> | undefined = undefined;
|
||||||
|
result = await raceAgainstDeadline(async () => {
|
||||||
|
try {
|
||||||
|
return await 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.
|
||||||
|
if (result?.timedOut)
|
||||||
|
testInfo._failWithError(e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}, options.timeout ? monotonicTime() + options.timeout : 0);
|
||||||
if (result.timedOut)
|
if (result.timedOut)
|
||||||
throw new errors.TimeoutError(`Step timeout ${options.timeout}ms exceeded.`);
|
throw new errors.TimeoutError(`Step timeout of ${options.timeout}ms exceeded.`);
|
||||||
step.complete({});
|
step.complete({});
|
||||||
return result.result;
|
return result.result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -329,6 +329,8 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||||
const jestError = isJestError(e) ? e : null;
|
const jestError = isJestError(e) ? e : null;
|
||||||
const error = jestError ? new ExpectError(jestError, customMessage, stackFrames) : e;
|
const error = jestError ? new ExpectError(jestError, customMessage, stackFrames) : e;
|
||||||
if (jestError?.matcherResult.suggestedRebaseline) {
|
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 });
|
step.complete({ suggestedRebaseline: jestError?.matcherResult.suggestedRebaseline });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,13 @@ import path from 'path';
|
||||||
type ToMatchAriaSnapshotExpected = {
|
type ToMatchAriaSnapshotExpected = {
|
||||||
name?: string;
|
name?: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
|
timeout?: number;
|
||||||
} | string;
|
} | string;
|
||||||
|
|
||||||
export async function toMatchAriaSnapshot(
|
export async function toMatchAriaSnapshot(
|
||||||
this: ExpectMatcherState,
|
this: ExpectMatcherState,
|
||||||
receiver: LocatorEx,
|
receiver: LocatorEx,
|
||||||
expectedParam: ToMatchAriaSnapshotExpected,
|
expectedParam?: ToMatchAriaSnapshotExpected,
|
||||||
options: { timeout?: number } = {},
|
options: { timeout?: number } = {},
|
||||||
): Promise<MatcherResult<string | RegExp, string>> {
|
): Promise<MatcherResult<string | RegExp, string>> {
|
||||||
const matcherName = 'toMatchAriaSnapshot';
|
const matcherName = 'toMatchAriaSnapshot';
|
||||||
|
|
@ -48,6 +49,8 @@ export async function toMatchAriaSnapshot(
|
||||||
return { pass: !this.isNot, message: () => '', name: 'toMatchAriaSnapshot', expected: '' };
|
return { pass: !this.isNot, message: () => '', name: 'toMatchAriaSnapshot', expected: '' };
|
||||||
|
|
||||||
const updateSnapshots = testInfo.config.updateSnapshots;
|
const updateSnapshots = testInfo.config.updateSnapshots;
|
||||||
|
const pathTemplate = testInfo._projectInternal.expect?.toMatchAriaSnapshot?.pathTemplate;
|
||||||
|
const defaultTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{ext}';
|
||||||
|
|
||||||
const matcherOptions = {
|
const matcherOptions = {
|
||||||
isNot: this.isNot,
|
isNot: this.isNot,
|
||||||
|
|
@ -55,14 +58,14 @@ export async function toMatchAriaSnapshot(
|
||||||
};
|
};
|
||||||
|
|
||||||
let expected: string;
|
let expected: string;
|
||||||
|
let timeout: number;
|
||||||
let expectedPath: string | undefined;
|
let expectedPath: string | undefined;
|
||||||
if (isString(expectedParam)) {
|
if (isString(expectedParam)) {
|
||||||
expected = expectedParam;
|
expected = expectedParam;
|
||||||
|
timeout = options.timeout ?? this.timeout;
|
||||||
} else {
|
} else {
|
||||||
if (expectedParam?.path) {
|
if (expectedParam?.name) {
|
||||||
expectedPath = expectedParam.path;
|
expectedPath = testInfo._resolveSnapshotPath(pathTemplate, defaultTemplate, [sanitizeFilePathBeforeExtension(expectedParam.name)]);
|
||||||
} else if (expectedParam?.name) {
|
|
||||||
expectedPath = testInfo.snapshotPath(sanitizeFilePathBeforeExtension(expectedParam.name));
|
|
||||||
} else {
|
} else {
|
||||||
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
|
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
|
||||||
if (!snapshotNames) {
|
if (!snapshotNames) {
|
||||||
|
|
@ -70,9 +73,10 @@ export async function toMatchAriaSnapshot(
|
||||||
(testInfo as any)[snapshotNamesSymbol] = snapshotNames;
|
(testInfo as any)[snapshotNamesSymbol] = snapshotNames;
|
||||||
}
|
}
|
||||||
const fullTitleWithoutSpec = [...testInfo.titlePath.slice(1), ++snapshotNames.anonymousSnapshotIndex].join(' ');
|
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(() => '');
|
expected = await fs.promises.readFile(expectedPath, 'utf8').catch(() => '');
|
||||||
|
timeout = expectedParam?.timeout ?? this.timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateMissingBaseline = updateSnapshots === 'missing' && !expected;
|
const generateMissingBaseline = updateSnapshots === 'missing' && !expected;
|
||||||
|
|
@ -86,7 +90,6 @@ export async function toMatchAriaSnapshot(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeout = options.timeout ?? this.timeout;
|
|
||||||
expected = unshift(expected);
|
expected = unshift(expected);
|
||||||
const { matches: pass, received, log, timedOut } = await receiver._expect('to.match.aria', { expectedValue: expected, isNot: this.isNot, timeout });
|
const { matches: pass, received, log, timedOut } = await receiver._expect('to.match.aria', { expectedValue: expected, isNot: this.isNot, timeout });
|
||||||
const typedReceived = received as MatcherReceived | typeof kNoElementsFoundError;
|
const typedReceived = received as MatcherReceived | typeof kNoElementsFoundError;
|
||||||
|
|
@ -136,7 +139,15 @@ export async function toMatchAriaSnapshot(
|
||||||
}
|
}
|
||||||
return { pass: true, message: () => '', name: 'toMatchAriaSnapshot' };
|
return { pass: true, message: () => '', name: 'toMatchAriaSnapshot' };
|
||||||
} else {
|
} else {
|
||||||
const suggestedRebaseline = `toMatchAriaSnapshot(\`\n${escapeTemplateString(indent(typedReceived.regex, '{indent} '))}\n{indent}\`)`;
|
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 };
|
return { pass: false, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,8 @@ class SnapshotHelper {
|
||||||
outputBasePath = testInfo._getOutputPath(sanitizedName);
|
outputBasePath = testInfo._getOutputPath(sanitizedName);
|
||||||
this.attachmentBaseName = 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.legacyExpectedPath = addSuffixToFilePath(outputBasePath, '-expected');
|
||||||
this.previousPath = addSuffixToFilePath(outputBasePath, '-previous');
|
this.previousPath = addSuffixToFilePath(outputBasePath, '-previous');
|
||||||
this.actualPath = addSuffixToFilePath(outputBasePath, '-actual');
|
this.actualPath = addSuffixToFilePath(outputBasePath, '-actual');
|
||||||
|
|
|
||||||
|
|
@ -282,11 +282,11 @@ async function mergeReports(reportDir: string | undefined, opts: { [key: string]
|
||||||
function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrides {
|
function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrides {
|
||||||
const shardPair = options.shard ? options.shard.split('/').map((t: string) => parseInt(t, 10)) : undefined;
|
const shardPair = options.shard ? options.shard.split('/').map((t: string) => parseInt(t, 10)) : undefined;
|
||||||
|
|
||||||
let updateSnapshots: 'all' | 'changed' | 'missing' | 'none';
|
let updateSnapshots: 'all' | 'changed' | 'missing' | 'none' | undefined;
|
||||||
if (['all', 'changed', 'missing', 'none'].includes(options.updateSnapshots))
|
if (['all', 'changed', 'missing', 'none'].includes(options.updateSnapshots))
|
||||||
updateSnapshots = options.updateSnapshots;
|
updateSnapshots = options.updateSnapshots;
|
||||||
else
|
else
|
||||||
updateSnapshots = 'updateSnapshots' in options ? 'changed' : 'missing';
|
updateSnapshots = 'updateSnapshots' in options ? 'changed' : undefined;
|
||||||
|
|
||||||
const overrides: ConfigCLIOverrides = {
|
const overrides: ConfigCLIOverrides = {
|
||||||
forbidOnly: options.forbidOnly ? true : undefined,
|
forbidOnly: options.forbidOnly ? true : undefined,
|
||||||
|
|
@ -303,7 +303,7 @@ function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrid
|
||||||
tsconfig: options.tsconfig ? path.resolve(process.cwd(), options.tsconfig) : undefined,
|
tsconfig: options.tsconfig ? path.resolve(process.cwd(), options.tsconfig) : undefined,
|
||||||
ignoreSnapshots: options.ignoreSnapshots ? !!options.ignoreSnapshots : undefined,
|
ignoreSnapshots: options.ignoreSnapshots ? !!options.ignoreSnapshots : undefined,
|
||||||
updateSnapshots,
|
updateSnapshots,
|
||||||
updateSourceMethod: options.updateSourceMethod || 'patch',
|
updateSourceMethod: options.updateSourceMethod,
|
||||||
workers: options.workers,
|
workers: options.workers,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,8 @@ class ListReporter extends TerminalReporter {
|
||||||
if (this._needNewLine) {
|
if (this._needNewLine) {
|
||||||
this._needNewLine = false;
|
this._needNewLine = false;
|
||||||
process.stdout.write('\n');
|
process.stdout.write('\n');
|
||||||
|
++this._lastRow;
|
||||||
|
this._lastColumn = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,6 +212,7 @@ class ListReporter extends TerminalReporter {
|
||||||
process.stdout.write('\n');
|
process.stdout.write('\n');
|
||||||
}
|
}
|
||||||
++this._lastRow;
|
++this._lastRow;
|
||||||
|
this._lastColumn = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateLine(row: number, text: string, prefix: string) {
|
private _updateLine(row: number, text: string, prefix: string) {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,10 @@ export function addSuggestedRebaseline(location: Location, suggestedRebaseline:
|
||||||
suggestedRebaselines.set(location.file, { location, code: suggestedRebaseline });
|
suggestedRebaselines.set(location.file, { location, code: suggestedRebaseline });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearSuggestedRebaselines() {
|
||||||
|
suggestedRebaselines.clear();
|
||||||
|
}
|
||||||
|
|
||||||
export async function applySuggestedRebaselines(config: FullConfigInternal, reporter: InternalReporter) {
|
export async function applySuggestedRebaselines(config: FullConfigInternal, reporter: InternalReporter) {
|
||||||
if (config.config.updateSnapshots === 'none')
|
if (config.config.updateSnapshots === 'none')
|
||||||
return;
|
return;
|
||||||
|
|
@ -68,24 +72,27 @@ export async function applySuggestedRebaselines(config: FullConfigInternal, repo
|
||||||
traverse(fileNode, {
|
traverse(fileNode, {
|
||||||
CallExpression: path => {
|
CallExpression: path => {
|
||||||
const node = path.node;
|
const node = path.node;
|
||||||
if (node.arguments.length !== 1)
|
if (node.arguments.length < 1)
|
||||||
return;
|
return;
|
||||||
if (!t.isMemberExpression(node.callee))
|
if (!t.isMemberExpression(node.callee))
|
||||||
return;
|
return;
|
||||||
const argument = node.arguments[0];
|
const argument = node.arguments[0];
|
||||||
if (!t.isStringLiteral(argument) && !t.isTemplateLiteral(argument))
|
if (!t.isStringLiteral(argument) && !t.isTemplateLiteral(argument))
|
||||||
return;
|
return;
|
||||||
|
const prop = node.callee.property;
|
||||||
const matcher = node.callee.property;
|
if (!prop.loc || !argument.start || !argument.end)
|
||||||
|
return;
|
||||||
|
// Replacements are anchored by the location of the call expression.
|
||||||
|
// However, replacement text is meant to only replace the first argument.
|
||||||
for (const replacement of replacements) {
|
for (const replacement of replacements) {
|
||||||
// In Babel, rows are 1-based, columns are 0-based.
|
// In Babel, rows are 1-based, columns are 0-based.
|
||||||
if (matcher.loc!.start.line !== replacement.location.line)
|
if (prop.loc.start.line !== replacement.location.line)
|
||||||
continue;
|
continue;
|
||||||
if (matcher.loc!.start.column + 1 !== replacement.location.column)
|
if (prop.loc.start.column + 1 !== replacement.location.column)
|
||||||
continue;
|
continue;
|
||||||
const indent = lines[matcher.loc!.start.line - 1].match(/^\s*/)![0];
|
const indent = lines[prop.loc.start.line - 1].match(/^\s*/)![0];
|
||||||
const newText = replacement.code.replace(/\{indent\}/g, indent);
|
const newText = replacement.code.replace(/\{indent\}/g, indent);
|
||||||
ranges.push({ start: matcher.start!, end: node.end!, oldText: source.substring(matcher.start!, node.end!), newText });
|
ranges.push({ start: argument.start, end: argument.end, oldText: source.substring(argument.start, argument.end), newText });
|
||||||
// We can have multiple, hopefully equal, replacements for the same location,
|
// We can have multiple, hopefully equal, replacements for the same location,
|
||||||
// for example when a single test runs multiple times because of projects or retries.
|
// for example when a single test runs multiple times because of projects or retries.
|
||||||
// Do not apply multiple replacements for the same assertion.
|
// Do not apply multiple replacements for the same assertion.
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ import { detectChangedTestFiles } from './vcs';
|
||||||
import type { InternalReporter } from '../reporters/internalReporter';
|
import type { InternalReporter } from '../reporters/internalReporter';
|
||||||
import { cacheDir } from '../transform/compilationCache';
|
import { cacheDir } from '../transform/compilationCache';
|
||||||
import type { FullResult } from '../../types/testReporter';
|
import type { FullResult } from '../../types/testReporter';
|
||||||
import { applySuggestedRebaselines } from './rebase';
|
import { applySuggestedRebaselines, clearSuggestedRebaselines } from './rebase';
|
||||||
|
|
||||||
const readDirAsync = promisify(fs.readdir);
|
const readDirAsync = promisify(fs.readdir);
|
||||||
|
|
||||||
|
|
@ -284,6 +284,9 @@ export function createLoadTask(mode: 'out-of-process' | 'in-process', options: {
|
||||||
export function createApplyRebaselinesTask(): Task<TestRun> {
|
export function createApplyRebaselinesTask(): Task<TestRun> {
|
||||||
return {
|
return {
|
||||||
title: 'apply rebaselines',
|
title: 'apply rebaselines',
|
||||||
|
setup: async () => {
|
||||||
|
clearSuggestedRebaselines();
|
||||||
|
},
|
||||||
teardown: async ({ config, reporter }) => {
|
teardown: async ({ config, reporter }) => {
|
||||||
await applySuggestedRebaselines(config, reporter);
|
await applySuggestedRebaselines(config, reporter);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -322,7 +322,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
location: data.location,
|
location: data.location,
|
||||||
};
|
};
|
||||||
this._onStepBegin(payload);
|
this._onStepBegin(payload);
|
||||||
this._tracing.appendBeforeActionForStep(stepId, parentStep?.stepId, data.apiName || data.title, data.params, data.location ? [data.location] : []);
|
this._tracing.appendBeforeActionForStep(stepId, parentStep?.stepId, data.category, data.apiName || data.title, data.params, data.location ? [data.location] : []);
|
||||||
return step;
|
return step;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -421,7 +421,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
} else {
|
} else {
|
||||||
// trace viewer has no means of representing attachments outside of a step, so we create an artificial action
|
// trace viewer has no means of representing attachments outside of a step, so we create an artificial action
|
||||||
const callId = `attach@${++this._lastStepId}`;
|
const callId = `attach@${++this._lastStepId}`;
|
||||||
this._tracing.appendBeforeActionForStep(callId, this._findLastStageStep(this._steps)?.stepId, `attach "${attachment.name}"`, undefined, []);
|
this._tracing.appendBeforeActionForStep(callId, this._findLastStageStep(this._steps)?.stepId, 'attach', `attach "${attachment.name}"`, undefined, []);
|
||||||
this._tracing.appendAfterActionForStep(callId, undefined, [attachment]);
|
this._tracing.appendAfterActionForStep(callId, undefined, [attachment]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -454,14 +454,15 @@ export class TestInfoImpl implements TestInfo {
|
||||||
return sanitizeForFilePath(trimLongString(fullTitleWithoutSpec));
|
return sanitizeForFilePath(trimLongString(fullTitleWithoutSpec));
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshotPath(...pathSegments: string[]) {
|
_resolveSnapshotPath(template: string | undefined, defaultTemplate: string, pathSegments: string[]) {
|
||||||
const subPath = path.join(...pathSegments);
|
const subPath = path.join(...pathSegments);
|
||||||
const parsedSubPath = path.parse(subPath);
|
const parsedSubPath = path.parse(subPath);
|
||||||
const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile);
|
const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile);
|
||||||
const parsedRelativeTestFilePath = path.parse(relativeTestFilePath);
|
const parsedRelativeTestFilePath = path.parse(relativeTestFilePath);
|
||||||
const projectNamePathSegment = sanitizeForFilePath(this.project.name);
|
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(/\{(.)?testDir\}/g, '$1' + this.project.testDir)
|
||||||
.replace(/\{(.)?snapshotDir\}/g, '$1' + this.project.snapshotDir)
|
.replace(/\{(.)?snapshotDir\}/g, '$1' + this.project.snapshotDir)
|
||||||
.replace(/\{(.)?snapshotSuffix\}/g, this.snapshotSuffix ? '$1' + this.snapshotSuffix : '')
|
.replace(/\{(.)?snapshotSuffix\}/g, this.snapshotSuffix ? '$1' + this.snapshotSuffix : '')
|
||||||
|
|
@ -477,6 +478,11 @@ export class TestInfoImpl implements TestInfo {
|
||||||
return path.normalize(path.resolve(this._configInternal.configDir, snapshotPath));
|
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]) {
|
skip(...args: [arg?: any, description?: string]) {
|
||||||
this._modifier('skip', args);
|
this._modifier('skip', args);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -245,14 +245,14 @@ export class TestTracing {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
appendBeforeActionForStep(callId: string, parentId: string | undefined, apiName: string, params: Record<string, any> | undefined, stack: StackFrame[]) {
|
appendBeforeActionForStep(callId: string, parentId: string | undefined, category: string, apiName: string, params: Record<string, any> | undefined, stack: StackFrame[]) {
|
||||||
this._appendTraceEvent({
|
this._appendTraceEvent({
|
||||||
type: 'before',
|
type: 'before',
|
||||||
callId,
|
callId,
|
||||||
parentId,
|
parentId,
|
||||||
startTime: monotonicTime(),
|
startTime: monotonicTime(),
|
||||||
class: 'Test',
|
class: 'Test',
|
||||||
method: 'step',
|
method: category,
|
||||||
apiName,
|
apiName,
|
||||||
params: Object.fromEntries(Object.entries(params || {}).map(([name, value]) => [name, generatePreview(value)])),
|
params: Object.fromEntries(Object.entries(params || {}).map(([name, value]) => [name, generatePreview(value)])),
|
||||||
stack,
|
stack,
|
||||||
|
|
|
||||||
|
|
@ -75,16 +75,20 @@ export class WorkerMain extends ProcessRunner {
|
||||||
|
|
||||||
process.on('unhandledRejection', reason => this.unhandledError(reason));
|
process.on('unhandledRejection', reason => this.unhandledError(reason));
|
||||||
process.on('uncaughtException', error => this.unhandledError(error));
|
process.on('uncaughtException', error => this.unhandledError(error));
|
||||||
process.stdout.write = (chunk: string | Buffer) => {
|
process.stdout.write = (chunk: string | Buffer, cb?: any) => {
|
||||||
this.dispatchEvent('stdOut', stdioChunkToParams(chunk));
|
this.dispatchEvent('stdOut', stdioChunkToParams(chunk));
|
||||||
this._currentTest?._tracing.appendStdioToTrace('stdout', chunk);
|
this._currentTest?._tracing.appendStdioToTrace('stdout', chunk);
|
||||||
|
if (typeof cb === 'function')
|
||||||
|
process.nextTick(cb);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!process.env.PW_RUNNER_DEBUG) {
|
if (!process.env.PW_RUNNER_DEBUG) {
|
||||||
process.stderr.write = (chunk: string | Buffer) => {
|
process.stderr.write = (chunk: string | Buffer, cb?: any) => {
|
||||||
this.dispatchEvent('stdErr', stdioChunkToParams(chunk));
|
this.dispatchEvent('stdErr', stdioChunkToParams(chunk));
|
||||||
this._currentTest?._tracing.appendStdioToTrace('stderr', chunk);
|
this._currentTest?._tracing.appendStdioToTrace('stderr', chunk);
|
||||||
|
if (typeof cb === 'function')
|
||||||
|
process.nextTick(cb);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
130
packages/playwright/types/test.d.ts
vendored
130
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).
|
* [page.screenshot([options])](https://playwright.dev/docs/api/class-page#page-screenshot).
|
||||||
*/
|
*/
|
||||||
stylePath?: string|Array<string>;
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -404,10 +425,14 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This option configures a template controlling location of snapshots generated by
|
* 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
|
* and
|
||||||
* [expect(value).toMatchSnapshot(name[, options])](https://playwright.dev/docs/api/class-snapshotassertions#snapshot-assertions-to-match-snapshot-1).
|
* [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**
|
* **Usage**
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
|
|
@ -416,7 +441,19 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
|
||||||
*
|
*
|
||||||
* export default defineConfig({
|
* export default defineConfig({
|
||||||
* testDir: './tests',
|
* testDir: './tests',
|
||||||
|
*
|
||||||
|
* // Single template for all assertions
|
||||||
* snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
|
* 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 +484,27 @@ interface TestProject<TestArgs = {}, WorkerArgs = {}> {
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* The list of supported tokens:
|
* The list of supported tokens:
|
||||||
* - `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the
|
* - `{arg}` - Relative snapshot path **without extension**. This comes from the arguments passed to
|
||||||
* `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated
|
* `toHaveScreenshot()`, `toMatchAriaSnapshot()` or `toMatchSnapshot()`; if called without arguments, this will be
|
||||||
* snapshot name.
|
* an auto-generated snapshot name.
|
||||||
* - Value: `foo/bar/baz`
|
* - Value: `foo/bar/baz`
|
||||||
* - `{ext}` - snapshot extension (with dots)
|
* - `{ext}` - Snapshot extension (with the leading dot).
|
||||||
* - Value: `.png`
|
* - Value: `.png`
|
||||||
* - `{platform}` - The value of `process.platform`.
|
* - `{platform}` - The value of `process.platform`.
|
||||||
* - `{projectName}` - Project's file-system-sanitized name, if any.
|
* - `{projectName}` - Project's file-system-sanitized name, if any.
|
||||||
* - Value: `''` (empty string).
|
* - Value: `''` (empty string).
|
||||||
* - `{snapshotDir}` - Project's
|
* - `{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`)
|
* - Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
|
||||||
* - `{testDir}` - Project's
|
* - `{testDir}` - Project's
|
||||||
* [testConfig.testDir](https://playwright.dev/docs/api/class-testconfig#test-config-test-dir).
|
* [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir).
|
||||||
* - Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with
|
* - Value: `/home/playwright/tests` (absolute path since `testDir` is resolved relative to directory with
|
||||||
* config)
|
* config)
|
||||||
* - `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
|
* - `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
|
||||||
* - Value: `page`
|
* - Value: `page`
|
||||||
* - `{testFileName}` - Test file name with extension.
|
* - `{testFileName}` - Test file name with extension.
|
||||||
* - Value: `page-click.spec.ts`
|
* - 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`
|
* - Value: `page/page-click.spec.ts`
|
||||||
* - `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
|
* - `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
|
||||||
* - Value: `suite-test-should-work`
|
* - Value: `suite-test-should-work`
|
||||||
|
|
@ -991,6 +1028,27 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
* [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
|
* [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
|
||||||
*/
|
*/
|
||||||
threshold?: number;
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1468,10 +1526,14 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This option configures a template controlling location of snapshots generated by
|
* 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
|
* and
|
||||||
* [expect(value).toMatchSnapshot(name[, options])](https://playwright.dev/docs/api/class-snapshotassertions#snapshot-assertions-to-match-snapshot-1).
|
* [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**
|
* **Usage**
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
|
|
@ -1480,7 +1542,19 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
*
|
*
|
||||||
* export default defineConfig({
|
* export default defineConfig({
|
||||||
* testDir: './tests',
|
* testDir: './tests',
|
||||||
|
*
|
||||||
|
* // Single template for all assertions
|
||||||
* snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
|
* 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}',
|
||||||
|
* },
|
||||||
|
* },
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
|
|
@ -1511,27 +1585,27 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* The list of supported tokens:
|
* The list of supported tokens:
|
||||||
* - `{arg}` - Relative snapshot path **without extension**. These come from the arguments passed to the
|
* - `{arg}` - Relative snapshot path **without extension**. This comes from the arguments passed to
|
||||||
* `toHaveScreenshot()` and `toMatchSnapshot()` calls; if called without arguments, this will be an auto-generated
|
* `toHaveScreenshot()`, `toMatchAriaSnapshot()` or `toMatchSnapshot()`; if called without arguments, this will be
|
||||||
* snapshot name.
|
* an auto-generated snapshot name.
|
||||||
* - Value: `foo/bar/baz`
|
* - Value: `foo/bar/baz`
|
||||||
* - `{ext}` - snapshot extension (with dots)
|
* - `{ext}` - Snapshot extension (with the leading dot).
|
||||||
* - Value: `.png`
|
* - Value: `.png`
|
||||||
* - `{platform}` - The value of `process.platform`.
|
* - `{platform}` - The value of `process.platform`.
|
||||||
* - `{projectName}` - Project's file-system-sanitized name, if any.
|
* - `{projectName}` - Project's file-system-sanitized name, if any.
|
||||||
* - Value: `''` (empty string).
|
* - Value: `''` (empty string).
|
||||||
* - `{snapshotDir}` - Project's
|
* - `{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`)
|
* - Value: `/home/playwright/tests` (since `snapshotDir` is not provided in config, it defaults to `testDir`)
|
||||||
* - `{testDir}` - Project's
|
* - `{testDir}` - Project's
|
||||||
* [testConfig.testDir](https://playwright.dev/docs/api/class-testconfig#test-config-test-dir).
|
* [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir).
|
||||||
* - Value: `/home/playwright/tests` (absolute path is since `testDir` is resolved relative to directory with
|
* - Value: `/home/playwright/tests` (absolute path since `testDir` is resolved relative to directory with
|
||||||
* config)
|
* config)
|
||||||
* - `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
|
* - `{testFileDir}` - Directories in relative path from `testDir` to **test file**.
|
||||||
* - Value: `page`
|
* - Value: `page`
|
||||||
* - `{testFileName}` - Test file name with extension.
|
* - `{testFileName}` - Test file name with extension.
|
||||||
* - Value: `page-click.spec.ts`
|
* - 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`
|
* - Value: `page/page-click.spec.ts`
|
||||||
* - `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
|
* - `{testName}` - File-system-sanitized test title, including parent describes but excluding file name.
|
||||||
* - Value: `suite-test-should-work`
|
* - Value: `suite-test-should-work`
|
||||||
|
|
@ -8685,28 +8759,25 @@ interface LocatorAssertions {
|
||||||
/**
|
/**
|
||||||
* Asserts that the target element matches the given [accessibility snapshot](https://playwright.dev/docs/aria-snapshots).
|
* 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**
|
* **Usage**
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
* await expect(page.locator('body')).toMatchAriaSnapshot();
|
* await expect(page.locator('body')).toMatchAriaSnapshot();
|
||||||
* await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
|
* await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'body.yml' });
|
||||||
* await expect(page.locator('body')).toMatchAriaSnapshot({ path: '/path/to/snapshot.yml' });
|
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
toMatchAriaSnapshot(options?: {
|
toMatchAriaSnapshot(options?: {
|
||||||
/**
|
/**
|
||||||
* Name of the snapshot to store in the snapshot folder corresponding to this test. Generates ordinal name if not
|
* Name of the snapshot to store in the snapshot folder corresponding to this test. Generates sequential names if not
|
||||||
* specified.
|
* specified.
|
||||||
*/
|
*/
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Path to the YAML snapshot file.
|
|
||||||
*/
|
|
||||||
path?: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
|
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
|
||||||
*/
|
*/
|
||||||
|
|
@ -9650,9 +9721,10 @@ interface TestConfigWebServer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal:
|
* How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal:
|
||||||
* 'SIGINT', timeout: 500 }`, the process group is sent a `SIGINT` signal, followed by `SIGKILL` if it doesn't exit
|
* '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 `SIGTERM` instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't
|
* within 500ms. You can also use `SIGINT` as the signal instead. A `0` timeout means no `SIGKILL` will be sent.
|
||||||
* support `SIGINT` and `SIGTERM` signals, so this option is ignored.
|
* Windows doesn't support `SIGTERM` and `SIGINT` signals, so this option is ignored on Windows. Note that shutting
|
||||||
|
* down a Docker container requires `SIGTERM`.
|
||||||
*/
|
*/
|
||||||
gracefulShutdown?: {
|
gracefulShutdown?: {
|
||||||
signal: "SIGINT"|"SIGTERM";
|
signal: "SIGINT"|"SIGTERM";
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@
|
||||||
color: var(--vscode-editorCodeLens-foreground);
|
color: var(--vscode-editorCodeLens-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-skipped {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.action-icon {
|
.action-icon {
|
||||||
flex: none;
|
flex: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ActionTraceEvent, AfterActionTraceEventAttachment } from '@trace/trace';
|
import type { ActionTraceEvent, AfterActionTraceEventAttachment } from '@trace/trace';
|
||||||
import { msToString } from '@web/uiUtils';
|
import { clsx, msToString } from '@web/uiUtils';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import './actionList.css';
|
import './actionList.css';
|
||||||
import * as modelUtil from './modelUtil';
|
import * as modelUtil from './modelUtil';
|
||||||
|
|
@ -25,6 +25,7 @@ import { TreeView } from '@web/components/treeView';
|
||||||
import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil';
|
import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil';
|
||||||
import type { Boundaries } from './geometry';
|
import type { Boundaries } from './geometry';
|
||||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
|
import { testStatusIcon } from './testUtils';
|
||||||
|
|
||||||
export interface ActionListProps {
|
export interface ActionListProps {
|
||||||
actions: ActionTraceEventInContext[],
|
actions: ActionTraceEventInContext[],
|
||||||
|
|
@ -119,6 +120,7 @@ export const renderAction = (
|
||||||
|
|
||||||
const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript');
|
const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript');
|
||||||
|
|
||||||
|
const isSkipped = action.class === 'Test' && action.method === 'test.step.skip';
|
||||||
let time: string = '';
|
let time: string = '';
|
||||||
if (action.endTime)
|
if (action.endTime)
|
||||||
time = msToString(action.endTime - action.startTime);
|
time = msToString(action.endTime - action.startTime);
|
||||||
|
|
@ -149,9 +151,10 @@ export const renderAction = (
|
||||||
{action.method === 'goto' && action.params.url && <div className='action-url' title={action.params.url}>{action.params.url}</div>}
|
{action.method === 'goto' && action.params.url && <div className='action-url' title={action.params.url}>{action.params.url}</div>}
|
||||||
{action.class === 'APIRequestContext' && action.params.url && <div className='action-url' title={action.params.url}>{excludeOrigin(action.params.url)}</div>}
|
{action.class === 'APIRequestContext' && action.params.url && <div className='action-url' title={action.params.url}>{excludeOrigin(action.params.url)}</div>}
|
||||||
</div>
|
</div>
|
||||||
{(showDuration || showBadges || showAttachments) && <div className='spacer'></div>}
|
{(showDuration || showBadges || showAttachments || isSkipped) && <div className='spacer'></div>}
|
||||||
{showAttachments && <ToolbarButton icon='attach' title='Open Attachment' onClick={() => revealAttachment(action.attachments![0])} />}
|
{showAttachments && <ToolbarButton icon='attach' title='Open Attachment' onClick={() => revealAttachment(action.attachments![0])} />}
|
||||||
{showDuration && <div className='action-duration'>{time || <span className='codicon codicon-loading'></span>}</div>}
|
{showDuration && !isSkipped && <div className='action-duration'>{time || <span className='codicon codicon-loading'></span>}</div>}
|
||||||
|
{isSkipped && <span className={clsx('action-skipped', 'codicon', testStatusIcon('skipped'))} title='skipped'></span>}
|
||||||
{showBadges && <div className='action-icons' onClick={() => revealConsole?.()}>
|
{showBadges && <div className='action-icons' onClick={() => revealConsole?.()}>
|
||||||
{!!errors && <div className='action-icon'><span className='codicon codicon-error'></span><span className='action-icon-value'>{errors}</span></div>}
|
{!!errors && <div className='action-icon'><span className='codicon codicon-error'></span><span className='action-icon-value'>{errors}</span></div>}
|
||||||
{!!warnings && <div className='action-icon'><span className='codicon codicon-warning'></span><span className='action-icon-value'>{warnings}</span></div>}
|
{!!warnings && <div className='action-icon'><span className='codicon codicon-warning'></span><span className='action-icon-value'>{warnings}</span></div>}
|
||||||
|
|
|
||||||
|
|
@ -55,3 +55,11 @@
|
||||||
a.codicon-cloud-download:hover{
|
a.codicon-cloud-download:hover{
|
||||||
background-color: var(--vscode-list-inactiveSelectionBackground)
|
background-color: var(--vscode-list-inactiveSelectionBackground)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.yellow-flash {
|
||||||
|
animation: yellowflash-bg 2s;
|
||||||
|
}
|
||||||
|
@keyframes yellowflash-bg {
|
||||||
|
from { background: var(--vscode-peekViewEditor-matchHighlightBackground); }
|
||||||
|
to { background: transparent; }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,36 +17,38 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import './attachmentsTab.css';
|
import './attachmentsTab.css';
|
||||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||||
import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
|
import type { MultiTraceModel } from './modelUtil';
|
||||||
import { PlaceholderPanel } from './placeholderPanel';
|
import { PlaceholderPanel } from './placeholderPanel';
|
||||||
import type { AfterActionTraceEventAttachment } from '@trace/trace';
|
import type { AfterActionTraceEventAttachment } from '@trace/trace';
|
||||||
import { CodeMirrorWrapper, lineHeight } from '@web/components/codeMirrorWrapper';
|
import { CodeMirrorWrapper, lineHeight } from '@web/components/codeMirrorWrapper';
|
||||||
import { isTextualMimeType } from '@isomorphic/mimeType';
|
import { isTextualMimeType } from '@isomorphic/mimeType';
|
||||||
import { Expandable } from '@web/components/expandable';
|
import { Expandable } from '@web/components/expandable';
|
||||||
import { linkifyText } from '@web/renderUtils';
|
import { linkifyText } from '@web/renderUtils';
|
||||||
import { clsx } from '@web/uiUtils';
|
import { clsx, useFlash } from '@web/uiUtils';
|
||||||
|
|
||||||
type Attachment = AfterActionTraceEventAttachment & { traceUrl: string };
|
type Attachment = AfterActionTraceEventAttachment & { traceUrl: string };
|
||||||
|
|
||||||
type ExpandableAttachmentProps = {
|
type ExpandableAttachmentProps = {
|
||||||
attachment: Attachment;
|
attachment: Attachment;
|
||||||
reveal: boolean;
|
reveal?: any;
|
||||||
highlight: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> = ({ attachment, reveal, highlight }) => {
|
const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> = ({ attachment, reveal }) => {
|
||||||
const [expanded, setExpanded] = React.useState(false);
|
const [expanded, setExpanded] = React.useState(false);
|
||||||
const [attachmentText, setAttachmentText] = React.useState<string | null>(null);
|
const [attachmentText, setAttachmentText] = React.useState<string | null>(null);
|
||||||
const [placeholder, setPlaceholder] = React.useState<string | null>(null);
|
const [placeholder, setPlaceholder] = React.useState<string | null>(null);
|
||||||
|
const [flash, triggerFlash] = useFlash();
|
||||||
const ref = React.useRef<HTMLSpanElement>(null);
|
const ref = React.useRef<HTMLSpanElement>(null);
|
||||||
|
|
||||||
const isTextAttachment = isTextualMimeType(attachment.contentType);
|
const isTextAttachment = isTextualMimeType(attachment.contentType);
|
||||||
const hasContent = !!attachment.sha1 || !!attachment.path;
|
const hasContent = !!attachment.sha1 || !!attachment.path;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (reveal)
|
if (reveal) {
|
||||||
ref.current?.scrollIntoView({ behavior: 'smooth' });
|
ref.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
}, [reveal]);
|
return triggerFlash();
|
||||||
|
}
|
||||||
|
}, [reveal, triggerFlash]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (expanded && attachmentText === null && placeholder === null) {
|
if (expanded && attachmentText === null && placeholder === null) {
|
||||||
|
|
@ -66,14 +68,14 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
|
||||||
}, [attachmentText]);
|
}, [attachmentText]);
|
||||||
|
|
||||||
const title = <span style={{ marginLeft: 5 }} ref={ref} aria-label={attachment.name}>
|
const title = <span style={{ marginLeft: 5 }} ref={ref} aria-label={attachment.name}>
|
||||||
<span className={clsx(highlight && 'attachment-title-highlight')}>{linkifyText(attachment.name)}</span>
|
<span>{linkifyText(attachment.name)}</span>
|
||||||
{hasContent && <a style={{ marginLeft: 5 }} href={downloadURL(attachment)}>download</a>}
|
{hasContent && <a style={{ marginLeft: 5 }} href={downloadURL(attachment)}>download</a>}
|
||||||
</span>;
|
</span>;
|
||||||
|
|
||||||
if (!isTextAttachment || !hasContent)
|
if (!isTextAttachment || !hasContent)
|
||||||
return <div style={{ marginLeft: 20 }}>{title}</div>;
|
return <div style={{ marginLeft: 20 }}>{title}</div>;
|
||||||
|
|
||||||
return <>
|
return <div className={clsx(flash && 'yellow-flash')}>
|
||||||
<Expandable title={title} expanded={expanded} setExpanded={setExpanded} expandOnTitleClick={true}>
|
<Expandable title={title} expanded={expanded} setExpanded={setExpanded} expandOnTitleClick={true}>
|
||||||
{placeholder && <i>{placeholder}</i>}
|
{placeholder && <i>{placeholder}</i>}
|
||||||
</Expandable>
|
</Expandable>
|
||||||
|
|
@ -87,14 +89,13 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
|
||||||
wrapLines={false}>
|
wrapLines={false}>
|
||||||
</CodeMirrorWrapper>
|
</CodeMirrorWrapper>
|
||||||
</div>}
|
</div>}
|
||||||
</>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AttachmentsTab: React.FunctionComponent<{
|
export const AttachmentsTab: React.FunctionComponent<{
|
||||||
model: MultiTraceModel | undefined,
|
model: MultiTraceModel | undefined,
|
||||||
selectedAction: ActionTraceEventInContext | undefined,
|
revealedAttachment?: [AfterActionTraceEventAttachment, number],
|
||||||
revealedAttachment?: AfterActionTraceEventAttachment,
|
}> = ({ model, revealedAttachment }) => {
|
||||||
}> = ({ model, selectedAction, revealedAttachment }) => {
|
|
||||||
const { diffMap, screenshots, attachments } = React.useMemo(() => {
|
const { diffMap, screenshots, attachments } = React.useMemo(() => {
|
||||||
const attachments = new Set<Attachment>();
|
const attachments = new Set<Attachment>();
|
||||||
const screenshots = new Set<Attachment>();
|
const screenshots = new Set<Attachment>();
|
||||||
|
|
@ -153,8 +154,7 @@ export const AttachmentsTab: React.FunctionComponent<{
|
||||||
return <div className='attachment-item' key={attachmentKey(a, i)}>
|
return <div className='attachment-item' key={attachmentKey(a, i)}>
|
||||||
<ExpandableAttachment
|
<ExpandableAttachment
|
||||||
attachment={a}
|
attachment={a}
|
||||||
highlight={selectedAction?.attachments?.some(selected => isEqualAttachment(a, selected)) ?? false}
|
reveal={(!!revealedAttachment && isEqualAttachment(a, revealedAttachment[0])) ? revealedAttachment : undefined}
|
||||||
reveal={!!revealedAttachment && isEqualAttachment(a, revealedAttachment)}
|
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, onOpenExternally, revealSource }) => {
|
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, onOpenExternally, revealSource }) => {
|
||||||
const [selectedCallId, setSelectedCallId] = React.useState<string | undefined>(undefined);
|
const [selectedCallId, setSelectedCallId] = React.useState<string | undefined>(undefined);
|
||||||
const [revealedError, setRevealedError] = React.useState<ErrorDescription | undefined>(undefined);
|
const [revealedError, setRevealedError] = React.useState<ErrorDescription | undefined>(undefined);
|
||||||
const [revealedAttachment, setRevealedAttachment] = React.useState<AfterActionTraceEventAttachment | undefined>(undefined);
|
const [revealedAttachment, setRevealedAttachment] = React.useState<[attachment: AfterActionTraceEventAttachment, renderCounter: number] | undefined>(undefined);
|
||||||
const [highlightedCallId, setHighlightedCallId] = React.useState<string | undefined>();
|
const [highlightedCallId, setHighlightedCallId] = React.useState<string | undefined>();
|
||||||
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
|
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
|
||||||
const [highlightedConsoleMessage, setHighlightedConsoleMessage] = React.useState<ConsoleEntry | undefined>();
|
const [highlightedConsoleMessage, setHighlightedConsoleMessage] = React.useState<ConsoleEntry | undefined>();
|
||||||
|
|
@ -148,7 +148,12 @@ export const Workbench: React.FunctionComponent<{
|
||||||
|
|
||||||
const revealAttachment = React.useCallback((attachment: AfterActionTraceEventAttachment) => {
|
const revealAttachment = React.useCallback((attachment: AfterActionTraceEventAttachment) => {
|
||||||
selectPropertiesTab('attachments');
|
selectPropertiesTab('attachments');
|
||||||
setRevealedAttachment(attachment);
|
setRevealedAttachment(currentValue => {
|
||||||
|
if (!currentValue)
|
||||||
|
return [attachment, 0];
|
||||||
|
const revealCounter = currentValue[1];
|
||||||
|
return [attachment, revealCounter + 1];
|
||||||
|
});
|
||||||
}, [selectPropertiesTab]);
|
}, [selectPropertiesTab]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -238,7 +243,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
id: 'attachments',
|
id: 'attachments',
|
||||||
title: 'Attachments',
|
title: 'Attachments',
|
||||||
count: attachments.length,
|
count: attachments.length,
|
||||||
render: () => <AttachmentsTab model={model} selectedAction={selectedAction} revealedAttachment={revealedAttachment} />
|
render: () => <AttachmentsTab model={model} revealedAttachment={revealedAttachment} />
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs: TabbedPaneTabModel[] = [
|
const tabs: TabbedPaneTabModel[] = [
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { EffectCallback } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
// Recalculates the value when dependencies change.
|
// Recalculates the value when dependencies change.
|
||||||
|
|
@ -224,3 +225,26 @@ export function scrollIntoViewIfNeeded(element: Element | undefined) {
|
||||||
|
|
||||||
const kControlCodesRe = '\\u0000-\\u0020\\u007f-\\u009f';
|
const kControlCodesRe = '\\u0000-\\u0020\\u007f-\\u009f';
|
||||||
export const kWebLinkRe = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + kControlCodesRe + '"]{2,}[^\\s' + kControlCodesRe + '"\')}\\],:;.!?]', 'ug');
|
export const kWebLinkRe = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + kControlCodesRe + '"]{2,}[^\\s' + kControlCodesRe + '"\')}\\],:;.!?]', 'ug');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages flash animation state.
|
||||||
|
* Calling `trigger` will turn `flash` to true for a second, and then back to false.
|
||||||
|
* If `trigger` is called while a flash is ongoing, the ongoing flash will be cancelled and after 50ms a new flash is started.
|
||||||
|
* @returns [flash, trigger]
|
||||||
|
*/
|
||||||
|
export function useFlash(): [boolean, EffectCallback] {
|
||||||
|
const [flash, setFlash] = React.useState(false);
|
||||||
|
const trigger = React.useCallback<React.EffectCallback>(() => {
|
||||||
|
const timeouts: any[] = [];
|
||||||
|
setFlash(currentlyFlashing => {
|
||||||
|
timeouts.push(setTimeout(() => setFlash(false), 1000));
|
||||||
|
if (!currentlyFlashing)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
timeouts.push(setTimeout(() => setFlash(true), 50));
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return () => timeouts.forEach(clearTimeout);
|
||||||
|
}, [setFlash]);
|
||||||
|
return [flash, trigger];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ export async function setupSocksForwardingServer({
|
||||||
const socksProxy = new SocksProxy();
|
const socksProxy = new SocksProxy();
|
||||||
socksProxy.setPattern('*');
|
socksProxy.setPattern('*');
|
||||||
socksProxy.addListener(SocksProxy.Events.SocksRequested, async (payload: SocksSocketRequestedPayload) => {
|
socksProxy.addListener(SocksProxy.Events.SocksRequested, async (payload: SocksSocketRequestedPayload) => {
|
||||||
if (!['127.0.0.1', '0:0:0:0:0:0:0:1', 'fake-localhost-127-0-0-1.nip.io', 'localhost'].includes(payload.host) || payload.port !== allowedTargetPort) {
|
if (!['127.0.0.1', 'fake-localhost-127-0-0-1.nip.io', 'localhost'].includes(payload.host) || payload.port !== allowedTargetPort) {
|
||||||
socksProxy.sendSocketError({ uid: payload.uid, error: 'ECONNREFUSED' });
|
socksProxy.sendSocketError({ uid: payload.uid, error: 'ECONNREFUSED' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,8 @@ const test = playwrightTest.extend<ExtraFixtures>({
|
||||||
const server = createHttpServer((req: http.IncomingMessage, res: http.ServerResponse) => {
|
const server = createHttpServer((req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||||
res.end('<html><body>from-dummy-server</body></html>');
|
res.end('<html><body>from-dummy-server</body></html>');
|
||||||
});
|
});
|
||||||
await new Promise<void>(resolve => server.listen(0, resolve));
|
// Only listen on IPv4 to check that we don't try to connect to it via IPv6.
|
||||||
|
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||||
await use((server.address() as net.AddressInfo).port);
|
await use((server.address() as net.AddressInfo).port);
|
||||||
await new Promise<Error>(resolve => server.close(resolve));
|
await new Promise<Error>(resolve => server.close(resolve));
|
||||||
},
|
},
|
||||||
|
|
@ -792,9 +793,23 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
||||||
const remoteServer = await startRemoteServer(kind);
|
const remoteServer = await startRemoteServer(kind);
|
||||||
const browser = await connect(remoteServer.wsEndpoint(), { _exposeNetwork: '*' } as any, dummyServerPort);
|
const browser = await connect(remoteServer.wsEndpoint(), { _exposeNetwork: '*' } as any, dummyServerPort);
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
|
{
|
||||||
|
await page.setContent('empty');
|
||||||
await page.goto(`http://127.0.0.1:${examplePort}/foo.html`);
|
await page.goto(`http://127.0.0.1:${examplePort}/foo.html`);
|
||||||
expect(await page.content()).toContain('from-dummy-server');
|
expect(await page.content()).toContain('from-dummy-server');
|
||||||
expect(reachedOriginalTarget).toBe(false);
|
expect(reachedOriginalTarget).toBe(false);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
await page.setContent('empty');
|
||||||
|
await page.goto(`http://localhost:${examplePort}/foo.html`);
|
||||||
|
expect(await page.content()).toContain('from-dummy-server');
|
||||||
|
expect(reachedOriginalTarget).toBe(false);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const error = await page.goto(`http://[::1]:${examplePort}/foo.html`).catch(() => 'failed');
|
||||||
|
expect(error).toBe('failed');
|
||||||
|
expect(reachedOriginalTarget).toBe(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should proxy ipv6 localhost requests @smoke', async ({ startRemoteServer, server, browserName, connect, platform, ipV6ServerPort }, testInfo) => {
|
test('should proxy ipv6 localhost requests @smoke', async ({ startRemoteServer, server, browserName, connect, platform, ipV6ServerPort }, testInfo) => {
|
||||||
|
|
@ -809,15 +824,26 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
||||||
const remoteServer = await startRemoteServer(kind);
|
const remoteServer = await startRemoteServer(kind);
|
||||||
const browser = await connect(remoteServer.wsEndpoint(), { exposeNetwork: '*' }, ipV6ServerPort);
|
const browser = await connect(remoteServer.wsEndpoint(), { exposeNetwork: '*' }, ipV6ServerPort);
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
|
{
|
||||||
|
await page.setContent('empty');
|
||||||
await page.goto(`http://[::1]:${examplePort}/foo.html`);
|
await page.goto(`http://[::1]:${examplePort}/foo.html`);
|
||||||
expect(await page.content()).toContain('from-ipv6-server');
|
expect(await page.content()).toContain('from-ipv6-server');
|
||||||
const page2 = await browser.newPage();
|
|
||||||
await page2.goto(`http://localhost:${examplePort}/foo.html`);
|
|
||||||
expect(await page2.content()).toContain('from-ipv6-server');
|
|
||||||
expect(reachedOriginalTarget).toBe(false);
|
expect(reachedOriginalTarget).toBe(false);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
await page.setContent('empty');
|
||||||
|
await page.goto(`http://localhost:${examplePort}/foo.html`);
|
||||||
|
expect(await page.content()).toContain('from-ipv6-server');
|
||||||
|
expect(reachedOriginalTarget).toBe(false);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const error = await page.goto(`http://127.0.0.1:${examplePort}/foo.html`).catch(() => 'failed');
|
||||||
|
expect(error).toBe('failed');
|
||||||
|
expect(reachedOriginalTarget).toBe(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should proxy localhost requests from fetch api', async ({ startRemoteServer, server, browserName, connect, channel, platform, dummyServerPort }, workerInfo) => {
|
test('should proxy requests from fetch api', async ({ startRemoteServer, server, browserName, connect, channel, platform, dummyServerPort }, workerInfo) => {
|
||||||
test.skip(browserName === 'webkit' && platform === 'darwin', 'no localhost proxying');
|
test.skip(browserName === 'webkit' && platform === 'darwin', 'no localhost proxying');
|
||||||
|
|
||||||
let reachedOriginalTarget = false;
|
let reachedOriginalTarget = false;
|
||||||
|
|
@ -829,10 +855,54 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
||||||
const remoteServer = await startRemoteServer(kind);
|
const remoteServer = await startRemoteServer(kind);
|
||||||
const browser = await connect(remoteServer.wsEndpoint(), { exposeNetwork: '*' }, dummyServerPort);
|
const browser = await connect(remoteServer.wsEndpoint(), { exposeNetwork: '*' }, dummyServerPort);
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
|
{
|
||||||
|
const response = await page.request.get(`http://localhost:${examplePort}/foo.html`);
|
||||||
|
expect(response.status()).toBe(200);
|
||||||
|
expect(await response.text()).toContain('from-dummy-server');
|
||||||
|
expect(reachedOriginalTarget).toBe(false);
|
||||||
|
}
|
||||||
|
{
|
||||||
const response = await page.request.get(`http://127.0.0.1:${examplePort}/foo.html`);
|
const response = await page.request.get(`http://127.0.0.1:${examplePort}/foo.html`);
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
expect(await response.text()).toContain('from-dummy-server');
|
expect(await response.text()).toContain('from-dummy-server');
|
||||||
expect(reachedOriginalTarget).toBe(false);
|
expect(reachedOriginalTarget).toBe(false);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const error = await page.request.get(`http://[::1]:${examplePort}/foo.html`).catch(e => 'failed');
|
||||||
|
expect(error).toBe('failed');
|
||||||
|
expect(reachedOriginalTarget).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should proxy requests from fetch api over ipv6', async ({ startRemoteServer, server, browserName, connect, channel, platform, ipV6ServerPort }, workerInfo) => {
|
||||||
|
test.skip(browserName === 'webkit' && platform === 'darwin', 'no localhost proxying');
|
||||||
|
|
||||||
|
let reachedOriginalTarget = false;
|
||||||
|
server.setRoute('/foo.html', async (req, res) => {
|
||||||
|
reachedOriginalTarget = true;
|
||||||
|
res.end('<html><body></body></html>');
|
||||||
|
});
|
||||||
|
const examplePort = 20_000 + workerInfo.workerIndex * 3;
|
||||||
|
const remoteServer = await startRemoteServer(kind);
|
||||||
|
const browser = await connect(remoteServer.wsEndpoint(), { exposeNetwork: '*' }, ipV6ServerPort);
|
||||||
|
const page = await browser.newPage();
|
||||||
|
{
|
||||||
|
const response = await page.request.get(`http://localhost:${examplePort}/foo.html`);
|
||||||
|
expect(response.status()).toBe(200);
|
||||||
|
expect(await response.text()).toContain('from-ipv6-server');
|
||||||
|
expect(reachedOriginalTarget).toBe(false);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const response = await page.request.get(`http://[::1]:${examplePort}/foo.html`);
|
||||||
|
expect(response.status()).toBe(200);
|
||||||
|
expect(await response.text()).toContain('from-ipv6-server');
|
||||||
|
expect(reachedOriginalTarget).toBe(false);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const error = await page.request.get(`http://127.0.0.1:${examplePort}/foo.html`).catch(e => 'failed');
|
||||||
|
expect(error).toBe('failed');
|
||||||
|
expect(reachedOriginalTarget).toBe(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should proxy local.playwright requests', async ({ connect, server, dummyServerPort, startRemoteServer }, workerInfo) => {
|
test('should proxy local.playwright requests', async ({ connect, server, dummyServerPort, startRemoteServer }, workerInfo) => {
|
||||||
|
|
|
||||||
|
|
@ -390,7 +390,7 @@ test.describe('browser', () => {
|
||||||
});
|
});
|
||||||
expect(connectHosts).toEqual([]);
|
expect(connectHosts).toEqual([]);
|
||||||
await page.goto(serverURL);
|
await page.goto(serverURL);
|
||||||
const host = browserName === 'webkit' && isMac ? '0:0:0:0:0:0:0:1' : '127.0.0.1';
|
const host = browserName === 'webkit' && isMac ? 'localhost' : '127.0.0.1';
|
||||||
expect(connectHosts).toEqual([`${host}:${serverPort}`]);
|
expect(connectHosts).toEqual([`${host}:${serverPort}`]);
|
||||||
await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!');
|
await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!');
|
||||||
await page.close();
|
await page.close();
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,9 @@ import type { Log } from '../../packages/trace/src/har';
|
||||||
import { parseHar } from '../config/utils';
|
import { parseHar } from '../config/utils';
|
||||||
const { createHttp2Server } = require('../../packages/playwright-core/lib/utils');
|
const { createHttp2Server } = require('../../packages/playwright-core/lib/utils');
|
||||||
|
|
||||||
async function pageWithHar(contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>, testInfo: any, options: { outputPath?: string, proxy?: BrowserContextOptions['proxy'] } & Partial<Pick<BrowserContextOptions['recordHar'], 'content' | 'omitContent' | 'mode'>> = {}) {
|
async function pageWithHar(contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>, testInfo: any, options: { outputPath?: string } & Partial<Pick<BrowserContextOptions['recordHar'], 'content' | 'omitContent' | 'mode'>> = {}) {
|
||||||
const harPath = testInfo.outputPath(options.outputPath || 'test.har');
|
const harPath = testInfo.outputPath(options.outputPath || 'test.har');
|
||||||
const context = await contextFactory({ recordHar: { path: harPath, ...options }, ignoreHTTPSErrors: true, proxy: options.proxy });
|
const context = await contextFactory({ recordHar: { path: harPath, ...options }, ignoreHTTPSErrors: true });
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
return {
|
return {
|
||||||
page,
|
page,
|
||||||
|
|
@ -861,38 +861,6 @@ it('should respect minimal mode for API Requests', async ({ contextFactory, serv
|
||||||
expect(entry.response.bodySize).toBe(-1);
|
expect(entry.response.bodySize).toBe(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include timings when using http proxy', async ({ contextFactory, server, proxyServer }, testInfo) => {
|
|
||||||
proxyServer.forwardTo(server.PORT, { allowConnectRequests: true });
|
|
||||||
const { page, getLog } = await pageWithHar(contextFactory, testInfo, { proxy: { server: `localhost:${proxyServer.PORT}` } });
|
|
||||||
const response = await page.request.get(server.EMPTY_PAGE);
|
|
||||||
expect(proxyServer.connectHosts).toEqual([`localhost:${server.PORT}`]);
|
|
||||||
await expect(response).toBeOK();
|
|
||||||
const log = await getLog();
|
|
||||||
expect(log.entries[0].timings.connect).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include timings when using socks proxy', async ({ contextFactory, server, socksPort }, testInfo) => {
|
|
||||||
const { page, getLog } = await pageWithHar(contextFactory, testInfo, { proxy: { server: `socks5://localhost:${socksPort}` } });
|
|
||||||
const response = await page.request.get(server.EMPTY_PAGE);
|
|
||||||
expect(await response.text()).toContain('Served by the SOCKS proxy');
|
|
||||||
await expect(response).toBeOK();
|
|
||||||
const log = await getLog();
|
|
||||||
expect(log.entries[0].timings.connect).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not have connect and dns timings when socket is reused', async ({ contextFactory, server }, testInfo) => {
|
|
||||||
const { page, getLog } = await pageWithHar(contextFactory, testInfo);
|
|
||||||
await page.request.get(server.EMPTY_PAGE);
|
|
||||||
await page.request.get(server.EMPTY_PAGE);
|
|
||||||
|
|
||||||
const log = await getLog();
|
|
||||||
expect(log.entries).toHaveLength(2);
|
|
||||||
const request2 = log.entries[1];
|
|
||||||
expect.soft(request2.timings.connect).toBe(-1);
|
|
||||||
expect.soft(request2.timings.dns).toBe(-1);
|
|
||||||
expect.soft(request2.timings.blocked).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include redirects from API request', async ({ contextFactory, server }, testInfo) => {
|
it('should include redirects from API request', async ({ contextFactory, server }, testInfo) => {
|
||||||
server.setRedirect('/redirect-me', '/simple.json');
|
server.setRedirect('/redirect-me', '/simple.json');
|
||||||
const { page, getLog } = await pageWithHar(contextFactory, testInfo);
|
const { page, getLog } = await pageWithHar(contextFactory, testInfo);
|
||||||
|
|
|
||||||
|
|
@ -778,6 +778,70 @@ await page.GetByText("link").ClickAsync();`);
|
||||||
expect(page.url()).toContain('about:blank#foo');
|
expect(page.url()).toContain('about:blank#foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should attribute navigation to press/fill', async ({ openRecorder }) => {
|
||||||
|
const { page, recorder } = await openRecorder();
|
||||||
|
|
||||||
|
await recorder.setContentAndWait(`<input /><script>document.querySelector('input').addEventListener('input', () => window.location.href = 'about:blank#foo');</script>`);
|
||||||
|
|
||||||
|
const locator = await recorder.hoverOverElement('input');
|
||||||
|
expect(locator).toBe(`getByRole('textbox')`);
|
||||||
|
await recorder.trustedClick();
|
||||||
|
await expect.poll(() => page.locator('input').evaluate(e => e === document.activeElement)).toBeTruthy();
|
||||||
|
const [, sources] = await Promise.all([
|
||||||
|
page.waitForNavigation(),
|
||||||
|
recorder.waitForOutput('JavaScript', '.fill'),
|
||||||
|
recorder.trustedPress('h'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect.soft(sources.get('JavaScript')!.text).toContain(`
|
||||||
|
await page.goto('about:blank');
|
||||||
|
await page.getByRole('textbox').click();
|
||||||
|
await page.getByRole('textbox').fill('h');
|
||||||
|
|
||||||
|
// ---------------------
|
||||||
|
await context.close();`);
|
||||||
|
|
||||||
|
expect.soft(sources.get('Playwright Test')!.text).toContain(`
|
||||||
|
await page.goto('about:blank');
|
||||||
|
await page.getByRole('textbox').click();
|
||||||
|
await page.getByRole('textbox').fill('h');
|
||||||
|
});`);
|
||||||
|
|
||||||
|
expect.soft(sources.get('Java')!.text).toContain(`
|
||||||
|
page.navigate(\"about:blank\");
|
||||||
|
page.getByRole(AriaRole.TEXTBOX).click();
|
||||||
|
page.getByRole(AriaRole.TEXTBOX).fill(\"h\");
|
||||||
|
}`);
|
||||||
|
|
||||||
|
expect.soft(sources.get('Python')!.text).toContain(`
|
||||||
|
page.goto("about:blank")
|
||||||
|
page.get_by_role("textbox").click()
|
||||||
|
page.get_by_role("textbox").fill("h")
|
||||||
|
|
||||||
|
# ---------------------
|
||||||
|
context.close()`);
|
||||||
|
|
||||||
|
expect.soft(sources.get('Python Async')!.text).toContain(`
|
||||||
|
await page.goto("about:blank")
|
||||||
|
await page.get_by_role("textbox").click()
|
||||||
|
await page.get_by_role("textbox").fill("h")
|
||||||
|
|
||||||
|
# ---------------------
|
||||||
|
await context.close()`);
|
||||||
|
|
||||||
|
expect.soft(sources.get('Pytest')!.text).toContain(`
|
||||||
|
page.goto("about:blank")
|
||||||
|
page.get_by_role("textbox").click()
|
||||||
|
page.get_by_role("textbox").fill("h")`);
|
||||||
|
|
||||||
|
expect.soft(sources.get('C#')!.text).toContain(`
|
||||||
|
await page.GotoAsync("about:blank");
|
||||||
|
await page.GetByRole(AriaRole.Textbox).ClickAsync();
|
||||||
|
await page.GetByRole(AriaRole.Textbox).FillAsync("h");`);
|
||||||
|
|
||||||
|
expect(page.url()).toContain('about:blank#foo');
|
||||||
|
});
|
||||||
|
|
||||||
test('should ignore AltGraph', async ({ openRecorder, browserName }) => {
|
test('should ignore AltGraph', async ({ openRecorder, browserName }) => {
|
||||||
test.skip(browserName === 'firefox', 'The TextInputProcessor in Firefox does not work with AltGraph.');
|
test.skip(browserName === 'firefox', 'The TextInputProcessor in Firefox does not work with AltGraph.');
|
||||||
const { recorder } = await openRecorder();
|
const { recorder } = await openRecorder();
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,10 @@ export class Recorder {
|
||||||
await this.page.mouse.up(options);
|
await this.page.mouse.up(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async trustedPress(text: string) {
|
||||||
|
await this.page.keyboard.press(text);
|
||||||
|
}
|
||||||
|
|
||||||
async trustedDblclick() {
|
async trustedDblclick() {
|
||||||
await this.page.mouse.down();
|
await this.page.mouse.down();
|
||||||
await this.page.mouse.up();
|
await this.page.mouse.up();
|
||||||
|
|
|
||||||
|
|
@ -495,6 +495,21 @@ test('should not include hidden pseudo into accessible name', async ({ page }) =
|
||||||
expect.soft(await getNameAndRole(page, 'a')).toEqual({ role: 'link', name: 'hello hello' });
|
expect.soft(await getNameAndRole(page, 'a')).toEqual({ role: 'link', name: 'hello hello' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should resolve pseudo content from attr', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<style>
|
||||||
|
.stars:before {
|
||||||
|
display: block;
|
||||||
|
content: attr(data-hello);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<a href="http://example.com">
|
||||||
|
<div class="stars" data-hello="hello">world</div>
|
||||||
|
</a>
|
||||||
|
`);
|
||||||
|
expect(await getNameAndRole(page, 'a')).toEqual({ role: 'link', name: 'hello world' });
|
||||||
|
});
|
||||||
|
|
||||||
test('should ignore invalid aria-labelledby', async ({ page }) => {
|
test('should ignore invalid aria-labelledby', async ({ page }) => {
|
||||||
await page.setContent(`
|
await page.setContent(`
|
||||||
<label>
|
<label>
|
||||||
|
|
|
||||||
|
|
@ -605,3 +605,16 @@ it('should escape special yaml values', async ({ page }) => {
|
||||||
- textbox: "555"
|
- textbox: "555"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not report textarea textContent', async ({ page }) => {
|
||||||
|
await page.setContent(`<textarea>Before</textarea>`);
|
||||||
|
await checkAndMatchSnapshot(page.locator('body'), `
|
||||||
|
- textbox: Before
|
||||||
|
`);
|
||||||
|
await page.evaluate(() => {
|
||||||
|
document.querySelector('textarea').value = 'After';
|
||||||
|
});
|
||||||
|
await checkAndMatchSnapshot(page.locator('body'), `
|
||||||
|
- textbox: After
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,19 @@ it('should fire for fetches', async ({ page, server }) => {
|
||||||
expect(requests.length).toBe(2);
|
expect(requests.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should fire for fetches with keepalive: true', {
|
||||||
|
annotation: {
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/microsoft/playwright/issues/34497'
|
||||||
|
}
|
||||||
|
}, async ({ page, server, browserName }) => {
|
||||||
|
const requests = [];
|
||||||
|
page.on('request', request => requests.push(request));
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
await page.evaluate(() => fetch('/empty.html', { keepalive: true }));
|
||||||
|
expect(requests.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
it('should report requests and responses handled by service worker', async ({ page, server, isAndroid, isElectron }) => {
|
it('should report requests and responses handled by service worker', async ({ page, server, isAndroid, isElectron }) => {
|
||||||
it.fixme(isAndroid);
|
it.fixme(isAndroid);
|
||||||
it.fixme(isElectron);
|
it.fixme(isElectron);
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,7 @@ test.describe.configure({ mode: 'parallel' });
|
||||||
|
|
||||||
test('should match snapshot with name', async ({ runInlineTest }, testInfo) => {
|
test('should match snapshot with name', async ({ runInlineTest }, testInfo) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.ts': `
|
'a.spec.ts-snapshots/test.yml': `
|
||||||
export default {
|
|
||||||
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
|
|
||||||
};
|
|
||||||
`,
|
|
||||||
'__snapshots__/a.spec.ts/test.yml': `
|
|
||||||
- heading "hello world"
|
- heading "hello world"
|
||||||
`,
|
`,
|
||||||
'a.spec.ts': `
|
'a.spec.ts': `
|
||||||
|
|
@ -42,31 +37,8 @@ test('should match snapshot with name', async ({ runInlineTest }, testInfo) => {
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should match snapshot with path', async ({ runInlineTest }, testInfo) => {
|
|
||||||
const result = await runInlineTest({
|
|
||||||
'test.yml': `
|
|
||||||
- heading "hello world"
|
|
||||||
`,
|
|
||||||
'a.spec.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import path from 'path';
|
|
||||||
test('test', async ({ page }) => {
|
|
||||||
await page.setContent(\`<h1>hello world</h1>\`);
|
|
||||||
await expect(page.locator('body')).toMatchAriaSnapshot({ path: path.resolve(__dirname, 'test.yml') });
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should generate multiple missing', async ({ runInlineTest }, testInfo) => {
|
test('should generate multiple missing', async ({ runInlineTest }, testInfo) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.ts': `
|
|
||||||
export default {
|
|
||||||
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
|
|
||||||
};
|
|
||||||
`,
|
|
||||||
'a.spec.ts': `
|
'a.spec.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
test('test', async ({ page }) => {
|
test('test', async ({ page }) => {
|
||||||
|
|
@ -79,25 +51,20 @@ test('should generate multiple missing', async ({ runInlineTest }, testInfo) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(result.output).toContain(`A snapshot doesn't exist at __snapshots__${path.sep}a.spec.ts${path.sep}test-1.yml, writing actual`);
|
expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-1.yml, writing actual`);
|
||||||
expect(result.output).toContain(`A snapshot doesn't exist at __snapshots__${path.sep}a.spec.ts${path.sep}test-2.yml, writing actual`);
|
expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-2.yml, writing actual`);
|
||||||
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-1.yml'), 'utf8');
|
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-1.yml'), 'utf8');
|
||||||
expect(snapshot1).toBe('- heading "hello world" [level=1]');
|
expect(snapshot1).toBe('- heading "hello world" [level=1]');
|
||||||
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-2.yml'), 'utf8');
|
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-2.yml'), 'utf8');
|
||||||
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
|
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should rebaseline all', async ({ runInlineTest }, testInfo) => {
|
test('should rebaseline all', async ({ runInlineTest }, testInfo) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.ts': `
|
'a.spec.ts-snapshots/test-1.yml': `
|
||||||
export default {
|
|
||||||
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
|
|
||||||
};
|
|
||||||
`,
|
|
||||||
'__snapshots__/a.spec.ts/test-1.yml': `
|
|
||||||
- heading "foo"
|
- heading "foo"
|
||||||
`,
|
`,
|
||||||
'__snapshots__/a.spec.ts/test-2.yml': `
|
'a.spec.ts-snapshots/test-2.yml': `
|
||||||
- heading "bar"
|
- heading "bar"
|
||||||
`,
|
`,
|
||||||
'a.spec.ts': `
|
'a.spec.ts': `
|
||||||
|
|
@ -112,22 +79,17 @@ test('should rebaseline all', async ({ runInlineTest }, testInfo) => {
|
||||||
}, { 'update-snapshots': 'all' });
|
}, { 'update-snapshots': 'all' });
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.output).toContain(`A snapshot is generated at __snapshots__${path.sep}a.spec.ts${path.sep}test-1.yml`);
|
expect(result.output).toContain(`A snapshot is generated at a.spec.ts-snapshots${path.sep}test-1.yml`);
|
||||||
expect(result.output).toContain(`A snapshot is generated at __snapshots__${path.sep}a.spec.ts${path.sep}test-2.yml`);
|
expect(result.output).toContain(`A snapshot is generated at a.spec.ts-snapshots${path.sep}test-2.yml`);
|
||||||
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-1.yml'), 'utf8');
|
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-1.yml'), 'utf8');
|
||||||
expect(snapshot1).toBe('- heading "hello world" [level=1]');
|
expect(snapshot1).toBe('- heading "hello world" [level=1]');
|
||||||
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-2.yml'), 'utf8');
|
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-2.yml'), 'utf8');
|
||||||
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
|
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not rebaseline matching', async ({ runInlineTest }, testInfo) => {
|
test('should not rebaseline matching', async ({ runInlineTest }, testInfo) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.ts': `
|
'a.spec.ts-snapshots/test.yml': `
|
||||||
export default {
|
|
||||||
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
|
|
||||||
};
|
|
||||||
`,
|
|
||||||
'__snapshots__/a.spec.ts/test.yml': `
|
|
||||||
- heading "hello world"
|
- heading "hello world"
|
||||||
`,
|
`,
|
||||||
'a.spec.ts': `
|
'a.spec.ts': `
|
||||||
|
|
@ -140,17 +102,12 @@ test('should not rebaseline matching', async ({ runInlineTest }, testInfo) => {
|
||||||
}, { 'update-snapshots': 'changed' });
|
}, { 'update-snapshots': 'changed' });
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test.yml'), 'utf8');
|
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test.yml'), 'utf8');
|
||||||
expect(snapshot1.trim()).toBe('- heading "hello world"');
|
expect(snapshot1.trim()).toBe('- heading "hello world"');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should generate snapshot name', async ({ runInlineTest }, testInfo) => {
|
test('should generate snapshot name', async ({ runInlineTest }, testInfo) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.ts': `
|
|
||||||
export default {
|
|
||||||
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
|
|
||||||
};
|
|
||||||
`,
|
|
||||||
'a.spec.ts': `
|
'a.spec.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
test('test name', async ({ page }) => {
|
test('test name', async ({ page }) => {
|
||||||
|
|
@ -163,10 +120,110 @@ test('should generate snapshot name', async ({ runInlineTest }, testInfo) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(result.output).toContain(`A snapshot doesn't exist at __snapshots__${path.sep}a.spec.ts${path.sep}test-name-1.yml, writing actual`);
|
expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-name-1.yml, writing actual`);
|
||||||
expect(result.output).toContain(`A snapshot doesn't exist at __snapshots__${path.sep}a.spec.ts${path.sep}test-name-2.yml, writing actual`);
|
expect(result.output).toContain(`A snapshot doesn't exist at a.spec.ts-snapshots${path.sep}test-name-2.yml, writing actual`);
|
||||||
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-name-1.yml'), 'utf8');
|
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-name-1.yml'), 'utf8');
|
||||||
expect(snapshot1).toBe('- heading "hello world" [level=1]');
|
expect(snapshot1).toBe('- heading "hello world" [level=1]');
|
||||||
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-name-2.yml'), 'utf8');
|
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('a.spec.ts-snapshots/test-name-2.yml'), 'utf8');
|
||||||
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
|
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const updateSnapshots of ['all', 'changed', 'missing', 'none']) {
|
||||||
|
test(`should update snapshot with the update-snapshots=${updateSnapshots} (config)`, async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
export default {
|
||||||
|
updateSnapshots: '${updateSnapshots}',
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'a.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', async ({ page }) => {
|
||||||
|
await page.setContent(\`<h1>New content</h1>\`);
|
||||||
|
await expect(page.locator('body')).toMatchAriaSnapshot({ timeout: 1 });
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
'a.spec.ts-snapshots/test-1.yml': '- heading "Old content" [level=1]',
|
||||||
|
});
|
||||||
|
|
||||||
|
const rebase = updateSnapshots === 'all' || updateSnapshots === 'changed';
|
||||||
|
expect(result.exitCode).toBe(rebase ? 0 : 1);
|
||||||
|
if (rebase) {
|
||||||
|
const snapshotOutputPath = testInfo.outputPath('a.spec.ts-snapshots/test-1.yml');
|
||||||
|
expect(result.output).toContain(`A snapshot is generated at`);
|
||||||
|
const data = fs.readFileSync(snapshotOutputPath);
|
||||||
|
expect(data.toString()).toBe('- heading "New content" [level=1]');
|
||||||
|
} else {
|
||||||
|
expect(result.output).toContain(`expect.toMatchAriaSnapshot`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should respect timeout', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import path from 'path';
|
||||||
|
test('test', async ({ page }) => {
|
||||||
|
await page.setContent(\`<h1>hello world</h1>\`);
|
||||||
|
await expect(page.locator('body')).toMatchAriaSnapshot({ timeout: 1 });
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
'a.spec.ts-snapshots/test-1.yml': '- heading "new world" [level=1]',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.output).toContain(`Timed out 1ms waiting for`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should respect config.snapshotPathTemplate', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
export default {
|
||||||
|
snapshotPathTemplate: 'my-snapshots/{testFilePath}/{arg}{ext}',
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'my-snapshots/dir/a.spec.ts/test.yml': `
|
||||||
|
- heading "hello world"
|
||||||
|
`,
|
||||||
|
'dir/a.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', async ({ page }) => {
|
||||||
|
await page.setContent(\`<h1>hello world</h1>\`);
|
||||||
|
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.yml' });
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should respect config.expect.toMatchAriaSnapshot.pathTemplate', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
export default {
|
||||||
|
snapshotPathTemplate: 'my-snapshots/{testFilePath}/{arg}{ext}',
|
||||||
|
expect: {
|
||||||
|
toMatchAriaSnapshot: {
|
||||||
|
pathTemplate: 'actual-snapshots/{testFilePath}/{arg}{ext}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'my-snapshots/dir/a.spec.ts/test.yml': `
|
||||||
|
- heading "wrong one"
|
||||||
|
`,
|
||||||
|
'actual-snapshots/dir/a.spec.ts/test.yml': `
|
||||||
|
- heading "hello world"
|
||||||
|
`,
|
||||||
|
'dir/a.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', async ({ page }) => {
|
||||||
|
await page.setContent(\`<h1>hello world</h1>\`);
|
||||||
|
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.yml' });
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -341,6 +341,36 @@ test('should update snapshot with the update-snapshots flag', async ({ runInline
|
||||||
expect(data.toString()).toBe(ACTUAL_SNAPSHOT);
|
expect(data.toString()).toBe(ACTUAL_SNAPSHOT);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const updateSnapshots of ['all', 'changed', 'missing', 'none']) {
|
||||||
|
test(`should update snapshot with the update-snapshots=${updateSnapshots} (config)`, async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `export default { updateSnapshots: '${updateSnapshots}' };`,
|
||||||
|
...files,
|
||||||
|
'a.spec.js-snapshots/snapshot.txt': 'Hello world',
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test, expect } = require('./helper');
|
||||||
|
test('is a test', ({}) => {
|
||||||
|
expect('Hello world updated').toMatchSnapshot('snapshot.txt');
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
const rebase = updateSnapshots === 'all' || updateSnapshots === 'changed';
|
||||||
|
expect(result.exitCode).toBe(rebase ? 0 : 1);
|
||||||
|
if (rebase) {
|
||||||
|
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.txt');
|
||||||
|
if (updateSnapshots === 'all')
|
||||||
|
expect(result.output).toContain(`${snapshotOutputPath} is not the same, writing actual.`);
|
||||||
|
if (updateSnapshots === 'changed')
|
||||||
|
expect(result.output).toContain(`${snapshotOutputPath} does not match, writing actual.`);
|
||||||
|
const data = fs.readFileSync(snapshotOutputPath);
|
||||||
|
expect(data.toString()).toBe('Hello world updated');
|
||||||
|
} else {
|
||||||
|
expect(result.output).toContain(`toMatchSnapshot`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
test('should ignore text snapshot with the ignore-snapshots flag', async ({ runInlineTest }, testInfo) => {
|
test('should ignore text snapshot with the ignore-snapshots flag', async ({ runInlineTest }, testInfo) => {
|
||||||
const EXPECTED_SNAPSHOT = 'Hello world';
|
const EXPECTED_SNAPSHOT = 'Hello world';
|
||||||
const ACTUAL_SNAPSHOT = 'Hello world updated';
|
const ACTUAL_SNAPSHOT = 'Hello world updated';
|
||||||
|
|
@ -1140,3 +1170,25 @@ test('should throw if a Promise was passed to toMatchSnapshot', async ({ runInli
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('should respect update snapshot option from config', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const EXPECTED_SNAPSHOT = 'Hello world';
|
||||||
|
const ACTUAL_SNAPSHOT = 'Hello world updated';
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...files,
|
||||||
|
'a.spec.js-snapshots/snapshot.txt': EXPECTED_SNAPSHOT,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test, expect } = require('./helper');
|
||||||
|
test('is a test', ({}) => {
|
||||||
|
expect('${ACTUAL_SNAPSHOT}').toMatchSnapshot('snapshot.txt');
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { 'update-snapshots': true });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.txt');
|
||||||
|
expect(result.output).toContain(`${snapshotOutputPath} does not match, writing actual.`);
|
||||||
|
const data = fs.readFileSync(snapshotOutputPath);
|
||||||
|
expect(data.toString()).toBe(ACTUAL_SNAPSHOT);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,7 @@ export function cleanEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||||
// END: Reserved CI
|
// END: Reserved CI
|
||||||
PW_TEST_HTML_REPORT_OPEN: undefined,
|
PW_TEST_HTML_REPORT_OPEN: undefined,
|
||||||
PLAYWRIGHT_HTML_OPEN: undefined,
|
PLAYWRIGHT_HTML_OPEN: undefined,
|
||||||
|
PW_TEST_DEBUG_REPORTERS: undefined,
|
||||||
PW_TEST_REPORTER: undefined,
|
PW_TEST_REPORTER: undefined,
|
||||||
PW_TEST_REPORTER_WS_ENDPOINT: undefined,
|
PW_TEST_REPORTER_WS_ENDPOINT: undefined,
|
||||||
PW_TEST_SOURCE_TRANSFORM: undefined,
|
PW_TEST_SOURCE_TRANSFORM: undefined,
|
||||||
|
|
|
||||||
|
|
@ -959,10 +959,9 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
await showReport();
|
await showReport();
|
||||||
await page.getByRole('link', { name: 'passing' }).click();
|
await page.getByRole('link', { name: 'passing' }).click();
|
||||||
|
|
||||||
const attachment = page.getByTestId('attachments').getByText('foo-2', { exact: true });
|
const attachment = page.getByText('foo-2', { exact: true });
|
||||||
await expect(attachment).not.toBeInViewport();
|
await expect(attachment).not.toBeInViewport();
|
||||||
await page.getByLabel('attach "foo-2"').click();
|
await page.getByLabel(`attach "foo-2"`).getByTitle('reveal attachment').click();
|
||||||
await page.getByTitle('see "foo-2"').click();
|
|
||||||
await expect(attachment).toBeInViewport();
|
await expect(attachment).toBeInViewport();
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
|
@ -989,10 +988,9 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
await showReport();
|
await showReport();
|
||||||
await page.getByRole('link', { name: 'passing' }).click();
|
await page.getByRole('link', { name: 'passing' }).click();
|
||||||
|
|
||||||
const attachment = page.getByTestId('attachments').getByText('attachment', { exact: true });
|
const attachment = page.getByText('attachment', { exact: true });
|
||||||
await expect(attachment).not.toBeInViewport();
|
await expect(attachment).not.toBeInViewport();
|
||||||
await page.getByLabel('step').click();
|
await page.getByLabel('step').getByTitle('reveal attachment').click();
|
||||||
await page.getByTitle('see "attachment"').click();
|
|
||||||
await expect(attachment).toBeInViewport();
|
await expect(attachment).toBeInViewport();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,51 @@ for (const useIntermediateMergeReport of [false, true] as const) {
|
||||||
expect(text).toContain('1) a.test.ts:3:15 › passes › outer 1.0 › inner 1.1 ──');
|
expect(text).toContain('1) a.test.ts:3:15 › passes › outer 1.0 › inner 1.1 ──');
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('print stdio', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('passes', async ({}) => {
|
||||||
|
await new Promise(resolve => process.stdout.write('line1', () => resolve()));
|
||||||
|
await new Promise(resolve => process.stdout.write('line2\\n', () => resolve()));
|
||||||
|
await new Promise(resolve => process.stderr.write(Buffer.from(''), () => resolve()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passes 2', async ({}) => {
|
||||||
|
await new Promise(resolve => process.stdout.write('partial', () => resolve()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passes 3', async ({}) => {
|
||||||
|
await new Promise(resolve => process.stdout.write('full\\n', () => resolve()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passes 4', async ({}) => {
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_FORCE_TTY: '80' });
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(4);
|
||||||
|
const expected = [
|
||||||
|
'#0 : 1 a.test.ts:3:15 › passes',
|
||||||
|
'line1line2',
|
||||||
|
`#0 : ${POSITIVE_STATUS_MARK} 1 a.test.ts:3:15 › passes`,
|
||||||
|
'',
|
||||||
|
'#3 : 2 a.test.ts:9:15 › passes 2',
|
||||||
|
`partial#3 : ${POSITIVE_STATUS_MARK} 2 a.test.ts:9:15 › passes 2`,
|
||||||
|
'',
|
||||||
|
'#5 : 3 a.test.ts:13:15 › passes 3',
|
||||||
|
'full',
|
||||||
|
`#5 : ${POSITIVE_STATUS_MARK} 3 a.test.ts:13:15 › passes 3`,
|
||||||
|
'#7 : 4 a.test.ts:17:15 › passes 4',
|
||||||
|
`#7 : ${POSITIVE_STATUS_MARK} 4 a.test.ts:17:15 › passes 4`,
|
||||||
|
];
|
||||||
|
const lines = result.output.split('\n');
|
||||||
|
const firstIndex = lines.indexOf(expected[0]);
|
||||||
|
expect(firstIndex, 'first line should be there').not.toBe(-1);
|
||||||
|
for (let i = 0; i < expected.length; ++i)
|
||||||
|
expect(lines[firstIndex + i]).toContain(expected[i]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -399,7 +399,7 @@ test('step timeout option', async ({ runInlineTest }) => {
|
||||||
}, { reporter: '', workers: 1 });
|
}, { reporter: '', workers: 1 });
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(result.failed).toBe(1);
|
expect(result.failed).toBe(1);
|
||||||
expect(result.output).toContain('Error: Step timeout 100ms exceeded.');
|
expect(result.output).toContain('Error: Step timeout of 100ms exceeded.');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('step timeout longer than test timeout', async ({ runInlineTest }) => {
|
test('step timeout longer than test timeout', async ({ runInlineTest }) => {
|
||||||
|
|
@ -422,6 +422,27 @@ test('step timeout longer than test timeout', async ({ runInlineTest }) => {
|
||||||
expect(result.output).toContain('Test timeout of 900ms exceeded.');
|
expect(result.output).toContain('Test timeout of 900ms exceeded.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('step timeout includes interrupted action errors', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('step with timeout', async ({ page }) => {
|
||||||
|
await test.step('my step', async () => {
|
||||||
|
await page.waitForTimeout(100_000);
|
||||||
|
}, { timeout: 1000 });
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { reporter: '', workers: 1 });
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
// Should include 2 errors, one for the step timeout and one for the aborted action.
|
||||||
|
expect.soft(result.output).toContain('TimeoutError: Step timeout of 1000ms exceeded.');
|
||||||
|
expect.soft(result.output).toContain(`> 4 | await test.step('my step', async () => {`);
|
||||||
|
expect.soft(result.output).toContain('Error: page.waitForTimeout: Test ended.');
|
||||||
|
expect.soft(result.output.split('Error: page.waitForTimeout: Test ended.').length).toBe(2);
|
||||||
|
expect.soft(result.output).toContain('> 5 | await page.waitForTimeout(100_000);');
|
||||||
|
});
|
||||||
|
|
||||||
test('step timeout is errors.TimeoutError', async ({ runInlineTest }) => {
|
test('step timeout is errors.TimeoutError', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
|
|
|
||||||
|
|
@ -740,6 +740,25 @@ test('should update snapshot with the update-snapshots flag', async ({ runInline
|
||||||
expect(comparePNGs(fs.readFileSync(snapshotOutputPath), whiteImage)).toBe(null);
|
expect(comparePNGs(fs.readFileSync(snapshotOutputPath), whiteImage)).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should respect config.expect.toHaveScreenshot.pathTemplate', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
...playwrightConfig({
|
||||||
|
snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}',
|
||||||
|
expect: { toHaveScreenshot: { pathTemplate: 'actual-screenshots/{testFilePath}/{arg}{ext}' } },
|
||||||
|
}),
|
||||||
|
'__screenshots__/a.spec.js/snapshot.png': blueImage,
|
||||||
|
'actual-screenshots/a.spec.js/snapshot.png': whiteImage,
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
test('is a test', async ({ page }) => {
|
||||||
|
await expect(page).toHaveScreenshot('snapshot.png');
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('shouldn\'t update snapshot with the update-snapshots flag for negated matcher', async ({ runInlineTest }, testInfo) => {
|
test('shouldn\'t update snapshot with the update-snapshots flag for negated matcher', async ({ runInlineTest }, testInfo) => {
|
||||||
const EXPECTED_SNAPSHOT = blueImage;
|
const EXPECTED_SNAPSHOT = blueImage;
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
|
|
|
||||||
|
|
@ -470,3 +470,32 @@ test('attachments tab shows all but top-level .push attachments', async ({ runUI
|
||||||
- button /bar-attach/
|
- button /bar-attach/
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('skipped steps should have an indicator', async ({ runUITest }) => {
|
||||||
|
const { page } = await runUITest({
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test with steps', async ({}) => {
|
||||||
|
await test.step('outer', async () => {
|
||||||
|
await test.step.skip('skipped1', () => {});
|
||||||
|
});
|
||||||
|
await test.step.skip('skipped2', () => {});
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('treeitem', { name: 'test with steps' }).dblclick();
|
||||||
|
const actionsTree = page.getByTestId('actions-tree');
|
||||||
|
await actionsTree.getByRole('treeitem', { name: 'outer' }).click();
|
||||||
|
await page.keyboard.press('ArrowRight');
|
||||||
|
await expect(actionsTree).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem /outer/ [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem /skipped1/
|
||||||
|
- treeitem /skipped2/
|
||||||
|
`);
|
||||||
|
const skippedMarker = actionsTree.getByRole('treeitem', { name: 'skipped1' }).locator('.action-skipped');
|
||||||
|
await expect(skippedMarker).toBeVisible();
|
||||||
|
await expect(skippedMarker).toHaveAccessibleName('skipped');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,8 @@ test('should update missing snapshots', async ({ runInlineTest }, testInfo) => {
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.output).toContain('Error: A snapshot is not provided, generating new baseline.');
|
||||||
|
|
||||||
expect(stripAnsi(result.output).replace(/\\/g, '/')).toContain(`New baselines created for:
|
expect(stripAnsi(result.output).replace(/\\/g, '/')).toContain(`New baselines created for:
|
||||||
|
|
||||||
|
|
@ -129,7 +130,7 @@ test('should update multiple missing snapshots', async ({ runInlineTest }, testI
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(1);
|
||||||
|
|
||||||
expect(stripAnsi(result.output).replace(/\\/g, '/')).toContain(`New baselines created for:
|
expect(stripAnsi(result.output).replace(/\\/g, '/')).toContain(`New baselines created for:
|
||||||
|
|
||||||
|
|
@ -188,7 +189,7 @@ test('should generate baseline with regex', async ({ runInlineTest }, testInfo)
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(1);
|
||||||
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
|
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
|
||||||
const data = fs.readFileSync(patchPath, 'utf-8');
|
const data = fs.readFileSync(patchPath, 'utf-8');
|
||||||
expect(trimPatch(data)).toBe(`diff --git a/a.spec.ts b/a.spec.ts
|
expect(trimPatch(data)).toBe(`diff --git a/a.spec.ts b/a.spec.ts
|
||||||
|
|
@ -249,7 +250,7 @@ test('should generate baseline with special characters', async ({ runInlineTest
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(1);
|
||||||
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
|
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
|
||||||
const data = fs.readFileSync(patchPath, 'utf-8');
|
const data = fs.readFileSync(patchPath, 'utf-8');
|
||||||
expect(trimPatch(data)).toBe(`diff --git a/a.spec.ts b/a.spec.ts
|
expect(trimPatch(data)).toBe(`diff --git a/a.spec.ts b/a.spec.ts
|
||||||
|
|
@ -314,7 +315,7 @@ test('should update missing snapshots in tsx', async ({ runInlineTest }, testInf
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(1);
|
||||||
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
|
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
|
||||||
const data = fs.readFileSync(patchPath, 'utf-8');
|
const data = fs.readFileSync(patchPath, 'utf-8');
|
||||||
expect(trimPatch(data)).toBe(`diff --git a/src/button.test.tsx b/src/button.test.tsx
|
expect(trimPatch(data)).toBe(`diff --git a/src/button.test.tsx b/src/button.test.tsx
|
||||||
|
|
@ -370,7 +371,7 @@ test('should update multiple files', async ({ runInlineTest }, testInfo) => {
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(1);
|
||||||
|
|
||||||
expect(stripAnsi(result.output).replace(/\\/g, '/')).toContain(`New baselines created for:
|
expect(stripAnsi(result.output).replace(/\\/g, '/')).toContain(`New baselines created for:
|
||||||
|
|
||||||
|
|
@ -430,7 +431,7 @@ test('should generate baseline for input values', async ({ runInlineTest }, test
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(1);
|
||||||
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
|
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
|
||||||
const data = fs.readFileSync(patchPath, 'utf-8');
|
const data = fs.readFileSync(patchPath, 'utf-8');
|
||||||
expect(trimPatch(data)).toBe(`diff --git a/a.spec.ts b/a.spec.ts
|
expect(trimPatch(data)).toBe(`diff --git a/a.spec.ts b/a.spec.ts
|
||||||
|
|
@ -454,6 +455,50 @@ test('should generate baseline for input values', async ({ runInlineTest }, test
|
||||||
expect(result2.exitCode).toBe(0);
|
expect(result2.exitCode).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should update when options are specified', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'.git/marker': '',
|
||||||
|
'a.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', async ({ page }) => {
|
||||||
|
await page.setContent(\`<input value="hello world">\`);
|
||||||
|
await expect(page.locator('body')).toMatchAriaSnapshot(\`\`, { timeout: 2500 });
|
||||||
|
await expect(page.locator('body')).toMatchAriaSnapshot('',
|
||||||
|
{
|
||||||
|
timeout: 2500
|
||||||
|
});
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
|
||||||
|
const data = fs.readFileSync(patchPath, 'utf-8');
|
||||||
|
expect(trimPatch(data)).toBe(`diff --git a/a.spec.ts b/a.spec.ts
|
||||||
|
--- a/a.spec.ts
|
||||||
|
+++ b/a.spec.ts
|
||||||
|
@@ -2,8 +2,12 @@
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', async ({ page }) => {
|
||||||
|
await page.setContent(\`<input value="hello world">\`);
|
||||||
|
- await expect(page.locator('body')).toMatchAriaSnapshot(\`\`, { timeout: 2500 });
|
||||||
|
- await expect(page.locator('body')).toMatchAriaSnapshot('',
|
||||||
|
+ await expect(page.locator('body')).toMatchAriaSnapshot(\`
|
||||||
|
+ - textbox: hello world
|
||||||
|
+ \`, { timeout: 2500 });
|
||||||
|
+ await expect(page.locator('body')).toMatchAriaSnapshot(\`
|
||||||
|
+ - textbox: hello world
|
||||||
|
+ \`,
|
||||||
|
{
|
||||||
|
timeout: 2500
|
||||||
|
});
|
||||||
|
`);
|
||||||
|
|
||||||
|
execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
|
||||||
|
const result2 = await runInlineTest({});
|
||||||
|
expect(result2.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
test('should not update snapshots when locator did not match', async ({ runInlineTest }, testInfo) => {
|
test('should not update snapshots when locator did not match', async ({ runInlineTest }, testInfo) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'.git/marker': '',
|
'.git/marker': '',
|
||||||
|
|
@ -617,4 +662,45 @@ test.describe('update-source-method', () => {
|
||||||
a.spec.ts
|
a.spec.ts
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should overwrite source when specified in the config', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'.git/marker': '',
|
||||||
|
'playwright.config.ts': `
|
||||||
|
export default { updateSourceMethod: 'overwrite' };
|
||||||
|
`,
|
||||||
|
'a.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', async ({ page }) => {
|
||||||
|
await page.setContent(\`<h1>hello</h1>\`);
|
||||||
|
await expect(page.locator('body')).toMatchAriaSnapshot(\`
|
||||||
|
- heading "world"
|
||||||
|
\`);
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { 'update-snapshots': 'all' });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
|
||||||
|
expect(fs.existsSync(patchPath)).toBeFalsy();
|
||||||
|
|
||||||
|
const data = fs.readFileSync(testInfo.outputPath('a.spec.ts'), 'utf-8');
|
||||||
|
expect(data).toBe(`
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', async ({ page }) => {
|
||||||
|
await page.setContent(\`<h1>hello</h1>\`);
|
||||||
|
await expect(page.locator('body')).toMatchAriaSnapshot(\`
|
||||||
|
- heading "hello" [level=1]
|
||||||
|
\`);
|
||||||
|
});
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(stripAnsi(result.output).replace(/\\/g, '/')).toContain(`New baselines created for:
|
||||||
|
|
||||||
|
a.spec.ts
|
||||||
|
`);
|
||||||
|
|
||||||
|
const result2 = await runInlineTest({});
|
||||||
|
expect(result2.exitCode).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ set -x
|
||||||
|
|
||||||
trap "cd $(pwd -P)" EXIT
|
trap "cd $(pwd -P)" EXIT
|
||||||
SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)"
|
SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)"
|
||||||
NODE_VERSION="22.13.0" # autogenerated via ./update-playwright-driver-version.mjs
|
NODE_VERSION="22.13.1" # autogenerated via ./update-playwright-driver-version.mjs
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
PACKAGE_VERSION=$(node -p "require('../../package.json').version")
|
PACKAGE_VERSION=$(node -p "require('../../package.json').version")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue