Compare commits
11 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d22178533 | ||
|
|
5d6ac9622d | ||
|
|
97b76b46af | ||
|
|
7bbcc3c624 | ||
|
|
2811a1d4f5 | ||
|
|
2b8d1ce260 | ||
|
|
715eb250e7 | ||
|
|
6106ef020f | ||
|
|
09cd74f7b0 | ||
|
|
cc6eb090ec | ||
|
|
cd02be37ae |
|
|
@ -2274,13 +2274,8 @@ assertThat(page.locator("body")).matchesAriaSnapshot(new LocatorAssertions.Match
|
||||||
* langs: js
|
* langs: js
|
||||||
- `name` <[string]>
|
- `name` <[string]>
|
||||||
|
|
||||||
Name of the snapshot to store in the snapshot folder corresponding to this test. Generates ordinal name if not specified.
|
Name of the snapshot to store in the snapshot (screenshot) folder corresponding to this test.
|
||||||
|
Generates sequential names if not specified.
|
||||||
### option: LocatorAssertions.toMatchAriaSnapshot#2.path
|
|
||||||
* since: v1.50
|
|
||||||
- `path` <[string]>
|
|
||||||
|
|
||||||
Path to the YAML snapshot file.
|
|
||||||
|
|
||||||
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-js-assertions-timeout-%%
|
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-js-assertions-timeout-%%
|
||||||
* since: v1.50
|
* since: v1.50
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,74 @@ toc_max_heading_level: 2
|
||||||
|
|
||||||
import LiteYouTube from '@site/src/components/LiteYouTube';
|
import LiteYouTube from '@site/src/components/LiteYouTube';
|
||||||
|
|
||||||
|
## Version 1.50
|
||||||
|
|
||||||
|
### Test runner
|
||||||
|
|
||||||
|
* New option [`option: Test.step.timeout`] allows specifying a maximum run time for an individual test step. A timed-out step will fail the execution of the test.
|
||||||
|
|
||||||
|
```js
|
||||||
|
test('some test', async ({ page }) => {
|
||||||
|
await test.step('a step', async () => {
|
||||||
|
// This step can time out separately from the test
|
||||||
|
}, { timeout: 1000 });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
* New method [`method: Test.step.skip`] to disable execution of a test step.
|
||||||
|
|
||||||
|
```js
|
||||||
|
test('some test', async ({ page }) => {
|
||||||
|
await test.step('before running step', async () => {
|
||||||
|
// Normal step
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step.skip('not yet ready', async () => {
|
||||||
|
// This step is skipped
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('after running step', async () => {
|
||||||
|
// This step still runs even though the previous one was skipped
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
* Expanded [`method: LocatorAssertions.toMatchAriaSnapshot#2`] to allow storing of aria snapshots in separate YAML files.
|
||||||
|
* Added method [`method: LocatorAssertions.toHaveAccessibleErrorMessage`] to assert the Locator points to an element with a given [aria errormessage](https://w3c.github.io/aria/#aria-errormessage).
|
||||||
|
* Option [`property: TestConfig.updateSnapshots`] added the configuration enum `changed`. `changed` updates only the snapshots that have changed, whereas `all` now updates all snapshots, regardless of whether there are any differences.
|
||||||
|
* New option [`property: TestConfig.updateSourceMethod`] defines the way source code is updated when [`property: TestConfig.updateSnapshots`] is configured. Added `overwrite` and `3-way` modes that write the changes into source code, on top of existing `patch` mode that creates a patch file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx playwright test --update-snapshots=changed --update-source-method=3way
|
||||||
|
```
|
||||||
|
|
||||||
|
* Option [`property: TestConfig.webServer`] added a `gracefulShutdown` field for specifying a process kill signal other than the default `SIGKILL`.
|
||||||
|
* Exposed [`property: TestStep.attachments`] from the reporter API to allow retrieval of all attachments created by that step.
|
||||||
|
|
||||||
|
### UI updates
|
||||||
|
|
||||||
|
* Updated default HTML reporter to improve display of attachments.
|
||||||
|
* New button for picking elements to produce aria snapshots.
|
||||||
|
* Additional details (such as keys pressed) are now displayed alongside action API calls in traces.
|
||||||
|
* Display of `canvas` content in traces is error-prone. Display is now disabled by default, and can be enabled via the `Display canvas content` UI setting.
|
||||||
|
* `Call` and `Network` panels now display additional time information.
|
||||||
|
|
||||||
|
### Breaking
|
||||||
|
|
||||||
|
* [`method: LocatorAssertions.toBeEditable`] and [`method: Locator.isEditable`] now throw if the target element is not `<input>`, `<select>`, or a number of other editable elements.
|
||||||
|
* Option [`property: TestConfig.updateSnapshots`] now updates all snapshots when set to `all`, rather than only the failed/changed snapshots. Use the new enum `changed` to keep the old functionality of only updating the changed snapshots.
|
||||||
|
|
||||||
|
### Browser Versions
|
||||||
|
|
||||||
|
* Chromium 133.0.6943.16
|
||||||
|
* Mozilla Firefox 134.0
|
||||||
|
* WebKit 18.2
|
||||||
|
|
||||||
|
This version was also tested against the following stable channels:
|
||||||
|
|
||||||
|
* Google Chrome 132
|
||||||
|
* Microsoft Edge 132
|
||||||
|
|
||||||
## Version 1.49
|
## Version 1.49
|
||||||
|
|
||||||
<LiteYouTube
|
<LiteYouTube
|
||||||
|
|
|
||||||
|
|
@ -1822,7 +1822,7 @@ Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout
|
||||||
* since: v1.50
|
* since: v1.50
|
||||||
- `timeout` <[float]>
|
- `timeout` <[float]>
|
||||||
|
|
||||||
Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout).
|
The maximum time, in milliseconds, allowed for the step to complete. If the step does not complete within the specified timeout, the [`method: Test.step`] method will throw a [TimeoutError]. Defaults to `0` (no timeout).
|
||||||
|
|
||||||
## method: Test.use
|
## method: Test.use
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
|
|
|
||||||
|
|
@ -629,7 +629,7 @@ export default defineConfig({
|
||||||
- `stdout` ?<["pipe"|"ignore"]> If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`.
|
- `stdout` ?<["pipe"|"ignore"]> If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`.
|
||||||
- `stderr` ?<["pipe"|"ignore"]> Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`.
|
- `stderr` ?<["pipe"|"ignore"]> Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`.
|
||||||
- `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
|
- `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
|
||||||
- `gracefulShutdown` ?<[Object]> How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal: 'SIGINT', timeout: 500 }`, the process group is sent a `SIGINT` signal, followed by `SIGKILL` if it doesn't exit within 500ms. You can also use `SIGTERM` instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't support `SIGINT` and `SIGTERM` signals, so this option is ignored.
|
- `gracefulShutdown` ?<[Object]> How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal: 'SIGTERM', timeout: 500 }`, the process group is sent a `SIGTERM` signal, followed by `SIGKILL` if it doesn't exit within 500ms. You can also use `SIGINT` as the signal instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't support `SIGTERM` and `SIGINT` signals, so this option is ignored on Windows. Note that shutting down a Docker container requires `SIGTERM`.
|
||||||
- `signal` <["SIGINT"|"SIGTERM"]>
|
- `signal` <["SIGINT"|"SIGTERM"]>
|
||||||
- `timeout` <[int]>
|
- `timeout` <[int]>
|
||||||
- `url` ?<[string]> The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is checked. Either `port` or `url` should be specified.
|
- `url` ?<[string]> The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is checked. Either `port` or `url` should be specified.
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export default defineConfig({
|
||||||
| `stdout` | If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`. |
|
| `stdout` | If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`. |
|
||||||
| `stderr` | Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`. |
|
| `stderr` | Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`. |
|
||||||
| `timeout` | How long to wait for the process to start up and be available in milliseconds. Defaults to 60000. |
|
| `timeout` | How long to wait for the process to start up and be available in milliseconds. Defaults to 60000. |
|
||||||
| `gracefulShutdown` | How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal: 'SIGINT', timeout: 500 }`, the process group is sent a `SIGINT` signal, followed by `SIGKILL` if it doesn't exit within 500ms. You can also use `SIGTERM` instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't support `SIGINT` and `SIGTERM` signals, so this option is ignored. |
|
| `gracefulShutdown` | How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal: 'SIGTERM', timeout: 500 }`, the process group is sent a `SIGTERM` signal, followed by `SIGKILL` if it doesn't exit within 500ms. You can also use `SIGINT` as the signal instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't support `SIGTERM` and `SIGINT` signals, so this option is ignored on Windows. Note that shutting down a Docker container requires `SIGTERM`. |
|
||||||
|
|
||||||
## Adding a server timeout
|
## Adding a server timeout
|
||||||
|
|
||||||
|
|
|
||||||
60
package-lock.json
generated
60
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-internal",
|
"name": "playwright-internal",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "playwright-internal",
|
"name": "playwright-internal",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
@ -7751,10 +7751,10 @@
|
||||||
"version": "0.0.0"
|
"version": "0.0.0"
|
||||||
},
|
},
|
||||||
"packages/playwright": {
|
"packages/playwright": {
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -7768,11 +7768,11 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-browser-chromium": {
|
"packages/playwright-browser-chromium": {
|
||||||
"name": "@playwright/browser-chromium",
|
"name": "@playwright/browser-chromium",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|
@ -7780,11 +7780,11 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-browser-firefox": {
|
"packages/playwright-browser-firefox": {
|
||||||
"name": "@playwright/browser-firefox",
|
"name": "@playwright/browser-firefox",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|
@ -7792,22 +7792,22 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-browser-webkit": {
|
"packages/playwright-browser-webkit": {
|
||||||
"name": "@playwright/browser-webkit",
|
"name": "@playwright/browser-webkit",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-chromium": {
|
"packages/playwright-chromium": {
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -7817,7 +7817,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-core": {
|
"packages/playwright-core": {
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright-core": "cli.js"
|
"playwright-core": "cli.js"
|
||||||
|
|
@ -7828,11 +7828,11 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-core": {
|
"packages/playwright-ct-core": {
|
||||||
"name": "@playwright/experimental-ct-core",
|
"name": "@playwright/experimental-ct-core",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.50.0-next",
|
"playwright": "1.50.0",
|
||||||
"playwright-core": "1.50.0-next",
|
"playwright-core": "1.50.0",
|
||||||
"vite": "^5.2.8"
|
"vite": "^5.2.8"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -7841,10 +7841,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-react": {
|
"packages/playwright-ct-react": {
|
||||||
"name": "@playwright/experimental-ct-react",
|
"name": "@playwright/experimental-ct-react",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
"@playwright/experimental-ct-core": "1.50.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1"
|
"@vitejs/plugin-react": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -7856,10 +7856,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-react17": {
|
"packages/playwright-ct-react17": {
|
||||||
"name": "@playwright/experimental-ct-react17",
|
"name": "@playwright/experimental-ct-react17",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
"@playwright/experimental-ct-core": "1.50.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1"
|
"@vitejs/plugin-react": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -7871,10 +7871,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-svelte": {
|
"packages/playwright-ct-svelte": {
|
||||||
"name": "@playwright/experimental-ct-svelte",
|
"name": "@playwright/experimental-ct-svelte",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
"@playwright/experimental-ct-core": "1.50.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -7889,10 +7889,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-vue": {
|
"packages/playwright-ct-vue": {
|
||||||
"name": "@playwright/experimental-ct-vue",
|
"name": "@playwright/experimental-ct-vue",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
"@playwright/experimental-ct-core": "1.50.0",
|
||||||
"@vitejs/plugin-vue": "^5.2.0"
|
"@vitejs/plugin-vue": "^5.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -7903,11 +7903,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-firefox": {
|
"packages/playwright-firefox": {
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -7918,10 +7918,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-test": {
|
"packages/playwright-test": {
|
||||||
"name": "@playwright/test",
|
"name": "@playwright/test",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.50.0-next"
|
"playwright": "1.50.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -7931,11 +7931,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-webkit": {
|
"packages/playwright-webkit": {
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.50.0-next"
|
"playwright-core": "1.50.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.50.0-next",
|
"version": "1.50.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",
|
||||||
|
|
|
||||||
|
|
@ -60,11 +60,6 @@
|
||||||
color: var(--color-scale-orange-6);
|
color: var(--color-scale-orange-6);
|
||||||
border: 1px solid var(--color-scale-orange-4);
|
border: 1px solid var(--color-scale-orange-4);
|
||||||
}
|
}
|
||||||
.label-color-gray {
|
|
||||||
background-color: var(--color-scale-gray-0);
|
|
||||||
color: var(--color-scale-gray-6);
|
|
||||||
border: 1px solid var(--color-scale-gray-4);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(prefers-color-scheme: dark) {
|
@media(prefers-color-scheme: dark) {
|
||||||
|
|
@ -98,11 +93,6 @@
|
||||||
color: var(--color-scale-orange-2);
|
color: var(--color-scale-orange-2);
|
||||||
border: 1px solid var(--color-scale-orange-4);
|
border: 1px solid var(--color-scale-orange-4);
|
||||||
}
|
}
|
||||||
.label-color-gray {
|
|
||||||
background-color: var(--color-scale-gray-9);
|
|
||||||
color: var(--color-scale-gray-2);
|
|
||||||
border: 1px solid var(--color-scale-gray-4);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-body {
|
.attachment-body {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import { TreeItem } from './treeItem';
|
||||||
import { CopyToClipboard } from './copyToClipboard';
|
import { CopyToClipboard } from './copyToClipboard';
|
||||||
import './links.css';
|
import './links.css';
|
||||||
import { linkifyText } from '@web/renderUtils';
|
import { linkifyText } from '@web/renderUtils';
|
||||||
import { clsx } from '@web/uiUtils';
|
import { clsx, useFlash } from '@web/uiUtils';
|
||||||
|
|
||||||
export function navigate(href: string | URL) {
|
export function navigate(href: string | URL) {
|
||||||
window.history.pushState({}, '', href);
|
window.history.pushState({}, '', href);
|
||||||
|
|
@ -73,7 +73,8 @@ export const AttachmentLink: React.FunctionComponent<{
|
||||||
linkName?: string,
|
linkName?: string,
|
||||||
openInNewTab?: boolean,
|
openInNewTab?: boolean,
|
||||||
}> = ({ attachment, result, href, linkName, openInNewTab }) => {
|
}> = ({ attachment, result, href, linkName, openInNewTab }) => {
|
||||||
const isAnchored = useIsAnchored('attachment-' + result.attachments.indexOf(attachment));
|
const [flash, triggerFlash] = useFlash();
|
||||||
|
useAnchor('attachment-' + result.attachments.indexOf(attachment), triggerFlash);
|
||||||
return <TreeItem title={<span>
|
return <TreeItem title={<span>
|
||||||
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
|
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
|
||||||
{attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>}
|
{attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>}
|
||||||
|
|
@ -84,7 +85,7 @@ export const AttachmentLink: React.FunctionComponent<{
|
||||||
)}
|
)}
|
||||||
</span>} loadChildren={attachment.body ? () => {
|
</span>} loadChildren={attachment.body ? () => {
|
||||||
return [<div key={1} className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>];
|
return [<div key={1} className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>];
|
||||||
} : undefined} depth={0} style={{ lineHeight: '32px' }} selected={isAnchored}></TreeItem>;
|
} : undefined} depth={0} style={{ lineHeight: '32px' }} flash={flash}></TreeItem>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SearchParamsContext = React.createContext<URLSearchParams>(new URLSearchParams(window.location.hash.slice(1)));
|
export const SearchParamsContext = React.createContext<URLSearchParams>(new URLSearchParams(window.location.hash.slice(1)));
|
||||||
|
|
@ -118,12 +119,12 @@ const kMissingContentType = 'x-playwright/missing';
|
||||||
|
|
||||||
export type AnchorID = string | string[] | ((id: string) => boolean) | undefined;
|
export type AnchorID = string | string[] | ((id: string) => boolean) | undefined;
|
||||||
|
|
||||||
export function useAnchor(id: AnchorID, onReveal: () => void) {
|
export function useAnchor(id: AnchorID, onReveal: React.EffectCallback) {
|
||||||
const searchParams = React.useContext(SearchParamsContext);
|
const searchParams = React.useContext(SearchParamsContext);
|
||||||
const isAnchored = useIsAnchored(id);
|
const isAnchored = useIsAnchored(id);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isAnchored)
|
if (isAnchored)
|
||||||
onReveal();
|
return onReveal();
|
||||||
}, [isAnchored, onReveal, searchParams]);
|
}, [isAnchored, onReveal, searchParams]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,7 @@ const StepTreeItem: React.FC<{
|
||||||
}> = ({ test, step, result, depth }) => {
|
}> = ({ test, step, result, depth }) => {
|
||||||
return <TreeItem title={<span aria-label={step.title}>
|
return <TreeItem title={<span aria-label={step.title}>
|
||||||
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
|
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
|
||||||
|
{step.attachments.length > 0 && <a style={{ float: 'right' }} title={`reveal attachment`} href={testResultHref({ test, result, anchor: `attachment-${step.attachments[0]}` })} onClick={evt => { evt.stopPropagation(); }}>{icons.attachment()}</a>}
|
||||||
{statusIcon(step.error || step.duration === -1 ? 'failed' : (step.skipped ? 'skipped' : 'passed'))}
|
{statusIcon(step.error || step.duration === -1 ? 'failed' : (step.skipped ? 'skipped' : 'passed'))}
|
||||||
<span>{step.title}</span>
|
<span>{step.title}</span>
|
||||||
{step.count > 1 && <> ✕ <span className='test-result-counter'>{step.count}</span></>}
|
{step.count > 1 && <> ✕ <span className='test-result-counter'>{step.count}</span></>}
|
||||||
|
|
@ -183,20 +184,6 @@ const StepTreeItem: React.FC<{
|
||||||
</span>} loadChildren={step.steps.length || step.snippet ? () => {
|
</span>} loadChildren={step.steps.length || step.snippet ? () => {
|
||||||
const snippet = step.snippet ? [<TestErrorView testId='test-snippet' key='line' error={step.snippet}/>] : [];
|
const snippet = step.snippet ? [<TestErrorView testId='test-snippet' key='line' error={step.snippet}/>] : [];
|
||||||
const steps = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1} result={result} test={test} />);
|
const steps = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1} result={result} test={test} />);
|
||||||
const attachments = step.attachments.map(attachmentIndex => (
|
return snippet.concat(steps);
|
||||||
<a key={'' + attachmentIndex}
|
|
||||||
href={testResultHref({ test, result, anchor: `attachment-${attachmentIndex}` })}
|
|
||||||
style={{ paddingLeft: depth * 22 + 4, textDecoration: 'none' }}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{ margin: '8px 0 0 8px', padding: '2px 10px', cursor: 'pointer' }}
|
|
||||||
className='label label-color-gray'
|
|
||||||
title={`see "${result.attachments[attachmentIndex].name}"`}
|
|
||||||
>
|
|
||||||
{icons.attachment()}{result.attachments[attachmentIndex].name}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
));
|
|
||||||
return snippet.concat(steps, attachments);
|
|
||||||
} : undefined} depth={depth}/>;
|
} : undefined} depth={depth}/>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,14 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-item-title.selected {
|
|
||||||
text-decoration: underline var(--color-underlinenav-icon);
|
|
||||||
text-decoration-thickness: 1.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-item-body {
|
.tree-item-body {
|
||||||
min-height: 18px;
|
min-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.yellow-flash {
|
||||||
|
animation: yellowflash-bg 2s;
|
||||||
|
}
|
||||||
|
@keyframes yellowflash-bg {
|
||||||
|
from { background: var(--color-attention-subtle); }
|
||||||
|
to { background: transparent; }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,12 @@ export const TreeItem: React.FunctionComponent<{
|
||||||
onClick?: () => void,
|
onClick?: () => void,
|
||||||
expandByDefault?: boolean,
|
expandByDefault?: boolean,
|
||||||
depth: number,
|
depth: number,
|
||||||
selected?: boolean,
|
|
||||||
style?: React.CSSProperties,
|
style?: React.CSSProperties,
|
||||||
}> = ({ title, loadChildren, onClick, expandByDefault, depth, selected, style }) => {
|
flash?: boolean
|
||||||
|
}> = ({ title, loadChildren, onClick, expandByDefault, depth, style, flash }) => {
|
||||||
const [expanded, setExpanded] = React.useState(expandByDefault || false);
|
const [expanded, setExpanded] = React.useState(expandByDefault || false);
|
||||||
return <div className={'tree-item'} style={style}>
|
return <div className={clsx('tree-item', flash && 'yellow-flash')} style={style}>
|
||||||
<span className={clsx('tree-item-title', selected && 'selected')} style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
|
<span className='tree-item-title' style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
|
||||||
{loadChildren && !!expanded && icons.downArrow()}
|
{loadChildren && !!expanded && icons.downArrow()}
|
||||||
{loadChildren && !expanded && icons.rightArrow()}
|
{loadChildren && !expanded && icons.rightArrow()}
|
||||||
{!loadChildren && <span style={{ visibility: 'hidden' }}>{icons.rightArrow()}</span>}
|
{!loadChildren && <span style={{ visibility: 'hidden' }}>{icons.rightArrow()}</span>}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/browser-chromium",
|
"name": "@playwright/browser-chromium",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.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.50.0-next"
|
"playwright-core": "1.50.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/browser-firefox",
|
"name": "@playwright/browser-firefox",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.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.50.0-next"
|
"playwright-core": "1.50.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/browser-webkit",
|
"name": "@playwright/browser-webkit",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.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.50.0-next"
|
"playwright-core": "1.50.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-chromium",
|
"name": "playwright-chromium",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.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.50.0-next"
|
"playwright-core": "1.50.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-core",
|
"name": "playwright-core",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.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.50.0-next",
|
"version": "1.50.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.50.0-next",
|
"playwright-core": "1.50.0",
|
||||||
"vite": "^5.2.8",
|
"vite": "^5.2.8",
|
||||||
"playwright": "1.50.0-next"
|
"playwright": "1.50.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/experimental-ct-react",
|
"name": "@playwright/experimental-ct-react",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.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.50.0-next",
|
"@playwright/experimental-ct-core": "1.50.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.50.0-next",
|
"version": "1.50.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.50.0-next",
|
"@playwright/experimental-ct-core": "1.50.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.50.0-next",
|
"version": "1.50.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.50.0-next",
|
"@playwright/experimental-ct-core": "1.50.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.50.0-next",
|
"version": "1.50.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.50.0-next",
|
"@playwright/experimental-ct-core": "1.50.0",
|
||||||
"@vitejs/plugin-vue": "^5.2.0"
|
"@vitejs/plugin-vue": "^5.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-firefox",
|
"name": "playwright-firefox",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.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.50.0-next"
|
"playwright-core": "1.50.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/test",
|
"name": "@playwright/test",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.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.50.0-next"
|
"playwright": "1.50.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-webkit",
|
"name": "playwright-webkit",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.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.50.0-next"
|
"playwright-core": "1.50.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright",
|
"name": "playwright",
|
||||||
"version": "1.50.0-next",
|
"version": "1.50.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.50.0-next"
|
"playwright-core": "1.50.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"fsevents": "2.3.2"
|
"fsevents": "2.3.2"
|
||||||
|
|
|
||||||
|
|
@ -270,9 +270,20 @@ export class TestTypeImpl {
|
||||||
const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box });
|
const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box });
|
||||||
return await zones.run('stepZone', step, async () => {
|
return await zones.run('stepZone', step, async () => {
|
||||||
try {
|
try {
|
||||||
const result = await raceAgainstDeadline(async () => body(), options.timeout ? monotonicTime() + options.timeout : 0);
|
let result: Awaited<ReturnType<typeof raceAgainstDeadline<T>>> | undefined = undefined;
|
||||||
|
result = await raceAgainstDeadline(async () => {
|
||||||
|
try {
|
||||||
|
return await body();
|
||||||
|
} catch (e) {
|
||||||
|
// If the step timed out, the test fixtures will tear down, which in turn
|
||||||
|
// will abort unfinished actions in the step body. Record such errors here.
|
||||||
|
if (result?.timedOut)
|
||||||
|
testInfo._failWithError(e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}, options.timeout ? monotonicTime() + options.timeout : 0);
|
||||||
if (result.timedOut)
|
if (result.timedOut)
|
||||||
throw new errors.TimeoutError(`Step timeout ${options.timeout}ms exceeded.`);
|
throw new errors.TimeoutError(`Step timeout of ${options.timeout}ms exceeded.`);
|
||||||
step.complete({});
|
step.complete({});
|
||||||
return result.result;
|
return result.result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -59,9 +59,7 @@ export async function toMatchAriaSnapshot(
|
||||||
if (isString(expectedParam)) {
|
if (isString(expectedParam)) {
|
||||||
expected = expectedParam;
|
expected = expectedParam;
|
||||||
} else {
|
} else {
|
||||||
if (expectedParam?.path) {
|
if (expectedParam?.name) {
|
||||||
expectedPath = expectedParam.path;
|
|
||||||
} else if (expectedParam?.name) {
|
|
||||||
expectedPath = testInfo.snapshotPath(sanitizeFilePathBeforeExtension(expectedParam.name));
|
expectedPath = testInfo.snapshotPath(sanitizeFilePathBeforeExtension(expectedParam.name));
|
||||||
} else {
|
} else {
|
||||||
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
|
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
|
||||||
|
|
@ -136,7 +134,7 @@ export async function toMatchAriaSnapshot(
|
||||||
}
|
}
|
||||||
return { pass: true, message: () => '', name: 'toMatchAriaSnapshot' };
|
return { pass: true, message: () => '', name: 'toMatchAriaSnapshot' };
|
||||||
} else {
|
} else {
|
||||||
const suggestedRebaseline = `toMatchAriaSnapshot(\`\n${escapeTemplateString(indent(typedReceived.regex, '{indent} '))}\n{indent}\`)`;
|
const suggestedRebaseline = `\`\n${escapeTemplateString(indent(typedReceived.regex, '{indent} '))}\n{indent}\``;
|
||||||
return { pass: false, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline };
|
return { pass: false, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -282,7 +282,7 @@ async function mergeReports(reportDir: string | undefined, opts: { [key: string]
|
||||||
function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrides {
|
function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrides {
|
||||||
const shardPair = options.shard ? options.shard.split('/').map((t: string) => parseInt(t, 10)) : undefined;
|
const shardPair = options.shard ? options.shard.split('/').map((t: string) => parseInt(t, 10)) : undefined;
|
||||||
|
|
||||||
let updateSnapshots: 'all' | 'changed' | 'missing' | 'none';
|
let updateSnapshots: 'all' | 'changed' | 'missing' | 'none' | undefined;
|
||||||
if (['all', 'changed', 'missing', 'none'].includes(options.updateSnapshots))
|
if (['all', 'changed', 'missing', 'none'].includes(options.updateSnapshots))
|
||||||
updateSnapshots = options.updateSnapshots;
|
updateSnapshots = options.updateSnapshots;
|
||||||
else
|
else
|
||||||
|
|
@ -303,7 +303,7 @@ function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrid
|
||||||
tsconfig: options.tsconfig ? path.resolve(process.cwd(), options.tsconfig) : undefined,
|
tsconfig: options.tsconfig ? path.resolve(process.cwd(), options.tsconfig) : undefined,
|
||||||
ignoreSnapshots: options.ignoreSnapshots ? !!options.ignoreSnapshots : undefined,
|
ignoreSnapshots: options.ignoreSnapshots ? !!options.ignoreSnapshots : undefined,
|
||||||
updateSnapshots,
|
updateSnapshots,
|
||||||
updateSourceMethod: options.updateSourceMethod || 'patch',
|
updateSourceMethod: options.updateSourceMethod,
|
||||||
workers: options.workers,
|
workers: options.workers,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,8 @@ class ListReporter extends TerminalReporter {
|
||||||
if (this._needNewLine) {
|
if (this._needNewLine) {
|
||||||
this._needNewLine = false;
|
this._needNewLine = false;
|
||||||
process.stdout.write('\n');
|
process.stdout.write('\n');
|
||||||
|
++this._lastRow;
|
||||||
|
this._lastColumn = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,6 +212,7 @@ class ListReporter extends TerminalReporter {
|
||||||
process.stdout.write('\n');
|
process.stdout.write('\n');
|
||||||
}
|
}
|
||||||
++this._lastRow;
|
++this._lastRow;
|
||||||
|
this._lastColumn = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateLine(row: number, text: string, prefix: string) {
|
private _updateLine(row: number, text: string, prefix: string) {
|
||||||
|
|
|
||||||
|
|
@ -68,24 +68,27 @@ export async function applySuggestedRebaselines(config: FullConfigInternal, repo
|
||||||
traverse(fileNode, {
|
traverse(fileNode, {
|
||||||
CallExpression: path => {
|
CallExpression: path => {
|
||||||
const node = path.node;
|
const node = path.node;
|
||||||
if (node.arguments.length !== 1)
|
if (node.arguments.length < 1)
|
||||||
return;
|
return;
|
||||||
if (!t.isMemberExpression(node.callee))
|
if (!t.isMemberExpression(node.callee))
|
||||||
return;
|
return;
|
||||||
const argument = node.arguments[0];
|
const argument = node.arguments[0];
|
||||||
if (!t.isStringLiteral(argument) && !t.isTemplateLiteral(argument))
|
if (!t.isStringLiteral(argument) && !t.isTemplateLiteral(argument))
|
||||||
return;
|
return;
|
||||||
|
const prop = node.callee.property;
|
||||||
const matcher = node.callee.property;
|
if (!prop.loc || !argument.start || !argument.end)
|
||||||
|
return;
|
||||||
|
// Replacements are anchored by the location of the call expression.
|
||||||
|
// However, replacement text is meant to only replace the first argument.
|
||||||
for (const replacement of replacements) {
|
for (const replacement of replacements) {
|
||||||
// In Babel, rows are 1-based, columns are 0-based.
|
// In Babel, rows are 1-based, columns are 0-based.
|
||||||
if (matcher.loc!.start.line !== replacement.location.line)
|
if (prop.loc.start.line !== replacement.location.line)
|
||||||
continue;
|
continue;
|
||||||
if (matcher.loc!.start.column + 1 !== replacement.location.column)
|
if (prop.loc.start.column + 1 !== replacement.location.column)
|
||||||
continue;
|
continue;
|
||||||
const indent = lines[matcher.loc!.start.line - 1].match(/^\s*/)![0];
|
const indent = lines[prop.loc.start.line - 1].match(/^\s*/)![0];
|
||||||
const newText = replacement.code.replace(/\{indent\}/g, indent);
|
const newText = replacement.code.replace(/\{indent\}/g, indent);
|
||||||
ranges.push({ start: matcher.start!, end: node.end!, oldText: source.substring(matcher.start!, node.end!), newText });
|
ranges.push({ start: argument.start, end: argument.end, oldText: source.substring(argument.start, argument.end), newText });
|
||||||
// We can have multiple, hopefully equal, replacements for the same location,
|
// We can have multiple, hopefully equal, replacements for the same location,
|
||||||
// for example when a single test runs multiple times because of projects or retries.
|
// for example when a single test runs multiple times because of projects or retries.
|
||||||
// Do not apply multiple replacements for the same assertion.
|
// Do not apply multiple replacements for the same assertion.
|
||||||
|
|
|
||||||
|
|
@ -322,7 +322,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
location: data.location,
|
location: data.location,
|
||||||
};
|
};
|
||||||
this._onStepBegin(payload);
|
this._onStepBegin(payload);
|
||||||
this._tracing.appendBeforeActionForStep(stepId, parentStep?.stepId, data.apiName || data.title, data.params, data.location ? [data.location] : []);
|
this._tracing.appendBeforeActionForStep(stepId, parentStep?.stepId, data.category, data.apiName || data.title, data.params, data.location ? [data.location] : []);
|
||||||
return step;
|
return step;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -421,7 +421,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
} else {
|
} else {
|
||||||
// trace viewer has no means of representing attachments outside of a step, so we create an artificial action
|
// trace viewer has no means of representing attachments outside of a step, so we create an artificial action
|
||||||
const callId = `attach@${++this._lastStepId}`;
|
const callId = `attach@${++this._lastStepId}`;
|
||||||
this._tracing.appendBeforeActionForStep(callId, this._findLastStageStep(this._steps)?.stepId, `attach "${attachment.name}"`, undefined, []);
|
this._tracing.appendBeforeActionForStep(callId, this._findLastStageStep(this._steps)?.stepId, 'attach', `attach "${attachment.name}"`, undefined, []);
|
||||||
this._tracing.appendAfterActionForStep(callId, undefined, [attachment]);
|
this._tracing.appendAfterActionForStep(callId, undefined, [attachment]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -245,14 +245,14 @@ export class TestTracing {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
appendBeforeActionForStep(callId: string, parentId: string | undefined, apiName: string, params: Record<string, any> | undefined, stack: StackFrame[]) {
|
appendBeforeActionForStep(callId: string, parentId: string | undefined, category: string, apiName: string, params: Record<string, any> | undefined, stack: StackFrame[]) {
|
||||||
this._appendTraceEvent({
|
this._appendTraceEvent({
|
||||||
type: 'before',
|
type: 'before',
|
||||||
callId,
|
callId,
|
||||||
parentId,
|
parentId,
|
||||||
startTime: monotonicTime(),
|
startTime: monotonicTime(),
|
||||||
class: 'Test',
|
class: 'Test',
|
||||||
method: 'step',
|
method: category,
|
||||||
apiName,
|
apiName,
|
||||||
params: Object.fromEntries(Object.entries(params || {}).map(([name, value]) => [name, generatePreview(value)])),
|
params: Object.fromEntries(Object.entries(params || {}).map(([name, value]) => [name, generatePreview(value)])),
|
||||||
stack,
|
stack,
|
||||||
|
|
|
||||||
|
|
@ -75,16 +75,20 @@ export class WorkerMain extends ProcessRunner {
|
||||||
|
|
||||||
process.on('unhandledRejection', reason => this.unhandledError(reason));
|
process.on('unhandledRejection', reason => this.unhandledError(reason));
|
||||||
process.on('uncaughtException', error => this.unhandledError(error));
|
process.on('uncaughtException', error => this.unhandledError(error));
|
||||||
process.stdout.write = (chunk: string | Buffer) => {
|
process.stdout.write = (chunk: string | Buffer, cb?: any) => {
|
||||||
this.dispatchEvent('stdOut', stdioChunkToParams(chunk));
|
this.dispatchEvent('stdOut', stdioChunkToParams(chunk));
|
||||||
this._currentTest?._tracing.appendStdioToTrace('stdout', chunk);
|
this._currentTest?._tracing.appendStdioToTrace('stdout', chunk);
|
||||||
|
if (typeof cb === 'function')
|
||||||
|
process.nextTick(cb);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!process.env.PW_RUNNER_DEBUG) {
|
if (!process.env.PW_RUNNER_DEBUG) {
|
||||||
process.stderr.write = (chunk: string | Buffer) => {
|
process.stderr.write = (chunk: string | Buffer, cb?: any) => {
|
||||||
this.dispatchEvent('stdErr', stdioChunkToParams(chunk));
|
this.dispatchEvent('stdErr', stdioChunkToParams(chunk));
|
||||||
this._currentTest?._tracing.appendStdioToTrace('stderr', chunk);
|
this._currentTest?._tracing.appendStdioToTrace('stderr', chunk);
|
||||||
|
if (typeof cb === 'function')
|
||||||
|
process.nextTick(cb);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
packages/playwright/types/test.d.ts
vendored
16
packages/playwright/types/test.d.ts
vendored
|
|
@ -8697,16 +8697,11 @@ interface LocatorAssertions {
|
||||||
*/
|
*/
|
||||||
toMatchAriaSnapshot(options?: {
|
toMatchAriaSnapshot(options?: {
|
||||||
/**
|
/**
|
||||||
* Name of the snapshot to store in the snapshot folder corresponding to this test. Generates ordinal name if not
|
* Name of the snapshot to store in the snapshot (screenshot) folder corresponding to this test. Generates sequential
|
||||||
* specified.
|
* names if not specified.
|
||||||
*/
|
*/
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Path to the YAML snapshot file.
|
|
||||||
*/
|
|
||||||
path?: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
|
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
|
||||||
*/
|
*/
|
||||||
|
|
@ -9650,9 +9645,10 @@ interface TestConfigWebServer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal:
|
* How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal:
|
||||||
* 'SIGINT', timeout: 500 }`, the process group is sent a `SIGINT` signal, followed by `SIGKILL` if it doesn't exit
|
* 'SIGTERM', timeout: 500 }`, the process group is sent a `SIGTERM` signal, followed by `SIGKILL` if it doesn't exit
|
||||||
* within 500ms. You can also use `SIGTERM` instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't
|
* within 500ms. You can also use `SIGINT` as the signal instead. A `0` timeout means no `SIGKILL` will be sent.
|
||||||
* support `SIGINT` and `SIGTERM` signals, so this option is ignored.
|
* Windows doesn't support `SIGTERM` and `SIGINT` signals, so this option is ignored on Windows. Note that shutting
|
||||||
|
* down a Docker container requires `SIGTERM`.
|
||||||
*/
|
*/
|
||||||
gracefulShutdown?: {
|
gracefulShutdown?: {
|
||||||
signal: "SIGINT"|"SIGTERM";
|
signal: "SIGINT"|"SIGTERM";
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@
|
||||||
color: var(--vscode-editorCodeLens-foreground);
|
color: var(--vscode-editorCodeLens-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-skipped {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.action-icon {
|
.action-icon {
|
||||||
flex: none;
|
flex: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ActionTraceEvent, AfterActionTraceEventAttachment } from '@trace/trace';
|
import type { ActionTraceEvent, AfterActionTraceEventAttachment } from '@trace/trace';
|
||||||
import { msToString } from '@web/uiUtils';
|
import { clsx, msToString } from '@web/uiUtils';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import './actionList.css';
|
import './actionList.css';
|
||||||
import * as modelUtil from './modelUtil';
|
import * as modelUtil from './modelUtil';
|
||||||
|
|
@ -25,6 +25,7 @@ import { TreeView } from '@web/components/treeView';
|
||||||
import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil';
|
import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil';
|
||||||
import type { Boundaries } from './geometry';
|
import type { Boundaries } from './geometry';
|
||||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
|
import { testStatusIcon } from './testUtils';
|
||||||
|
|
||||||
export interface ActionListProps {
|
export interface ActionListProps {
|
||||||
actions: ActionTraceEventInContext[],
|
actions: ActionTraceEventInContext[],
|
||||||
|
|
@ -119,6 +120,7 @@ export const renderAction = (
|
||||||
|
|
||||||
const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript');
|
const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript');
|
||||||
|
|
||||||
|
const isSkipped = action.class === 'Test' && action.method === 'test.step.skip';
|
||||||
let time: string = '';
|
let time: string = '';
|
||||||
if (action.endTime)
|
if (action.endTime)
|
||||||
time = msToString(action.endTime - action.startTime);
|
time = msToString(action.endTime - action.startTime);
|
||||||
|
|
@ -149,9 +151,10 @@ export const renderAction = (
|
||||||
{action.method === 'goto' && action.params.url && <div className='action-url' title={action.params.url}>{action.params.url}</div>}
|
{action.method === 'goto' && action.params.url && <div className='action-url' title={action.params.url}>{action.params.url}</div>}
|
||||||
{action.class === 'APIRequestContext' && action.params.url && <div className='action-url' title={action.params.url}>{excludeOrigin(action.params.url)}</div>}
|
{action.class === 'APIRequestContext' && action.params.url && <div className='action-url' title={action.params.url}>{excludeOrigin(action.params.url)}</div>}
|
||||||
</div>
|
</div>
|
||||||
{(showDuration || showBadges || showAttachments) && <div className='spacer'></div>}
|
{(showDuration || showBadges || showAttachments || isSkipped) && <div className='spacer'></div>}
|
||||||
{showAttachments && <ToolbarButton icon='attach' title='Open Attachment' onClick={() => revealAttachment(action.attachments![0])} />}
|
{showAttachments && <ToolbarButton icon='attach' title='Open Attachment' onClick={() => revealAttachment(action.attachments![0])} />}
|
||||||
{showDuration && <div className='action-duration'>{time || <span className='codicon codicon-loading'></span>}</div>}
|
{showDuration && !isSkipped && <div className='action-duration'>{time || <span className='codicon codicon-loading'></span>}</div>}
|
||||||
|
{isSkipped && <span className={clsx('action-skipped', 'codicon', testStatusIcon('skipped'))} title='skipped'></span>}
|
||||||
{showBadges && <div className='action-icons' onClick={() => revealConsole?.()}>
|
{showBadges && <div className='action-icons' onClick={() => revealConsole?.()}>
|
||||||
{!!errors && <div className='action-icon'><span className='codicon codicon-error'></span><span className='action-icon-value'>{errors}</span></div>}
|
{!!errors && <div className='action-icon'><span className='codicon codicon-error'></span><span className='action-icon-value'>{errors}</span></div>}
|
||||||
{!!warnings && <div className='action-icon'><span className='codicon codicon-warning'></span><span className='action-icon-value'>{warnings}</span></div>}
|
{!!warnings && <div className='action-icon'><span className='codicon codicon-warning'></span><span className='action-icon-value'>{warnings}</span></div>}
|
||||||
|
|
|
||||||
|
|
@ -55,3 +55,11 @@
|
||||||
a.codicon-cloud-download:hover{
|
a.codicon-cloud-download:hover{
|
||||||
background-color: var(--vscode-list-inactiveSelectionBackground)
|
background-color: var(--vscode-list-inactiveSelectionBackground)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.yellow-flash {
|
||||||
|
animation: yellowflash-bg 2s;
|
||||||
|
}
|
||||||
|
@keyframes yellowflash-bg {
|
||||||
|
from { background: var(--vscode-peekViewEditor-matchHighlightBackground); }
|
||||||
|
to { background: transparent; }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,36 +17,38 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import './attachmentsTab.css';
|
import './attachmentsTab.css';
|
||||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||||
import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
|
import type { MultiTraceModel } from './modelUtil';
|
||||||
import { PlaceholderPanel } from './placeholderPanel';
|
import { PlaceholderPanel } from './placeholderPanel';
|
||||||
import type { AfterActionTraceEventAttachment } from '@trace/trace';
|
import type { AfterActionTraceEventAttachment } from '@trace/trace';
|
||||||
import { CodeMirrorWrapper, lineHeight } from '@web/components/codeMirrorWrapper';
|
import { CodeMirrorWrapper, lineHeight } from '@web/components/codeMirrorWrapper';
|
||||||
import { isTextualMimeType } from '@isomorphic/mimeType';
|
import { isTextualMimeType } from '@isomorphic/mimeType';
|
||||||
import { Expandable } from '@web/components/expandable';
|
import { Expandable } from '@web/components/expandable';
|
||||||
import { linkifyText } from '@web/renderUtils';
|
import { linkifyText } from '@web/renderUtils';
|
||||||
import { clsx } from '@web/uiUtils';
|
import { clsx, useFlash } from '@web/uiUtils';
|
||||||
|
|
||||||
type Attachment = AfterActionTraceEventAttachment & { traceUrl: string };
|
type Attachment = AfterActionTraceEventAttachment & { traceUrl: string };
|
||||||
|
|
||||||
type ExpandableAttachmentProps = {
|
type ExpandableAttachmentProps = {
|
||||||
attachment: Attachment;
|
attachment: Attachment;
|
||||||
reveal: boolean;
|
reveal?: any;
|
||||||
highlight: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> = ({ attachment, reveal, highlight }) => {
|
const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> = ({ attachment, reveal }) => {
|
||||||
const [expanded, setExpanded] = React.useState(false);
|
const [expanded, setExpanded] = React.useState(false);
|
||||||
const [attachmentText, setAttachmentText] = React.useState<string | null>(null);
|
const [attachmentText, setAttachmentText] = React.useState<string | null>(null);
|
||||||
const [placeholder, setPlaceholder] = React.useState<string | null>(null);
|
const [placeholder, setPlaceholder] = React.useState<string | null>(null);
|
||||||
|
const [flash, triggerFlash] = useFlash();
|
||||||
const ref = React.useRef<HTMLSpanElement>(null);
|
const ref = React.useRef<HTMLSpanElement>(null);
|
||||||
|
|
||||||
const isTextAttachment = isTextualMimeType(attachment.contentType);
|
const isTextAttachment = isTextualMimeType(attachment.contentType);
|
||||||
const hasContent = !!attachment.sha1 || !!attachment.path;
|
const hasContent = !!attachment.sha1 || !!attachment.path;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (reveal)
|
if (reveal) {
|
||||||
ref.current?.scrollIntoView({ behavior: 'smooth' });
|
ref.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
}, [reveal]);
|
return triggerFlash();
|
||||||
|
}
|
||||||
|
}, [reveal, triggerFlash]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (expanded && attachmentText === null && placeholder === null) {
|
if (expanded && attachmentText === null && placeholder === null) {
|
||||||
|
|
@ -66,14 +68,14 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
|
||||||
}, [attachmentText]);
|
}, [attachmentText]);
|
||||||
|
|
||||||
const title = <span style={{ marginLeft: 5 }} ref={ref} aria-label={attachment.name}>
|
const title = <span style={{ marginLeft: 5 }} ref={ref} aria-label={attachment.name}>
|
||||||
<span className={clsx(highlight && 'attachment-title-highlight')}>{linkifyText(attachment.name)}</span>
|
<span>{linkifyText(attachment.name)}</span>
|
||||||
{hasContent && <a style={{ marginLeft: 5 }} href={downloadURL(attachment)}>download</a>}
|
{hasContent && <a style={{ marginLeft: 5 }} href={downloadURL(attachment)}>download</a>}
|
||||||
</span>;
|
</span>;
|
||||||
|
|
||||||
if (!isTextAttachment || !hasContent)
|
if (!isTextAttachment || !hasContent)
|
||||||
return <div style={{ marginLeft: 20 }}>{title}</div>;
|
return <div style={{ marginLeft: 20 }}>{title}</div>;
|
||||||
|
|
||||||
return <>
|
return <div className={clsx(flash && 'yellow-flash')}>
|
||||||
<Expandable title={title} expanded={expanded} setExpanded={setExpanded} expandOnTitleClick={true}>
|
<Expandable title={title} expanded={expanded} setExpanded={setExpanded} expandOnTitleClick={true}>
|
||||||
{placeholder && <i>{placeholder}</i>}
|
{placeholder && <i>{placeholder}</i>}
|
||||||
</Expandable>
|
</Expandable>
|
||||||
|
|
@ -87,14 +89,13 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
|
||||||
wrapLines={false}>
|
wrapLines={false}>
|
||||||
</CodeMirrorWrapper>
|
</CodeMirrorWrapper>
|
||||||
</div>}
|
</div>}
|
||||||
</>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AttachmentsTab: React.FunctionComponent<{
|
export const AttachmentsTab: React.FunctionComponent<{
|
||||||
model: MultiTraceModel | undefined,
|
model: MultiTraceModel | undefined,
|
||||||
selectedAction: ActionTraceEventInContext | undefined,
|
revealedAttachment?: [AfterActionTraceEventAttachment, number],
|
||||||
revealedAttachment?: AfterActionTraceEventAttachment,
|
}> = ({ model, revealedAttachment }) => {
|
||||||
}> = ({ model, selectedAction, revealedAttachment }) => {
|
|
||||||
const { diffMap, screenshots, attachments } = React.useMemo(() => {
|
const { diffMap, screenshots, attachments } = React.useMemo(() => {
|
||||||
const attachments = new Set<Attachment>();
|
const attachments = new Set<Attachment>();
|
||||||
const screenshots = new Set<Attachment>();
|
const screenshots = new Set<Attachment>();
|
||||||
|
|
@ -153,8 +154,7 @@ export const AttachmentsTab: React.FunctionComponent<{
|
||||||
return <div className='attachment-item' key={attachmentKey(a, i)}>
|
return <div className='attachment-item' key={attachmentKey(a, i)}>
|
||||||
<ExpandableAttachment
|
<ExpandableAttachment
|
||||||
attachment={a}
|
attachment={a}
|
||||||
highlight={selectedAction?.attachments?.some(selected => isEqualAttachment(a, selected)) ?? false}
|
reveal={(!!revealedAttachment && isEqualAttachment(a, revealedAttachment[0])) ? revealedAttachment : undefined}
|
||||||
reveal={!!revealedAttachment && isEqualAttachment(a, revealedAttachment)}
|
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, onOpenExternally, revealSource }) => {
|
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, onOpenExternally, revealSource }) => {
|
||||||
const [selectedCallId, setSelectedCallId] = React.useState<string | undefined>(undefined);
|
const [selectedCallId, setSelectedCallId] = React.useState<string | undefined>(undefined);
|
||||||
const [revealedError, setRevealedError] = React.useState<ErrorDescription | undefined>(undefined);
|
const [revealedError, setRevealedError] = React.useState<ErrorDescription | undefined>(undefined);
|
||||||
const [revealedAttachment, setRevealedAttachment] = React.useState<AfterActionTraceEventAttachment | undefined>(undefined);
|
const [revealedAttachment, setRevealedAttachment] = React.useState<[attachment: AfterActionTraceEventAttachment, renderCounter: number] | undefined>(undefined);
|
||||||
const [highlightedCallId, setHighlightedCallId] = React.useState<string | undefined>();
|
const [highlightedCallId, setHighlightedCallId] = React.useState<string | undefined>();
|
||||||
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
|
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
|
||||||
const [highlightedConsoleMessage, setHighlightedConsoleMessage] = React.useState<ConsoleEntry | undefined>();
|
const [highlightedConsoleMessage, setHighlightedConsoleMessage] = React.useState<ConsoleEntry | undefined>();
|
||||||
|
|
@ -148,7 +148,12 @@ export const Workbench: React.FunctionComponent<{
|
||||||
|
|
||||||
const revealAttachment = React.useCallback((attachment: AfterActionTraceEventAttachment) => {
|
const revealAttachment = React.useCallback((attachment: AfterActionTraceEventAttachment) => {
|
||||||
selectPropertiesTab('attachments');
|
selectPropertiesTab('attachments');
|
||||||
setRevealedAttachment(attachment);
|
setRevealedAttachment(currentValue => {
|
||||||
|
if (!currentValue)
|
||||||
|
return [attachment, 0];
|
||||||
|
const revealCounter = currentValue[1];
|
||||||
|
return [attachment, revealCounter + 1];
|
||||||
|
});
|
||||||
}, [selectPropertiesTab]);
|
}, [selectPropertiesTab]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -238,7 +243,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
id: 'attachments',
|
id: 'attachments',
|
||||||
title: 'Attachments',
|
title: 'Attachments',
|
||||||
count: attachments.length,
|
count: attachments.length,
|
||||||
render: () => <AttachmentsTab model={model} selectedAction={selectedAction} revealedAttachment={revealedAttachment} />
|
render: () => <AttachmentsTab model={model} revealedAttachment={revealedAttachment} />
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs: TabbedPaneTabModel[] = [
|
const tabs: TabbedPaneTabModel[] = [
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { EffectCallback } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
// Recalculates the value when dependencies change.
|
// Recalculates the value when dependencies change.
|
||||||
|
|
@ -224,3 +225,26 @@ export function scrollIntoViewIfNeeded(element: Element | undefined) {
|
||||||
|
|
||||||
const kControlCodesRe = '\\u0000-\\u0020\\u007f-\\u009f';
|
const kControlCodesRe = '\\u0000-\\u0020\\u007f-\\u009f';
|
||||||
export const kWebLinkRe = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + kControlCodesRe + '"]{2,}[^\\s' + kControlCodesRe + '"\')}\\],:;.!?]', 'ug');
|
export const kWebLinkRe = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + kControlCodesRe + '"]{2,}[^\\s' + kControlCodesRe + '"\')}\\],:;.!?]', 'ug');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages flash animation state.
|
||||||
|
* Calling `trigger` will turn `flash` to true for a second, and then back to false.
|
||||||
|
* If `trigger` is called while a flash is ongoing, the ongoing flash will be cancelled and after 50ms a new flash is started.
|
||||||
|
* @returns [flash, trigger]
|
||||||
|
*/
|
||||||
|
export function useFlash(): [boolean, EffectCallback] {
|
||||||
|
const [flash, setFlash] = React.useState(false);
|
||||||
|
const trigger = React.useCallback<React.EffectCallback>(() => {
|
||||||
|
const timeouts: any[] = [];
|
||||||
|
setFlash(currentlyFlashing => {
|
||||||
|
timeouts.push(setTimeout(() => setFlash(false), 1000));
|
||||||
|
if (!currentlyFlashing)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
timeouts.push(setTimeout(() => setFlash(true), 50));
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return () => timeouts.forEach(clearTimeout);
|
||||||
|
}, [setFlash]);
|
||||||
|
return [flash, trigger];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,24 +42,6 @@ test('should match snapshot with name', async ({ runInlineTest }, testInfo) => {
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should match snapshot with path', async ({ runInlineTest }, testInfo) => {
|
|
||||||
const result = await runInlineTest({
|
|
||||||
'test.yml': `
|
|
||||||
- heading "hello world"
|
|
||||||
`,
|
|
||||||
'a.spec.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import path from 'path';
|
|
||||||
test('test', async ({ page }) => {
|
|
||||||
await page.setContent(\`<h1>hello world</h1>\`);
|
|
||||||
await expect(page.locator('body')).toMatchAriaSnapshot({ path: path.resolve(__dirname, 'test.yml') });
|
|
||||||
});
|
|
||||||
`
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should generate multiple missing', async ({ runInlineTest }, testInfo) => {
|
test('should generate multiple missing', async ({ runInlineTest }, testInfo) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,7 @@ export function cleanEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||||
// END: Reserved CI
|
// END: Reserved CI
|
||||||
PW_TEST_HTML_REPORT_OPEN: undefined,
|
PW_TEST_HTML_REPORT_OPEN: undefined,
|
||||||
PLAYWRIGHT_HTML_OPEN: undefined,
|
PLAYWRIGHT_HTML_OPEN: undefined,
|
||||||
|
PW_TEST_DEBUG_REPORTERS: undefined,
|
||||||
PW_TEST_REPORTER: undefined,
|
PW_TEST_REPORTER: undefined,
|
||||||
PW_TEST_REPORTER_WS_ENDPOINT: undefined,
|
PW_TEST_REPORTER_WS_ENDPOINT: undefined,
|
||||||
PW_TEST_SOURCE_TRANSFORM: undefined,
|
PW_TEST_SOURCE_TRANSFORM: undefined,
|
||||||
|
|
|
||||||
|
|
@ -959,10 +959,9 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
await showReport();
|
await showReport();
|
||||||
await page.getByRole('link', { name: 'passing' }).click();
|
await page.getByRole('link', { name: 'passing' }).click();
|
||||||
|
|
||||||
const attachment = page.getByTestId('attachments').getByText('foo-2', { exact: true });
|
const attachment = page.getByText('foo-2', { exact: true });
|
||||||
await expect(attachment).not.toBeInViewport();
|
await expect(attachment).not.toBeInViewport();
|
||||||
await page.getByLabel('attach "foo-2"').click();
|
await page.getByLabel(`attach "foo-2"`).getByTitle('reveal attachment').click();
|
||||||
await page.getByTitle('see "foo-2"').click();
|
|
||||||
await expect(attachment).toBeInViewport();
|
await expect(attachment).toBeInViewport();
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
|
@ -989,10 +988,9 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
await showReport();
|
await showReport();
|
||||||
await page.getByRole('link', { name: 'passing' }).click();
|
await page.getByRole('link', { name: 'passing' }).click();
|
||||||
|
|
||||||
const attachment = page.getByTestId('attachments').getByText('attachment', { exact: true });
|
const attachment = page.getByText('attachment', { exact: true });
|
||||||
await expect(attachment).not.toBeInViewport();
|
await expect(attachment).not.toBeInViewport();
|
||||||
await page.getByLabel('step').click();
|
await page.getByLabel('step').getByTitle('reveal attachment').click();
|
||||||
await page.getByTitle('see "attachment"').click();
|
|
||||||
await expect(attachment).toBeInViewport();
|
await expect(attachment).toBeInViewport();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,51 @@ for (const useIntermediateMergeReport of [false, true] as const) {
|
||||||
expect(text).toContain('1) a.test.ts:3:15 › passes › outer 1.0 › inner 1.1 ──');
|
expect(text).toContain('1) a.test.ts:3:15 › passes › outer 1.0 › inner 1.1 ──');
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('print stdio', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('passes', async ({}) => {
|
||||||
|
await new Promise(resolve => process.stdout.write('line1', () => resolve()));
|
||||||
|
await new Promise(resolve => process.stdout.write('line2\\n', () => resolve()));
|
||||||
|
await new Promise(resolve => process.stderr.write(Buffer.from(''), () => resolve()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passes 2', async ({}) => {
|
||||||
|
await new Promise(resolve => process.stdout.write('partial', () => resolve()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passes 3', async ({}) => {
|
||||||
|
await new Promise(resolve => process.stdout.write('full\\n', () => resolve()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passes 4', async ({}) => {
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1', PLAYWRIGHT_FORCE_TTY: '80' });
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(4);
|
||||||
|
const expected = [
|
||||||
|
'#0 : 1 a.test.ts:3:15 › passes',
|
||||||
|
'line1line2',
|
||||||
|
`#0 : ${POSITIVE_STATUS_MARK} 1 a.test.ts:3:15 › passes`,
|
||||||
|
'',
|
||||||
|
'#3 : 2 a.test.ts:9:15 › passes 2',
|
||||||
|
`partial#3 : ${POSITIVE_STATUS_MARK} 2 a.test.ts:9:15 › passes 2`,
|
||||||
|
'',
|
||||||
|
'#5 : 3 a.test.ts:13:15 › passes 3',
|
||||||
|
'full',
|
||||||
|
`#5 : ${POSITIVE_STATUS_MARK} 3 a.test.ts:13:15 › passes 3`,
|
||||||
|
'#7 : 4 a.test.ts:17:15 › passes 4',
|
||||||
|
`#7 : ${POSITIVE_STATUS_MARK} 4 a.test.ts:17:15 › passes 4`,
|
||||||
|
];
|
||||||
|
const lines = result.output.split('\n');
|
||||||
|
const firstIndex = lines.indexOf(expected[0]);
|
||||||
|
expect(firstIndex, 'first line should be there').not.toBe(-1);
|
||||||
|
for (let i = 0; i < expected.length; ++i)
|
||||||
|
expect(lines[firstIndex + i]).toContain(expected[i]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -399,7 +399,7 @@ test('step timeout option', async ({ runInlineTest }) => {
|
||||||
}, { reporter: '', workers: 1 });
|
}, { reporter: '', workers: 1 });
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(result.failed).toBe(1);
|
expect(result.failed).toBe(1);
|
||||||
expect(result.output).toContain('Error: Step timeout 100ms exceeded.');
|
expect(result.output).toContain('Error: Step timeout of 100ms exceeded.');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('step timeout longer than test timeout', async ({ runInlineTest }) => {
|
test('step timeout longer than test timeout', async ({ runInlineTest }) => {
|
||||||
|
|
@ -422,6 +422,27 @@ test('step timeout longer than test timeout', async ({ runInlineTest }) => {
|
||||||
expect(result.output).toContain('Test timeout of 900ms exceeded.');
|
expect(result.output).toContain('Test timeout of 900ms exceeded.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('step timeout includes interrupted action errors', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('step with timeout', async ({ page }) => {
|
||||||
|
await test.step('my step', async () => {
|
||||||
|
await page.waitForTimeout(100_000);
|
||||||
|
}, { timeout: 1000 });
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { reporter: '', workers: 1 });
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
// Should include 2 errors, one for the step timeout and one for the aborted action.
|
||||||
|
expect.soft(result.output).toContain('TimeoutError: Step timeout of 1000ms exceeded.');
|
||||||
|
expect.soft(result.output).toContain(`> 4 | await test.step('my step', async () => {`);
|
||||||
|
expect.soft(result.output).toContain('Error: page.waitForTimeout: Test ended.');
|
||||||
|
expect.soft(result.output.split('Error: page.waitForTimeout: Test ended.').length).toBe(2);
|
||||||
|
expect.soft(result.output).toContain('> 5 | await page.waitForTimeout(100_000);');
|
||||||
|
});
|
||||||
|
|
||||||
test('step timeout is errors.TimeoutError', async ({ runInlineTest }) => {
|
test('step timeout is errors.TimeoutError', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
|
|
|
||||||
|
|
@ -470,3 +470,32 @@ test('attachments tab shows all but top-level .push attachments', async ({ runUI
|
||||||
- button /bar-attach/
|
- button /bar-attach/
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('skipped steps should have an indicator', async ({ runUITest }) => {
|
||||||
|
const { page } = await runUITest({
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test with steps', async ({}) => {
|
||||||
|
await test.step('outer', async () => {
|
||||||
|
await test.step.skip('skipped1', () => {});
|
||||||
|
});
|
||||||
|
await test.step.skip('skipped2', () => {});
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('treeitem', { name: 'test with steps' }).dblclick();
|
||||||
|
const actionsTree = page.getByTestId('actions-tree');
|
||||||
|
await actionsTree.getByRole('treeitem', { name: 'outer' }).click();
|
||||||
|
await page.keyboard.press('ArrowRight');
|
||||||
|
await expect(actionsTree).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem /outer/ [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem /skipped1/
|
||||||
|
- treeitem /skipped2/
|
||||||
|
`);
|
||||||
|
const skippedMarker = actionsTree.getByRole('treeitem', { name: 'skipped1' }).locator('.action-skipped');
|
||||||
|
await expect(skippedMarker).toBeVisible();
|
||||||
|
await expect(skippedMarker).toHaveAccessibleName('skipped');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -454,6 +454,50 @@ test('should generate baseline for input values', async ({ runInlineTest }, test
|
||||||
expect(result2.exitCode).toBe(0);
|
expect(result2.exitCode).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should update when options are specified', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'.git/marker': '',
|
||||||
|
'a.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', async ({ page }) => {
|
||||||
|
await page.setContent(\`<input value="hello world">\`);
|
||||||
|
await expect(page.locator('body')).toMatchAriaSnapshot(\`\`, { timeout: 2500 });
|
||||||
|
await expect(page.locator('body')).toMatchAriaSnapshot('',
|
||||||
|
{
|
||||||
|
timeout: 2500
|
||||||
|
});
|
||||||
|
});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
|
||||||
|
const data = fs.readFileSync(patchPath, 'utf-8');
|
||||||
|
expect(trimPatch(data)).toBe(`diff --git a/a.spec.ts b/a.spec.ts
|
||||||
|
--- a/a.spec.ts
|
||||||
|
+++ b/a.spec.ts
|
||||||
|
@@ -2,8 +2,12 @@
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', async ({ page }) => {
|
||||||
|
await page.setContent(\`<input value="hello world">\`);
|
||||||
|
- await expect(page.locator('body')).toMatchAriaSnapshot(\`\`, { timeout: 2500 });
|
||||||
|
- await expect(page.locator('body')).toMatchAriaSnapshot('',
|
||||||
|
+ await expect(page.locator('body')).toMatchAriaSnapshot(\`
|
||||||
|
+ - textbox: hello world
|
||||||
|
+ \`, { timeout: 2500 });
|
||||||
|
+ await expect(page.locator('body')).toMatchAriaSnapshot(\`
|
||||||
|
+ - textbox: hello world
|
||||||
|
+ \`,
|
||||||
|
{
|
||||||
|
timeout: 2500
|
||||||
|
});
|
||||||
|
`);
|
||||||
|
|
||||||
|
execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
|
||||||
|
const result2 = await runInlineTest({});
|
||||||
|
expect(result2.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
test('should not update snapshots when locator did not match', async ({ runInlineTest }, testInfo) => {
|
test('should not update snapshots when locator did not match', async ({ runInlineTest }, testInfo) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'.git/marker': '',
|
'.git/marker': '',
|
||||||
|
|
@ -617,4 +661,45 @@ test.describe('update-source-method', () => {
|
||||||
a.spec.ts
|
a.spec.ts
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should overwrite source when specified in the config', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'.git/marker': '',
|
||||||
|
'playwright.config.ts': `
|
||||||
|
export default { updateSourceMethod: 'overwrite' };
|
||||||
|
`,
|
||||||
|
'a.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', async ({ page }) => {
|
||||||
|
await page.setContent(\`<h1>hello</h1>\`);
|
||||||
|
await expect(page.locator('body')).toMatchAriaSnapshot(\`
|
||||||
|
- heading "world"
|
||||||
|
\`);
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { 'update-snapshots': 'all' });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
|
||||||
|
expect(fs.existsSync(patchPath)).toBeFalsy();
|
||||||
|
|
||||||
|
const data = fs.readFileSync(testInfo.outputPath('a.spec.ts'), 'utf-8');
|
||||||
|
expect(data).toBe(`
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', async ({ page }) => {
|
||||||
|
await page.setContent(\`<h1>hello</h1>\`);
|
||||||
|
await expect(page.locator('body')).toMatchAriaSnapshot(\`
|
||||||
|
- heading "hello" [level=1]
|
||||||
|
\`);
|
||||||
|
});
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(stripAnsi(result.output).replace(/\\/g, '/')).toContain(`New baselines created for:
|
||||||
|
|
||||||
|
a.spec.ts
|
||||||
|
`);
|
||||||
|
|
||||||
|
const result2 = await runInlineTest({});
|
||||||
|
expect(result2.exitCode).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ set -x
|
||||||
|
|
||||||
trap "cd $(pwd -P)" EXIT
|
trap "cd $(pwd -P)" EXIT
|
||||||
SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)"
|
SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)"
|
||||||
NODE_VERSION="22.13.0" # autogenerated via ./update-playwright-driver-version.mjs
|
NODE_VERSION="22.13.1" # autogenerated via ./update-playwright-driver-version.mjs
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
PACKAGE_VERSION=$(node -p "require('../../package.json').version")
|
PACKAGE_VERSION=$(node -p "require('../../package.json').version")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue