Compare commits
7 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
471930b1ce | |
![]() |
0259acf7b3 | |
![]() |
a0fdb3fb99 | |
![]() |
25fd261262 | |
![]() |
6da84b3994 | |
![]() |
2914d1b12f | |
![]() |
5465ffbc25 |
|
@ -284,7 +284,7 @@ By default, a template containing the subset of children will be matched:
|
||||||
|
|
||||||
|
|
||||||
The `/children` property can be used to control how child elements are matched:
|
The `/children` property can be used to control how child elements are matched:
|
||||||
- `contain` (default): Matches if all specified children are present in any order
|
- `contain` (default): Matches if all specified children are present in order
|
||||||
- `equal`: Matches if the children exactly match the specified list in order
|
- `equal`: Matches if the children exactly match the specified list in order
|
||||||
- `deep-equal`: Matches if the children exactly match the specified list in order, including nested children
|
- `deep-equal`: Matches if the children exactly match the specified list in order, including nested children
|
||||||
|
|
||||||
|
@ -296,7 +296,7 @@ The `/children` property can be used to control how child elements are matched:
|
||||||
</ul>
|
</ul>
|
||||||
```
|
```
|
||||||
|
|
||||||
*aria snapshot will fail due Feature C not being in the template*
|
*aria snapshot will fail due to Feature C not being in the template*
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- list
|
- list
|
||||||
|
|
|
@ -4,6 +4,53 @@ title: "Release notes"
|
||||||
toc_max_heading_level: 2
|
toc_max_heading_level: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Version 1.52
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
|
||||||
|
- New method [`method: LocatorAssertions.toContainClass`] to ergonomically assert individual class names on the element.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
await Expect(Page.GetByRole(AriaRole.Listitem, new() { Name = "Ship v1.52" })).ToContainClassAsync("done");
|
||||||
|
```
|
||||||
|
|
||||||
|
- [Aria Snapshots](./aria-snapshots.md) got two new properties: [`/children`](./aria-snapshots.md#strict-matching) for strict matching and `/url` for links.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
await Expect(locator).ToMatchAriaSnapshotAsync(@"
|
||||||
|
- list
|
||||||
|
- /children: equal
|
||||||
|
- listitem: Feature A
|
||||||
|
- listitem:
|
||||||
|
- link ""Feature B"":
|
||||||
|
- /url: ""https://playwright.dev""
|
||||||
|
");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
|
||||||
|
- New option [`option: APIRequest.newContext.maxRedirects`] in [`method: APIRequest.newContext`] to control the maximum number of redirects.
|
||||||
|
- New option [`option: Locator.ariaSnapshot.ref`] in [`method: Locator.ariaSnapshot`] to generate reference for each element in the snapshot which can later be used to locate the element.
|
||||||
|
- HTML reporter now supports *NOT filtering* via `!@my-tag` or `!my-file.spec.ts` or `!p:my-project`.
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- Base URL matching is not supported in [`method: Page.frame`] anymore. We recommend migrating to [`method: Page.frameLocator`] instead for having a more convenient API.
|
||||||
|
- Glob URL patterns in methods like [`method: Page.route`] do not support `?` and `[]` anymore. We recommend using regular expressions instead.
|
||||||
|
- Method [`method: Route.continue`] does not allow to override the `Cookie` header anymore. If a `Cookie` header is provided, it will be ignored, and the cookie will be loaded from the browser's cookie store. To set custom cookies, use [`method: BrowserContext.addCookies`].
|
||||||
|
- macOS 13 is now deprecated and will no longer receive WebKit updates. Please upgrade to a more recent macOS version to continue benefiting from the latest WebKit improvements.
|
||||||
|
|
||||||
|
### Browser Versions
|
||||||
|
|
||||||
|
- Chromium 136.0.7103.25
|
||||||
|
- Mozilla Firefox 137.0
|
||||||
|
- WebKit 18.4
|
||||||
|
|
||||||
|
This version was also tested against the following stable channels:
|
||||||
|
|
||||||
|
- Google Chrome 135
|
||||||
|
- Microsoft Edge 135
|
||||||
|
|
||||||
## Version 1.51
|
## Version 1.51
|
||||||
|
|
||||||
### Highlights
|
### Highlights
|
||||||
|
|
|
@ -4,6 +4,53 @@ title: "Release notes"
|
||||||
toc_max_heading_level: 2
|
toc_max_heading_level: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Version 1.52
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
|
||||||
|
- New method [`method: LocatorAssertions.toContainClass`] to ergonomically assert individual class names on the element.
|
||||||
|
|
||||||
|
```java
|
||||||
|
assertThat(page.getByRole(AriaRole.LISTITEM, new Page.GetByRoleOptions().setName("Ship v1.52"))).containsClass("done");
|
||||||
|
```
|
||||||
|
|
||||||
|
- [Aria Snapshots](./aria-snapshots.md) got two new properties: [`/children`](./aria-snapshots.md#strict-matching) for strict matching and `/url` for links.
|
||||||
|
|
||||||
|
```java
|
||||||
|
assertThat(locator).toMatchAriaSnapshot("""
|
||||||
|
- list
|
||||||
|
- /children: equal
|
||||||
|
- listitem: Feature A
|
||||||
|
- listitem:
|
||||||
|
- link "Feature B":
|
||||||
|
- /url: "https://playwright.dev"
|
||||||
|
""");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
|
||||||
|
- New option [`option: APIRequest.newContext.maxRedirects`] in [`method: APIRequest.newContext`] to control the maximum number of redirects.
|
||||||
|
- New option [`option: Locator.ariaSnapshot.ref`] in [`method: Locator.ariaSnapshot`] to generate reference for each element in the snapshot which can later be used to locate the element.
|
||||||
|
- HTML reporter now supports *NOT filtering* via `!@my-tag` or `!my-file.spec.ts` or `!p:my-project`.
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- Base URL matching is not supported in [`method: Page.frame`] anymore. We recommend migrating to [`method: Page.frameLocator`] instead for having a more convenient API.
|
||||||
|
- Glob URL patterns in methods like [`method: Page.route`] do not support `?` and `[]` anymore. We recommend using regular expressions instead.
|
||||||
|
- Method [`method: Route.continue`] does not allow to override the `Cookie` header anymore. If a `Cookie` header is provided, it will be ignored, and the cookie will be loaded from the browser's cookie store. To set custom cookies, use [`method: BrowserContext.addCookies`].
|
||||||
|
- macOS 13 is now deprecated and will no longer receive WebKit updates. Please upgrade to a more recent macOS version to continue benefiting from the latest WebKit improvements.
|
||||||
|
|
||||||
|
### Browser Versions
|
||||||
|
|
||||||
|
- Chromium 136.0.7103.25
|
||||||
|
- Mozilla Firefox 137.0
|
||||||
|
- WebKit 18.4
|
||||||
|
|
||||||
|
This version was also tested against the following stable channels:
|
||||||
|
|
||||||
|
- Google Chrome 135
|
||||||
|
- Microsoft Edge 135
|
||||||
|
|
||||||
## Version 1.51
|
## Version 1.51
|
||||||
|
|
||||||
### Highlights
|
### Highlights
|
||||||
|
|
|
@ -6,6 +6,58 @@ toc_max_heading_level: 2
|
||||||
|
|
||||||
import LiteYouTube from '@site/src/components/LiteYouTube';
|
import LiteYouTube from '@site/src/components/LiteYouTube';
|
||||||
|
|
||||||
|
## Version 1.52
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
|
||||||
|
- New method [`method: LocatorAssertions.toContainClass`] to ergonomically assert individual class names on the element.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await expect(page.getByRole('listitem', { name: 'Ship v1.52' })).toContainClass('done');
|
||||||
|
```
|
||||||
|
|
||||||
|
- [Aria Snapshots](./aria-snapshots.md) got two new properties: [`/children`](./aria-snapshots.md#strict-matching) for strict matching and `/url` for links.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await expect(locator).toMatchAriaSnapshot(`
|
||||||
|
- list
|
||||||
|
- /children: equal
|
||||||
|
- listitem: Feature A
|
||||||
|
- listitem:
|
||||||
|
- link "Feature B":
|
||||||
|
- /url: "https://playwright.dev"
|
||||||
|
`);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Runner
|
||||||
|
|
||||||
|
- New property [`property: TestProject.workers`] allows to specify the number of concurrent worker processes to use for a test project. The global limit of property [`property: TestConfig.workers`] still applies.
|
||||||
|
- New [`property: TestConfig.failOnFlakyTests`] option to fail the test run if any flaky tests are detected, similarly to `--fail-on-flaky-tests`. This is useful for CI/CD environments where you want to ensure that all tests are stable before deploying.
|
||||||
|
- New property [`property: TestResult.annotations`] contains annotations for each test retry.
|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
|
||||||
|
- New option [`option: APIRequest.newContext.maxRedirects`] in [`method: APIRequest.newContext`] to control the maximum number of redirects.
|
||||||
|
- New option [`option: Locator.ariaSnapshot.ref`] in [`method: Locator.ariaSnapshot`] to generate reference for each element in the snapshot which can later be used to locate the element.
|
||||||
|
- HTML reporter now supports *NOT filtering* via `!@my-tag` or `!my-file.spec.ts` or `!p:my-project`.
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- Glob URL patterns in methods like [`method: Page.route`] do not support `?` and `[]` anymore. We recommend using regular expressions instead.
|
||||||
|
- Method [`method: Route.continue`] does not allow to override the `Cookie` header anymore. If a `Cookie` header is provided, it will be ignored, and the cookie will be loaded from the browser's cookie store. To set custom cookies, use [`method: BrowserContext.addCookies`].
|
||||||
|
- macOS 13 is now deprecated and will no longer receive WebKit updates. Please upgrade to a more recent macOS version to continue benefiting from the latest WebKit improvements.
|
||||||
|
|
||||||
|
### Browser Versions
|
||||||
|
|
||||||
|
- Chromium 136.0.7103.25
|
||||||
|
- Mozilla Firefox 137.0
|
||||||
|
- WebKit 18.4
|
||||||
|
|
||||||
|
This version was also tested against the following stable channels:
|
||||||
|
|
||||||
|
- Google Chrome 135
|
||||||
|
- Microsoft Edge 135
|
||||||
|
|
||||||
## Version 1.51
|
## Version 1.51
|
||||||
|
|
||||||
### StorageState for indexedDB
|
### StorageState for indexedDB
|
||||||
|
|
|
@ -4,6 +4,53 @@ title: "Release notes"
|
||||||
toc_max_heading_level: 2
|
toc_max_heading_level: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Version 1.52
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
|
||||||
|
- New method [`method: LocatorAssertions.toContainClass`] to ergonomically assert individual class names on the element.
|
||||||
|
|
||||||
|
```python
|
||||||
|
expect(page.get_by_role('listitem', name='Ship v1.52')).to_contain_class('done')
|
||||||
|
```
|
||||||
|
|
||||||
|
- [Aria Snapshots](./aria-snapshots.md) got two new properties: [`/children`](./aria-snapshots.md#strict-matching) for strict matching and `/url` for links.
|
||||||
|
|
||||||
|
```python
|
||||||
|
expect(locator).to_match_aria_snapshot("""
|
||||||
|
- list
|
||||||
|
- /children: equal
|
||||||
|
- listitem: Feature A
|
||||||
|
- listitem:
|
||||||
|
- link "Feature B":
|
||||||
|
- /url: "https://playwright.dev"
|
||||||
|
""")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
|
||||||
|
- New option [`option: APIRequest.newContext.maxRedirects`] in [`method: APIRequest.newContext`] to control the maximum number of redirects.
|
||||||
|
- New option [`option: Locator.ariaSnapshot.ref`] in [`method: Locator.ariaSnapshot`] to generate reference for each element in the snapshot which can later be used to locate the element.
|
||||||
|
- HTML reporter now supports *NOT filtering* via `!@my-tag` or `!my-file.spec.ts` or `!p:my-project`.
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- Base URL matching is not supported in [`method: Page.frame`] anymore. We recommend migrating to [`method: Page.frameLocator`] instead for having a more convenient API.
|
||||||
|
- Glob URL patterns in methods like [`method: Page.route`] do not support `?` and `[]` anymore. We recommend using regular expressions instead.
|
||||||
|
- Method [`method: Route.continue`] does not allow to override the `Cookie` header anymore. If a `Cookie` header is provided, it will be ignored, and the cookie will be loaded from the browser's cookie store. To set custom cookies, use [`method: BrowserContext.addCookies`].
|
||||||
|
- macOS 13 is now deprecated and will no longer receive WebKit updates. Please upgrade to a more recent macOS version to continue benefiting from the latest WebKit improvements.
|
||||||
|
|
||||||
|
### Browser Versions
|
||||||
|
|
||||||
|
- Chromium 136.0.7103.25
|
||||||
|
- Mozilla Firefox 137.0
|
||||||
|
- WebKit 18.4
|
||||||
|
|
||||||
|
This version was also tested against the following stable channels:
|
||||||
|
|
||||||
|
- Google Chrome 135
|
||||||
|
- Microsoft Edge 135
|
||||||
|
|
||||||
## Version 1.51
|
## Version 1.51
|
||||||
|
|
||||||
### Highlights
|
### Highlights
|
||||||
|
|
|
@ -19,7 +19,6 @@ test('basic test', async ({ page }, testInfo) => {
|
||||||
- type: <[Array]<[Object]>>
|
- type: <[Array]<[Object]>>
|
||||||
- `type` <[string]> Annotation type, for example `'skip'` or `'fail'`.
|
- `type` <[string]> Annotation type, for example `'skip'` or `'fail'`.
|
||||||
- `description` ?<[string]> Optional description.
|
- `description` ?<[string]> Optional description.
|
||||||
- `location` ?<[Location]> Optional location in the source where the annotation is added.
|
|
||||||
|
|
||||||
The list of annotations applicable to the current test. Includes annotations from the test, annotations from all [`method: Test.describe`] groups the test belongs to and file-level annotations for the test file.
|
The list of annotations applicable to the current test. Includes annotations from the test, annotations from all [`method: Test.describe`] groups the test belongs to and file-level annotations for the test file.
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
- type: <[Array]<[Object]>>
|
- type: <[Array]<[Object]>>
|
||||||
- `type` <[string]> Annotation type, for example `'skip'` or `'fail'`.
|
- `type` <[string]> Annotation type, for example `'skip'` or `'fail'`.
|
||||||
- `description` ?<[string]> Optional description.
|
- `description` ?<[string]> Optional description.
|
||||||
- `location` ?<[Location]> Optional location in the source where the annotation is added.
|
|
||||||
|
|
||||||
[`property: TestResult.annotations`] of the last test run.
|
[`property: TestResult.annotations`] of the last test run.
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@ The list of files or buffers attached during the test execution through [`proper
|
||||||
- type: <[Array]<[Object]>>
|
- type: <[Array]<[Object]>>
|
||||||
- `type` <[string]> Annotation type, for example `'skip'` or `'fail'`.
|
- `type` <[string]> Annotation type, for example `'skip'` or `'fail'`.
|
||||||
- `description` ?<[string]> Optional description.
|
- `description` ?<[string]> Optional description.
|
||||||
- `location` ?<[Location]> Optional location in the source where the annotation is added.
|
|
||||||
|
|
||||||
The list of annotations applicable to the current test. Includes:
|
The list of annotations applicable to the current test. Includes:
|
||||||
* annotations defined on the test or suite via [`method: Test.(call)`] and [`method: Test.describe`];
|
* annotations defined on the test or suite via [`method: Test.(call)`] and [`method: Test.describe`];
|
||||||
|
|
|
@ -55,7 +55,6 @@ List of steps inside this step.
|
||||||
- type: <[Array]<[Object]>>
|
- type: <[Array]<[Object]>>
|
||||||
- `type` <[string]> Annotation type, for example `'skip'`.
|
- `type` <[string]> Annotation type, for example `'skip'`.
|
||||||
- `description` ?<[string]> Optional description.
|
- `description` ?<[string]> Optional description.
|
||||||
- `location` ?<[Location]> Optional location in the source where the annotation is added.
|
|
||||||
|
|
||||||
The list of annotations applicable to the current test step.
|
The list of annotations applicable to the current test step.
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-internal",
|
"name": "playwright-internal",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "playwright-internal",
|
"name": "playwright-internal",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
@ -7906,10 +7906,10 @@
|
||||||
"version": "0.0.0"
|
"version": "0.0.0"
|
||||||
},
|
},
|
||||||
"packages/playwright": {
|
"packages/playwright": {
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.52.0-next"
|
"playwright-core": "1.52.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
@ -7923,11 +7923,11 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-browser-chromium": {
|
"packages/playwright-browser-chromium": {
|
||||||
"name": "@playwright/browser-chromium",
|
"name": "@playwright/browser-chromium",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.52.0-next"
|
"playwright-core": "1.52.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
@ -7935,11 +7935,11 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-browser-firefox": {
|
"packages/playwright-browser-firefox": {
|
||||||
"name": "@playwright/browser-firefox",
|
"name": "@playwright/browser-firefox",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.52.0-next"
|
"playwright-core": "1.52.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
@ -7947,22 +7947,22 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-browser-webkit": {
|
"packages/playwright-browser-webkit": {
|
||||||
"name": "@playwright/browser-webkit",
|
"name": "@playwright/browser-webkit",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.52.0-next"
|
"playwright-core": "1.52.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-chromium": {
|
"packages/playwright-chromium": {
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.52.0-next"
|
"playwright-core": "1.52.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
@ -7976,14 +7976,14 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.52.0-next"
|
"playwright-core": "1.52.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-core": {
|
"packages/playwright-core": {
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright-core": "cli.js"
|
"playwright-core": "cli.js"
|
||||||
|
@ -7994,11 +7994,11 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-core": {
|
"packages/playwright-ct-core": {
|
||||||
"name": "@playwright/experimental-ct-core",
|
"name": "@playwright/experimental-ct-core",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.52.0-next",
|
"playwright": "1.52.0",
|
||||||
"playwright-core": "1.52.0-next",
|
"playwright-core": "1.52.0",
|
||||||
"vite": "^6.2.6"
|
"vite": "^6.2.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -8007,10 +8007,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-react": {
|
"packages/playwright-ct-react": {
|
||||||
"name": "@playwright/experimental-ct-react",
|
"name": "@playwright/experimental-ct-react",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.52.0-next",
|
"@playwright/experimental-ct-core": "1.52.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1"
|
"@vitejs/plugin-react": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -8022,10 +8022,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-react17": {
|
"packages/playwright-ct-react17": {
|
||||||
"name": "@playwright/experimental-ct-react17",
|
"name": "@playwright/experimental-ct-react17",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.52.0-next",
|
"@playwright/experimental-ct-core": "1.52.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1"
|
"@vitejs/plugin-react": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -8037,10 +8037,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-svelte": {
|
"packages/playwright-ct-svelte": {
|
||||||
"name": "@playwright/experimental-ct-svelte",
|
"name": "@playwright/experimental-ct-svelte",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.52.0-next",
|
"@playwright/experimental-ct-core": "1.52.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -8598,10 +8598,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-vue": {
|
"packages/playwright-ct-vue": {
|
||||||
"name": "@playwright/experimental-ct-vue",
|
"name": "@playwright/experimental-ct-vue",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.52.0-next",
|
"@playwright/experimental-ct-core": "1.52.0",
|
||||||
"@vitejs/plugin-vue": "^5.2.0"
|
"@vitejs/plugin-vue": "^5.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -8621,18 +8621,18 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@octokit/graphql-schema": "^15.26.0",
|
"@octokit/graphql-schema": "^15.26.0",
|
||||||
"@playwright/test": "1.52.0-next"
|
"@playwright/test": "1.52.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-firefox": {
|
"packages/playwright-firefox": {
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.52.0-next"
|
"playwright-core": "1.52.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
@ -8664,10 +8664,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-test": {
|
"packages/playwright-test": {
|
||||||
"name": "@playwright/test",
|
"name": "@playwright/test",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.52.0-next"
|
"playwright": "1.52.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
@ -8694,11 +8694,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-webkit": {
|
"packages/playwright-webkit": {
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.52.0-next"
|
"playwright-core": "1.52.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-internal",
|
"name": "playwright-internal",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"description": "A high-level API to automate web browsers",
|
"description": "A high-level API to automate web browsers",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -14,8 +14,7 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { TestAnnotation } from '@playwright/test';
|
import type { TestCase, TestAnnotation, TestCaseSummary } from './types';
|
||||||
import type { TestCase, TestCaseSummary } from './types';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { TabbedPane } from './tabbedPane';
|
import { TabbedPane } from './tabbedPane';
|
||||||
import { AutoChip } from './chip';
|
import { AutoChip } from './chip';
|
||||||
|
|
|
@ -20,17 +20,18 @@ import './testErrorView.css';
|
||||||
import type { ImageDiff } from '@web/shared/imageDiffView';
|
import type { ImageDiff } from '@web/shared/imageDiffView';
|
||||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||||
import { TestAttachment } from './types';
|
import { TestAttachment } from './types';
|
||||||
|
import { fixTestInstructions } from '@web/prompts';
|
||||||
|
|
||||||
export const TestErrorView: React.FC<{
|
export const TestErrorView: React.FC<{
|
||||||
error: string;
|
error: string;
|
||||||
testId?: string;
|
testId?: string;
|
||||||
prompt?: TestAttachment;
|
context?: TestAttachment;
|
||||||
}> = ({ error, testId, prompt }) => {
|
}> = ({ error, testId, context }) => {
|
||||||
return (
|
return (
|
||||||
<CodeSnippet code={error} testId={testId}>
|
<CodeSnippet code={error} testId={testId}>
|
||||||
{prompt && (
|
{context && (
|
||||||
<div style={{ position: 'absolute', right: 0, padding: '10px' }}>
|
<div style={{ position: 'absolute', right: 0, padding: '10px' }}>
|
||||||
<PromptButton prompt={prompt} />
|
<PromptButton context={context} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CodeSnippet>
|
</CodeSnippet>
|
||||||
|
@ -47,14 +48,14 @@ export const CodeSnippet = ({ code, children, testId }: React.PropsWithChildren<
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PromptButton: React.FC<{ prompt: TestAttachment }> = ({ prompt }) => {
|
const PromptButton: React.FC<{ context: TestAttachment }> = ({ context }) => {
|
||||||
const [copied, setCopied] = React.useState(false);
|
const [copied, setCopied] = React.useState(false);
|
||||||
return <button
|
return <button
|
||||||
className='button'
|
className='button'
|
||||||
style={{ minWidth: 100 }}
|
style={{ minWidth: 100 }}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const text = prompt.body ? prompt.body : await fetch(prompt.path!).then(r => r.text());
|
const text = context.body ? context.body : await fetch(context.path!).then(r => r.text());
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(fixTestInstructions + text);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setCopied(false);
|
setCopied(false);
|
||||||
|
|
|
@ -90,7 +90,7 @@ export const TestResultView: React.FC<{
|
||||||
{errors.map((error, index) => {
|
{errors.map((error, index) => {
|
||||||
if (error.type === 'screenshot')
|
if (error.type === 'screenshot')
|
||||||
return <TestScreenshotErrorView key={'test-result-error-message-' + index} errorPrefix={error.errorPrefix} diff={error.diff!} errorSuffix={error.errorSuffix}></TestScreenshotErrorView>;
|
return <TestScreenshotErrorView key={'test-result-error-message-' + index} errorPrefix={error.errorPrefix} diff={error.diff!} errorSuffix={error.errorSuffix}></TestScreenshotErrorView>;
|
||||||
return <TestErrorView key={'test-result-error-message-' + index} error={error.error!} prompt={error.prompt}></TestErrorView>;
|
return <TestErrorView key={'test-result-error-message-' + index} error={error.error!} context={error.context}></TestErrorView>;
|
||||||
})}
|
})}
|
||||||
</AutoChip>}
|
</AutoChip>}
|
||||||
{!!result.steps.length && <AutoChip header='Test Steps'>
|
{!!result.steps.length && <AutoChip header='Test Steps'>
|
||||||
|
@ -165,8 +165,8 @@ function classifyErrors(testErrors: string[], diffs: ImageDiff[], attachments: T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const prompt = attachments.find(a => a.name === `_prompt-${i}`);
|
const context = attachments.find(a => a.name === `_error-context-${i}`);
|
||||||
return { type: 'regular', error, prompt };
|
return { type: 'regular', error, context };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { TestAnnotation, Metadata } from '@playwright/test';
|
import type { Metadata } from '@playwright/test';
|
||||||
|
|
||||||
export type Stats = {
|
export type Stats = {
|
||||||
total: number;
|
total: number;
|
||||||
|
@ -59,6 +59,8 @@ export type TestFileSummary = {
|
||||||
stats: Stats;
|
stats: Stats;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TestAnnotation = { type: string, description?: string };
|
||||||
|
|
||||||
export type TestCaseSummary = {
|
export type TestCaseSummary = {
|
||||||
testId: string,
|
testId: string,
|
||||||
title: string;
|
title: string;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/browser-chromium",
|
"name": "@playwright/browser-chromium",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"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.52.0-next"
|
"playwright-core": "1.52.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/browser-firefox",
|
"name": "@playwright/browser-firefox",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"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.52.0-next"
|
"playwright-core": "1.52.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/browser-webkit",
|
"name": "@playwright/browser-webkit",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"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.52.0-next"
|
"playwright-core": "1.52.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-chromium",
|
"name": "playwright-chromium",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"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.52.0-next"
|
"playwright-core": "1.52.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,6 @@
|
||||||
"watch": "esbuild ./src/index.ts --outdir=lib --format=cjs --bundle --platform=node --target=ES2019 --watch"
|
"watch": "esbuild ./src/index.ts --outdir=lib --format=cjs --bundle --platform=node --target=ES2019 --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.52.0-next"
|
"playwright-core": "1.52.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-core",
|
"name": "playwright-core",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"description": "A high-level API to automate web browsers",
|
"description": "A high-level API to automate web browsers",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/experimental-ct-core",
|
"name": "@playwright/experimental-ct-core",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"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.52.0-next",
|
"playwright-core": "1.52.0",
|
||||||
"vite": "^6.2.6",
|
"vite": "^6.2.6",
|
||||||
"playwright": "1.52.0-next"
|
"playwright": "1.52.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/experimental-ct-react",
|
"name": "@playwright/experimental-ct-react",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"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.52.0-next",
|
"@playwright/experimental-ct-core": "1.52.0",
|
||||||
"@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.52.0-next",
|
"version": "1.52.0",
|
||||||
"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.52.0-next",
|
"@playwright/experimental-ct-core": "1.52.0",
|
||||||
"@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.52.0-next",
|
"version": "1.52.0",
|
||||||
"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.52.0-next",
|
"@playwright/experimental-ct-core": "1.52.0",
|
||||||
"@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.52.0-next",
|
"version": "1.52.0",
|
||||||
"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.52.0-next",
|
"@playwright/experimental-ct-core": "1.52.0",
|
||||||
"@vitejs/plugin-vue": "^5.2.0"
|
"@vitejs/plugin-vue": "^5.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
@ -24,6 +24,6 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@octokit/graphql-schema": "^15.26.0",
|
"@octokit/graphql-schema": "^15.26.0",
|
||||||
"@playwright/test": "1.52.0-next"
|
"@playwright/test": "1.52.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-firefox",
|
"name": "playwright-firefox",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"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.52.0-next"
|
"playwright-core": "1.52.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/test",
|
"name": "@playwright/test",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"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.52.0-next"
|
"playwright": "1.52.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-webkit",
|
"name": "playwright-webkit",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"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.52.0-next"
|
"playwright-core": "1.52.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright",
|
"name": "playwright",
|
||||||
"version": "1.52.0-next",
|
"version": "1.52.0",
|
||||||
"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.52.0-next"
|
"playwright-core": "1.52.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"fsevents": "2.3.2"
|
"fsevents": "2.3.2"
|
||||||
|
|
|
@ -14,5 +14,5 @@ common/
|
||||||
[internalsForTest.ts]
|
[internalsForTest.ts]
|
||||||
**
|
**
|
||||||
|
|
||||||
[prompt.ts]
|
[errorContext.ts]
|
||||||
./transform/babelBundle.ts
|
./transform/babelBundle.ts
|
||||||
|
|
|
@ -36,6 +36,7 @@ export type FixturesWithLocation = {
|
||||||
fixtures: Fixtures;
|
fixtures: Fixtures;
|
||||||
location: Location;
|
location: Location;
|
||||||
};
|
};
|
||||||
|
export type Annotation = { type: string, description?: string };
|
||||||
|
|
||||||
export const defaultTimeout = 30000;
|
export const defaultTimeout = 30000;
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,9 @@
|
||||||
import { rootTestType } from './testType';
|
import { rootTestType } from './testType';
|
||||||
import { computeTestCaseOutcome } from '../isomorphic/teleReceiver';
|
import { computeTestCaseOutcome } from '../isomorphic/teleReceiver';
|
||||||
|
|
||||||
import type { FixturesWithLocation, FullProjectInternal } from './config';
|
import type { Annotation, FixturesWithLocation, FullProjectInternal } from './config';
|
||||||
import type { FixturePool } from './fixtures';
|
import type { FixturePool } from './fixtures';
|
||||||
import type { TestTypeImpl } from './testType';
|
import type { TestTypeImpl } from './testType';
|
||||||
import type { TestAnnotation } from '../../types/test';
|
|
||||||
import type * as reporterTypes from '../../types/testReporter';
|
import type * as reporterTypes from '../../types/testReporter';
|
||||||
import type { FullProject, Location } from '../../types/testReporter';
|
import type { FullProject, Location } from '../../types/testReporter';
|
||||||
|
|
||||||
|
@ -51,7 +50,7 @@ export class Suite extends Base {
|
||||||
_timeout: number | undefined;
|
_timeout: number | undefined;
|
||||||
_retries: number | undefined;
|
_retries: number | undefined;
|
||||||
// Annotations known statically before running the test, e.g. `test.describe.skip()` or `test.describe({ annotation }, body)`.
|
// Annotations known statically before running the test, e.g. `test.describe.skip()` or `test.describe({ annotation }, body)`.
|
||||||
_staticAnnotations: TestAnnotation[] = [];
|
_staticAnnotations: Annotation[] = [];
|
||||||
// Explicitly declared tags that are not a part of the title.
|
// Explicitly declared tags that are not a part of the title.
|
||||||
_tags: string[] = [];
|
_tags: string[] = [];
|
||||||
_modifiers: Modifier[] = [];
|
_modifiers: Modifier[] = [];
|
||||||
|
@ -253,7 +252,7 @@ export class TestCase extends Base implements reporterTypes.TestCase {
|
||||||
|
|
||||||
expectedStatus: reporterTypes.TestStatus = 'passed';
|
expectedStatus: reporterTypes.TestStatus = 'passed';
|
||||||
timeout = 0;
|
timeout = 0;
|
||||||
annotations: TestAnnotation[] = [];
|
annotations: Annotation[] = [];
|
||||||
retries = 0;
|
retries = 0;
|
||||||
repeatEachIndex = 0;
|
repeatEachIndex = 0;
|
||||||
|
|
||||||
|
|
|
@ -25,8 +25,6 @@ import { wrapFunctionWithLocation } from '../transform/transform';
|
||||||
import type { FixturesWithLocation } from './config';
|
import type { FixturesWithLocation } from './config';
|
||||||
import type { Fixtures, TestDetails, TestStepInfo, TestType } from '../../types/test';
|
import type { Fixtures, TestDetails, TestStepInfo, TestType } from '../../types/test';
|
||||||
import type { Location } from '../../types/testReporter';
|
import type { Location } from '../../types/testReporter';
|
||||||
import type { TestInfoImpl, TestStepInternal } from '../worker/testInfo';
|
|
||||||
|
|
||||||
|
|
||||||
const testTypeSymbol = Symbol('testType');
|
const testTypeSymbol = Symbol('testType');
|
||||||
|
|
||||||
|
@ -104,7 +102,7 @@ export class TestTypeImpl {
|
||||||
details = fnOrDetails;
|
details = fnOrDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validatedDetails = validateTestDetails(details, location);
|
const validatedDetails = validateTestDetails(details);
|
||||||
const test = new TestCase(title, body, this, location);
|
const test = new TestCase(title, body, this, location);
|
||||||
test._requireFile = suite._requireFile;
|
test._requireFile = suite._requireFile;
|
||||||
test.annotations.push(...validatedDetails.annotations);
|
test.annotations.push(...validatedDetails.annotations);
|
||||||
|
@ -114,9 +112,9 @@ export class TestTypeImpl {
|
||||||
if (type === 'only' || type === 'fail.only')
|
if (type === 'only' || type === 'fail.only')
|
||||||
test._only = true;
|
test._only = true;
|
||||||
if (type === 'skip' || type === 'fixme' || type === 'fail')
|
if (type === 'skip' || type === 'fixme' || type === 'fail')
|
||||||
test.annotations.push({ type, location });
|
test.annotations.push({ type });
|
||||||
else if (type === 'fail.only')
|
else if (type === 'fail.only')
|
||||||
test.annotations.push({ type: 'fail', location });
|
test.annotations.push({ type: 'fail' });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _describe(type: 'default' | 'only' | 'serial' | 'serial.only' | 'parallel' | 'parallel.only' | 'skip' | 'fixme', location: Location, titleOrFn: string | Function, fnOrDetails?: TestDetails | Function, fn?: Function) {
|
private _describe(type: 'default' | 'only' | 'serial' | 'serial.only' | 'parallel' | 'parallel.only' | 'skip' | 'fixme', location: Location, titleOrFn: string | Function, fnOrDetails?: TestDetails | Function, fn?: Function) {
|
||||||
|
@ -143,7 +141,7 @@ export class TestTypeImpl {
|
||||||
body = fn!;
|
body = fn!;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validatedDetails = validateTestDetails(details, location);
|
const validatedDetails = validateTestDetails(details);
|
||||||
const child = new Suite(title, 'describe');
|
const child = new Suite(title, 'describe');
|
||||||
child._requireFile = suite._requireFile;
|
child._requireFile = suite._requireFile;
|
||||||
child.location = location;
|
child.location = location;
|
||||||
|
@ -158,7 +156,7 @@ export class TestTypeImpl {
|
||||||
if (type === 'parallel' || type === 'parallel.only')
|
if (type === 'parallel' || type === 'parallel.only')
|
||||||
child._parallelMode = 'parallel';
|
child._parallelMode = 'parallel';
|
||||||
if (type === 'skip' || type === 'fixme')
|
if (type === 'skip' || type === 'fixme')
|
||||||
child._staticAnnotations.push({ type, location });
|
child._staticAnnotations.push({ type });
|
||||||
|
|
||||||
for (let parent: Suite | undefined = suite; parent; parent = parent.parent) {
|
for (let parent: Suite | undefined = suite; parent; parent = parent.parent) {
|
||||||
if (parent._parallelMode === 'serial' && child._parallelMode === 'parallel')
|
if (parent._parallelMode === 'serial' && child._parallelMode === 'parallel')
|
||||||
|
@ -229,7 +227,7 @@ export class TestTypeImpl {
|
||||||
if (modifierArgs.length >= 1 && !modifierArgs[0])
|
if (modifierArgs.length >= 1 && !modifierArgs[0])
|
||||||
return;
|
return;
|
||||||
const description = modifierArgs[1];
|
const description = modifierArgs[1];
|
||||||
suite._staticAnnotations.push({ type, description, location });
|
suite._staticAnnotations.push({ type, description });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -239,7 +237,7 @@ export class TestTypeImpl {
|
||||||
throw new Error(`test.${type}() can only be called inside test, describe block or fixture`);
|
throw new Error(`test.${type}() can only be called inside test, describe block or fixture`);
|
||||||
if (typeof modifierArgs[0] === 'function')
|
if (typeof modifierArgs[0] === 'function')
|
||||||
throw new Error(`test.${type}() with a function can only be called inside describe block`);
|
throw new Error(`test.${type}() with a function can only be called inside describe block`);
|
||||||
testInfo._modifier(type, location, modifierArgs as [any, any]);
|
testInfo[type](...modifierArgs as [any, any]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setTimeout(location: Location, timeout: number) {
|
private _setTimeout(location: Location, timeout: number) {
|
||||||
|
@ -262,21 +260,17 @@ export class TestTypeImpl {
|
||||||
suite._use.push({ fixtures, location });
|
suite._use.push({ fixtures, location });
|
||||||
}
|
}
|
||||||
|
|
||||||
_step<T>(expectation: 'pass'|'skip', title: string, body: (step: TestStepInfo) => T | Promise<T>, options: {box?: boolean, location?: Location, timeout?: number } = {}): Promise<T> {
|
async _step<T>(expectation: 'pass'|'skip', title: string, body: (step: TestStepInfo) => T | Promise<T>, options: {box?: boolean, location?: Location, timeout?: number } = {}): Promise<T> {
|
||||||
const testInfo = currentTestInfo();
|
const testInfo = currentTestInfo();
|
||||||
if (!testInfo)
|
if (!testInfo)
|
||||||
throw new Error(`test.step() can only be called from a test`);
|
throw new Error(`test.step() can only be called from a test`);
|
||||||
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 testInfo._floatingPromiseScope.wrapPromiseAPIResult(this._stepInternal(expectation, testInfo, step, body, options), step.location);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _stepInternal<T>(expectation: 'pass'|'skip', testInfo: TestInfoImpl, step: TestStepInternal, body: (step: TestStepInfo) => T | Promise<T>, options: {box?: boolean, location?: Location, timeout?: number } = {}): Promise<T> {
|
|
||||||
return await currentZone().with('stepZone', step).run(async () => {
|
return await currentZone().with('stepZone', step).run(async () => {
|
||||||
try {
|
try {
|
||||||
let result: Awaited<ReturnType<typeof raceAgainstDeadline<T>>> | undefined = undefined;
|
let result: Awaited<ReturnType<typeof raceAgainstDeadline<T>>> | undefined = undefined;
|
||||||
result = await raceAgainstDeadline(async () => {
|
result = await raceAgainstDeadline(async () => {
|
||||||
try {
|
try {
|
||||||
return await step.info._runStepBody(expectation === 'skip', body, step.location);
|
return await step.info._runStepBody(expectation === 'skip', body);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If the step timed out, the test fixtures will tear down, which in turn
|
// 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.
|
// will abort unfinished actions in the step body. Record such errors here.
|
||||||
|
@ -315,9 +309,8 @@ function throwIfRunningInsideJest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateTestDetails(details: TestDetails, location: Location) {
|
function validateTestDetails(details: TestDetails) {
|
||||||
const originalAnnotations = Array.isArray(details.annotation) ? details.annotation : (details.annotation ? [details.annotation] : []);
|
const annotations = Array.isArray(details.annotation) ? details.annotation : (details.annotation ? [details.annotation] : []);
|
||||||
const annotations = originalAnnotations.map(annotation => ({ ...annotation, location }));
|
|
||||||
const tags = Array.isArray(details.tag) ? details.tag : (details.tag ? [details.tag] : []);
|
const tags = Array.isArray(details.tag) ? details.tag : (details.tag ? [details.tag] : []);
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
if (tag[0] !== '@')
|
if (tag[0] !== '@')
|
||||||
|
|
|
@ -22,14 +22,25 @@ import { parseErrorStack } from 'playwright-core/lib/utils';
|
||||||
import { stripAnsiEscapes } from './util';
|
import { stripAnsiEscapes } from './util';
|
||||||
import { codeFrameColumns } from './transform/babelBundle';
|
import { codeFrameColumns } from './transform/babelBundle';
|
||||||
|
|
||||||
import type { TestInfo } from '../types/test';
|
|
||||||
import type { MetadataWithCommitInfo } from './isomorphic/types';
|
import type { MetadataWithCommitInfo } from './isomorphic/types';
|
||||||
import type { TestInfoImpl } from './worker/testInfo';
|
import type { TestInfoImpl } from './worker/testInfo';
|
||||||
|
|
||||||
export async function attachErrorPrompts(testInfo: TestInfo, sourceCache: Map<string, string>, ariaSnapshot: string | undefined) {
|
export async function attachErrorContext(testInfo: TestInfoImpl, format: 'markdown' | 'json', sourceCache: Map<string, string>, ariaSnapshot: string | undefined) {
|
||||||
if (process.env.PLAYWRIGHT_NO_COPY_PROMPT)
|
if (format === 'json') {
|
||||||
|
if (!ariaSnapshot)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
testInfo._attach({
|
||||||
|
name: `_error-context`,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: Buffer.from(JSON.stringify({
|
||||||
|
pageSnapshot: ariaSnapshot,
|
||||||
|
})),
|
||||||
|
}, undefined);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const meaningfulSingleLineErrors = new Set(testInfo.errors.filter(e => e.message && !e.message.includes('\n')).map(e => e.message!));
|
const meaningfulSingleLineErrors = new Set(testInfo.errors.filter(e => e.message && !e.message.includes('\n')).map(e => e.message!));
|
||||||
for (const error of testInfo.errors) {
|
for (const error of testInfo.errors) {
|
||||||
for (const singleLineError of meaningfulSingleLineErrors.keys()) {
|
for (const singleLineError of meaningfulSingleLineErrors.keys()) {
|
||||||
|
@ -51,16 +62,10 @@ export async function attachErrorPrompts(testInfo: TestInfo, sourceCache: Map<st
|
||||||
|
|
||||||
for (const [index, error] of errors) {
|
for (const [index, error] of errors) {
|
||||||
const metadata = testInfo.config.metadata as MetadataWithCommitInfo;
|
const metadata = testInfo.config.metadata as MetadataWithCommitInfo;
|
||||||
if (testInfo.attachments.find(a => a.name === `_prompt-${index}`))
|
if (testInfo.attachments.find(a => a.name === `_error-context-${index}`))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const promptParts = [
|
const lines = [
|
||||||
`# Instructions`,
|
|
||||||
'',
|
|
||||||
`- Following Playwright test failed.`,
|
|
||||||
`- Explain why, be concise, respect Playwright best practices.`,
|
|
||||||
`- Provide a snippet of code with the fix, if possible.`,
|
|
||||||
'',
|
|
||||||
`# Test info`,
|
`# Test info`,
|
||||||
'',
|
'',
|
||||||
`- Name: ${testInfo.titlePath.slice(1).join(' >> ')}`,
|
`- Name: ${testInfo.titlePath.slice(1).join(' >> ')}`,
|
||||||
|
@ -74,7 +79,7 @@ export async function attachErrorPrompts(testInfo: TestInfo, sourceCache: Map<st
|
||||||
];
|
];
|
||||||
|
|
||||||
if (ariaSnapshot) {
|
if (ariaSnapshot) {
|
||||||
promptParts.push(
|
lines.push(
|
||||||
'',
|
'',
|
||||||
'# Page snapshot',
|
'# Page snapshot',
|
||||||
'',
|
'',
|
||||||
|
@ -103,7 +108,7 @@ export async function attachErrorPrompts(testInfo: TestInfo, sourceCache: Map<st
|
||||||
message: inlineMessage || undefined,
|
message: inlineMessage || undefined,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
promptParts.push(
|
lines.push(
|
||||||
'',
|
'',
|
||||||
'# Test source',
|
'# Test source',
|
||||||
'',
|
'',
|
||||||
|
@ -113,7 +118,7 @@ export async function attachErrorPrompts(testInfo: TestInfo, sourceCache: Map<st
|
||||||
);
|
);
|
||||||
|
|
||||||
if (metadata.gitDiff) {
|
if (metadata.gitDiff) {
|
||||||
promptParts.push(
|
lines.push(
|
||||||
'',
|
'',
|
||||||
'# Local changes',
|
'# Local changes',
|
||||||
'',
|
'',
|
||||||
|
@ -123,30 +128,17 @@ export async function attachErrorPrompts(testInfo: TestInfo, sourceCache: Map<st
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const promptPath = testInfo.outputPath(errors.length === 1 ? `prompt.md` : `prompt-${index}.md`);
|
const filePath = testInfo.outputPath(errors.length === 1 ? `error-context.md` : `error-context-${index}.md`);
|
||||||
await fs.writeFile(promptPath, promptParts.join('\n'), 'utf8');
|
await fs.writeFile(filePath, lines.join('\n'), 'utf8');
|
||||||
|
|
||||||
(testInfo as TestInfoImpl)._attach({
|
(testInfo as TestInfoImpl)._attach({
|
||||||
name: `_prompt-${index}`,
|
name: `_error-context-${index}`,
|
||||||
contentType: 'text/markdown',
|
contentType: 'text/markdown',
|
||||||
path: promptPath,
|
path: filePath,
|
||||||
}, undefined);
|
}, undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function attachErrorContext(testInfo: TestInfo, ariaSnapshot: string | undefined) {
|
|
||||||
if (!ariaSnapshot)
|
|
||||||
return;
|
|
||||||
|
|
||||||
(testInfo as TestInfoImpl)._attach({
|
|
||||||
name: `_error-context`,
|
|
||||||
contentType: 'application/json',
|
|
||||||
body: Buffer.from(JSON.stringify({
|
|
||||||
pageSnapshot: ariaSnapshot,
|
|
||||||
})),
|
|
||||||
}, undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadSource(file: string, sourceCache: Map<string, string>) {
|
async function loadSource(file: string, sourceCache: Map<string, string>) {
|
||||||
let source = sourceCache.get(file);
|
let source = sourceCache.get(file);
|
||||||
if (!source) {
|
if (!source) {
|
|
@ -22,7 +22,7 @@ import { setBoxedStackPrefixes, asLocator, createGuid, currentZone, debugMode, i
|
||||||
|
|
||||||
import { currentTestInfo } from './common/globals';
|
import { currentTestInfo } from './common/globals';
|
||||||
import { rootTestType } from './common/testType';
|
import { rootTestType } from './common/testType';
|
||||||
import { attachErrorContext, attachErrorPrompts } from './prompt';
|
import { attachErrorContext } from './errorContext';
|
||||||
|
|
||||||
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test';
|
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test';
|
||||||
import type { ContextReuseMode } from './common/config';
|
import type { ContextReuseMode } from './common/config';
|
||||||
|
@ -55,13 +55,15 @@ type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
|
||||||
_contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
|
_contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ErrorContextOption = { format: 'json' | 'markdown' } | undefined;
|
||||||
|
|
||||||
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
|
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
|
||||||
playwright: PlaywrightImpl;
|
playwright: PlaywrightImpl;
|
||||||
_browserOptions: LaunchOptions;
|
_browserOptions: LaunchOptions;
|
||||||
_optionContextReuseMode: ContextReuseMode,
|
_optionContextReuseMode: ContextReuseMode,
|
||||||
_optionConnectOptions: PlaywrightWorkerOptions['connectOptions'],
|
_optionConnectOptions: PlaywrightWorkerOptions['connectOptions'],
|
||||||
_reuseContext: boolean,
|
_reuseContext: boolean,
|
||||||
_optionAttachErrorContext: boolean,
|
_optionErrorContext: ErrorContextOption,
|
||||||
};
|
};
|
||||||
|
|
||||||
const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
|
@ -245,13 +247,13 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
playwright._defaultContextNavigationTimeout = undefined;
|
playwright._defaultContextNavigationTimeout = undefined;
|
||||||
}, { auto: 'all-hooks-included', title: 'context configuration', box: true } as any],
|
}, { auto: 'all-hooks-included', title: 'context configuration', box: true } as any],
|
||||||
|
|
||||||
_setupArtifacts: [async ({ playwright, screenshot, _optionAttachErrorContext }, use, testInfo) => {
|
_setupArtifacts: [async ({ playwright, screenshot, _optionErrorContext }, use, testInfo) => {
|
||||||
// This fixture has a separate zero-timeout slot to ensure that artifact collection
|
// This fixture has a separate zero-timeout slot to ensure that artifact collection
|
||||||
// happens even after some fixtures or hooks time out.
|
// happens even after some fixtures or hooks time out.
|
||||||
// Now that default test timeout is known, we can replace zero with an actual value.
|
// Now that default test timeout is known, we can replace zero with an actual value.
|
||||||
testInfo.setTimeout(testInfo.project.timeout);
|
testInfo.setTimeout(testInfo.project.timeout);
|
||||||
|
|
||||||
const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot, _optionAttachErrorContext);
|
const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot, _optionErrorContext);
|
||||||
await artifactsRecorder.willStartTest(testInfo as TestInfoImpl);
|
await artifactsRecorder.willStartTest(testInfo as TestInfoImpl);
|
||||||
|
|
||||||
const tracingGroupSteps: TestStepInternal[] = [];
|
const tracingGroupSteps: TestStepInternal[] = [];
|
||||||
|
@ -393,7 +395,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
|
|
||||||
_optionContextReuseMode: ['none', { scope: 'worker', option: true }],
|
_optionContextReuseMode: ['none', { scope: 'worker', option: true }],
|
||||||
_optionConnectOptions: [undefined, { scope: 'worker', option: true }],
|
_optionConnectOptions: [undefined, { scope: 'worker', option: true }],
|
||||||
_optionAttachErrorContext: [false, { scope: 'worker', option: true }],
|
_optionErrorContext: [process.env.PLAYWRIGHT_NO_COPY_PROMPT ? undefined : { format: 'markdown' }, { scope: 'worker', option: true }],
|
||||||
|
|
||||||
_reuseContext: [async ({ video, _optionContextReuseMode }, use) => {
|
_reuseContext: [async ({ video, _optionContextReuseMode }, use) => {
|
||||||
let mode = _optionContextReuseMode;
|
let mode = _optionContextReuseMode;
|
||||||
|
@ -622,12 +624,12 @@ class ArtifactsRecorder {
|
||||||
private _screenshotRecorder: SnapshotRecorder;
|
private _screenshotRecorder: SnapshotRecorder;
|
||||||
private _pageSnapshot: string | undefined;
|
private _pageSnapshot: string | undefined;
|
||||||
private _sourceCache: Map<string, string> = new Map();
|
private _sourceCache: Map<string, string> = new Map();
|
||||||
private _attachErrorContext: boolean;
|
private _errorContext: ErrorContextOption;
|
||||||
|
|
||||||
constructor(playwright: PlaywrightImpl, artifactsDir: string, screenshot: ScreenshotOption, attachErrorContext: boolean) {
|
constructor(playwright: PlaywrightImpl, artifactsDir: string, screenshot: ScreenshotOption, errorContext: ErrorContextOption) {
|
||||||
this._playwright = playwright;
|
this._playwright = playwright;
|
||||||
this._artifactsDir = artifactsDir;
|
this._artifactsDir = artifactsDir;
|
||||||
this._attachErrorContext = attachErrorContext;
|
this._errorContext = errorContext;
|
||||||
const screenshotOptions = typeof screenshot === 'string' ? undefined : screenshot;
|
const screenshotOptions = typeof screenshot === 'string' ? undefined : screenshot;
|
||||||
this._startedCollectingArtifacts = Symbol('startedCollectingArtifacts');
|
this._startedCollectingArtifacts = Symbol('startedCollectingArtifacts');
|
||||||
|
|
||||||
|
@ -671,7 +673,7 @@ class ArtifactsRecorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _takePageSnapshot(context: BrowserContext) {
|
private async _takePageSnapshot(context: BrowserContext) {
|
||||||
if (process.env.PLAYWRIGHT_NO_COPY_PROMPT)
|
if (!this._errorContext)
|
||||||
return;
|
return;
|
||||||
if (this._testInfo.errors.length === 0)
|
if (this._testInfo.errors.length === 0)
|
||||||
return;
|
return;
|
||||||
|
@ -719,10 +721,8 @@ class ArtifactsRecorder {
|
||||||
if (context)
|
if (context)
|
||||||
await this._takePageSnapshot(context);
|
await this._takePageSnapshot(context);
|
||||||
|
|
||||||
if (this._attachErrorContext)
|
if (this._errorContext)
|
||||||
await attachErrorContext(this._testInfo, this._pageSnapshot);
|
await attachErrorContext(this._testInfo, this._errorContext.format, this._sourceCache, this._pageSnapshot);
|
||||||
else
|
|
||||||
await attachErrorPrompts(this._testInfo, this._sourceCache, this._pageSnapshot);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _startTraceChunkOnContextCreation(tracing: Tracing) {
|
private async _startTraceChunkOnContextCreation(tracing: Tracing) {
|
||||||
|
|
|
@ -14,8 +14,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Metadata, TestAnnotation } from '../../types/test';
|
import type { Metadata } from '../../types/test';
|
||||||
import type * as reporterTypes from '../../types/testReporter';
|
import type * as reporterTypes from '../../types/testReporter';
|
||||||
|
import type { Annotation } from '../common/config';
|
||||||
import type { ReporterV2 } from '../reporters/reporterV2';
|
import type { ReporterV2 } from '../reporters/reporterV2';
|
||||||
|
|
||||||
export type StringIntern = (s: string) => string;
|
export type StringIntern = (s: string) => string;
|
||||||
|
@ -67,7 +68,7 @@ export type JsonTestCase = {
|
||||||
retries: number;
|
retries: number;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
repeatEachIndex: number;
|
repeatEachIndex: number;
|
||||||
annotations?: TestAnnotation[];
|
annotations?: Annotation[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type JsonTestEnd = {
|
export type JsonTestEnd = {
|
||||||
|
@ -94,7 +95,7 @@ export type JsonTestResultEnd = {
|
||||||
status: reporterTypes.TestStatus;
|
status: reporterTypes.TestStatus;
|
||||||
errors: reporterTypes.TestError[];
|
errors: reporterTypes.TestError[];
|
||||||
attachments: JsonAttachment[];
|
attachments: JsonAttachment[];
|
||||||
annotations?: TestAnnotation[];
|
annotations?: Annotation[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type JsonTestStepStart = {
|
export type JsonTestStepStart = {
|
||||||
|
@ -111,7 +112,7 @@ export type JsonTestStepEnd = {
|
||||||
duration: number;
|
duration: number;
|
||||||
error?: reporterTypes.TestError;
|
error?: reporterTypes.TestError;
|
||||||
attachments?: number[]; // index of JsonTestResultEnd.attachments
|
attachments?: number[]; // index of JsonTestResultEnd.attachments
|
||||||
annotations?: TestAnnotation[];
|
annotations?: Annotation[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type JsonFullResult = {
|
export type JsonFullResult = {
|
||||||
|
@ -242,7 +243,7 @@ export class TeleReporterReceiver {
|
||||||
result.error = result.errors?.[0];
|
result.error = result.errors?.[0];
|
||||||
result.attachments = this._parseAttachments(payload.attachments);
|
result.attachments = this._parseAttachments(payload.attachments);
|
||||||
if (payload.annotations) {
|
if (payload.annotations) {
|
||||||
result.annotations = this._absoluteAnnotationLocations(payload.annotations);
|
result.annotations = payload.annotations;
|
||||||
test.annotations = result.annotations;
|
test.annotations = result.annotations;
|
||||||
}
|
}
|
||||||
this._reporter.onTestEnd?.(test, result);
|
this._reporter.onTestEnd?.(test, result);
|
||||||
|
@ -374,18 +375,10 @@ export class TeleReporterReceiver {
|
||||||
test.location = this._absoluteLocation(payload.location);
|
test.location = this._absoluteLocation(payload.location);
|
||||||
test.retries = payload.retries;
|
test.retries = payload.retries;
|
||||||
test.tags = payload.tags ?? [];
|
test.tags = payload.tags ?? [];
|
||||||
test.annotations = this._absoluteAnnotationLocations(payload.annotations ?? []);
|
test.annotations = payload.annotations ?? [];
|
||||||
return test;
|
return test;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _absoluteAnnotationLocations(annotations: TestAnnotation[]): TestAnnotation[] {
|
|
||||||
return annotations.map(annotation => {
|
|
||||||
if (annotation.location)
|
|
||||||
annotation.location = this._absoluteLocation(annotation.location);
|
|
||||||
return annotation;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _absoluteLocation(location: reporterTypes.Location): reporterTypes.Location;
|
private _absoluteLocation(location: reporterTypes.Location): reporterTypes.Location;
|
||||||
private _absoluteLocation(location?: reporterTypes.Location): reporterTypes.Location | undefined;
|
private _absoluteLocation(location?: reporterTypes.Location): reporterTypes.Location | undefined;
|
||||||
private _absoluteLocation(location: reporterTypes.Location | undefined): reporterTypes.Location | undefined {
|
private _absoluteLocation(location: reporterTypes.Location | undefined): reporterTypes.Location | undefined {
|
||||||
|
@ -486,7 +479,7 @@ export class TeleTestCase implements reporterTypes.TestCase {
|
||||||
|
|
||||||
expectedStatus: reporterTypes.TestStatus = 'passed';
|
expectedStatus: reporterTypes.TestStatus = 'passed';
|
||||||
timeout = 0;
|
timeout = 0;
|
||||||
annotations: TestAnnotation[] = [];
|
annotations: Annotation[] = [];
|
||||||
retries = 0;
|
retries = 0;
|
||||||
tags: string[] = [];
|
tags: string[] = [];
|
||||||
repeatEachIndex = 0;
|
repeatEachIndex = 0;
|
||||||
|
|
|
@ -102,7 +102,7 @@ export interface TestServerInterface {
|
||||||
projects?: string[];
|
projects?: string[];
|
||||||
reuseContext?: boolean;
|
reuseContext?: boolean;
|
||||||
connectWsEndpoint?: string;
|
connectWsEndpoint?: string;
|
||||||
attachErrorContext?: boolean;
|
errorContext?: { format: 'json' | 'markdown' };
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
status: reporterTypes.FullResult['status'];
|
status: reporterTypes.FullResult['status'];
|
||||||
}>;
|
}>;
|
||||||
|
|
|
@ -385,10 +385,8 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||||
setMatcherCallContext({ expectInfo: this._info, testInfo, step: step.info });
|
setMatcherCallContext({ expectInfo: this._info, testInfo, step: step.info });
|
||||||
const callback = () => matcher.call(target, ...args);
|
const callback = () => matcher.call(target, ...args);
|
||||||
const result = currentZone().with('stepZone', step).run(callback);
|
const result = currentZone().with('stepZone', step).run(callback);
|
||||||
if (result instanceof Promise) {
|
if (result instanceof Promise)
|
||||||
const promise = result.then(finalizer).catch(reportStepError);
|
return result.then(finalizer).catch(reportStepError);
|
||||||
return testInfo._floatingPromiseScope.wrapPromiseAPIResult(promise, stackFrames[0]);
|
|
||||||
}
|
|
||||||
finalizer();
|
finalizer();
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -26,7 +26,6 @@ import { getEastAsianWidth } from '../utilsBundle';
|
||||||
import type { ReporterV2 } from './reporterV2';
|
import type { ReporterV2 } from './reporterV2';
|
||||||
import type { FullConfig, FullResult, Location, Suite, TestCase, TestError, TestResult, TestStep } from '../../types/testReporter';
|
import type { FullConfig, FullResult, Location, Suite, TestCase, TestError, TestResult, TestStep } from '../../types/testReporter';
|
||||||
import type { Colors } from '@isomorphic/colors';
|
import type { Colors } from '@isomorphic/colors';
|
||||||
import type { TestAnnotation } from '../../types/test';
|
|
||||||
|
|
||||||
export type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' };
|
export type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' };
|
||||||
export const kOutputSymbol = Symbol('output');
|
export const kOutputSymbol = Symbol('output');
|
||||||
|
@ -320,7 +319,6 @@ export function formatFailure(screen: Screen, config: FullConfig, test: TestCase
|
||||||
const header = formatTestHeader(screen, config, test, { indent: ' ', index, mode: 'error' });
|
const header = formatTestHeader(screen, config, test, { indent: ' ', index, mode: 'error' });
|
||||||
lines.push(screen.colors.red(header));
|
lines.push(screen.colors.red(header));
|
||||||
for (const result of test.results) {
|
for (const result of test.results) {
|
||||||
const warnings = result.annotations.filter(a => a.type === 'warning');
|
|
||||||
const resultLines: string[] = [];
|
const resultLines: string[] = [];
|
||||||
const errors = formatResultFailure(screen, test, result, ' ');
|
const errors = formatResultFailure(screen, test, result, ' ');
|
||||||
if (!errors.length)
|
if (!errors.length)
|
||||||
|
@ -330,15 +328,11 @@ export function formatFailure(screen: Screen, config: FullConfig, test: TestCase
|
||||||
resultLines.push(screen.colors.gray(separator(screen, ` Retry #${result.retry}`)));
|
resultLines.push(screen.colors.gray(separator(screen, ` Retry #${result.retry}`)));
|
||||||
}
|
}
|
||||||
resultLines.push(...errors.map(error => '\n' + error.message));
|
resultLines.push(...errors.map(error => '\n' + error.message));
|
||||||
if (warnings.length) {
|
|
||||||
resultLines.push('');
|
|
||||||
resultLines.push(...formatTestWarning(screen, config, warnings));
|
|
||||||
}
|
|
||||||
for (let i = 0; i < result.attachments.length; ++i) {
|
for (let i = 0; i < result.attachments.length; ++i) {
|
||||||
const attachment = result.attachments[i];
|
const attachment = result.attachments[i];
|
||||||
if (attachment.name.startsWith('_prompt') && attachment.path) {
|
if (attachment.name.startsWith('_error-context') && attachment.path) {
|
||||||
resultLines.push('');
|
resultLines.push('');
|
||||||
resultLines.push(screen.colors.dim(` Error Prompt: ${relativeFilePath(screen, config, attachment.path)}`));
|
resultLines.push(screen.colors.dim(` Error Context: ${relativeFilePath(screen, config, attachment.path)}`));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (attachment.name.startsWith('_'))
|
if (attachment.name.startsWith('_'))
|
||||||
|
@ -376,26 +370,6 @@ export function formatFailure(screen: Screen, config: FullConfig, test: TestCase
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTestWarning(screen: Screen, config: FullConfig, warnings: TestAnnotation[]): string[] {
|
|
||||||
warnings.sort((a, b) => {
|
|
||||||
const aLocationKey = a.location ? `${a.location.file}:${a.location.line}:${a.location.column}` : undefined;
|
|
||||||
const bLocationKey = b.location ? `${b.location.file}:${b.location.line}:${b.location.column}` : undefined;
|
|
||||||
|
|
||||||
if (!aLocationKey && !bLocationKey)
|
|
||||||
return 0;
|
|
||||||
if (!aLocationKey)
|
|
||||||
return 1;
|
|
||||||
if (!bLocationKey)
|
|
||||||
return -1;
|
|
||||||
return aLocationKey.localeCompare(bLocationKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
return warnings.filter(w => !!w.description).map(w => {
|
|
||||||
const location = !!w.location ? `${relativeFilePath(screen, config, w.location.file)}:${w.location.line}:${w.location.column}: ` : '';
|
|
||||||
return `${screen.colors.yellow(` Warning: ${location}${w.description}`)}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatRetry(screen: Screen, result: TestResult) {
|
export function formatRetry(screen: Screen, result: TestResult) {
|
||||||
const retryLines = [];
|
const retryLines = [];
|
||||||
if (result.retry) {
|
if (result.retry) {
|
||||||
|
|
|
@ -29,9 +29,9 @@ import { codeFrameColumns } from '../transform/babelBundle';
|
||||||
import { resolveReporterOutputPath, stripAnsiEscapes } from '../util';
|
import { resolveReporterOutputPath, stripAnsiEscapes } from '../util';
|
||||||
|
|
||||||
import type { ReporterV2 } from './reporterV2';
|
import type { ReporterV2 } from './reporterV2';
|
||||||
import type { Metadata, TestAnnotation } from '../../types/test';
|
import type { Metadata } from '../../types/test';
|
||||||
import type * as api from '../../types/testReporter';
|
import type * as api from '../../types/testReporter';
|
||||||
import type { HTMLReport, Stats, TestAttachment, TestCase, TestCaseSummary, TestFile, TestFileSummary, TestResult, TestStep } from '@html-reporter/types';
|
import type { HTMLReport, Stats, TestAttachment, TestCase, TestCaseSummary, TestFile, TestFileSummary, TestResult, TestStep, TestAnnotation } from '@html-reporter/types';
|
||||||
import type { ZipFile } from 'playwright-core/lib/zipBundle';
|
import type { ZipFile } from 'playwright-core/lib/zipBundle';
|
||||||
import type { TransformCallback } from 'stream';
|
import type { TransformCallback } from 'stream';
|
||||||
|
|
||||||
|
@ -503,7 +503,7 @@ class HtmlBuilder {
|
||||||
|
|
||||||
private _serializeAnnotations(annotations: api.TestCase['annotations']): TestAnnotation[] {
|
private _serializeAnnotations(annotations: api.TestCase['annotations']): TestAnnotation[] {
|
||||||
// Annotations can be pushed directly, with a wrong type.
|
// Annotations can be pushed directly, with a wrong type.
|
||||||
return annotations.map(a => ({ type: a.type, description: a.description === undefined ? undefined : String(a.description), location: a.location }));
|
return annotations.map(a => ({ type: a.type, description: a.description === undefined ? undefined : String(a.description) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createTestResult(test: api.TestCase, result: api.TestResult): TestResult {
|
private _createTestResult(test: api.TestCase, result: api.TestResult): TestResult {
|
||||||
|
|
|
@ -27,10 +27,10 @@ import { createReporters } from '../runner/reporters';
|
||||||
import { relativeFilePath } from '../util';
|
import { relativeFilePath } from '../util';
|
||||||
|
|
||||||
import type { BlobReportMetadata } from './blob';
|
import type { BlobReportMetadata } from './blob';
|
||||||
import type { ReporterDescription, TestAnnotation } from '../../types/test';
|
import type { ReporterDescription } from '../../types/test';
|
||||||
import type { TestError } from '../../types/testReporter';
|
import type { TestError } from '../../types/testReporter';
|
||||||
import type { FullConfigInternal } from '../common/config';
|
import type { FullConfigInternal } from '../common/config';
|
||||||
import type { JsonConfig, JsonEvent, JsonFullResult, JsonLocation, JsonProject, JsonSuite, JsonTestCase, JsonTestEnd, JsonTestResultEnd, JsonTestStepEnd, JsonTestStepStart } from '../isomorphic/teleReceiver';
|
import type { JsonConfig, JsonEvent, JsonFullResult, JsonLocation, JsonProject, JsonSuite, JsonTestCase, JsonTestResultEnd, JsonTestStepEnd, JsonTestStepStart } from '../isomorphic/teleReceiver';
|
||||||
import type * as blobV1 from './versions/blobV1';
|
import type * as blobV1 from './versions/blobV1';
|
||||||
|
|
||||||
type StatusCallback = (message: string) => void;
|
type StatusCallback = (message: string) => void;
|
||||||
|
@ -474,10 +474,7 @@ class PathSeparatorPatcher {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (jsonEvent.method === 'onTestEnd') {
|
if (jsonEvent.method === 'onTestEnd') {
|
||||||
const test = jsonEvent.params.test as JsonTestEnd;
|
|
||||||
test.annotations?.forEach(annotation => this._updateAnnotationLocations(annotation));
|
|
||||||
const testResult = jsonEvent.params.result as JsonTestResultEnd;
|
const testResult = jsonEvent.params.result as JsonTestResultEnd;
|
||||||
testResult.annotations?.forEach(annotation => this._updateAnnotationLocations(annotation));
|
|
||||||
testResult.errors.forEach(error => this._updateErrorLocations(error));
|
testResult.errors.forEach(error => this._updateErrorLocations(error));
|
||||||
testResult.attachments.forEach(attachment => {
|
testResult.attachments.forEach(attachment => {
|
||||||
if (attachment.path)
|
if (attachment.path)
|
||||||
|
@ -493,7 +490,6 @@ class PathSeparatorPatcher {
|
||||||
if (jsonEvent.method === 'onStepEnd') {
|
if (jsonEvent.method === 'onStepEnd') {
|
||||||
const step = jsonEvent.params.step as JsonTestStepEnd;
|
const step = jsonEvent.params.step as JsonTestStepEnd;
|
||||||
this._updateErrorLocations(step.error);
|
this._updateErrorLocations(step.error);
|
||||||
step.annotations?.forEach(annotation => this._updateAnnotationLocations(annotation));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -510,14 +506,12 @@ class PathSeparatorPatcher {
|
||||||
if (isFileSuite)
|
if (isFileSuite)
|
||||||
suite.title = this._updatePath(suite.title);
|
suite.title = this._updatePath(suite.title);
|
||||||
for (const entry of suite.entries) {
|
for (const entry of suite.entries) {
|
||||||
if ('testId' in entry) {
|
if ('testId' in entry)
|
||||||
this._updateLocation(entry.location);
|
this._updateLocation(entry.location);
|
||||||
entry.annotations?.forEach(annotation => this._updateAnnotationLocations(annotation));
|
else
|
||||||
} else {
|
|
||||||
this._updateSuite(entry);
|
this._updateSuite(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private _updateErrorLocations(error: TestError | undefined) {
|
private _updateErrorLocations(error: TestError | undefined) {
|
||||||
while (error) {
|
while (error) {
|
||||||
|
@ -526,10 +520,6 @@ class PathSeparatorPatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateAnnotationLocations(annotation: TestAnnotation) {
|
|
||||||
this._updateLocation(annotation.location);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateLocation(location?: JsonLocation) {
|
private _updateLocation(location?: JsonLocation) {
|
||||||
if (location)
|
if (location)
|
||||||
location.file = this._updatePath(location.file);
|
location.file = this._updatePath(location.file);
|
||||||
|
|
|
@ -22,7 +22,6 @@ import { serializeRegexPatterns } from '../isomorphic/teleReceiver';
|
||||||
|
|
||||||
import type { ReporterV2 } from './reporterV2';
|
import type { ReporterV2 } from './reporterV2';
|
||||||
import type * as reporterTypes from '../../types/testReporter';
|
import type * as reporterTypes from '../../types/testReporter';
|
||||||
import type { TestAnnotation } from '../../types/test';
|
|
||||||
import type * as teleReceiver from '../isomorphic/teleReceiver';
|
import type * as teleReceiver from '../isomorphic/teleReceiver';
|
||||||
|
|
||||||
export type TeleReporterEmitterOptions = {
|
export type TeleReporterEmitterOptions = {
|
||||||
|
@ -217,7 +216,7 @@ export class TeleReporterEmitter implements ReporterV2 {
|
||||||
retries: test.retries,
|
retries: test.retries,
|
||||||
tags: test.tags,
|
tags: test.tags,
|
||||||
repeatEachIndex: test.repeatEachIndex,
|
repeatEachIndex: test.repeatEachIndex,
|
||||||
annotations: this._relativeAnnotationLocations(test.annotations),
|
annotations: test.annotations,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,7 +237,7 @@ export class TeleReporterEmitter implements ReporterV2 {
|
||||||
status: result.status,
|
status: result.status,
|
||||||
errors: result.errors,
|
errors: result.errors,
|
||||||
attachments: this._serializeAttachments(result.attachments),
|
attachments: this._serializeAttachments(result.attachments),
|
||||||
annotations: result.annotations?.length ? this._relativeAnnotationLocations(result.annotations) : undefined,
|
annotations: result.annotations?.length ? result.annotations : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,18 +268,10 @@ export class TeleReporterEmitter implements ReporterV2 {
|
||||||
duration: step.duration,
|
duration: step.duration,
|
||||||
error: step.error,
|
error: step.error,
|
||||||
attachments: step.attachments.length ? step.attachments.map(a => result.attachments.indexOf(a)) : undefined,
|
attachments: step.attachments.length ? step.attachments.map(a => result.attachments.indexOf(a)) : undefined,
|
||||||
annotations: step.annotations.length ? this._relativeAnnotationLocations(step.annotations) : undefined,
|
annotations: step.annotations.length ? step.annotations : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _relativeAnnotationLocations(annotations: TestAnnotation[]): TestAnnotation[] {
|
|
||||||
return annotations.map(annotation => {
|
|
||||||
if (annotation.location)
|
|
||||||
annotation.location = this._relativeLocation(annotation.location);
|
|
||||||
return annotation;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _relativeLocation(location: reporterTypes.Location): reporterTypes.Location;
|
private _relativeLocation(location: reporterTypes.Location): reporterTypes.Location;
|
||||||
private _relativeLocation(location?: reporterTypes.Location): reporterTypes.Location | undefined;
|
private _relativeLocation(location?: reporterTypes.Location): reporterTypes.Location | undefined;
|
||||||
private _relativeLocation(location: reporterTypes.Location | undefined): reporterTypes.Location | undefined {
|
private _relativeLocation(location: reporterTypes.Location | undefined): reporterTypes.Location | undefined {
|
||||||
|
|
|
@ -314,7 +314,7 @@ export class TestServerDispatcher implements TestServerInterface {
|
||||||
...(params.headed !== undefined ? { headless: !params.headed } : {}),
|
...(params.headed !== undefined ? { headless: !params.headed } : {}),
|
||||||
_optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined,
|
_optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined,
|
||||||
_optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined,
|
_optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined,
|
||||||
_optionAttachErrorContext: params.attachErrorContext,
|
_optionErrorContext: params.errorContext,
|
||||||
},
|
},
|
||||||
...(params.updateSnapshots ? { updateSnapshots: params.updateSnapshots } : {}),
|
...(params.updateSnapshots ? { updateSnapshots: params.updateSnapshots } : {}),
|
||||||
...(params.updateSourceMethod ? { updateSourceMethod: params.updateSourceMethod } : {}),
|
...(params.updateSourceMethod ? { updateSourceMethod: params.updateSourceMethod } : {}),
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { Location } from '../../types/test';
|
|
||||||
|
|
||||||
export class FloatingPromiseScope {
|
|
||||||
readonly _floatingCalls: Map<Promise<any>, Location | undefined> = new Map();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables a promise API call to be tracked by the test, alerting if unawaited.
|
|
||||||
*
|
|
||||||
* **NOTE:** Returning from an async function wraps the result in a promise, regardless of whether the return value is a promise. This will automatically mark the promise as awaited. Avoid this.
|
|
||||||
*/
|
|
||||||
wrapPromiseAPIResult<T>(promise: Promise<T>, location: Location | undefined): Promise<T> {
|
|
||||||
if (process.env.PW_DISABLE_FLOATING_PROMISES_WARNING)
|
|
||||||
return promise;
|
|
||||||
|
|
||||||
const promiseProxy = new Proxy(promise, {
|
|
||||||
get: (target, prop, receiver) => {
|
|
||||||
if (prop === 'then') {
|
|
||||||
return (...args: any[]) => {
|
|
||||||
this._floatingCalls.delete(promise);
|
|
||||||
|
|
||||||
const originalThen = Reflect.get(target, prop, receiver) as Promise<T>['then'];
|
|
||||||
return originalThen.call(target, ...args);
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return Reflect.get(target, prop, receiver);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this._floatingCalls.set(promise, location);
|
|
||||||
|
|
||||||
return promiseProxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
this._floatingCalls.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
hasFloatingPromises(): boolean {
|
|
||||||
return this._floatingCalls.size > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
floatingPromises(): Array<{ location: Location | undefined, promise: Promise<any> }> {
|
|
||||||
return Array.from(this._floatingCalls.entries()).map(([promise, location]) => ({ location, promise }));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -23,17 +23,16 @@ import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutMana
|
||||||
import { filteredStackTrace, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util';
|
import { filteredStackTrace, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util';
|
||||||
import { TestTracing } from './testTracing';
|
import { TestTracing } from './testTracing';
|
||||||
import { testInfoError } from './util';
|
import { testInfoError } from './util';
|
||||||
import { FloatingPromiseScope } from './floatingPromiseScope';
|
|
||||||
import { wrapFunctionWithLocation } from '../transform/transform';
|
|
||||||
|
|
||||||
import type { RunnableDescription } from './timeoutManager';
|
import type { RunnableDescription } from './timeoutManager';
|
||||||
import type { FullProject, TestAnnotation, TestInfo, TestStatus, TestStepInfo } from '../../types/test';
|
import type { FullProject, TestInfo, TestStatus, TestStepInfo } from '../../types/test';
|
||||||
import type { FullConfig, Location } from '../../types/testReporter';
|
import type { FullConfig, Location } from '../../types/testReporter';
|
||||||
import type { FullConfigInternal, FullProjectInternal } from '../common/config';
|
import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config';
|
||||||
import type { AttachmentPayload, StepBeginPayload, StepEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc';
|
import type { AttachmentPayload, StepBeginPayload, StepEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc';
|
||||||
import type { TestCase } from '../common/test';
|
import type { TestCase } from '../common/test';
|
||||||
import type { StackFrame } from '@protocol/channels';
|
import type { StackFrame } from '@protocol/channels';
|
||||||
|
|
||||||
|
|
||||||
export interface TestStepInternal {
|
export interface TestStepInternal {
|
||||||
complete(result: { error?: Error | unknown, suggestedRebaseline?: string }): void;
|
complete(result: { error?: Error | unknown, suggestedRebaseline?: string }): void;
|
||||||
info: TestStepInfoImpl
|
info: TestStepInfoImpl
|
||||||
|
@ -60,7 +59,6 @@ export class TestInfoImpl implements TestInfo {
|
||||||
readonly _startTime: number;
|
readonly _startTime: number;
|
||||||
readonly _startWallTime: number;
|
readonly _startWallTime: number;
|
||||||
readonly _tracing: TestTracing;
|
readonly _tracing: TestTracing;
|
||||||
readonly _floatingPromiseScope: FloatingPromiseScope = new FloatingPromiseScope();
|
|
||||||
readonly _uniqueSymbol;
|
readonly _uniqueSymbol;
|
||||||
|
|
||||||
_wasInterrupted = false;
|
_wasInterrupted = false;
|
||||||
|
@ -75,12 +73,6 @@ export class TestInfoImpl implements TestInfo {
|
||||||
_hasUnhandledError = false;
|
_hasUnhandledError = false;
|
||||||
_allowSkips = false;
|
_allowSkips = false;
|
||||||
|
|
||||||
// ------------ Main methods ------------
|
|
||||||
skip: (arg?: any, description?: string) => void;
|
|
||||||
fixme: (arg?: any, description?: string) => void;
|
|
||||||
fail: (arg?: any, description?: string) => void;
|
|
||||||
slow: (arg?: any, description?: string) => void;
|
|
||||||
|
|
||||||
// ------------ TestInfo fields ------------
|
// ------------ TestInfo fields ------------
|
||||||
readonly testId: string;
|
readonly testId: string;
|
||||||
readonly repeatEachIndex: number;
|
readonly repeatEachIndex: number;
|
||||||
|
@ -98,7 +90,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
readonly fn: Function;
|
readonly fn: Function;
|
||||||
expectedStatus: TestStatus;
|
expectedStatus: TestStatus;
|
||||||
duration: number = 0;
|
duration: number = 0;
|
||||||
readonly annotations: TestAnnotation[] = [];
|
readonly annotations: Annotation[] = [];
|
||||||
readonly attachments: TestInfo['attachments'] = [];
|
readonly attachments: TestInfo['attachments'] = [];
|
||||||
status: TestStatus = 'passed';
|
status: TestStatus = 'passed';
|
||||||
snapshotSuffix: string = '';
|
snapshotSuffix: string = '';
|
||||||
|
@ -206,14 +198,9 @@ export class TestInfoImpl implements TestInfo {
|
||||||
};
|
};
|
||||||
|
|
||||||
this._tracing = new TestTracing(this, workerParams.artifactsDir);
|
this._tracing = new TestTracing(this, workerParams.artifactsDir);
|
||||||
|
|
||||||
this.skip = wrapFunctionWithLocation((location, ...args) => this._modifier('skip', location, args));
|
|
||||||
this.fixme = wrapFunctionWithLocation((location, ...args) => this._modifier('fixme', location, args));
|
|
||||||
this.fail = wrapFunctionWithLocation((location, ...args) => this._modifier('fail', location, args));
|
|
||||||
this.slow = wrapFunctionWithLocation((location, ...args) => this._modifier('slow', location, args));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_modifier(type: 'skip' | 'fail' | 'fixme' | 'slow', location: Location, modifierArgs: [arg?: any, description?: string]) {
|
private _modifier(type: 'skip' | 'fail' | 'fixme' | 'slow', modifierArgs: [arg?: any, description?: string]) {
|
||||||
if (typeof modifierArgs[1] === 'function') {
|
if (typeof modifierArgs[1] === 'function') {
|
||||||
throw new Error([
|
throw new Error([
|
||||||
'It looks like you are calling test.skip() inside the test and pass a callback.',
|
'It looks like you are calling test.skip() inside the test and pass a callback.',
|
||||||
|
@ -228,7 +215,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const description = modifierArgs[1];
|
const description = modifierArgs[1];
|
||||||
this.annotations.push({ type, description, location });
|
this.annotations.push({ type, description });
|
||||||
if (type === 'slow') {
|
if (type === 'slow') {
|
||||||
this._timeoutManager.slow();
|
this._timeoutManager.slow();
|
||||||
} else if (type === 'skip' || type === 'fixme') {
|
} else if (type === 'skip' || type === 'fixme') {
|
||||||
|
@ -492,13 +479,29 @@ export class TestInfoImpl implements TestInfo {
|
||||||
return this._resolveSnapshotPath(undefined, legacyTemplate, pathSegments);
|
return this._resolveSnapshotPath(undefined, legacyTemplate, pathSegments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
skip(...args: [arg?: any, description?: string]) {
|
||||||
|
this._modifier('skip', args);
|
||||||
|
}
|
||||||
|
|
||||||
|
fixme(...args: [arg?: any, description?: string]) {
|
||||||
|
this._modifier('fixme', args);
|
||||||
|
}
|
||||||
|
|
||||||
|
fail(...args: [arg?: any, description?: string]) {
|
||||||
|
this._modifier('fail', args);
|
||||||
|
}
|
||||||
|
|
||||||
|
slow(...args: [arg?: any, description?: string]) {
|
||||||
|
this._modifier('slow', args);
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(timeout: number) {
|
setTimeout(timeout: number) {
|
||||||
this._timeoutManager.setTimeout(timeout);
|
this._timeoutManager.setTimeout(timeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TestStepInfoImpl implements TestStepInfo {
|
export class TestStepInfoImpl implements TestStepInfo {
|
||||||
annotations: TestAnnotation[] = [];
|
annotations: Annotation[] = [];
|
||||||
|
|
||||||
private _testInfo: TestInfoImpl;
|
private _testInfo: TestInfoImpl;
|
||||||
private _stepId: string;
|
private _stepId: string;
|
||||||
|
@ -508,9 +511,9 @@ export class TestStepInfoImpl implements TestStepInfo {
|
||||||
this._stepId = stepId;
|
this._stepId = stepId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _runStepBody<T>(skip: boolean, body: (step: TestStepInfo) => T | Promise<T>, location?: Location) {
|
async _runStepBody<T>(skip: boolean, body: (step: TestStepInfo) => T | Promise<T>) {
|
||||||
if (skip) {
|
if (skip) {
|
||||||
this.annotations.push({ type: 'skip', location });
|
this.annotations.push({ type: 'skip' });
|
||||||
return undefined as T;
|
return undefined as T;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
@ -530,15 +533,15 @@ export class TestStepInfoImpl implements TestStepInfo {
|
||||||
this._attachToStep(await normalizeAndSaveAttachment(this._testInfo.outputPath(), name, options));
|
this._attachToStep(await normalizeAndSaveAttachment(this._testInfo.outputPath(), name, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
skip = wrapFunctionWithLocation((location: Location, ...args: unknown[]) => {
|
skip(...args: unknown[]) {
|
||||||
// skip();
|
// skip();
|
||||||
// skip(condition: boolean, description: string);
|
// skip(condition: boolean, description: string);
|
||||||
if (args.length > 0 && !args[0])
|
if (args.length > 0 && !args[0])
|
||||||
return;
|
return;
|
||||||
const description = args[1] as (string|undefined);
|
const description = args[1] as (string|undefined);
|
||||||
this.annotations.push({ type: 'skip', description, location });
|
this.annotations.push({ type: 'skip', description });
|
||||||
throw new StepSkipError(description);
|
throw new StepSkipError(description);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TestSkipError extends Error {
|
export class TestSkipError extends Error {
|
||||||
|
|
|
@ -32,10 +32,9 @@ import { loadTestFile } from '../common/testLoader';
|
||||||
|
|
||||||
import type { TimeSlot } from './timeoutManager';
|
import type { TimeSlot } from './timeoutManager';
|
||||||
import type { Location } from '../../types/testReporter';
|
import type { Location } from '../../types/testReporter';
|
||||||
import type { FullConfigInternal, FullProjectInternal } from '../common/config';
|
import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config';
|
||||||
import type { DonePayload, RunPayload, TeardownErrorsPayload, TestBeginPayload, TestEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc';
|
import type { DonePayload, RunPayload, TeardownErrorsPayload, TestBeginPayload, TestEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc';
|
||||||
import type { Suite, TestCase } from '../common/test';
|
import type { Suite, TestCase } from '../common/test';
|
||||||
import type { TestAnnotation } from '../../types/test';
|
|
||||||
|
|
||||||
export class WorkerMain extends ProcessRunner {
|
export class WorkerMain extends ProcessRunner {
|
||||||
private _params: WorkerInitParams;
|
private _params: WorkerInitParams;
|
||||||
|
@ -61,7 +60,7 @@ export class WorkerMain extends ProcessRunner {
|
||||||
// Suites that had their beforeAll hooks, but not afterAll hooks executed.
|
// Suites that had their beforeAll hooks, but not afterAll hooks executed.
|
||||||
// These suites still need afterAll hooks to be executed for the proper cleanup.
|
// These suites still need afterAll hooks to be executed for the proper cleanup.
|
||||||
// Contains dynamic annotations originated by modifiers with a callback, e.g. `test.skip(() => true)`.
|
// Contains dynamic annotations originated by modifiers with a callback, e.g. `test.skip(() => true)`.
|
||||||
private _activeSuites = new Map<Suite, TestAnnotation[]>();
|
private _activeSuites = new Map<Suite, Annotation[]>();
|
||||||
|
|
||||||
constructor(params: WorkerInitParams) {
|
constructor(params: WorkerInitParams) {
|
||||||
super();
|
super();
|
||||||
|
@ -265,7 +264,7 @@ export class WorkerMain extends ProcessRunner {
|
||||||
stepEndPayload => this.dispatchEvent('stepEnd', stepEndPayload),
|
stepEndPayload => this.dispatchEvent('stepEnd', stepEndPayload),
|
||||||
attachment => this.dispatchEvent('attach', attachment));
|
attachment => this.dispatchEvent('attach', attachment));
|
||||||
|
|
||||||
const processAnnotation = (annotation: TestAnnotation) => {
|
const processAnnotation = (annotation: Annotation) => {
|
||||||
testInfo.annotations.push(annotation);
|
testInfo.annotations.push(annotation);
|
||||||
switch (annotation.type) {
|
switch (annotation.type) {
|
||||||
case 'fixme':
|
case 'fixme':
|
||||||
|
@ -428,25 +427,9 @@ export class WorkerMain extends ProcessRunner {
|
||||||
throw firstAfterHooksError;
|
throw firstAfterHooksError;
|
||||||
}).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors.
|
}).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors.
|
||||||
|
|
||||||
if (testInfo._isFailure()) {
|
if (testInfo._isFailure())
|
||||||
this._isStopped = true;
|
this._isStopped = true;
|
||||||
|
|
||||||
// Only if failed, create warning if any of the async calls were not awaited in various stages.
|
|
||||||
if (!process.env.PW_DISABLE_FLOATING_PROMISES_WARNING && testInfo._floatingPromiseScope.hasFloatingPromises()) {
|
|
||||||
// Dedupe by location
|
|
||||||
const annotationLocations = new Map<string | undefined, Location | undefined>(testInfo._floatingPromiseScope.floatingPromises().map(
|
|
||||||
({ location }) => {
|
|
||||||
const locationKey = location ? `${location.file}:${location.line}:${location.column}` : undefined;
|
|
||||||
return [locationKey, location];
|
|
||||||
}));
|
|
||||||
|
|
||||||
testInfo.annotations.push(...[...annotationLocations.values()].map(location => ({
|
|
||||||
type: 'warning', description: `This async call was not awaited by the end of the test. This can cause flakiness. It is recommended to run ESLint with "@typescript-eslint/no-floating-promises" to verify.`, location
|
|
||||||
})));
|
|
||||||
testInfo._floatingPromiseScope.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._isStopped) {
|
if (this._isStopped) {
|
||||||
// Run all remaining "afterAll" hooks and teardown all fixtures when worker is shutting down.
|
// Run all remaining "afterAll" hooks and teardown all fixtures when worker is shutting down.
|
||||||
// Mark as "cleaned up" early to avoid running cleanup twice.
|
// Mark as "cleaned up" early to avoid running cleanup twice.
|
||||||
|
@ -510,7 +493,7 @@ export class WorkerMain extends ProcessRunner {
|
||||||
continue;
|
continue;
|
||||||
const fn = async (fixtures: any) => {
|
const fn = async (fixtures: any) => {
|
||||||
const result = await modifier.fn(fixtures);
|
const result = await modifier.fn(fixtures);
|
||||||
testInfo._modifier(modifier.type, modifier.location, [!!result, modifier.description]);
|
testInfo[modifier.type](!!result, modifier.description);
|
||||||
};
|
};
|
||||||
inheritFixtureNames(modifier.fn, fn);
|
inheritFixtureNames(modifier.fn, fn);
|
||||||
runnables.push({
|
runnables.push({
|
||||||
|
@ -528,12 +511,12 @@ export class WorkerMain extends ProcessRunner {
|
||||||
private async _runBeforeAllHooksForSuite(suite: Suite, testInfo: TestInfoImpl) {
|
private async _runBeforeAllHooksForSuite(suite: Suite, testInfo: TestInfoImpl) {
|
||||||
if (this._activeSuites.has(suite))
|
if (this._activeSuites.has(suite))
|
||||||
return;
|
return;
|
||||||
const extraAnnotations: TestAnnotation[] = [];
|
const extraAnnotations: Annotation[] = [];
|
||||||
this._activeSuites.set(suite, extraAnnotations);
|
this._activeSuites.set(suite, extraAnnotations);
|
||||||
await this._runAllHooksForSuite(suite, testInfo, 'beforeAll', extraAnnotations);
|
await this._runAllHooksForSuite(suite, testInfo, 'beforeAll', extraAnnotations);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _runAllHooksForSuite(suite: Suite, testInfo: TestInfoImpl, type: 'beforeAll' | 'afterAll', extraAnnotations?: TestAnnotation[]) {
|
private async _runAllHooksForSuite(suite: Suite, testInfo: TestInfoImpl, type: 'beforeAll' | 'afterAll', extraAnnotations?: Annotation[]) {
|
||||||
// Always run all the hooks, and capture the first error.
|
// Always run all the hooks, and capture the first error.
|
||||||
let firstError: Error | undefined;
|
let firstError: Error | undefined;
|
||||||
for (const hook of this._collectHooksAndModifiers(suite, type, testInfo)) {
|
for (const hook of this._collectHooksAndModifiers(suite, type, testInfo)) {
|
||||||
|
|
|
@ -2049,10 +2049,6 @@ export type TestDetailsAnnotation = {
|
||||||
description?: string;
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TestAnnotation = TestDetailsAnnotation & {
|
|
||||||
location?: Location;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TestDetails = {
|
export type TestDetails = {
|
||||||
tag?: string | string[];
|
tag?: string | string[];
|
||||||
annotation?: TestDetailsAnnotation | TestDetailsAnnotation[];
|
annotation?: TestDetailsAnnotation | TestDetailsAnnotation[];
|
||||||
|
@ -9442,11 +9438,6 @@ export interface TestInfo {
|
||||||
* Optional description.
|
* Optional description.
|
||||||
*/
|
*/
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional location in the source where the annotation is added.
|
|
||||||
*/
|
|
||||||
location?: Location;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -451,11 +451,6 @@ export interface TestCase {
|
||||||
* Optional description.
|
* Optional description.
|
||||||
*/
|
*/
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional location in the source where the annotation is added.
|
|
||||||
*/
|
|
||||||
location?: Location;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -612,11 +607,6 @@ export interface TestResult {
|
||||||
* Optional description.
|
* Optional description.
|
||||||
*/
|
*/
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional location in the source where the annotation is added.
|
|
||||||
*/
|
|
||||||
location?: Location;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -732,11 +722,6 @@ export interface TestStep {
|
||||||
* Optional description.
|
* Optional description.
|
||||||
*/
|
*/
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional location in the source where the annotation is added.
|
|
||||||
*/
|
|
||||||
location?: Location;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -18,10 +18,11 @@ import * as React from 'react';
|
||||||
import './annotationsTab.css';
|
import './annotationsTab.css';
|
||||||
import { PlaceholderPanel } from './placeholderPanel';
|
import { PlaceholderPanel } from './placeholderPanel';
|
||||||
import { linkifyText } from '@web/renderUtils';
|
import { linkifyText } from '@web/renderUtils';
|
||||||
import type { TestAnnotation } from '@playwright/test';
|
|
||||||
|
type Annotation = { type: string; description?: string; };
|
||||||
|
|
||||||
export const AnnotationsTab: React.FunctionComponent<{
|
export const AnnotationsTab: React.FunctionComponent<{
|
||||||
annotations: TestAnnotation[],
|
annotations: Annotation[],
|
||||||
}> = ({ annotations }) => {
|
}> = ({ annotations }) => {
|
||||||
|
|
||||||
if (!annotations.length)
|
if (!annotations.length)
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
import { useIsLLMAvailable, useLLMChat } from './llm';
|
import { useIsLLMAvailable, useLLMChat } from './llm';
|
||||||
import { useAsyncMemo } from '@web/uiUtils';
|
import { useAsyncMemo } from '@web/uiUtils';
|
||||||
import { attachmentURL } from './attachmentsTab';
|
import { attachmentURL } from './attachmentsTab';
|
||||||
|
import { fixTestInstructions } from '@web/prompts';
|
||||||
|
|
||||||
const CopyPromptButton: React.FC<{ prompt: string }> = ({ prompt }) => {
|
const CopyPromptButton: React.FC<{ prompt: string }> = ({ prompt }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -67,10 +68,10 @@ function Error({ message, error, errorId, sdkLanguage, revealInSource }: { messa
|
||||||
}
|
}
|
||||||
|
|
||||||
const prompt = useAsyncMemo(async () => {
|
const prompt = useAsyncMemo(async () => {
|
||||||
if (!error.prompt)
|
if (!error.context)
|
||||||
return;
|
return;
|
||||||
const response = await fetch(attachmentURL(error.prompt));
|
const response = await fetch(attachmentURL(error.context));
|
||||||
return await response.text();
|
return fixTestInstructions + await response.text();
|
||||||
}, [error], undefined);
|
}, [error], undefined);
|
||||||
|
|
||||||
return <div style={{ display: 'flex', flexDirection: 'column', overflowX: 'clip' }}>
|
return <div style={{ display: 'flex', flexDirection: 'column', overflowX: 'clip' }}>
|
||||||
|
|
|
@ -56,7 +56,7 @@ export type ErrorDescription = {
|
||||||
action?: ActionTraceEventInContext;
|
action?: ActionTraceEventInContext;
|
||||||
stack?: StackFrame[];
|
stack?: StackFrame[];
|
||||||
message: string;
|
message: string;
|
||||||
prompt?: trace.AfterActionTraceEventAttachment & { traceUrl: string };
|
context?: trace.AfterActionTraceEventAttachment & { traceUrl: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Attachment = trace.AfterActionTraceEventAttachment & { traceUrl: string };
|
export type Attachment = trace.AfterActionTraceEventAttachment & { traceUrl: string };
|
||||||
|
@ -141,7 +141,7 @@ export class MultiTraceModel {
|
||||||
return this.errors.filter(e => !!e.message).map((error, i) => ({
|
return this.errors.filter(e => !!e.message).map((error, i) => ({
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
message: error.message,
|
message: error.message,
|
||||||
prompt: this.attachments.find(a => a.name === `_prompt-${i}`),
|
context: this.attachments.find(a => a.name === `_error-context-${i}`),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,6 @@ import { testStatusIcon, testStatusText } from './testUtils';
|
||||||
import type { UITestStatus } from './testUtils';
|
import type { UITestStatus } from './testUtils';
|
||||||
import type { AfterActionTraceEventAttachment } from '@trace/trace';
|
import type { AfterActionTraceEventAttachment } from '@trace/trace';
|
||||||
import type { HighlightedElement } from './snapshotTab';
|
import type { HighlightedElement } from './snapshotTab';
|
||||||
import type { TestAnnotation } from '@playwright/test';
|
|
||||||
|
|
||||||
export const Workbench: React.FunctionComponent<{
|
export const Workbench: React.FunctionComponent<{
|
||||||
model?: modelUtil.MultiTraceModel,
|
model?: modelUtil.MultiTraceModel,
|
||||||
|
@ -52,7 +51,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
isLive?: boolean,
|
isLive?: boolean,
|
||||||
hideTimeline?: boolean,
|
hideTimeline?: boolean,
|
||||||
status?: UITestStatus,
|
status?: UITestStatus,
|
||||||
annotations?: TestAnnotation[];
|
annotations?: { type: string; description?: string; }[];
|
||||||
inert?: boolean,
|
inert?: boolean,
|
||||||
onOpenExternally?: (location: modelUtil.SourceLocation) => void,
|
onOpenExternally?: (location: modelUtil.SourceLocation) => void,
|
||||||
revealSource?: boolean,
|
revealSource?: boolean,
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const fixTestInstructions = `
|
||||||
|
# Instructions
|
||||||
|
|
||||||
|
- Following Playwright test failed.
|
||||||
|
- Explain why, be concise, respect Playwright best practices.
|
||||||
|
- Provide a snippet of code with the fix, if possible.
|
||||||
|
`.trimStart();
|
|
@ -61,7 +61,7 @@ test('should access annotations in fixture', async ({ runInlineTest }) => {
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
const test = report.suites[0].specs[0].tests[0];
|
const test = report.suites[0].specs[0].tests[0];
|
||||||
expect(test.annotations).toEqual([
|
expect(test.annotations).toEqual([
|
||||||
{ type: 'slow', description: 'just slow', location: { file: expect.any(String), line: 10, column: 14 } },
|
{ type: 'slow', description: 'just slow' },
|
||||||
{ type: 'myname', description: 'hello' }
|
{ type: 'myname', description: 'hello' }
|
||||||
]);
|
]);
|
||||||
expect(test.results[0].stdout).toEqual([{ text: 'console.log\n' }]);
|
expect(test.results[0].stdout).toEqual([{ text: 'console.log\n' }]);
|
||||||
|
|
|
@ -167,7 +167,7 @@ test('should print debug log when failed to connect', async ({ runInlineTest })
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(result.failed).toBe(1);
|
expect(result.failed).toBe(1);
|
||||||
expect(result.output).toContain('b-debug-log-string');
|
expect(result.output).toContain('b-debug-log-string');
|
||||||
expect(result.results[0].attachments).toEqual([expect.objectContaining({ name: '_prompt-0' })]);
|
expect(result.results[0].attachments).toEqual([expect.objectContaining({ name: '_error-context-0' })]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should record trace', async ({ runInlineTest }) => {
|
test('should record trace', async ({ runInlineTest }) => {
|
||||||
|
@ -223,7 +223,7 @@ test('should record trace', async ({ runInlineTest }) => {
|
||||||
'After Hooks',
|
'After Hooks',
|
||||||
'fixture: page',
|
'fixture: page',
|
||||||
'fixture: context',
|
'fixture: context',
|
||||||
'_attach "_prompt-0"',
|
'_attach "_error-context-0"',
|
||||||
'Worker Cleanup',
|
'Worker Cleanup',
|
||||||
'fixture: browser',
|
'fixture: browser',
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -510,13 +510,13 @@ test('should work with video: on-first-retry', async ({ runInlineTest }) => {
|
||||||
expect(fs.existsSync(dirPass)).toBeFalsy();
|
expect(fs.existsSync(dirPass)).toBeFalsy();
|
||||||
|
|
||||||
const dirFail = test.info().outputPath('test-results', 'a-fail-chromium');
|
const dirFail = test.info().outputPath('test-results', 'a-fail-chromium');
|
||||||
expect(fs.readdirSync(dirFail)).toEqual(['prompt.md']);
|
expect(fs.readdirSync(dirFail)).toEqual(['error-context.md']);
|
||||||
|
|
||||||
const dirRetry = test.info().outputPath('test-results', 'a-fail-chromium-retry1');
|
const dirRetry = test.info().outputPath('test-results', 'a-fail-chromium-retry1');
|
||||||
const videoFailRetry = fs.readdirSync(dirRetry).find(file => file.endsWith('webm'));
|
const videoFailRetry = fs.readdirSync(dirRetry).find(file => file.endsWith('webm'));
|
||||||
expect(videoFailRetry).toBeTruthy();
|
expect(videoFailRetry).toBeTruthy();
|
||||||
|
|
||||||
const errorPrompt = expect.objectContaining({ name: '_prompt-0' });
|
const errorPrompt = expect.objectContaining({ name: '_error-context-0' });
|
||||||
expect(result.report.suites[0].specs[1].tests[0].results[0].attachments).toEqual([errorPrompt]);
|
expect(result.report.suites[0].specs[1].tests[0].results[0].attachments).toEqual([errorPrompt]);
|
||||||
expect(result.report.suites[0].specs[1].tests[0].results[1].attachments).toEqual([{
|
expect(result.report.suites[0].specs[1].tests[0].results[1].attachments).toEqual([{
|
||||||
name: 'video',
|
name: 'video',
|
||||||
|
|
|
@ -486,72 +486,5 @@ for (const useIntermediateMergeReport of [false, true] as const) {
|
||||||
expect(text).toContain('› passes @bar1 @bar2 (');
|
expect(text).toContain('› passes @bar1 @bar2 (');
|
||||||
expect(text).toContain('› passes @baz1 @baz2 (');
|
expect(text).toContain('› passes @baz1 @baz2 (');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show warnings on failing tests', async ({ runInlineTest }) => {
|
|
||||||
const result = await runInlineTest({
|
|
||||||
'a.spec.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('fail', async ({ page }, testInfo) => {
|
|
||||||
testInfo.annotations.push({ type: 'warning', description: 'foo' });
|
|
||||||
expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
|
||||||
throw new Error();
|
|
||||||
});
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
expect(result.exitCode).toBe(1);
|
|
||||||
expect(result.passed).toBe(0);
|
|
||||||
expect(result.output).toContain('Warning: a.spec.ts:5:41: This async call was not awaited by the end of the test.');
|
|
||||||
expect(result.output).toContain('Warning: foo');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not show warnings on passing tests', async ({ runInlineTest }) => {
|
|
||||||
const result = await runInlineTest({
|
|
||||||
'a.spec.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('success', async ({ page }, testInfo) => {
|
|
||||||
testInfo.annotations.push({ type: 'warning', description: 'foo' });
|
|
||||||
});
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
expect(result.exitCode).toBe(0);
|
|
||||||
expect(result.passed).toBe(1);
|
|
||||||
expect(result.output).not.toContain('Warning: foo');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should properly sort warnings', async ({ runInlineTest }) => {
|
|
||||||
const result = await runInlineTest({
|
|
||||||
'external.js': `
|
|
||||||
import { expect } from '@playwright/test';
|
|
||||||
export const externalAsyncCall = (page) => {
|
|
||||||
expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
|
||||||
};
|
|
||||||
`,
|
|
||||||
'a.spec.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import { externalAsyncCall } from './external.js';
|
|
||||||
test('fail a', async ({ page }, testInfo) => {
|
|
||||||
testInfo.annotations.push({ type: 'warning', description: 'foo' });
|
|
||||||
externalAsyncCall(page);
|
|
||||||
expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
|
||||||
testInfo.annotations.push({ type: 'warning', description: 'bar' });
|
|
||||||
throw new Error();
|
|
||||||
});
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
expect(result.exitCode).toBe(1);
|
|
||||||
expect(result.passed).toBe(0);
|
|
||||||
expect(result.output).toContain('Warning: a.spec.ts:7:41: This async call was not awaited by the end of the test.');
|
|
||||||
expect(result.output).toContain('Warning: external.js:4:41: This async call was not awaited by the end of the test.');
|
|
||||||
expect(result.output).toContain('Warning: foo');
|
|
||||||
expect(result.output).toContain('Warning: bar');
|
|
||||||
|
|
||||||
const manualIndexFoo = result.output.indexOf('Warning: foo');
|
|
||||||
const manualIndexBar = result.output.indexOf('Warning: bar');
|
|
||||||
const externalIndex = result.output.indexOf('Warning: external.js:4:41');
|
|
||||||
const specIndex = result.output.indexOf('Warning: a.spec.ts:7:41');
|
|
||||||
expect(specIndex).toBeLessThan(externalIndex);
|
|
||||||
expect(externalIndex).toBeLessThan(manualIndexFoo);
|
|
||||||
expect(manualIndexFoo).toBeLessThan(manualIndexBar);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1721,9 +1721,6 @@ test('merge reports with different rootDirs and path separators', async ({ runIn
|
||||||
console.log('test:', test.location.file);
|
console.log('test:', test.location.file);
|
||||||
console.log('test title:', test.titlePath()[2]);
|
console.log('test title:', test.titlePath()[2]);
|
||||||
}
|
}
|
||||||
onTestEnd(test) {
|
|
||||||
console.log('annotations:', test.annotations.map(a => 'type: ' + a.type + ', description: ' + a.description + ', file: ' + a.location.file).join(','));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
'merge.config.ts': `module.exports = {
|
'merge.config.ts': `module.exports = {
|
||||||
|
@ -1735,7 +1732,7 @@ test('merge reports with different rootDirs and path separators', async ({ runIn
|
||||||
};`,
|
};`,
|
||||||
'dir1/tests1/a.test.js': `
|
'dir1/tests1/a.test.js': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
test('math 1', { annotation: { type: 'warning', description: 'Some warning' } }, async ({}) => { });
|
test('math 1', async ({}) => { });
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
await runInlineTest(files1, { workers: 1 }, undefined, { additionalArgs: ['--config', test.info().outputPath('dir1/playwright.config.ts')] });
|
await runInlineTest(files1, { workers: 1 }, undefined, { additionalArgs: ['--config', test.info().outputPath('dir1/playwright.config.ts')] });
|
||||||
|
@ -1746,7 +1743,7 @@ test('merge reports with different rootDirs and path separators', async ({ runIn
|
||||||
};`,
|
};`,
|
||||||
'dir2/tests2/b.test.js': `
|
'dir2/tests2/b.test.js': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
test('math 2', { annotation: { type: 'issue' } }, async ({}) => { });
|
test('math 2', async ({}) => { });
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
await runInlineTest(files2, { workers: 1 }, undefined, { additionalArgs: ['--config', test.info().outputPath('dir2/playwright.config.ts')] });
|
await runInlineTest(files2, { workers: 1 }, undefined, { additionalArgs: ['--config', test.info().outputPath('dir2/playwright.config.ts')] });
|
||||||
|
@ -1768,16 +1765,12 @@ test('merge reports with different rootDirs and path separators', async ({ runIn
|
||||||
|
|
||||||
{
|
{
|
||||||
const { exitCode, output } = await mergeReports(allReportsDir, undefined, { additionalArgs: ['--config', 'merge.config.ts'] });
|
const { exitCode, output } = await mergeReports(allReportsDir, undefined, { additionalArgs: ['--config', 'merge.config.ts'] });
|
||||||
const testPath1 = test.info().outputPath('mergeRoot', 'tests1', 'a.test.js');
|
|
||||||
const testPath2 = test.info().outputPath('mergeRoot', 'tests2', 'b.test.js');
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(output).toContain(`rootDir: ${test.info().outputPath('mergeRoot')}`);
|
expect(output).toContain(`rootDir: ${test.info().outputPath('mergeRoot')}`);
|
||||||
expect(output).toContain(`test: ${testPath1}`);
|
expect(output).toContain(`test: ${test.info().outputPath('mergeRoot', 'tests1', 'a.test.js')}`);
|
||||||
expect(output).toContain(`test title: ${'tests1' + path.sep + 'a.test.js'}`);
|
expect(output).toContain(`test title: ${'tests1' + path.sep + 'a.test.js'}`);
|
||||||
expect(output).toContain(`annotations: type: warning, description: Some warning, file: ${testPath1}`);
|
expect(output).toContain(`test: ${test.info().outputPath('mergeRoot', 'tests2', 'b.test.js')}`);
|
||||||
expect(output).toContain(`test: ${testPath2}`);
|
|
||||||
expect(output).toContain(`test title: ${'tests2' + path.sep + 'b.test.js'}`);
|
expect(output).toContain(`test title: ${'tests2' + path.sep + 'b.test.js'}`);
|
||||||
expect(output).toContain(`annotations: type: issue, description: undefined, file: ${testPath2}`);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1793,9 +1786,6 @@ test('merge reports without --config preserves path separators', async ({ runInl
|
||||||
console.log('test:', test.location.file);
|
console.log('test:', test.location.file);
|
||||||
console.log('test title:', test.titlePath()[2]);
|
console.log('test title:', test.titlePath()[2]);
|
||||||
}
|
}
|
||||||
onTestEnd(test) {
|
|
||||||
console.log('annotations:', test.annotations.map(a => 'type: ' + a.type + ', description: ' + a.description + ', file: ' + a.location.file).join(','));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
'dir1/playwright.config.ts': `module.exports = {
|
'dir1/playwright.config.ts': `module.exports = {
|
||||||
|
@ -1803,11 +1793,11 @@ test('merge reports without --config preserves path separators', async ({ runInl
|
||||||
};`,
|
};`,
|
||||||
'dir1/tests1/a.test.js': `
|
'dir1/tests1/a.test.js': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
test('math 1', { annotation: { type: 'warning', description: 'Some warning' } }, async ({}) => { });
|
test('math 1', async ({}) => { });
|
||||||
`,
|
`,
|
||||||
'dir1/tests2/b.test.js': `
|
'dir1/tests2/b.test.js': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
test('math 2', { annotation: { type: 'issue' } }, async ({}) => { });
|
test('math 2', async ({}) => { });
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
await runInlineTest(files1, { workers: 1 }, undefined, { additionalArgs: ['--config', test.info().outputPath('dir1/playwright.config.ts')] });
|
await runInlineTest(files1, { workers: 1 }, undefined, { additionalArgs: ['--config', test.info().outputPath('dir1/playwright.config.ts')] });
|
||||||
|
@ -1827,15 +1817,11 @@ test('merge reports without --config preserves path separators', async ({ runInl
|
||||||
const { exitCode, output } = await mergeReports(allReportsDir, undefined, { additionalArgs: ['--reporter', './echo-reporter.js'] });
|
const { exitCode, output } = await mergeReports(allReportsDir, undefined, { additionalArgs: ['--reporter', './echo-reporter.js'] });
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
const otherSeparator = path.sep === '/' ? '\\' : '/';
|
const otherSeparator = path.sep === '/' ? '\\' : '/';
|
||||||
const testPath1 = test.info().outputPath('dir1', 'tests1', 'a.test.js').replaceAll(path.sep, otherSeparator);
|
|
||||||
const testPath2 = test.info().outputPath('dir1', 'tests2', 'b.test.js').replaceAll(path.sep, otherSeparator);
|
|
||||||
expect(output).toContain(`rootDir: ${test.info().outputPath('dir1').replaceAll(path.sep, otherSeparator)}`);
|
expect(output).toContain(`rootDir: ${test.info().outputPath('dir1').replaceAll(path.sep, otherSeparator)}`);
|
||||||
expect(output).toContain(`test: ${testPath1}`);
|
expect(output).toContain(`test: ${test.info().outputPath('dir1', 'tests1', 'a.test.js').replaceAll(path.sep, otherSeparator)}`);
|
||||||
expect(output).toContain(`test title: ${'tests1' + otherSeparator + 'a.test.js'}`);
|
expect(output).toContain(`test title: ${'tests1' + otherSeparator + 'a.test.js'}`);
|
||||||
expect(output).toContain(`annotations: type: warning, description: Some warning, file: ${testPath1}`);
|
expect(output).toContain(`test: ${test.info().outputPath('dir1', 'tests2', 'b.test.js').replaceAll(path.sep, otherSeparator)}`);
|
||||||
expect(output).toContain(`test: ${testPath2}`);
|
|
||||||
expect(output).toContain(`test title: ${'tests2' + otherSeparator + 'b.test.js'}`);
|
expect(output).toContain(`test title: ${'tests2' + otherSeparator + 'b.test.js'}`);
|
||||||
expect(output).toContain(`annotations: type: issue, description: undefined, file: ${testPath2}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('merge reports must not change test ids when there is no need to', async ({ runInlineTest, mergeReports }) => {
|
test('merge reports must not change test ids when there is no need to', async ({ runInlineTest, mergeReports }) => {
|
||||||
|
|
|
@ -359,7 +359,7 @@ test('should report parallelIndex', async ({ runInlineTest }, testInfo) => {
|
||||||
test('attaches error context', async ({ runInlineTest }) => {
|
test('attaches error context', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
export default { use: { _optionAttachErrorContext: true } };
|
export default { use: { _optionErrorContext: { format: 'json' } } };
|
||||||
`,
|
`,
|
||||||
'a.test.js': `
|
'a.test.js': `
|
||||||
const { test, expect } = require('@playwright/test');
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
|
@ -190,7 +190,7 @@ for (const useIntermediateMergeReport of [false, true] as const) {
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show error prompt with relative path', async ({ runInlineTest, useIntermediateMergeReport }) => {
|
test('should show error context with relative path', async ({ runInlineTest, useIntermediateMergeReport }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'a.test.js': `
|
'a.test.js': `
|
||||||
const { test, expect } = require('@playwright/test');
|
const { test, expect } = require('@playwright/test');
|
||||||
|
@ -201,9 +201,9 @@ for (const useIntermediateMergeReport of [false, true] as const) {
|
||||||
}, { reporter: 'line' });
|
}, { reporter: 'line' });
|
||||||
const text = result.output;
|
const text = result.output;
|
||||||
if (useIntermediateMergeReport)
|
if (useIntermediateMergeReport)
|
||||||
expect(text).toContain(`Error Prompt: ${path.join('blob-report', 'resources')}`);
|
expect(text).toContain(`Error Context: ${path.join('blob-report', 'resources')}`);
|
||||||
else
|
else
|
||||||
expect(text).toContain(`Error Prompt: ${path.join('test-results', 'a-one', 'prompt.md')}`);
|
expect(text).toContain(`Error Context: ${path.join('test-results', 'a-one', 'error-context.md')}`);
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -584,9 +584,7 @@ test('should report annotations from test declaration', async ({ runInlineTest }
|
||||||
const visit = suite => {
|
const visit = suite => {
|
||||||
for (const test of suite.tests || []) {
|
for (const test of suite.tests || []) {
|
||||||
const annotations = test.annotations.map(a => {
|
const annotations = test.annotations.map(a => {
|
||||||
const description = a.description ? a.type + '=' + a.description : a.type;
|
return a.description ? a.type + '=' + a.description : a.type;
|
||||||
const location = a.location ? '(' + a.location.line + ':' + a.location.column + ')' : '';
|
|
||||||
return description + location;
|
|
||||||
});
|
});
|
||||||
console.log('\\n%%title=' + test.title + ', annotations=' + annotations.join(','));
|
console.log('\\n%%title=' + test.title + ', annotations=' + annotations.join(','));
|
||||||
}
|
}
|
||||||
|
@ -611,7 +609,7 @@ test('should report annotations from test declaration', async ({ runInlineTest }
|
||||||
expect(test.info().annotations).toEqual([]);
|
expect(test.info().annotations).toEqual([]);
|
||||||
});
|
});
|
||||||
test('foo', { annotation: { type: 'foo' } }, () => {
|
test('foo', { annotation: { type: 'foo' } }, () => {
|
||||||
expect(test.info().annotations).toEqual([{ type: 'foo', location: { file: expect.any(String), line: 6, column: 11 } }]);
|
expect(test.info().annotations).toEqual([{ type: 'foo' }]);
|
||||||
});
|
});
|
||||||
test('foo-bar', {
|
test('foo-bar', {
|
||||||
annotation: [
|
annotation: [
|
||||||
|
@ -620,8 +618,8 @@ test('should report annotations from test declaration', async ({ runInlineTest }
|
||||||
],
|
],
|
||||||
}, () => {
|
}, () => {
|
||||||
expect(test.info().annotations).toEqual([
|
expect(test.info().annotations).toEqual([
|
||||||
{ type: 'foo', description: 'desc', location: { file: expect.any(String), line: 9, column: 11 } },
|
{ type: 'foo', description: 'desc' },
|
||||||
{ type: 'bar', location: { file: expect.any(String), line: 9, column: 11 } },
|
{ type: 'bar' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
test.skip('skip-foo', { annotation: { type: 'foo' } }, () => {
|
test.skip('skip-foo', { annotation: { type: 'foo' } }, () => {
|
||||||
|
@ -638,14 +636,11 @@ test('should report annotations from test declaration', async ({ runInlineTest }
|
||||||
});
|
});
|
||||||
test.describe('suite', { annotation: { type: 'foo' } }, () => {
|
test.describe('suite', { annotation: { type: 'foo' } }, () => {
|
||||||
test('foo-suite', () => {
|
test('foo-suite', () => {
|
||||||
expect(test.info().annotations).toEqual([{ type: 'foo', location: { file: expect.any(String), line: 32, column: 12 } }]);
|
expect(test.info().annotations).toEqual([{ type: 'foo' }]);
|
||||||
});
|
});
|
||||||
test.describe('inner', { annotation: { type: 'bar' } }, () => {
|
test.describe('inner', { annotation: { type: 'bar' } }, () => {
|
||||||
test('foo-bar-suite', () => {
|
test('foo-bar-suite', () => {
|
||||||
expect(test.info().annotations).toEqual([
|
expect(test.info().annotations).toEqual([{ type: 'foo' }, { type: 'bar' }]);
|
||||||
{ type: 'foo', location: { file: expect.any(String), line: 32, column: 12 } },
|
|
||||||
{ type: 'bar', location: { file: expect.any(String), line: 36, column: 14 } }
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -662,15 +657,15 @@ test('should report annotations from test declaration', async ({ runInlineTest }
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.outputLines).toEqual([
|
expect(result.outputLines).toEqual([
|
||||||
`title=none, annotations=`,
|
`title=none, annotations=`,
|
||||||
`title=foo, annotations=foo(6:11)`,
|
`title=foo, annotations=foo`,
|
||||||
`title=foo-bar, annotations=foo=desc(9:11),bar(9:11)`,
|
`title=foo-bar, annotations=foo=desc,bar`,
|
||||||
`title=skip-foo, annotations=foo(20:12),skip(20:12)`,
|
`title=skip-foo, annotations=foo,skip`,
|
||||||
`title=fixme-bar, annotations=bar(22:12),fixme(22:12)`,
|
`title=fixme-bar, annotations=bar,fixme`,
|
||||||
`title=fail-foo-bar, annotations=foo(24:12),bar=desc(24:12),fail(24:12)`,
|
`title=fail-foo-bar, annotations=foo,bar=desc,fail`,
|
||||||
`title=foo-suite, annotations=foo(32:12)`,
|
`title=foo-suite, annotations=foo`,
|
||||||
`title=foo-bar-suite, annotations=foo(32:12),bar(36:14)`,
|
`title=foo-bar-suite, annotations=foo,bar`,
|
||||||
`title=skip-foo-suite, annotations=foo(45:21),skip(45:21)`,
|
`title=skip-foo-suite, annotations=foo,skip`,
|
||||||
`title=fixme-bar-suite, annotations=bar(49:21),fixme(49:21)`,
|
`title=fixme-bar-suite, annotations=bar,fixme`,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -260,5 +260,5 @@ test('failed and skipped on retry should be marked as flaky', async ({ runInline
|
||||||
expect(result.failed).toBe(0);
|
expect(result.failed).toBe(0);
|
||||||
expect(result.flaky).toBe(1);
|
expect(result.flaky).toBe(1);
|
||||||
expect(result.output).toContain('Failed on first run');
|
expect(result.output).toContain('Failed on first run');
|
||||||
expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([{ type: 'skip', description: 'Skipped on first retry', location: expect.anything() }]);
|
expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([{ type: 'skip', description: 'Skipped on first retry' }]);
|
||||||
});
|
});
|
||||||
|
|
|
@ -113,19 +113,19 @@ test('test modifiers should work', async ({ runInlineTest }) => {
|
||||||
expectTest('passed3', 'passed', 'passed', []);
|
expectTest('passed3', 'passed', 'passed', []);
|
||||||
expectTest('passed4', 'passed', 'passed', []);
|
expectTest('passed4', 'passed', 'passed', []);
|
||||||
expectTest('passed5', 'passed', 'passed', []);
|
expectTest('passed5', 'passed', 'passed', []);
|
||||||
expectTest('skipped1', 'skipped', 'skipped', [{ type: 'skip', location: { file: expect.any(String), line: 20, column: 14 } }]);
|
expectTest('skipped1', 'skipped', 'skipped', [{ type: 'skip' }]);
|
||||||
expectTest('skipped2', 'skipped', 'skipped', [{ type: 'skip', location: { file: expect.any(String), line: 23, column: 14 } }]);
|
expectTest('skipped2', 'skipped', 'skipped', [{ type: 'skip' }]);
|
||||||
expectTest('skipped3', 'skipped', 'skipped', [{ type: 'skip', location: { file: expect.any(String), line: 26, column: 14 } }]);
|
expectTest('skipped3', 'skipped', 'skipped', [{ type: 'skip' }]);
|
||||||
expectTest('skipped4', 'skipped', 'skipped', [{ type: 'skip', description: 'reason', location: { file: expect.any(String), line: 29, column: 14 } }]);
|
expectTest('skipped4', 'skipped', 'skipped', [{ type: 'skip', description: 'reason' }]);
|
||||||
expectTest('skipped5', 'skipped', 'skipped', [{ type: 'fixme', location: { file: expect.any(String), line: 32, column: 14 } }]);
|
expectTest('skipped5', 'skipped', 'skipped', [{ type: 'fixme' }]);
|
||||||
expectTest('skipped6', 'skipped', 'skipped', [{ type: 'fixme', description: 'reason', location: { file: expect.any(String), line: 35, column: 14 } }]);
|
expectTest('skipped6', 'skipped', 'skipped', [{ type: 'fixme', description: 'reason' }]);
|
||||||
expectTest('failed1', 'failed', 'failed', [{ type: 'fail', location: { file: expect.any(String), line: 39, column: 14 } }]);
|
expectTest('failed1', 'failed', 'failed', [{ type: 'fail' }]);
|
||||||
expectTest('failed2', 'failed', 'failed', [{ type: 'fail', location: { file: expect.any(String), line: 43, column: 14 } }]);
|
expectTest('failed2', 'failed', 'failed', [{ type: 'fail' }]);
|
||||||
expectTest('failed3', 'failed', 'failed', [{ type: 'fail', location: { file: expect.any(String), line: 47, column: 14 } }]);
|
expectTest('failed3', 'failed', 'failed', [{ type: 'fail' }]);
|
||||||
expectTest('failed4', 'failed', 'failed', [{ type: 'fail', description: 'reason', location: { file: expect.any(String), line: 51, column: 14 } }]);
|
expectTest('failed4', 'failed', 'failed', [{ type: 'fail', description: 'reason' }]);
|
||||||
expectTest('suite1', 'skipped', 'skipped', [{ type: 'skip', location: { file: expect.any(String), line: 56, column: 14 } }]);
|
expectTest('suite1', 'skipped', 'skipped', [{ type: 'skip' }]);
|
||||||
expectTest('suite2', 'skipped', 'skipped', [{ type: 'skip', location: { file: expect.any(String), line: 61, column: 14 } }]);
|
expectTest('suite2', 'skipped', 'skipped', [{ type: 'skip' }]);
|
||||||
expectTest('suite3', 'skipped', 'skipped', [{ type: 'skip', description: 'reason', location: { file: expect.any(String), line: 66, column: 14 } }]);
|
expectTest('suite3', 'skipped', 'skipped', [{ type: 'skip', description: 'reason' }]);
|
||||||
expectTest('suite4', 'passed', 'passed', []);
|
expectTest('suite4', 'passed', 'passed', []);
|
||||||
expect(result.passed).toBe(10);
|
expect(result.passed).toBe(10);
|
||||||
expect(result.skipped).toBe(9);
|
expect(result.skipped).toBe(9);
|
||||||
|
@ -407,7 +407,7 @@ test('should skip inside fixture', async ({ runInlineTest }) => {
|
||||||
});
|
});
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.skipped).toBe(1);
|
expect(result.skipped).toBe(1);
|
||||||
expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([{ type: 'skip', description: 'reason', location: { file: expect.any(String), line: 5, column: 20 } }]);
|
expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([{ type: 'skip', description: 'reason' }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('modifier with a function should throw in the test', async ({ runInlineTest }) => {
|
test('modifier with a function should throw in the test', async ({ runInlineTest }) => {
|
||||||
|
@ -460,8 +460,8 @@ test('test.skip with worker fixtures only should skip before hooks and tests', a
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
expect(result.skipped).toBe(2);
|
expect(result.skipped).toBe(2);
|
||||||
expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([]);
|
expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([]);
|
||||||
expect(result.report.suites[0].suites![0].specs[0].tests[0].annotations).toEqual([{ type: 'skip', description: 'reason', location: { file: expect.any(String), line: 14, column: 14 } }]);
|
expect(result.report.suites[0].suites![0].specs[0].tests[0].annotations).toEqual([{ type: 'skip', description: 'reason' }]);
|
||||||
expect(result.report.suites[0].suites![0].suites![0].specs[0].tests[0].annotations).toEqual([{ type: 'skip', description: 'reason', location: { file: expect.any(String), line: 14, column: 14 } }]);
|
expect(result.report.suites[0].suites![0].suites![0].specs[0].tests[0].annotations).toEqual([{ type: 'skip', description: 'reason' }]);
|
||||||
expect(result.outputLines).toEqual([
|
expect(result.outputLines).toEqual([
|
||||||
'beforeEach',
|
'beforeEach',
|
||||||
'passed',
|
'passed',
|
||||||
|
@ -493,8 +493,8 @@ test('test.skip without a callback in describe block should skip hooks', async (
|
||||||
});
|
});
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.skipped).toBe(2);
|
expect(result.skipped).toBe(2);
|
||||||
expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([{ type: 'skip', description: 'reason', location: { file: expect.any(String), line: 10, column: 12 } }]);
|
expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([{ type: 'skip', description: 'reason' }]);
|
||||||
expect(result.report.suites[0].suites![0].specs[0].tests[0].annotations).toEqual([{ type: 'skip', description: 'reason', location: { file: expect.any(String), line: 10, column: 12 } }]);
|
expect(result.report.suites[0].suites![0].specs[0].tests[0].annotations).toEqual([{ type: 'skip', description: 'reason' }]);
|
||||||
expect(result.output).not.toContain('%%');
|
expect(result.output).not.toContain('%%');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -598,8 +598,8 @@ test('should skip all tests from beforeAll', async ({ runInlineTest }) => {
|
||||||
'beforeAll',
|
'beforeAll',
|
||||||
'afterAll',
|
'afterAll',
|
||||||
]);
|
]);
|
||||||
expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([{ type: 'skip', description: 'reason', location: { file: expect.any(String), line: 5, column: 14 } }]);
|
expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([{ type: 'skip', description: 'reason' }]);
|
||||||
expect(result.report.suites[0].specs[1].tests[0].annotations).toEqual([{ type: 'skip', description: 'reason', location: { file: expect.any(String), line: 5, column: 14 } }]);
|
expect(result.report.suites[0].specs[1].tests[0].annotations).toEqual([{ type: 'skip', description: 'reason' }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should report skipped tests in-order with correct properties', async ({ runInlineTest }) => {
|
test('should report skipped tests in-order with correct properties', async ({ runInlineTest }) => {
|
||||||
|
@ -695,9 +695,9 @@ test('static modifiers should be added in serial mode', async ({ runInlineTest }
|
||||||
expect(result.passed).toBe(0);
|
expect(result.passed).toBe(0);
|
||||||
expect(result.skipped).toBe(2);
|
expect(result.skipped).toBe(2);
|
||||||
expect(result.didNotRun).toBe(1);
|
expect(result.didNotRun).toBe(1);
|
||||||
expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([{ type: 'slow', location: { file: expect.any(String), line: 6, column: 14 } }]);
|
expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([{ type: 'slow' }]);
|
||||||
expect(result.report.suites[0].specs[1].tests[0].annotations).toEqual([{ type: 'fixme', location: { file: expect.any(String), line: 9, column: 12 } }]);
|
expect(result.report.suites[0].specs[1].tests[0].annotations).toEqual([{ type: 'fixme' }]);
|
||||||
expect(result.report.suites[0].specs[2].tests[0].annotations).toEqual([{ type: 'skip', location: { file: expect.any(String), line: 11, column: 12 } }]);
|
expect(result.report.suites[0].specs[2].tests[0].annotations).toEqual([{ type: 'skip' }]);
|
||||||
expect(result.report.suites[0].specs[3].tests[0].annotations).toEqual([]);
|
expect(result.report.suites[0].specs[3].tests[0].annotations).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -721,18 +721,9 @@ test('should contain only one slow modifier', async ({ runInlineTest }) => {
|
||||||
});
|
});
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([
|
expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([{ type: 'fixme' }, { type: 'issue', description: 'my-value' }]);
|
||||||
{ type: 'fixme', location: { file: expect.any(String), line: 3, column: 12 } },
|
expect(result.report.suites[1].specs[0].tests[0].annotations).toEqual([{ type: 'skip' }, { type: 'issue', description: 'my-value' }]);
|
||||||
{ type: 'issue', description: 'my-value', location: { file: expect.any(String), line: 4, column: 11 } }
|
expect(result.report.suites[2].specs[0].tests[0].annotations).toEqual([{ type: 'slow' }, { type: 'issue', description: 'my-value' }]);
|
||||||
]);
|
|
||||||
expect(result.report.suites[1].specs[0].tests[0].annotations).toEqual([
|
|
||||||
{ type: 'skip', location: { file: expect.any(String), line: 3, column: 12 } },
|
|
||||||
{ type: 'issue', description: 'my-value', location: { file: expect.any(String), line: 4, column: 11 } }
|
|
||||||
]);
|
|
||||||
expect(result.report.suites[2].specs[0].tests[0].annotations).toEqual([
|
|
||||||
{ type: 'slow', location: { file: expect.any(String), line: 3, column: 12 } },
|
|
||||||
{ type: 'issue', description: 'my-value', location: { file: expect.any(String), line: 4, column: 11 } }
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should skip beforeEach hooks upon modifiers', async ({ runInlineTest }) => {
|
test('should skip beforeEach hooks upon modifiers', async ({ runInlineTest }) => {
|
||||||
|
|
|
@ -1,291 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { test, expect } from './playwright-test-fixtures';
|
|
||||||
|
|
||||||
const description = 'This async call was not awaited by the end of the test. This can cause flakiness. It is recommended to run ESLint with "@typescript-eslint/no-floating-promises" to verify.';
|
|
||||||
|
|
||||||
test.describe.configure({ mode: 'parallel' });
|
|
||||||
|
|
||||||
test.describe('await', () => {
|
|
||||||
test('should not care about non-API promises', async ({ runInlineTest }) => {
|
|
||||||
const { exitCode, results } = await runInlineTest({
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test } from '@playwright/test';
|
|
||||||
test('test', async () => {
|
|
||||||
new Promise(() => {});
|
|
||||||
await expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
expect(exitCode).toBe(1);
|
|
||||||
expect(results[0].annotations).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should warn on failure', async ({ runInlineTest }) => {
|
|
||||||
const { exitCode, results } = await runInlineTest({
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('custom test name', async ({ page }) => {
|
|
||||||
expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
|
||||||
// Timeout to make sure the expect actually gets processed
|
|
||||||
await new Promise(f => setTimeout(f, 1000));
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
expect(exitCode).toBe(1);
|
|
||||||
expect(results[0].annotations).toEqual([{ type: 'warning', description, location: expect.objectContaining({ file: expect.stringMatching(/a\.test\.ts$/), line: 4, column: 39 }) }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not warn on success', async ({ runInlineTest }) => {
|
|
||||||
const { exitCode, results } = await runInlineTest({
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('custom test name', async ({ page }) => {
|
|
||||||
await page.setContent('<div>A</div>');
|
|
||||||
expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
|
||||||
await new Promise(f => setTimeout(f, 1000));
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
expect(exitCode).toBe(0);
|
|
||||||
expect(results[0].annotations).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should warn about missing await on expects', async ({ runInlineTest }) => {
|
|
||||||
const { exitCode, results } = await runInlineTest({
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('custom test name', async ({ page }) => {
|
|
||||||
expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
|
||||||
await new Promise(f => setTimeout(f, 1000));
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
expect(exitCode).toBe(1);
|
|
||||||
expect(results[0].annotations).toEqual([{ type: 'warning', description, location: expect.objectContaining({ file: expect.stringMatching(/a\.test\.ts$/), line: 4, column: 39 }) }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not warn when not missing await on expects', async ({ runInlineTest }) => {
|
|
||||||
const { exitCode, results } = await runInlineTest({
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('test', async ({ page }) => {
|
|
||||||
await expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
expect(exitCode).toBe(1);
|
|
||||||
expect(results[0].annotations).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not warn when using then() on expects', async ({ runInlineTest }) => {
|
|
||||||
const { exitCode, results } = await runInlineTest({
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('test', async ({ page }) => {
|
|
||||||
expect(page.locator('div')).toHaveText('A').then(() => {});
|
|
||||||
await new Promise(f => setTimeout(f, 1000));
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
expect(exitCode).toBe(1);
|
|
||||||
expect(results[0].annotations).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should warn about missing await on resolve', async ({ runInlineTest }) => {
|
|
||||||
const { exitCode, results } = await runInlineTest({
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('test', async ({ page }) => {
|
|
||||||
expect(Promise.reject(new Error('foo'))).resolves.toBe('foo');
|
|
||||||
await new Promise(f => setTimeout(f, 1000));
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
expect(exitCode).toBe(1);
|
|
||||||
expect(results[0].annotations).toEqual([{ type: 'warning', description, location: expect.objectContaining({ file: expect.stringMatching(/a\.test\.ts$/), line: 4, column: 61 }) }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should warn about missing await on reject.not', async ({ runInlineTest }) => {
|
|
||||||
const { exitCode, results } = await runInlineTest({
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('test', async ({ page }) => {
|
|
||||||
expect(Promise.reject(new Error('foo'))).rejects.not.toThrow('foo');
|
|
||||||
await new Promise(f => setTimeout(f, 1000));
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
expect(exitCode).toBe(1);
|
|
||||||
expect(results[0].annotations).toEqual([{ type: 'warning', description, location: expect.objectContaining({ file: expect.stringMatching(/a\.test\.ts$/), line: 4, column: 64 }) }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should warn about missing await on test.step', async ({ runInlineTest }) => {
|
|
||||||
const { exitCode, results } = await runInlineTest({
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('test', async ({ page }) => {
|
|
||||||
test.step('step', () => {});
|
|
||||||
await expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
expect(exitCode).toBe(1);
|
|
||||||
expect(results[0].annotations).toEqual([{ type: 'warning', description, location: expect.objectContaining({ file: expect.stringMatching(/a\.test\.ts$/), line: 4, column: 16 }) }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not warn when not missing await on test.step', async ({ runInlineTest }) => {
|
|
||||||
const { exitCode, results } = await runInlineTest({
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('test', async ({ page }) => {
|
|
||||||
await test.step('step', () => {});
|
|
||||||
await expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
expect(exitCode).toBe(1);
|
|
||||||
expect(results[0].annotations).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should warn about missing await on test.step.skip', async ({ runInlineTest }) => {
|
|
||||||
const { exitCode, results } = await runInlineTest({
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('test', async ({ page }) => {
|
|
||||||
test.step.skip('step', () => {});
|
|
||||||
await expect(page.locator('div')).toHaveText('A');
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
expect(exitCode).toBe(1);
|
|
||||||
expect(results[0].annotations).toEqual([{ type: 'warning', description, location: expect.objectContaining({ file: expect.stringMatching(/a\.test\.ts$/), line: 4, column: 21 }) }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('traced promise should be instanceof Promise', async ({ runInlineTest }) => {
|
|
||||||
const { exitCode } = await runInlineTest({
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('test', async ({ page }) => {
|
|
||||||
await page.setContent('<div>A</div>');
|
|
||||||
const expectPromise = expect(page.locator('div')).toHaveText('A');
|
|
||||||
expect(expectPromise instanceof Promise).toBeTruthy();
|
|
||||||
await new Promise(f => setTimeout(f, 1000));
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
expect(exitCode).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should warn about missing await in before hooks', async ({ runInlineTest }) => {
|
|
||||||
const group = ['beforeAll', 'beforeEach'];
|
|
||||||
for (const hook of group) {
|
|
||||||
await test.step(hook, async () => {
|
|
||||||
const { exitCode, results } = await runInlineTest({
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
let page;
|
|
||||||
test.${hook}(async ({ browser }) => {
|
|
||||||
page = await browser.newPage();
|
|
||||||
await page.setContent('<div>A</div>');
|
|
||||||
expect(page.locator('div')).toHaveText('A');
|
|
||||||
await new Promise(f => setTimeout(f, 1000));
|
|
||||||
});
|
|
||||||
test('test ${hook}', async () => {
|
|
||||||
await expect(page.locator('button')).toBeVisible({ timeout: 100 });
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(exitCode).toBe(1);
|
|
||||||
expect(results[0].annotations).toEqual([{ type: 'warning', description, location: expect.objectContaining({ file: expect.stringMatching(/a\.test\.ts$/), line: 7, column: 43 }) }]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('should warn about missing await in after hooks', () => {
|
|
||||||
const group = ['afterAll', 'afterEach'];
|
|
||||||
for (const hook of group) {
|
|
||||||
test(hook, async ({ runInlineTest }) => {
|
|
||||||
const { exitCode, results } = await runInlineTest({
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
let page;
|
|
||||||
test('test ${hook}', async ({ browser }) => {
|
|
||||||
await expect(Promise.resolve()).resolves.toBe('A');
|
|
||||||
});
|
|
||||||
test.${hook}(async () => {
|
|
||||||
expect(Promise.resolve()).resolves.toBe(undefined);
|
|
||||||
await new Promise(f => setTimeout(f, 1000));
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(exitCode).toBe(1);
|
|
||||||
expect(results[0].annotations).toEqual([{ type: 'warning', description, location: expect.objectContaining({ file: expect.stringMatching(/a\.test\.ts$/), line: 8, column: 50 }) }]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should warn about missing await across hooks and test', async ({ runInlineTest }) => {
|
|
||||||
const { exitCode, results } = await runInlineTest({
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test.beforeAll(async () => {
|
|
||||||
expect(Promise.resolve()).resolves.toBe(undefined);
|
|
||||||
await new Promise(f => setTimeout(f, 1000));
|
|
||||||
});
|
|
||||||
test('test', async () => {
|
|
||||||
expect(Promise.resolve()).resolves.toBe('A');
|
|
||||||
await new Promise(f => setTimeout(f, 1000));
|
|
||||||
});
|
|
||||||
test.afterEach(async () => {
|
|
||||||
expect(Promise.resolve()).resolves.toBe(undefined);
|
|
||||||
await new Promise(f => setTimeout(f, 1000));
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
expect(exitCode).toBe(1);
|
|
||||||
expect(results[0].annotations).toEqual([
|
|
||||||
{ type: 'warning', description, location: expect.objectContaining({ file: expect.stringMatching(/a\.test\.ts$/), line: 4, column: 46 }) },
|
|
||||||
{ type: 'warning', description, location: expect.objectContaining({ file: expect.stringMatching(/a\.test\.ts$/), line: 8, column: 46 }) },
|
|
||||||
{ type: 'warning', description, location: expect.objectContaining({ file: expect.stringMatching(/a\.test\.ts$/), line: 12, column: 46 }) },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should dedupe warnings that occur at the same location', async ({ runInlineTest }) => {
|
|
||||||
const { exitCode, results } = await runInlineTest({
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('test', async ({ page }) => {
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
|
||||||
}
|
|
||||||
expect(page.locator('div')).toHaveText('A', { timeout: 100 });
|
|
||||||
await new Promise(f => setTimeout(f, 1000));
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(exitCode).toBe(1);
|
|
||||||
|
|
||||||
expect(results[0].annotations).toEqual([
|
|
||||||
{ type: 'warning', description, location: expect.objectContaining({ file: expect.stringMatching(/a\.test\.ts$/), line: 5, column: 41 }) },
|
|
||||||
{ type: 'warning', description, location: expect.objectContaining({ file: expect.stringMatching(/a\.test\.ts$/), line: 7, column: 39 }) },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -70,10 +70,6 @@ export type TestDetailsAnnotation = {
|
||||||
description?: string;
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TestAnnotation = TestDetailsAnnotation & {
|
|
||||||
location?: Location;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TestDetails = {
|
export type TestDetails = {
|
||||||
tag?: string | string[];
|
tag?: string | string[];
|
||||||
annotation?: TestDetailsAnnotation | TestDetailsAnnotation[];
|
annotation?: TestDetailsAnnotation | TestDetailsAnnotation[];
|
||||||
|
|
Loading…
Reference in New Issue