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
|
||||
- `name` <[string]>
|
||||
|
||||
Name of the snapshot to store in the snapshot folder corresponding to this test. Generates ordinal name if not specified.
|
||||
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot#2.path
|
||||
* since: v1.50
|
||||
- `path` <[string]>
|
||||
|
||||
Path to the YAML snapshot file.
|
||||
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.timeout = %%-js-assertions-timeout-%%
|
||||
* since: v1.50
|
||||
|
|
|
|||
|
|
@ -6,6 +6,74 @@ toc_max_heading_level: 2
|
|||
|
||||
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
|
||||
|
||||
<LiteYouTube
|
||||
|
|
|
|||
|
|
@ -1822,7 +1822,7 @@ Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout
|
|||
* since: v1.50
|
||||
- `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
|
||||
* 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"`.
|
||||
- `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.
|
||||
- `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"]>
|
||||
- `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.
|
||||
|
|
|
|||
|
|
@ -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"`. |
|
||||
| `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. |
|
||||
| `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
|
||||
|
||||
|
|
|
|||
60
package-lock.json
generated
60
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "playwright-internal",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "playwright-internal",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
|
@ -7751,10 +7751,10 @@
|
|||
"version": "0.0.0"
|
||||
},
|
||||
"packages/playwright": {
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.50.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7768,11 +7768,11 @@
|
|||
},
|
||||
"packages/playwright-browser-chromium": {
|
||||
"name": "@playwright/browser-chromium",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.50.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
|
@ -7780,11 +7780,11 @@
|
|||
},
|
||||
"packages/playwright-browser-firefox": {
|
||||
"name": "@playwright/browser-firefox",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.50.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
|
@ -7792,22 +7792,22 @@
|
|||
},
|
||||
"packages/playwright-browser-webkit": {
|
||||
"name": "@playwright/browser-webkit",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.50.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"packages/playwright-chromium": {
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.50.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7817,7 +7817,7 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-core": {
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
|
|
@ -7828,11 +7828,11 @@
|
|||
},
|
||||
"packages/playwright-ct-core": {
|
||||
"name": "@playwright/experimental-ct-core",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.50.0-next",
|
||||
"playwright-core": "1.50.0-next",
|
||||
"playwright": "1.50.0",
|
||||
"playwright-core": "1.50.0",
|
||||
"vite": "^5.2.8"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -7841,10 +7841,10 @@
|
|||
},
|
||||
"packages/playwright-ct-react": {
|
||||
"name": "@playwright/experimental-ct-react",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@playwright/experimental-ct-core": "1.50.0",
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -7856,10 +7856,10 @@
|
|||
},
|
||||
"packages/playwright-ct-react17": {
|
||||
"name": "@playwright/experimental-ct-react17",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@playwright/experimental-ct-core": "1.50.0",
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -7871,10 +7871,10 @@
|
|||
},
|
||||
"packages/playwright-ct-svelte": {
|
||||
"name": "@playwright/experimental-ct-svelte",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@playwright/experimental-ct-core": "1.50.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -7889,10 +7889,10 @@
|
|||
},
|
||||
"packages/playwright-ct-vue": {
|
||||
"name": "@playwright/experimental-ct-vue",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@playwright/experimental-ct-core": "1.50.0",
|
||||
"@vitejs/plugin-vue": "^5.2.0"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -7903,11 +7903,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-firefox": {
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.50.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7918,10 +7918,10 @@
|
|||
},
|
||||
"packages/playwright-test": {
|
||||
"name": "@playwright/test",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.50.0-next"
|
||||
"playwright": "1.50.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7931,11 +7931,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-webkit": {
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.50.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "playwright-internal",
|
||||
"private": true,
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
|
|
@ -60,11 +60,6 @@
|
|||
color: var(--color-scale-orange-6);
|
||||
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) {
|
||||
|
|
@ -98,11 +93,6 @@
|
|||
color: var(--color-scale-orange-2);
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { TreeItem } from './treeItem';
|
|||
import { CopyToClipboard } from './copyToClipboard';
|
||||
import './links.css';
|
||||
import { linkifyText } from '@web/renderUtils';
|
||||
import { clsx } from '@web/uiUtils';
|
||||
import { clsx, useFlash } from '@web/uiUtils';
|
||||
|
||||
export function navigate(href: string | URL) {
|
||||
window.history.pushState({}, '', href);
|
||||
|
|
@ -73,7 +73,8 @@ export const AttachmentLink: React.FunctionComponent<{
|
|||
linkName?: string,
|
||||
openInNewTab?: boolean,
|
||||
}> = ({ 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>
|
||||
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
|
||||
{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 ? () => {
|
||||
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)));
|
||||
|
|
@ -118,12 +119,12 @@ const kMissingContentType = 'x-playwright/missing';
|
|||
|
||||
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 isAnchored = useIsAnchored(id);
|
||||
React.useEffect(() => {
|
||||
if (isAnchored)
|
||||
onReveal();
|
||||
return onReveal();
|
||||
}, [isAnchored, onReveal, searchParams]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -176,6 +176,7 @@ const StepTreeItem: React.FC<{
|
|||
}> = ({ test, step, result, depth }) => {
|
||||
return <TreeItem title={<span aria-label={step.title}>
|
||||
<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'))}
|
||||
<span>{step.title}</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 ? () => {
|
||||
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 attachments = step.attachments.map(attachmentIndex => (
|
||||
<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);
|
||||
return snippet.concat(steps);
|
||||
} : undefined} depth={depth}/>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,11 +25,14 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tree-item-title.selected {
|
||||
text-decoration: underline var(--color-underlinenav-icon);
|
||||
text-decoration-thickness: 1.5px;
|
||||
}
|
||||
|
||||
.tree-item-body {
|
||||
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,
|
||||
expandByDefault?: boolean,
|
||||
depth: number,
|
||||
selected?: boolean,
|
||||
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);
|
||||
return <div className={'tree-item'} style={style}>
|
||||
<span className={clsx('tree-item-title', selected && 'selected')} style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
|
||||
return <div className={clsx('tree-item', flash && 'yellow-flash')} style={style}>
|
||||
<span className='tree-item-title' style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
|
||||
{loadChildren && !!expanded && icons.downArrow()}
|
||||
{loadChildren && !expanded && icons.rightArrow()}
|
||||
{!loadChildren && <span style={{ visibility: 'hidden' }}>{icons.rightArrow()}</span>}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/browser-chromium",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"description": "Playwright package that automatically installs Chromium",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -27,6 +27,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.50.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/browser-firefox",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"description": "Playwright package that automatically installs Firefox",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -27,6 +27,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.50.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/browser-webkit",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"description": "Playwright package that automatically installs WebKit",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -27,6 +27,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.50.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-chromium",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"description": "A high-level API to automate Chromium",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,6 +30,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.50.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-core",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-core",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"description": "Playwright Component Testing Helpers",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -26,8 +26,8 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next",
|
||||
"playwright-core": "1.50.0",
|
||||
"vite": "^5.2.8",
|
||||
"playwright": "1.50.0-next"
|
||||
"playwright": "1.50.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-react",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"description": "Playwright Component Testing for React",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@playwright/experimental-ct-core": "1.50.0",
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-react17",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"description": "Playwright Component Testing for React",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@playwright/experimental-ct-core": "1.50.0",
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-svelte",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"description": "Playwright Component Testing for Svelte",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@playwright/experimental-ct-core": "1.50.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-vue",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"description": "Playwright Component Testing for Vue",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@playwright/experimental-ct-core": "1.50.0",
|
||||
"@vitejs/plugin-vue": "^5.2.0"
|
||||
},
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-firefox",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"description": "A high-level API to automate Firefox",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,6 +30,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.50.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/test",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,6 +30,6 @@
|
|||
},
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"playwright": "1.50.0-next"
|
||||
"playwright": "1.50.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-webkit",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"description": "A high-level API to automate WebKit",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,6 +30,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.50.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright",
|
||||
"version": "1.50.0-next",
|
||||
"version": "1.50.0",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
},
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0-next"
|
||||
"playwright-core": "1.50.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"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 });
|
||||
return await zones.run('stepZone', step, async () => {
|
||||
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)
|
||||
throw new errors.TimeoutError(`Step timeout ${options.timeout}ms exceeded.`);
|
||||
throw new errors.TimeoutError(`Step timeout of ${options.timeout}ms exceeded.`);
|
||||
step.complete({});
|
||||
return result.result;
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -59,9 +59,7 @@ export async function toMatchAriaSnapshot(
|
|||
if (isString(expectedParam)) {
|
||||
expected = expectedParam;
|
||||
} else {
|
||||
if (expectedParam?.path) {
|
||||
expectedPath = expectedParam.path;
|
||||
} else if (expectedParam?.name) {
|
||||
if (expectedParam?.name) {
|
||||
expectedPath = testInfo.snapshotPath(sanitizeFilePathBeforeExtension(expectedParam.name));
|
||||
} else {
|
||||
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
|
||||
|
|
@ -136,7 +134,7 @@ export async function toMatchAriaSnapshot(
|
|||
}
|
||||
return { pass: true, message: () => '', name: 'toMatchAriaSnapshot' };
|
||||
} 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 };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@ async function mergeReports(reportDir: string | undefined, opts: { [key: string]
|
|||
function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrides {
|
||||
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))
|
||||
updateSnapshots = options.updateSnapshots;
|
||||
else
|
||||
|
|
@ -303,7 +303,7 @@ function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrid
|
|||
tsconfig: options.tsconfig ? path.resolve(process.cwd(), options.tsconfig) : undefined,
|
||||
ignoreSnapshots: options.ignoreSnapshots ? !!options.ignoreSnapshots : undefined,
|
||||
updateSnapshots,
|
||||
updateSourceMethod: options.updateSourceMethod || 'patch',
|
||||
updateSourceMethod: options.updateSourceMethod,
|
||||
workers: options.workers,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -129,6 +129,8 @@ class ListReporter extends TerminalReporter {
|
|||
if (this._needNewLine) {
|
||||
this._needNewLine = false;
|
||||
process.stdout.write('\n');
|
||||
++this._lastRow;
|
||||
this._lastColumn = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -210,6 +212,7 @@ class ListReporter extends TerminalReporter {
|
|||
process.stdout.write('\n');
|
||||
}
|
||||
++this._lastRow;
|
||||
this._lastColumn = 0;
|
||||
}
|
||||
|
||||
private _updateLine(row: number, text: string, prefix: string) {
|
||||
|
|
|
|||
|
|
@ -68,24 +68,27 @@ export async function applySuggestedRebaselines(config: FullConfigInternal, repo
|
|||
traverse(fileNode, {
|
||||
CallExpression: path => {
|
||||
const node = path.node;
|
||||
if (node.arguments.length !== 1)
|
||||
if (node.arguments.length < 1)
|
||||
return;
|
||||
if (!t.isMemberExpression(node.callee))
|
||||
return;
|
||||
const argument = node.arguments[0];
|
||||
if (!t.isStringLiteral(argument) && !t.isTemplateLiteral(argument))
|
||||
return;
|
||||
|
||||
const matcher = node.callee.property;
|
||||
const prop = 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) {
|
||||
// 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;
|
||||
if (matcher.loc!.start.column + 1 !== replacement.location.column)
|
||||
if (prop.loc.start.column + 1 !== replacement.location.column)
|
||||
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);
|
||||
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,
|
||||
// for example when a single test runs multiple times because of projects or retries.
|
||||
// Do not apply multiple replacements for the same assertion.
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@ export class TestInfoImpl implements TestInfo {
|
|||
location: data.location,
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -421,7 +421,7 @@ export class TestInfoImpl implements TestInfo {
|
|||
} else {
|
||||
// trace viewer has no means of representing attachments outside of a step, so we create an artificial action
|
||||
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]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
type: 'before',
|
||||
callId,
|
||||
parentId,
|
||||
startTime: monotonicTime(),
|
||||
class: 'Test',
|
||||
method: 'step',
|
||||
method: category,
|
||||
apiName,
|
||||
params: Object.fromEntries(Object.entries(params || {}).map(([name, value]) => [name, generatePreview(value)])),
|
||||
stack,
|
||||
|
|
|
|||
|
|
@ -75,16 +75,20 @@ export class WorkerMain extends ProcessRunner {
|
|||
|
||||
process.on('unhandledRejection', reason => this.unhandledError(reason));
|
||||
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._currentTest?._tracing.appendStdioToTrace('stdout', chunk);
|
||||
if (typeof cb === 'function')
|
||||
process.nextTick(cb);
|
||||
return true;
|
||||
};
|
||||
|
||||
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._currentTest?._tracing.appendStdioToTrace('stderr', chunk);
|
||||
if (typeof cb === 'function')
|
||||
process.nextTick(cb);
|
||||
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?: {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Path to the YAML snapshot file.
|
||||
*/
|
||||
path?: string;
|
||||
|
||||
/**
|
||||
* 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:
|
||||
* '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.
|
||||
* '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`.
|
||||
*/
|
||||
gracefulShutdown?: {
|
||||
signal: "SIGINT"|"SIGTERM";
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@
|
|||
color: var(--vscode-editorCodeLens-foreground);
|
||||
}
|
||||
|
||||
.action-skipped {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
flex: none;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import type { ActionTraceEvent, AfterActionTraceEventAttachment } from '@trace/trace';
|
||||
import { msToString } from '@web/uiUtils';
|
||||
import { clsx, msToString } from '@web/uiUtils';
|
||||
import * as React from 'react';
|
||||
import './actionList.css';
|
||||
import * as modelUtil from './modelUtil';
|
||||
|
|
@ -25,6 +25,7 @@ import { TreeView } from '@web/components/treeView';
|
|||
import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil';
|
||||
import type { Boundaries } from './geometry';
|
||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||
import { testStatusIcon } from './testUtils';
|
||||
|
||||
export interface ActionListProps {
|
||||
actions: ActionTraceEventInContext[],
|
||||
|
|
@ -119,6 +120,7 @@ export const renderAction = (
|
|||
|
||||
const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript');
|
||||
|
||||
const isSkipped = action.class === 'Test' && action.method === 'test.step.skip';
|
||||
let time: string = '';
|
||||
if (action.endTime)
|
||||
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.class === 'APIRequestContext' && action.params.url && <div className='action-url' title={action.params.url}>{excludeOrigin(action.params.url)}</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])} />}
|
||||
{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?.()}>
|
||||
{!!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>}
|
||||
|
|
|
|||
|
|
@ -55,3 +55,11 @@
|
|||
a.codicon-cloud-download:hover{
|
||||
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 './attachmentsTab.css';
|
||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||
import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
|
||||
import type { MultiTraceModel } from './modelUtil';
|
||||
import { PlaceholderPanel } from './placeholderPanel';
|
||||
import type { AfterActionTraceEventAttachment } from '@trace/trace';
|
||||
import { CodeMirrorWrapper, lineHeight } from '@web/components/codeMirrorWrapper';
|
||||
import { isTextualMimeType } from '@isomorphic/mimeType';
|
||||
import { Expandable } from '@web/components/expandable';
|
||||
import { linkifyText } from '@web/renderUtils';
|
||||
import { clsx } from '@web/uiUtils';
|
||||
import { clsx, useFlash } from '@web/uiUtils';
|
||||
|
||||
type Attachment = AfterActionTraceEventAttachment & { traceUrl: string };
|
||||
|
||||
type ExpandableAttachmentProps = {
|
||||
attachment: Attachment;
|
||||
reveal: boolean;
|
||||
highlight: boolean;
|
||||
reveal?: any;
|
||||
};
|
||||
|
||||
const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> = ({ attachment, reveal, highlight }) => {
|
||||
const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> = ({ attachment, reveal }) => {
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
const [attachmentText, setAttachmentText] = React.useState<string | null>(null);
|
||||
const [placeholder, setPlaceholder] = React.useState<string | null>(null);
|
||||
const [flash, triggerFlash] = useFlash();
|
||||
const ref = React.useRef<HTMLSpanElement>(null);
|
||||
|
||||
const isTextAttachment = isTextualMimeType(attachment.contentType);
|
||||
const hasContent = !!attachment.sha1 || !!attachment.path;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (reveal)
|
||||
if (reveal) {
|
||||
ref.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [reveal]);
|
||||
return triggerFlash();
|
||||
}
|
||||
}, [reveal, triggerFlash]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (expanded && attachmentText === null && placeholder === null) {
|
||||
|
|
@ -66,14 +68,14 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
|
|||
}, [attachmentText]);
|
||||
|
||||
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>}
|
||||
</span>;
|
||||
|
||||
if (!isTextAttachment || !hasContent)
|
||||
return <div style={{ marginLeft: 20 }}>{title}</div>;
|
||||
|
||||
return <>
|
||||
return <div className={clsx(flash && 'yellow-flash')}>
|
||||
<Expandable title={title} expanded={expanded} setExpanded={setExpanded} expandOnTitleClick={true}>
|
||||
{placeholder && <i>{placeholder}</i>}
|
||||
</Expandable>
|
||||
|
|
@ -87,14 +89,13 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
|
|||
wrapLines={false}>
|
||||
</CodeMirrorWrapper>
|
||||
</div>}
|
||||
</>;
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const AttachmentsTab: React.FunctionComponent<{
|
||||
model: MultiTraceModel | undefined,
|
||||
selectedAction: ActionTraceEventInContext | undefined,
|
||||
revealedAttachment?: AfterActionTraceEventAttachment,
|
||||
}> = ({ model, selectedAction, revealedAttachment }) => {
|
||||
revealedAttachment?: [AfterActionTraceEventAttachment, number],
|
||||
}> = ({ model, revealedAttachment }) => {
|
||||
const { diffMap, screenshots, attachments } = React.useMemo(() => {
|
||||
const attachments = 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)}>
|
||||
<ExpandableAttachment
|
||||
attachment={a}
|
||||
highlight={selectedAction?.attachments?.some(selected => isEqualAttachment(a, selected)) ?? false}
|
||||
reveal={!!revealedAttachment && isEqualAttachment(a, revealedAttachment)}
|
||||
reveal={(!!revealedAttachment && isEqualAttachment(a, revealedAttachment[0])) ? revealedAttachment : undefined}
|
||||
/>
|
||||
</div>;
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export const Workbench: React.FunctionComponent<{
|
|||
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, onOpenExternally, revealSource }) => {
|
||||
const [selectedCallId, setSelectedCallId] = React.useState<string | 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 [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
|
||||
const [highlightedConsoleMessage, setHighlightedConsoleMessage] = React.useState<ConsoleEntry | undefined>();
|
||||
|
|
@ -148,7 +148,12 @@ export const Workbench: React.FunctionComponent<{
|
|||
|
||||
const revealAttachment = React.useCallback((attachment: AfterActionTraceEventAttachment) => {
|
||||
selectPropertiesTab('attachments');
|
||||
setRevealedAttachment(attachment);
|
||||
setRevealedAttachment(currentValue => {
|
||||
if (!currentValue)
|
||||
return [attachment, 0];
|
||||
const revealCounter = currentValue[1];
|
||||
return [attachment, revealCounter + 1];
|
||||
});
|
||||
}, [selectPropertiesTab]);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
|
@ -238,7 +243,7 @@ export const Workbench: React.FunctionComponent<{
|
|||
id: 'attachments',
|
||||
title: 'Attachments',
|
||||
count: attachments.length,
|
||||
render: () => <AttachmentsTab model={model} selectedAction={selectedAction} revealedAttachment={revealedAttachment} />
|
||||
render: () => <AttachmentsTab model={model} revealedAttachment={revealedAttachment} />
|
||||
};
|
||||
|
||||
const tabs: TabbedPaneTabModel[] = [
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { EffectCallback } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
// Recalculates the value when dependencies change.
|
||||
|
|
@ -224,3 +225,26 @@ export function scrollIntoViewIfNeeded(element: Element | undefined) {
|
|||
|
||||
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');
|
||||
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
|
||||
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) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
|
|
|
|||
|
|
@ -229,6 +229,7 @@ export function cleanEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|||
// END: Reserved CI
|
||||
PW_TEST_HTML_REPORT_OPEN: undefined,
|
||||
PLAYWRIGHT_HTML_OPEN: undefined,
|
||||
PW_TEST_DEBUG_REPORTERS: undefined,
|
||||
PW_TEST_REPORTER: undefined,
|
||||
PW_TEST_REPORTER_WS_ENDPOINT: undefined,
|
||||
PW_TEST_SOURCE_TRANSFORM: undefined,
|
||||
|
|
|
|||
|
|
@ -959,10 +959,9 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
|||
await showReport();
|
||||
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 page.getByLabel('attach "foo-2"').click();
|
||||
await page.getByTitle('see "foo-2"').click();
|
||||
await page.getByLabel(`attach "foo-2"`).getByTitle('reveal attachment').click();
|
||||
await expect(attachment).toBeInViewport();
|
||||
|
||||
await page.reload();
|
||||
|
|
@ -989,10 +988,9 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
|||
await showReport();
|
||||
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 page.getByLabel('step').click();
|
||||
await page.getByTitle('see "attachment"').click();
|
||||
await page.getByLabel('step').getByTitle('reveal attachment').click();
|
||||
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(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 });
|
||||
expect(result.exitCode).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 }) => {
|
||||
|
|
@ -422,6 +422,27 @@ test('step timeout longer than test timeout', async ({ runInlineTest }) => {
|
|||
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 }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
|
|
|
|||
|
|
@ -470,3 +470,32 @@ test('attachments tab shows all but top-level .push attachments', async ({ runUI
|
|||
- 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);
|
||||
});
|
||||
|
||||
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) => {
|
||||
const result = await runInlineTest({
|
||||
'.git/marker': '',
|
||||
|
|
@ -617,4 +661,45 @@ test.describe('update-source-method', () => {
|
|||
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
|
||||
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")"
|
||||
PACKAGE_VERSION=$(node -p "require('../../package.json').version")
|
||||
|
|
|
|||
Loading…
Reference in a new issue