This commit is contained in:
JacksonLei123 2025-01-20 12:10:45 -05:00
commit 905f921722
96 changed files with 1188 additions and 1468 deletions

View file

@ -1,6 +1,6 @@
# 🎭 Playwright
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-132.0.6834.57-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-134.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord)
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-133.0.6943.16-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-134.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord)
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->132.0.6834.57<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->133.0.6943.16<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->134.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |

View file

@ -1313,7 +1313,7 @@ await Expect(locator).ToHaveAccessibleNameAsync("Save to disk");
### param: LocatorAssertions.toHaveAccessibleName.name
* since: v1.44
- `name` <[string]|[RegExp]|[Array]<[string]|[RegExp]>>
- `name` <[string]|[RegExp]>
Expected accessible name.
@ -1413,49 +1413,48 @@ Attribute name.
* langs:
- alias-java: hasClass
Ensures the [Locator] points to an element with given CSS classes. This needs to be a full match
or using a relaxed regular expression.
Ensures the [Locator] points to an element with given CSS classes. When a string is provided, it must fully match the element's `class` attribute. To match individual classes or perform partial matches, use a regular expression:
**Usage**
```html
<div class='selected row' id='component'></div>
<div class='middle selected row' id='component'></div>
```
```js
const locator = page.locator('#component');
await expect(locator).toHaveClass(/selected/);
await expect(locator).toHaveClass('selected row');
await expect(locator).toHaveClass('middle selected row');
await expect(locator).toHaveClass(/(^|\s)selected(\s|$)/);
```
```java
assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
assertThat(page.locator("#component")).hasClass("selected row");
assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
assertThat(page.locator("#component")).hasClass("middle selected row");
```
```python async
from playwright.async_api import expect
locator = page.locator("#component")
await expect(locator).to_have_class(re.compile(r"selected"))
await expect(locator).to_have_class("selected row")
await expect(locator).to_have_class(re.compile(r"(^|\\s)selected(\\s|$)"))
await expect(locator).to_have_class("middle selected row")
```
```python sync
from playwright.sync_api import expect
locator = page.locator("#component")
expect(locator).to_have_class(re.compile(r"selected"))
expect(locator).to_have_class("selected row")
expect(locator).to_have_class(re.compile(r"(^|\\s)selected(\\s|$)"))
expect(locator).to_have_class("middle selected row")
```
```csharp
var locator = Page.Locator("#component");
await Expect(locator).ToHaveClassAsync(new Regex("selected"));
await Expect(locator).ToHaveClassAsync("selected row");
await Expect(locator).ToHaveClassAsync(new Regex("(^|\\s)selected(\\s|$)"));
await Expect(locator).ToHaveClassAsync("middle selected row");
```
Note that if array is passed as an expected value, entire lists of elements can be asserted:
When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected class values. Each element's class attribute is matched against the corresponding string or regular expression in the array:
```js
const locator = page.locator('list > .component');
@ -2242,8 +2241,7 @@ assertThat(page.locator("body")).matchesAriaSnapshot("""
## async method: LocatorAssertions.toMatchAriaSnapshot#2
* since: v1.50
* langs:
- alias-java: matchesAriaSnapshot
* langs: js
Asserts that the target element matches the given [accessibility snapshot](../aria-snapshots.md).
@ -2264,7 +2262,7 @@ expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml'
```
```csharp
await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(new { Path = "/path/to/snapshot.yml" });
await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(new { Path = "/path/to/snapshot.yml" });
```
```java
@ -2276,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

View file

@ -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

60
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "playwright-internal",
"version": "1.50.0-next",
"version": "1.51.0-next",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "playwright-internal",
"version": "1.50.0-next",
"version": "1.51.0-next",
"license": "Apache-2.0",
"workspaces": [
"packages/*"
@ -7751,10 +7751,10 @@
"version": "0.0.0"
},
"packages/playwright": {
"version": "1.50.0-next",
"version": "1.51.0-next",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
},
"bin": {
"playwright": "cli.js"
@ -7768,11 +7768,11 @@
},
"packages/playwright-browser-chromium": {
"name": "@playwright/browser-chromium",
"version": "1.50.0-next",
"version": "1.51.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
},
"engines": {
"node": ">=18"
@ -7780,11 +7780,11 @@
},
"packages/playwright-browser-firefox": {
"name": "@playwright/browser-firefox",
"version": "1.50.0-next",
"version": "1.51.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
},
"engines": {
"node": ">=18"
@ -7792,22 +7792,22 @@
},
"packages/playwright-browser-webkit": {
"name": "@playwright/browser-webkit",
"version": "1.50.0-next",
"version": "1.51.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
},
"engines": {
"node": ">=18"
}
},
"packages/playwright-chromium": {
"version": "1.50.0-next",
"version": "1.51.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
},
"bin": {
"playwright": "cli.js"
@ -7817,7 +7817,7 @@
}
},
"packages/playwright-core": {
"version": "1.50.0-next",
"version": "1.51.0-next",
"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.51.0-next",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.50.0-next",
"playwright-core": "1.50.0-next",
"playwright": "1.51.0-next",
"playwright-core": "1.51.0-next",
"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.51.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.50.0-next",
"@playwright/experimental-ct-core": "1.51.0-next",
"@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.51.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.50.0-next",
"@playwright/experimental-ct-core": "1.51.0-next",
"@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.51.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.50.0-next",
"@playwright/experimental-ct-core": "1.51.0-next",
"@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.51.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.50.0-next",
"@playwright/experimental-ct-core": "1.51.0-next",
"@vitejs/plugin-vue": "^5.2.0"
},
"bin": {
@ -7903,11 +7903,11 @@
}
},
"packages/playwright-firefox": {
"version": "1.50.0-next",
"version": "1.51.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
},
"bin": {
"playwright": "cli.js"
@ -7918,10 +7918,10 @@
},
"packages/playwright-test": {
"name": "@playwright/test",
"version": "1.50.0-next",
"version": "1.51.0-next",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.50.0-next"
"playwright": "1.51.0-next"
},
"bin": {
"playwright": "cli.js"
@ -7931,11 +7931,11 @@
}
},
"packages/playwright-webkit": {
"version": "1.50.0-next",
"version": "1.51.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
},
"bin": {
"playwright": "cli.js"

View file

@ -1,7 +1,7 @@
{
"name": "playwright-internal",
"private": true,
"version": "1.50.0-next",
"version": "1.51.0-next",
"description": "A high-level API to automate web browsers",
"repository": {
"type": "git",

View file

@ -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 {

View file

@ -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]);
}

View file

@ -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}/>;
};

View file

@ -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; }
}

View file

@ -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>}

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/browser-chromium",
"version": "1.50.0-next",
"version": "1.51.0-next",
"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.51.0-next"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/browser-firefox",
"version": "1.50.0-next",
"version": "1.51.0-next",
"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.51.0-next"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/browser-webkit",
"version": "1.50.0-next",
"version": "1.51.0-next",
"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.51.0-next"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "playwright-chromium",
"version": "1.50.0-next",
"version": "1.51.0-next",
"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.51.0-next"
}
}

View file

@ -3,15 +3,15 @@
"browsers": [
{
"name": "chromium",
"revision": "1153",
"revision": "1155",
"installByDefault": true,
"browserVersion": "132.0.6834.57"
"browserVersion": "133.0.6943.16"
},
{
"name": "chromium-tip-of-tree",
"revision": "1293",
"revision": "1295",
"installByDefault": false,
"browserVersion": "133.0.6943.0"
"browserVersion": "134.0.6960.0"
},
{
"name": "firefox",

View file

@ -1,6 +1,6 @@
{
"name": "playwright-core",
"version": "1.50.0-next",
"version": "1.51.0-next",
"description": "A high-level API to automate web browsers",
"repository": {
"type": "git",

View file

@ -65,7 +65,6 @@ commandWithOpenOptions('codegen [url]', 'open page and generate code for user ac
[
['-o, --output <file name>', 'saves the generated script to a file'],
['--target <language>', `language to generate, one of javascript, playwright-test, python, python-async, python-pytest, csharp, csharp-mstest, csharp-nunit, java, java-junit`, codegenId()],
['--save-trace <filename>', 'record a trace for the session and save it to a file'],
['--test-id-attribute <attributeName>', 'use the specified attribute to generate data test ID selectors'],
]).action(function(url, options) {
codegen(options, url).catch(logErrorAndExit);
@ -353,7 +352,6 @@ type Options = {
saveHar?: string;
saveHarGlob?: string;
saveStorage?: string;
saveTrace?: string;
timeout: string;
timezone?: string;
viewportSize?: string;
@ -508,8 +506,6 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
if (closingBrowser)
return;
closingBrowser = true;
if (options.saveTrace)
await context.tracing.stop({ path: options.saveTrace });
if (options.saveStorage)
await context.storageState({ path: options.saveStorage }).catch(e => null);
if (options.saveHar)
@ -536,9 +532,6 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
context.setDefaultTimeout(timeout);
context.setDefaultNavigationTimeout(timeout);
if (options.saveTrace)
await context.tracing.start({ screenshots: true, snapshots: true });
// Omit options that we add automatically for presentation purpose.
delete launchOptions.headless;
delete launchOptions.executablePath;
@ -595,7 +588,6 @@ async function codegen(options: Options & { target: string, output?: string, tes
device: options.device,
saveStorage: options.saveStorage,
mode: 'recording',
codegenMode: process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions',
testIdAttributeName,
outputFile: outputFile ? path.resolve(outputFile) : undefined,
handleSIGINT: false,

View file

@ -970,7 +970,6 @@ scheme.BrowserContextPauseResult = tOptional(tObject({}));
scheme.BrowserContextEnableRecorderParams = tObject({
language: tOptional(tString),
mode: tOptional(tEnum(['inspecting', 'recording'])),
codegenMode: tOptional(tEnum(['actions', 'trace-events'])),
pauseOnNextStatement: tOptional(tBoolean),
testIdAttributeName: tOptional(tString),
launchOptions: tOptional(tAny),

View file

@ -133,6 +133,12 @@ function defaultProfilePreferences(
'dom.max_chrome_script_run_time': 0,
'dom.max_script_run_time': 0,
// Disable background timer throttling to allow tests to run in parallel
// without a decrease in performance.
'dom.min_background_timeout_value': 0,
'dom.min_background_timeout_value_without_budget_throttling': 0,
'dom.timeout.enable_budget_timer_throttling': false,
// Only load extensions from the application and user profile
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
'extensions.autoDisableScopes': 0,
@ -175,6 +181,9 @@ function defaultProfilePreferences(
// Show chrome errors and warnings in the error console
'javascript.options.showInConsole': true,
// Do not throttle rendering (requestAnimationFrame) in background tabs
'layout.testing.top-level-always-active': true,
// Disable download and usage of OpenH264: and Widevine plugins
'media.gmp-manager.updateEnabled': false,

View file

@ -130,7 +130,7 @@ export abstract class BrowserContext extends SdkObject {
// When PWDEBUG=1, show inspector for each context.
if (debugMode() === 'inspector')
await Recorder.show('actions', this, RecorderApp.factory(this), { pauseOnNextStatement: true });
await Recorder.show(this, RecorderApp.factory(this), { pauseOnNextStatement: true });
// When paused, show inspector.
if (this._debugger.isPaused())

View file

@ -38,7 +38,10 @@ export const chromiumSwitches = [
// ThirdPartyStoragePartitioning - https://github.com/microsoft/playwright/issues/32230
// LensOverlay - Hides the Lens feature in the URL address bar. Its not working in unofficial builds.
// PlzDedicatedWorker - https://github.com/microsoft/playwright/issues/31747
'--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate,HttpsUpgrades,PaintHolding,ThirdPartyStoragePartitioning,LensOverlay,PlzDedicatedWorker',
// DeferRendererTasksAfterInput - this makes Page.frameScheduledNavigation arrive much later after a click,
// making our navigation auto-wait after click not working. Can be removed once we deperecate noWaitAfter.
// See https://github.com/microsoft/playwright/pull/34372.
'--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate,HttpsUpgrades,PaintHolding,ThirdPartyStoragePartitioning,LensOverlay,PlzDedicatedWorker,DeferRendererTasksAfterInput',
'--allow-pre-commit-input',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',

View file

@ -685,8 +685,8 @@ percentage [0 - 100] for scroll driven animations
/**
* The unique request id.
*/
requestId: Network.RequestId;
url?: string;
requestId?: Network.RequestId;
url: string;
}
/**
* Information about the frame affected by an inspector issue.
@ -697,6 +697,20 @@ percentage [0 - 100] for scroll driven animations
export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout"|"ExcludePortMismatch"|"ExcludeSchemeMismatch";
export type CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"|"WarnDeprecationTrialMetadata"|"WarnThirdPartyCookieHeuristic";
export type CookieOperation = "SetCookie"|"ReadCookie";
/**
* Represents the category of insight that a cookie issue falls under.
*/
export type InsightType = "GitHubResource"|"GracePeriod"|"Heuristics";
/**
* Information about the suggested solution to a cookie issue.
*/
export interface CookieIssueInsight {
type: InsightType;
/**
* Link to table entry in third-party cookie migration readiness list.
*/
tableEntryUrl?: string;
}
/**
* This information is currently necessary, as the front-end has a difficult
time finding a specific cookie. With this, we can convey specific error
@ -721,6 +735,10 @@ may be used by the front-end as additional context.
siteForCookies?: string;
cookieUrl?: string;
request?: AffectedRequest;
/**
* The recommended solution to the issue.
*/
insight?: CookieIssueInsight;
}
export type MixedContentResolutionStatus = "MixedContentBlocked"|"MixedContentAutomaticallyUpgraded"|"MixedContentWarning";
export type MixedContentResourceType = "AttributionSrc"|"Audio"|"Beacon"|"CSPReport"|"Download"|"EventSource"|"Favicon"|"Font"|"Form"|"Frame"|"Image"|"Import"|"JSON"|"Manifest"|"Ping"|"PluginData"|"PluginResource"|"Prefetch"|"Resource"|"Script"|"ServiceWorker"|"SharedWorker"|"SpeculationRules"|"Stylesheet"|"Track"|"Video"|"Worker"|"XMLHttpRequest"|"XSLT";
@ -758,7 +776,7 @@ Does not always exist (e.g. for unsafe form submission urls).
* Enum indicating the reason a response has been blocked. These reasons are
refinements of the net error BLOCKED_BY_RESPONSE.
*/
export type BlockedByResponseReason = "CoepFrameResourceNeedsCoepHeader"|"CoopSandboxedIFrameCannotNavigateToCoopPage"|"CorpNotSameOrigin"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoep"|"CorpNotSameOriginAfterDefaultedToSameOriginByDip"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoepAndDip"|"CorpNotSameSite";
export type BlockedByResponseReason = "CoepFrameResourceNeedsCoepHeader"|"CoopSandboxedIFrameCannotNavigateToCoopPage"|"CorpNotSameOrigin"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoep"|"CorpNotSameOriginAfterDefaultedToSameOriginByDip"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoepAndDip"|"CorpNotSameSite"|"SRIMessageSignatureMismatch";
/**
* Details for a request that has been blocked with the BLOCKED_BY_RESPONSE
code. Currently only used for COEP/COOP, but may be extended to include
@ -963,6 +981,15 @@ features, encourage the use of new ones, and provide general guidance.
failureMessage: string;
requestId?: Network.RequestId;
}
export type SelectElementAccessibilityIssueReason = "DisallowedSelectChild"|"DisallowedOptGroupChild"|"NonPhrasingContentOptionChild"|"InteractiveContentOptionChild"|"InteractiveContentLegendChild";
/**
* This isue warns about errors in the select element content model.
*/
export interface SelectElementAccessibilityIssueDetails {
nodeId: DOM.BackendNodeId;
selectElementAccessibilityIssueReason: SelectElementAccessibilityIssueReason;
hasDisallowedAttributes: boolean;
}
export type StyleSheetLoadingIssueReason = "LateImportRule"|"RequestFailed";
/**
* This issue warns when a referenced stylesheet couldn't be loaded.
@ -1005,7 +1032,7 @@ registrations being ignored.
optional fields in InspectorIssueDetails to convey more specific
information about the kind of issue.
*/
export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue";
export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue"|"SelectElementAccessibilityIssue";
/**
* This struct holds a list of optional fields with additional information
specific to the kind of issue. When adding a new issue code, please also
@ -1033,6 +1060,7 @@ add a new optional field to this type.
propertyRuleIssueDetails?: PropertyRuleIssueDetails;
federatedAuthUserInfoRequestIssueDetails?: FederatedAuthUserInfoRequestIssueDetails;
sharedDictionaryIssueDetails?: SharedDictionaryIssueDetails;
selectElementAccessibilityIssueDetails?: SelectElementAccessibilityIssueDetails;
}
/**
* A unique id for a DevTools inspector issue. Allows other entities (e.g.
@ -1534,7 +1562,7 @@ events afterwards if enabled and recording.
*/
windowState?: WindowState;
}
export type PermissionType = "accessibilityEvents"|"audioCapture"|"backgroundSync"|"backgroundFetch"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"flash"|"geolocation"|"idleDetection"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"protectedMediaIdentifier"|"sensors"|"storageAccess"|"speakerSelection"|"topLevelStorageAccess"|"videoCapture"|"videoCapturePanTiltZoom"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"windowManagement";
export type PermissionType = "ar"|"audioCapture"|"automaticFullscreen"|"backgroundFetch"|"backgroundSync"|"cameraPanTiltZoom"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"geolocation"|"handTracking"|"idleDetection"|"keyboardLock"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"pointerLock"|"protectedMediaIdentifier"|"sensors"|"smartCard"|"speakerSelection"|"storageAccess"|"topLevelStorageAccess"|"videoCapture"|"vr"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"webPrinting"|"windowManagement";
export type PermissionSetting = "granted"|"denied"|"prompt";
/**
* Definition of PermissionDescriptor defined in the Permissions API:
@ -1961,6 +1989,19 @@ inspector" rules), "regular" for regular stylesheets.
*/
matches: RuleMatch[];
}
/**
* CSS style coming from animations with the name of the animation.
*/
export interface CSSAnimationStyle {
/**
* The name of the animation.
*/
name?: string;
/**
* The style coming from the animation.
*/
style: CSSStyle;
}
/**
* Inherited CSS rule collection from ancestor node.
*/
@ -1974,6 +2015,19 @@ inspector" rules), "regular" for regular stylesheets.
*/
matchedCSSRules: RuleMatch[];
}
/**
* Inherited CSS style collection for animated styles from ancestor node.
*/
export interface InheritedAnimatedStyleEntry {
/**
* Styles coming from the animations of the ancestor, if any, in the style inheritance chain.
*/
animationStyles?: CSSAnimationStyle[];
/**
* The style coming from the transitions of the ancestor, if any, in the style inheritance chain.
*/
transitionsStyle?: CSSStyle;
}
/**
* Inherited pseudo element matches from pseudos of an ancestor node.
*/
@ -2897,6 +2951,21 @@ the browser.
}
export type forcePseudoStateReturnValue = {
}
/**
* Ensures that the given node is in its starting-style state.
*/
export type forceStartingStyleParameters = {
/**
* The element id for which to force the starting-style state.
*/
nodeId: DOM.NodeId;
/**
* Boolean indicating if this is on or off.
*/
forced: boolean;
}
export type forceStartingStyleReturnValue = {
}
export type getBackgroundColorsParameters = {
/**
* Id of the node to get background colors for.
@ -2934,6 +3003,46 @@ be ignored (as if the image had failed to load).
*/
computedStyle: CSSComputedStyleProperty[];
}
/**
* Resolve the specified values in the context of the provided element.
For example, a value of '1em' is evaluated according to the computed
'font-size' of the element and a value 'calc(1px + 2px)' will be
resolved to '3px'.
*/
export type resolveValuesParameters = {
/**
* Substitution functions (var()/env()/attr()) and cascade-dependent
keywords (revert/revert-layer) do not work.
*/
values: string[];
/**
* Id of the node in whose context the expression is evaluated
*/
nodeId: DOM.NodeId;
/**
* Only longhands and custom property names are accepted.
*/
propertyName?: string;
/**
* Pseudo element type, only works for pseudo elements that generate
elements in the tree, such as ::before and ::after.
*/
pseudoType?: DOM.PseudoType;
/**
* Pseudo element custom ident.
*/
pseudoIdentifier?: string;
}
export type resolveValuesReturnValue = {
results: string[];
}
export type getLonghandPropertiesParameters = {
shorthandName: string;
value: string;
}
export type getLonghandPropertiesReturnValue = {
longhandProperties: CSSProperty[];
}
/**
* Returns the styles defined inline (explicitly in the "style" attribute and implicitly, using DOM
attributes) for a DOM node identified by `nodeId`.
@ -2951,6 +3060,28 @@ attributes) for a DOM node identified by `nodeId`.
*/
attributesStyle?: CSSStyle;
}
/**
* Returns the styles coming from animations & transitions
including the animation & transition styles coming from inheritance chain.
*/
export type getAnimatedStylesForNodeParameters = {
nodeId: DOM.NodeId;
}
export type getAnimatedStylesForNodeReturnValue = {
/**
* Styles coming from animations.
*/
animationStyles?: CSSAnimationStyle[];
/**
* Style coming from transitions.
*/
transitionsStyle?: CSSStyle;
/**
* Inherited style entries for animationsStyle and transitionsStyle from
the inheritance chain of the element.
*/
inherited?: InheritedAnimatedStyleEntry[];
}
/**
* Returns requested styles for a DOM node identified by `nodeId`.
*/
@ -3603,7 +3734,7 @@ front-end.
/**
* Pseudo element type.
*/
export type PseudoType = "first-line"|"first-letter"|"check"|"before"|"after"|"select-arrow"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"placeholder"|"file-selector-button"|"details-content"|"picker";
export type PseudoType = "first-line"|"first-letter"|"checkmark"|"before"|"after"|"picker-icon"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"placeholder"|"file-selector-button"|"details-content"|"picker";
/**
* Shadow root type.
*/
@ -8616,7 +8747,7 @@ applicable or not known.
/**
* The reason why request was blocked.
*/
export type BlockedReason = "other"|"csp"|"mixed-content"|"origin"|"inspector"|"subresource-filter"|"content-type"|"coep-frame-resource-needs-coep-header"|"coop-sandboxed-iframe-cannot-navigate-to-coop-page"|"corp-not-same-origin"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep"|"corp-not-same-origin-after-defaulted-to-same-origin-by-dip"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip"|"corp-not-same-site";
export type BlockedReason = "other"|"csp"|"mixed-content"|"origin"|"inspector"|"subresource-filter"|"content-type"|"coep-frame-resource-needs-coep-header"|"coop-sandboxed-iframe-cannot-navigate-to-coop-page"|"corp-not-same-origin"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep"|"corp-not-same-origin-after-defaulted-to-same-origin-by-dip"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip"|"corp-not-same-site"|"sri-message-signature-mismatch";
/**
* The reason why request was blocked.
*/
@ -9917,6 +10048,9 @@ are represented by the invalid cookie line string instead of a proper cookie.
blockedCookies: BlockedSetCookieWithReason[];
/**
* Raw response headers as they were received over the wire.
Duplicate headers in the response are represented as a single key with their values
concatentated using `\n` as the separator.
See also `headersText` that contains verbatim text for HTTP/1.*.
*/
headers: Headers;
/**
@ -9962,6 +10096,9 @@ Only one responseReceivedEarlyHints may be fired for eached responseReceived eve
requestId: RequestId;
/**
* Raw response headers as they were received over the wire.
Duplicate headers in the response are represented as a single key with their values
concatentated using `\n` as the separator.
See also `headersText` that contains verbatim text for HTTP/1.*.
*/
headers: Headers;
}
@ -9978,7 +10115,7 @@ or after the response was received.
of the operation already exists und thus, the operation was abort
preemptively (e.g. a cache hit).
*/
status: "Ok"|"InvalidArgument"|"MissingIssuerKeys"|"FailedPrecondition"|"ResourceExhausted"|"AlreadyExists"|"ResourceLimited"|"Unauthorized"|"BadResponse"|"InternalError"|"UnknownError"|"FulfilledLocally";
status: "Ok"|"InvalidArgument"|"MissingIssuerKeys"|"FailedPrecondition"|"ResourceExhausted"|"AlreadyExists"|"ResourceLimited"|"Unauthorized"|"BadResponse"|"InternalError"|"UnknownError"|"FulfilledLocally"|"SiteIssuerLimit";
type: TrustTokenOperationType;
requestId: RequestId;
/**
@ -10672,6 +10809,26 @@ should be omitted for worker targets.
export type loadNetworkResourceReturnValue = {
resource: LoadNetworkResourcePageResult;
}
/**
* Sets Controls for third-party cookie access
Page reload is required before the new cookie bahavior will be observed
*/
export type setCookieControlsParameters = {
/**
* Whether 3pc restriction is enabled.
*/
enableThirdPartyCookieRestriction: boolean;
/**
* Whether 3pc grace period exception should be enabled; false by default.
*/
disableThirdPartyCookieMetadata: boolean;
/**
* Whether 3pc heuristics exceptions should be enabled; false by default.
*/
disableThirdPartyCookieHeuristics: boolean;
}
export type setCookieControlsReturnValue = {
}
}
/**
@ -11545,7 +11702,7 @@ as an ad.
* All Permissions Policy features. This enum should match the one defined
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.
*/
export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"deferred-fetch-minimal"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
/**
* Reason for a permissions policy feature to be disabled.
*/
@ -15519,6 +15676,14 @@ Parts of the URL other than those constituting origin are ignored.
* The initial URL the page will be navigated to. An empty string indicates about:blank.
*/
url: string;
/**
* Frame left origin in DIP (headless chrome only).
*/
left?: number;
/**
* Frame top origin in DIP (headless chrome only).
*/
top?: number;
/**
* Frame width in DIP (headless chrome only).
*/
@ -17151,6 +17316,16 @@ possible for multiple rule sets and links to trigger a single attempt.
ruleSetIds: RuleSetId[];
nodeIds: DOM.BackendNodeId[];
}
/**
* Chrome manages different types of preloads together using a
concept of preloading pipeline. For example, if a site uses a
SpeculationRules for prerender, Chrome first starts a prefetch and
then upgrades it to prerender.
CDP events for them are emitted separately but they share
`PreloadPipelineId`.
*/
export type PreloadPipelineId = string;
/**
* List of FinalStatus reasons for Prerender2.
*/
@ -17198,6 +17373,7 @@ filter out the ones that aren't necessary to the developers.
*/
export type prefetchStatusUpdatedPayload = {
key: PreloadingAttemptKey;
pipelineId: PreloadPipelineId;
/**
* The frame id of the frame initiating prefetch.
*/
@ -17212,6 +17388,7 @@ filter out the ones that aren't necessary to the developers.
*/
export type prerenderStatusUpdatedPayload = {
key: PreloadingAttemptKey;
pipelineId: PreloadPipelineId;
status: PreloadingStatus;
prerenderStatus?: PrerenderFinalStatus;
/**
@ -17922,6 +18099,10 @@ variables as its properties.
* Content hash of the script, SHA-256.
*/
hash: string;
/**
* For Wasm modules, the content of the `build_id` custom section.
*/
buildId: string;
/**
* Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
*/
@ -17996,6 +18177,10 @@ scripts upon enabling debugger.
* Content hash of the script, SHA-256.
*/
hash: string;
/**
* For Wasm modules, the content of the `build_id` custom section.
*/
buildId: string;
/**
* Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
*/
@ -20507,9 +20692,13 @@ Error was thrown.
"CSS.disable": CSS.disableParameters;
"CSS.enable": CSS.enableParameters;
"CSS.forcePseudoState": CSS.forcePseudoStateParameters;
"CSS.forceStartingStyle": CSS.forceStartingStyleParameters;
"CSS.getBackgroundColors": CSS.getBackgroundColorsParameters;
"CSS.getComputedStyleForNode": CSS.getComputedStyleForNodeParameters;
"CSS.resolveValues": CSS.resolveValuesParameters;
"CSS.getLonghandProperties": CSS.getLonghandPropertiesParameters;
"CSS.getInlineStylesForNode": CSS.getInlineStylesForNodeParameters;
"CSS.getAnimatedStylesForNode": CSS.getAnimatedStylesForNodeParameters;
"CSS.getMatchedStylesForNode": CSS.getMatchedStylesForNodeParameters;
"CSS.getMediaQueries": CSS.getMediaQueriesParameters;
"CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeParameters;
@ -20751,6 +20940,7 @@ Error was thrown.
"Network.getSecurityIsolationStatus": Network.getSecurityIsolationStatusParameters;
"Network.enableReportingApi": Network.enableReportingApiParameters;
"Network.loadNetworkResource": Network.loadNetworkResourceParameters;
"Network.setCookieControls": Network.setCookieControlsParameters;
"Overlay.disable": Overlay.disableParameters;
"Overlay.enable": Overlay.enableParameters;
"Overlay.getHighlightObjectForTest": Overlay.getHighlightObjectForTestParameters;
@ -21119,9 +21309,13 @@ Error was thrown.
"CSS.disable": CSS.disableReturnValue;
"CSS.enable": CSS.enableReturnValue;
"CSS.forcePseudoState": CSS.forcePseudoStateReturnValue;
"CSS.forceStartingStyle": CSS.forceStartingStyleReturnValue;
"CSS.getBackgroundColors": CSS.getBackgroundColorsReturnValue;
"CSS.getComputedStyleForNode": CSS.getComputedStyleForNodeReturnValue;
"CSS.resolveValues": CSS.resolveValuesReturnValue;
"CSS.getLonghandProperties": CSS.getLonghandPropertiesReturnValue;
"CSS.getInlineStylesForNode": CSS.getInlineStylesForNodeReturnValue;
"CSS.getAnimatedStylesForNode": CSS.getAnimatedStylesForNodeReturnValue;
"CSS.getMatchedStylesForNode": CSS.getMatchedStylesForNodeReturnValue;
"CSS.getMediaQueries": CSS.getMediaQueriesReturnValue;
"CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeReturnValue;
@ -21363,6 +21557,7 @@ Error was thrown.
"Network.getSecurityIsolationStatus": Network.getSecurityIsolationStatusReturnValue;
"Network.enableReportingApi": Network.enableReportingApiReturnValue;
"Network.loadNetworkResource": Network.loadNetworkResourceReturnValue;
"Network.setCookieControls": Network.setCookieControlsReturnValue;
"Overlay.disable": Overlay.disableReturnValue;
"Overlay.enable": Overlay.enableReturnValue;
"Overlay.getHighlightObjectForTest": Overlay.getHighlightObjectForTestReturnValue;

View file

@ -110,7 +110,7 @@
"defaultBrowserType": "webkit"
},
"Galaxy S5": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@ -121,7 +121,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@ -132,7 +132,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 740
@ -143,7 +143,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 740,
"height": 360
@ -154,7 +154,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 320,
"height": 658
@ -165,7 +165,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+ landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 658,
"height": 320
@ -176,7 +176,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36",
"viewport": {
"width": 712,
"height": 1138
@ -187,7 +187,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36",
"viewport": {
"width": 1138,
"height": 712
@ -1098,7 +1098,7 @@
"defaultBrowserType": "webkit"
},
"LG Optimus L70": {
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 384,
"height": 640
@ -1109,7 +1109,7 @@
"defaultBrowserType": "chromium"
},
"LG Optimus L70 landscape": {
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 384
@ -1120,7 +1120,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 550": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36 Edge/14.14263",
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 640,
"height": 360
@ -1131,7 +1131,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 550 landscape": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36 Edge/14.14263",
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 360,
"height": 640
@ -1142,7 +1142,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 950": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36 Edge/14.14263",
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 360,
"height": 640
@ -1153,7 +1153,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 950 landscape": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36 Edge/14.14263",
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 640,
"height": 360
@ -1164,7 +1164,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36",
"viewport": {
"width": 800,
"height": 1280
@ -1175,7 +1175,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36",
"viewport": {
"width": 1280,
"height": 800
@ -1186,7 +1186,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 384,
"height": 640
@ -1197,7 +1197,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 384
@ -1208,7 +1208,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@ -1219,7 +1219,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@ -1230,7 +1230,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@ -1241,7 +1241,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@ -1252,7 +1252,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@ -1263,7 +1263,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@ -1274,7 +1274,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@ -1285,7 +1285,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@ -1296,7 +1296,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36",
"viewport": {
"width": 600,
"height": 960
@ -1307,7 +1307,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36",
"viewport": {
"width": 960,
"height": 600
@ -1362,7 +1362,7 @@
"defaultBrowserType": "webkit"
},
"Pixel 2": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 411,
"height": 731
@ -1373,7 +1373,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 731,
"height": 411
@ -1384,7 +1384,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 XL": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 411,
"height": 823
@ -1395,7 +1395,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 XL landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 823,
"height": 411
@ -1406,7 +1406,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 393,
"height": 786
@ -1417,7 +1417,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 786,
"height": 393
@ -1428,7 +1428,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 353,
"height": 745
@ -1439,7 +1439,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 745,
"height": 353
@ -1450,7 +1450,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G)": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"screen": {
"width": 412,
"height": 892
@ -1465,7 +1465,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G) landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"screen": {
"height": 892,
"width": 412
@ -1480,7 +1480,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"screen": {
"width": 393,
"height": 851
@ -1495,7 +1495,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"screen": {
"width": 851,
"height": 393
@ -1510,7 +1510,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"screen": {
"width": 412,
"height": 915
@ -1525,7 +1525,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"screen": {
"width": 915,
"height": 412
@ -1540,7 +1540,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@ -1551,7 +1551,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@ -1562,7 +1562,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Chrome HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36",
"screen": {
"width": 1792,
"height": 1120
@ -1577,7 +1577,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36 Edg/132.0.6834.57",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36 Edg/133.0.6943.16",
"screen": {
"width": 1792,
"height": 1120
@ -1622,7 +1622,7 @@
"defaultBrowserType": "webkit"
},
"Desktop Chrome": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36",
"screen": {
"width": 1920,
"height": 1080
@ -1637,7 +1637,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36 Edg/132.0.6834.57",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36 Edg/133.0.6943.16",
"screen": {
"width": 1920,
"height": 1080

View file

@ -39,7 +39,6 @@ import type { Dialog } from '../dialog';
import type { ConsoleMessage } from '../console';
import { serializeError } from '../errors';
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer';
import { RecorderApp } from '../recorder/recorderApp';
import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher';
@ -301,17 +300,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
}
async enableRecorder(params: channels.BrowserContextEnableRecorderParams): Promise<void> {
if (params.codegenMode === 'trace-events') {
await this._context.tracing.start({
name: 'trace',
snapshots: true,
screenshots: true,
live: true,
});
await Recorder.show('trace-events', this._context, RecorderInTraceViewer.factory(this._context), params);
} else {
await Recorder.show('actions', this._context, RecorderApp.factory(this._context), params);
}
await Recorder.show(this._context, RecorderApp.factory(this._context), params);
}
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {

View file

@ -1437,8 +1437,6 @@ export class InjectedScript {
received = elements.map(e => options.useInnerText ? (e as HTMLElement).innerText : elementText(new Map(), e).full);
else if (expression === 'to.have.class.array')
received = elements.map(e => e.classList.toString());
else if (expression === 'to.have.accessible.name.array')
received = elements.map(e => getElementAccessibleName(e, false));
if (received && options.expectedText) {
// "To match an array" is "to contain an array" + "equal length"

View file

@ -1138,25 +1138,28 @@ export class Recorder {
let highlight: HighlightModel | 'clear' | 'noop' = 'noop';
if (state.actionSelector !== this._lastHighlightedSelector) {
this._lastHighlightedSelector = state.actionSelector;
const model = state.actionSelector ? querySelector(this.injectedScript, state.actionSelector, this.document) : null;
highlight = model?.elements.length ? model : 'clear';
this._lastHighlightedSelector = model?.elements.length ? state.actionSelector : undefined;
}
const ariaTemplateJSON = JSON.stringify(state.ariaTemplate);
if (this._lastHighlightedAriaTemplateJSON !== ariaTemplateJSON) {
this._lastHighlightedAriaTemplateJSON = ariaTemplateJSON;
const elements = state.ariaTemplate ? this.injectedScript.getAllByAria(this.document, state.ariaTemplate) : [];
if (elements.length)
if (elements.length) {
highlight = { elements };
else
highlight = 'clear';
this._lastHighlightedAriaTemplateJSON = ariaTemplateJSON;
} else {
if (!this._lastHighlightedSelector)
highlight = 'clear';
this._lastHighlightedAriaTemplateJSON = 'undefined';
}
}
if (highlight === 'clear')
this.clearHighlight();
else if (highlight !== 'noop')
this.updateHighlight(highlight, false);
this._updateHighlight(highlight, false);
}
clearHighlight() {
@ -1299,6 +1302,12 @@ export class Recorder {
}
updateHighlight(model: HighlightModel | null, userGesture: boolean) {
this._lastHighlightedSelector = undefined;
this._lastHighlightedAriaTemplateJSON = 'undefined';
this._updateHighlight(model, userGesture);
}
private _updateHighlight(model: HighlightModel | null, userGesture: boolean) {
let tooltipText = model?.tooltipText;
if (tooltipText === undefined && !model?.tooltipList && model?.selector)
tooltipText = this.injectedScript.utils.asLocator(this.state.language, model.selector);

View file

@ -27,9 +27,8 @@ import { Debugger } from './debugger';
import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation';
import { ContextRecorder, generateFrameSelector } from './recorder/contextRecorder';
import type { IRecorderAppFactory, IRecorderApp, IRecorder } from './recorder/recorderFrontend';
import { metadataToCallLog } from './recorder/recorderUtils';
import { buildFullSelector, metadataToCallLog } from './recorder/recorderUtils';
import type * as actions from '@recorder/actions';
import { buildFullSelector } from '../utils/isomorphic/recorderUtils';
import { stringifySelector } from '../utils/isomorphic/selectorParser';
import type { Frame } from './frames';
import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot';
@ -54,33 +53,33 @@ export class Recorder implements InstrumentationListener, IRecorder {
static async showInspector(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, recorderAppFactory: IRecorderAppFactory) {
if (isUnderTest())
params.language = process.env.TEST_INSPECTOR_LANGUAGE;
return await Recorder.show('actions', context, recorderAppFactory, params);
return await Recorder.show(context, recorderAppFactory, params);
}
static showInspectorNoReply(context: BrowserContext, recorderAppFactory: IRecorderAppFactory) {
Recorder.showInspector(context, {}, recorderAppFactory).catch(() => {});
}
static show(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams): Promise<Recorder> {
static show(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams): Promise<Recorder> {
let recorderPromise = (context as any)[recorderSymbol] as Promise<Recorder>;
if (!recorderPromise) {
recorderPromise = Recorder._create(codegenMode, context, recorderAppFactory, params);
recorderPromise = Recorder._create(context, recorderAppFactory, params);
(context as any)[recorderSymbol] = recorderPromise;
}
return recorderPromise;
}
private static async _create(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams = {}): Promise<Recorder> {
const recorder = new Recorder(codegenMode, context, params);
private static async _create(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams = {}): Promise<Recorder> {
const recorder = new Recorder(context, params);
const recorderApp = await recorderAppFactory(recorder);
await recorder._install(recorderApp);
return recorder;
}
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) {
constructor(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) {
this._mode = params.mode || 'none';
this.handleSIGINT = params.handleSIGINT;
this._contextRecorder = new ContextRecorder(codegenMode, context, params, {});
this._contextRecorder = new ContextRecorder(context, params, {});
this._context = context;
this._omitCallTracking = !!params.omitCallTracking;
this._debugger = context.debugger();

View file

@ -10,6 +10,3 @@
../../utils/**
../../utilsBundle.ts
../../zipBundle.ts
[recorderInTraceViewer.ts]
../trace/viewer/traceViewer.ts

View file

@ -54,11 +54,9 @@ export class ContextRecorder extends EventEmitter {
private _throttledOutputFile: ThrottledFile | null = null;
private _orderedLanguages: LanguageGenerator[] = [];
private _listeners: RegisteredListener[] = [];
private _codegenMode: 'actions' | 'trace-events';
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, delegate: ContextRecorderDelegate) {
constructor(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, delegate: ContextRecorderDelegate) {
super();
this._codegenMode = codegenMode;
this._context = context;
this._params = params;
this._delegate = delegate;
@ -150,12 +148,6 @@ export class ContextRecorder extends EventEmitter {
setEnabled(enabled: boolean) {
this._collection.setEnabled(enabled);
if (this._codegenMode === 'trace-events') {
if (enabled)
this._context.tracing.startChunk({ name: 'trace', title: 'trace' }).catch(() => {});
else
this._context.tracing.stopChunk({ mode: 'discard' }).catch(() => {});
}
}
dispose() {

View file

@ -20,10 +20,8 @@ import type { Page } from '../page';
import type { Signal } from '../../../../recorder/src/actions';
import type * as actions from '@recorder/actions';
import { monotonicTime } from '../../utils/time';
import { callMetadataForAction, collapseActions } from './recorderUtils';
import { serializeError } from '../errors';
import { collapseActions } from './recorderUtils';
import { performAction } from './recorderRunner';
import type { CallMetadata } from '@protocol/callMetadata';
import { isUnderTest } from '../../utils/debug';
export class RecorderCollection extends EventEmitter {
@ -46,8 +44,8 @@ export class RecorderCollection extends EventEmitter {
}
async performAction(actionInContext: actions.ActionInContext) {
await this._addAction(actionInContext, async callMetadata => {
await performAction(callMetadata, this._pageAliases, actionInContext);
await this._addAction(actionInContext, async () => {
await performAction(this._pageAliases, actionInContext);
});
}
@ -60,7 +58,7 @@ export class RecorderCollection extends EventEmitter {
this._addAction(actionInContext).catch(() => {});
}
private async _addAction(actionInContext: actions.ActionInContext, callback?: (callMetadata: CallMetadata) => Promise<void>) {
private async _addAction(actionInContext: actions.ActionInContext, callback?: () => Promise<void>) {
if (!this._enabled)
return;
if (actionInContext.action.name === 'openPage' || actionInContext.action.name === 'closePage') {
@ -69,18 +67,10 @@ export class RecorderCollection extends EventEmitter {
return;
}
const { callMetadata, mainFrame } = callMetadataForAction(this._pageAliases, actionInContext);
await mainFrame.instrumentation.onBeforeCall(mainFrame, callMetadata);
this._actions.push(actionInContext);
this._fireChange();
const error = await callback?.(callMetadata).catch((e: Error) => e);
callMetadata.endTime = monotonicTime();
actionInContext.endTime = callMetadata.endTime;
callMetadata.error = error ? serializeError(error) : undefined;
// Do not wait for onAfterCall so that performAction returned immediately after the action.
mainFrame.instrumentation.onAfterCall(mainFrame, callMetadata).then(() => {
this._fireChange();
}).catch(() => {});
await callback?.().catch();
actionInContext.endTime = monotonicTime();
}
signal(pageAlias: string, frame: Frame, signal: Signal) {

View file

@ -1,126 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import path from 'path';
import type { CallLog, ElementInfo, Mode, Source } from '@recorder/recorderTypes';
import { EventEmitter } from 'events';
import type { IRecorder, IRecorderApp, IRecorderAppFactory } from './recorderFrontend';
import { installRootRedirect, openTraceViewerApp, startTraceViewerServer } from '../trace/viewer/traceViewer';
import type { TraceViewerServerOptions } from '../trace/viewer/traceViewer';
import type { BrowserContext } from '../browserContext';
import type { HttpServer, Transport } from '../../utils/httpServer';
import type { Page } from '../page';
import { ManualPromise } from '../../utils/manualPromise';
import type * as actions from '@recorder/actions';
export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp {
readonly wsEndpointForTest: string | undefined;
private _transport: RecorderTransport;
private _tracePage: Page;
private _traceServer: HttpServer;
static factory(context: BrowserContext): IRecorderAppFactory {
return async (recorder: IRecorder) => {
const transport = new RecorderTransport();
const trace = path.join(context._browser.options.tracesDir, 'trace');
const { wsEndpointForTest, tracePage, traceServer } = await openApp(trace, { transport, headless: !context._browser.options.headful });
return new RecorderInTraceViewer(transport, tracePage, traceServer, wsEndpointForTest);
};
}
constructor(transport: RecorderTransport, tracePage: Page, traceServer: HttpServer, wsEndpointForTest: string | undefined) {
super();
this._transport = transport;
this._transport.eventSink.resolve(this);
this._tracePage = tracePage;
this._traceServer = traceServer;
this.wsEndpointForTest = wsEndpointForTest;
this._tracePage.once('close', () => {
this.close();
});
}
async close(): Promise<void> {
await this._tracePage.context().close({ reason: 'Recorder window closed' });
await this._traceServer.stop();
}
async setPaused(paused: boolean): Promise<void> {
this._transport.deliverEvent('setPaused', { paused });
}
async setMode(mode: Mode): Promise<void> {
this._transport.deliverEvent('setMode', { mode });
}
async setRunningFile(file: string | undefined): Promise<void> {
this._transport.deliverEvent('setRunningFile', { file });
}
async elementPicked(elementInfo: ElementInfo, userGesture?: boolean): Promise<void> {
this._transport.deliverEvent('elementPicked', { elementInfo, userGesture });
}
async updateCallLogs(callLogs: CallLog[]): Promise<void> {
this._transport.deliverEvent('updateCallLogs', { callLogs });
}
async setSources(sources: Source[]): Promise<void> {
this._transport.deliverEvent('setSources', { sources });
if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length) {
if ((process as any)._didSetSourcesForTest(sources[0].text))
this.close();
}
}
async setActions(actions: actions.ActionInContext[], sources: Source[]): Promise<void> {
this._transport.deliverEvent('setActions', { actions, sources });
}
}
async function openApp(trace: string, options?: TraceViewerServerOptions & { headless?: boolean }): Promise<{ wsEndpointForTest: string | undefined, tracePage: Page, traceServer: HttpServer }> {
const traceServer = await startTraceViewerServer(options);
await installRootRedirect(traceServer, [trace], { ...options, webApp: 'recorder.html' });
const page = await openTraceViewerApp(traceServer.urlPrefix('precise'), 'chromium', options);
return { wsEndpointForTest: page.context()._browser.options.wsEndpoint, tracePage: page, traceServer };
}
class RecorderTransport implements Transport {
private _connected = new ManualPromise<void>();
readonly eventSink = new ManualPromise<EventEmitter>();
constructor() {
}
onconnect() {
this._connected.resolve();
}
async dispatch(method: string, params: any): Promise<any> {
const eventSink = await this.eventSink;
eventSink.emit('event', { event: method, params });
}
onclose() {
}
deliverEvent(method: string, params: any) {
this._connected.then(() => this.sendEvent?.(method, params));
}
sendEvent?: (method: string, params: any) => void;
close?: () => void;
}

View file

@ -16,14 +16,14 @@
import { serializeExpectedTextValues } from '../../utils';
import { toKeyboardModifiers } from '../codegen/language';
import type { CallMetadata } from '../instrumentation';
import { serverSideCallMetadata } from '../instrumentation';
import type { Page } from '../page';
import type * as actions from '@recorder/actions';
import type * as types from '../types';
import { mainFrameForAction } from './recorderUtils';
import { buildFullSelector } from '../../utils/isomorphic/recorderUtils';
import { buildFullSelector, mainFrameForAction } from './recorderUtils';
export async function performAction(callMetadata: CallMetadata, pageAliases: Map<Page, string>, actionInContext: actions.ActionInContext) {
export async function performAction(pageAliases: Map<Page, string>, actionInContext: actions.ActionInContext) {
const callMetadata = serverSideCallMetadata();
const mainFrame = mainFrameForAction(pageAliases, actionInContext);
const { action } = actionInContext;

View file

@ -19,8 +19,10 @@ import type { CallLog, CallLogStatus } from '@recorder/recorderTypes';
import type { Page } from '../page';
import type { Frame } from '../frames';
import type * as actions from '@recorder/actions';
import { createGuid } from '../../utils';
import { buildFullSelector, traceParamsForAction } from '../../utils/isomorphic/recorderUtils';
export function buildFullSelector(framePath: string[], selector: string) {
return [...framePath, selector].join(' >> internal:control=enter-frame >> ');
}
export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog {
let title = metadata.apiName || metadata.method;
@ -70,26 +72,6 @@ export async function frameForAction(pageAliases: Map<Page, string>, actionInCon
return result.frame;
}
export function callMetadataForAction(pageAliases: Map<Page, string>, actionInContext: actions.ActionInContext): { callMetadata: CallMetadata, mainFrame: Frame } {
const mainFrame = mainFrameForAction(pageAliases, actionInContext);
const { method, apiName, params } = traceParamsForAction(actionInContext);
const callMetadata: CallMetadata = {
id: `call@${createGuid()}`,
apiName,
objectId: mainFrame.guid,
pageId: mainFrame._page.guid,
frameId: mainFrame.guid,
startTime: actionInContext.startTime,
endTime: 0,
type: 'Frame',
method,
params,
log: [],
};
return { callMetadata, mainFrame };
}
export function collapseActions(actions: actions.ActionInContext[]): actions.ActionInContext[] {
const result: actions.ActionInContext[] = [];
for (const action of actions) {

View file

@ -95,7 +95,7 @@ export async function installDependenciesLinux(targets: Set<DependencyGroup>, dr
for (const target of targets) {
const info = deps[platform];
if (!info) {
console.warn(`Cannot install dependencies for ${platform}!`); // eslint-disable-line no-console
console.warn(`Cannot install dependencies for ${platform} with Playwright ${getPlaywrightVersion()}!`); // eslint-disable-line no-console
return;
}
libraries.push(...info[target]);

View file

@ -74,14 +74,18 @@ function calculatePlatform(): { hostPlatform: HostPlatform, isOfficiallySupporte
// KDE Neon is ubuntu-based and has the same versions.
// TUXEDO OS is ubuntu-based and has the same versions.
if (distroInfo?.id === 'ubuntu' || distroInfo?.id === 'pop' || distroInfo?.id === 'neon' || distroInfo?.id === 'tuxedo') {
const isOfficiallySupportedPlatform = distroInfo?.id === 'ubuntu';
if (parseInt(distroInfo.version, 10) <= 19)
const isUbuntu = distroInfo?.id === 'ubuntu';
const version = distroInfo?.version;
const major = parseInt(distroInfo.version, 10);
if (major < 20)
return { hostPlatform: ('ubuntu18.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false };
if (parseInt(distroInfo.version, 10) <= 21)
return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform };
if (parseInt(distroInfo.version, 10) <= 22)
return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform };
return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform };
if (major < 22)
return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '20.04' };
if (major < 24)
return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '22.04' };
if (major < 26)
return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '24.04' };
return { hostPlatform: ('ubuntu' + distroInfo.version + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false };
}
// Linux Mint is ubuntu-based but does not have the same versions
if (distroInfo?.id === 'linuxmint') {

View file

@ -1,163 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type * as recorderActions from '@recorder/actions';
import type * as channels from '@protocol/channels';
import type * as types from '../../server/types';
export function buildFullSelector(framePath: string[], selector: string) {
return [...framePath, selector].join(' >> internal:control=enter-frame >> ');
}
const kDefaultTimeout = 5_000;
export function traceParamsForAction(actionInContext: recorderActions.ActionInContext): { method: string, apiName: string, params: any } {
const { action } = actionInContext;
switch (action.name) {
case 'navigate': {
const params: channels.FrameGotoParams = {
url: action.url,
};
return { method: 'goto', apiName: 'page.goto', params };
}
case 'openPage':
case 'closePage':
throw new Error('Not reached');
}
const selector = buildFullSelector(actionInContext.frame.framePath, action.selector);
switch (action.name) {
case 'click': {
const params: channels.FrameClickParams = {
selector,
strict: true,
modifiers: toKeyboardModifiers(action.modifiers),
button: action.button,
clickCount: action.clickCount,
position: action.position,
};
return { method: 'click', apiName: 'locator.click', params };
}
case 'press': {
const params: channels.FramePressParams = {
selector,
strict: true,
key: [...toKeyboardModifiers(action.modifiers), action.key].join('+'),
};
return { method: 'press', apiName: 'locator.press', params };
}
case 'fill': {
const params: channels.FrameFillParams = {
selector,
strict: true,
value: action.text,
};
return { method: 'fill', apiName: 'locator.fill', params };
}
case 'setInputFiles': {
const params: channels.FrameSetInputFilesParams = {
selector,
strict: true,
localPaths: action.files,
};
return { method: 'setInputFiles', apiName: 'locator.setInputFiles', params };
}
case 'check': {
const params: channels.FrameCheckParams = {
selector,
strict: true,
};
return { method: 'check', apiName: 'locator.check', params };
}
case 'uncheck': {
const params: channels.FrameUncheckParams = {
selector,
strict: true,
};
return { method: 'uncheck', apiName: 'locator.uncheck', params };
}
case 'select': {
const params: channels.FrameSelectOptionParams = {
selector,
strict: true,
options: action.options.map(option => ({ value: option })),
};
return { method: 'selectOption', apiName: 'locator.selectOption', params };
}
case 'assertChecked': {
const params: channels.FrameExpectParams = {
selector: action.selector,
expression: 'to.be.checked',
isNot: !action.checked,
timeout: kDefaultTimeout,
};
return { method: 'expect', apiName: 'expect.toBeChecked', params };
}
case 'assertText': {
const params: channels.FrameExpectParams = {
selector,
expression: 'to.have.text',
expectedText: [],
isNot: false,
timeout: kDefaultTimeout,
};
return { method: 'expect', apiName: 'expect.toContainText', params };
}
case 'assertValue': {
const params: channels.FrameExpectParams = {
selector,
expression: 'to.have.value',
expectedValue: undefined,
isNot: false,
timeout: kDefaultTimeout,
};
return { method: 'expect', apiName: 'expect.toHaveValue', params };
}
case 'assertVisible': {
const params: channels.FrameExpectParams = {
selector,
expression: 'to.be.visible',
isNot: false,
timeout: kDefaultTimeout,
};
return { method: 'expect', apiName: 'expect.toBeVisible', params };
}
case 'assertSnapshot': {
const params: channels.FrameExpectParams = {
selector,
expression: 'to.match.snapshot',
expectedText: [],
isNot: false,
timeout: kDefaultTimeout,
};
return { method: 'expect', apiName: 'expect.toMatchAriaSnapshot', params };
}
}
}
export function toKeyboardModifiers(modifiers: number): types.SmartKeyboardModifier[] {
const result: types.SmartKeyboardModifier[] = [];
if (modifiers & 1)
result.push('Alt');
if (modifiers & 2)
result.push('ControlOrMeta');
if (modifiers & 4)
result.push('ControlOrMeta');
if (modifiers & 8)
result.push('Shift');
return result;
}

View file

@ -156,8 +156,3 @@ export function compressCallLog(log: string[]): string[] {
}
return lines;
}
export type ExpectZone = {
title: string;
stepId: string;
};

View file

@ -16,7 +16,7 @@
import { AsyncLocalStorage } from 'async_hooks';
export type ZoneType = 'apiZone' | 'expectZone' | 'stepZone';
export type ZoneType = 'apiZone' | 'stepZone';
class ZoneManager {
private readonly _asyncLocalStorage = new AsyncLocalStorage<Zone | undefined>();

View file

@ -685,8 +685,8 @@ percentage [0 - 100] for scroll driven animations
/**
* The unique request id.
*/
requestId: Network.RequestId;
url?: string;
requestId?: Network.RequestId;
url: string;
}
/**
* Information about the frame affected by an inspector issue.
@ -697,6 +697,20 @@ percentage [0 - 100] for scroll driven animations
export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout"|"ExcludePortMismatch"|"ExcludeSchemeMismatch";
export type CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"|"WarnDeprecationTrialMetadata"|"WarnThirdPartyCookieHeuristic";
export type CookieOperation = "SetCookie"|"ReadCookie";
/**
* Represents the category of insight that a cookie issue falls under.
*/
export type InsightType = "GitHubResource"|"GracePeriod"|"Heuristics";
/**
* Information about the suggested solution to a cookie issue.
*/
export interface CookieIssueInsight {
type: InsightType;
/**
* Link to table entry in third-party cookie migration readiness list.
*/
tableEntryUrl?: string;
}
/**
* This information is currently necessary, as the front-end has a difficult
time finding a specific cookie. With this, we can convey specific error
@ -721,6 +735,10 @@ may be used by the front-end as additional context.
siteForCookies?: string;
cookieUrl?: string;
request?: AffectedRequest;
/**
* The recommended solution to the issue.
*/
insight?: CookieIssueInsight;
}
export type MixedContentResolutionStatus = "MixedContentBlocked"|"MixedContentAutomaticallyUpgraded"|"MixedContentWarning";
export type MixedContentResourceType = "AttributionSrc"|"Audio"|"Beacon"|"CSPReport"|"Download"|"EventSource"|"Favicon"|"Font"|"Form"|"Frame"|"Image"|"Import"|"JSON"|"Manifest"|"Ping"|"PluginData"|"PluginResource"|"Prefetch"|"Resource"|"Script"|"ServiceWorker"|"SharedWorker"|"SpeculationRules"|"Stylesheet"|"Track"|"Video"|"Worker"|"XMLHttpRequest"|"XSLT";
@ -758,7 +776,7 @@ Does not always exist (e.g. for unsafe form submission urls).
* Enum indicating the reason a response has been blocked. These reasons are
refinements of the net error BLOCKED_BY_RESPONSE.
*/
export type BlockedByResponseReason = "CoepFrameResourceNeedsCoepHeader"|"CoopSandboxedIFrameCannotNavigateToCoopPage"|"CorpNotSameOrigin"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoep"|"CorpNotSameOriginAfterDefaultedToSameOriginByDip"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoepAndDip"|"CorpNotSameSite";
export type BlockedByResponseReason = "CoepFrameResourceNeedsCoepHeader"|"CoopSandboxedIFrameCannotNavigateToCoopPage"|"CorpNotSameOrigin"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoep"|"CorpNotSameOriginAfterDefaultedToSameOriginByDip"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoepAndDip"|"CorpNotSameSite"|"SRIMessageSignatureMismatch";
/**
* Details for a request that has been blocked with the BLOCKED_BY_RESPONSE
code. Currently only used for COEP/COOP, but may be extended to include
@ -963,6 +981,15 @@ features, encourage the use of new ones, and provide general guidance.
failureMessage: string;
requestId?: Network.RequestId;
}
export type SelectElementAccessibilityIssueReason = "DisallowedSelectChild"|"DisallowedOptGroupChild"|"NonPhrasingContentOptionChild"|"InteractiveContentOptionChild"|"InteractiveContentLegendChild";
/**
* This isue warns about errors in the select element content model.
*/
export interface SelectElementAccessibilityIssueDetails {
nodeId: DOM.BackendNodeId;
selectElementAccessibilityIssueReason: SelectElementAccessibilityIssueReason;
hasDisallowedAttributes: boolean;
}
export type StyleSheetLoadingIssueReason = "LateImportRule"|"RequestFailed";
/**
* This issue warns when a referenced stylesheet couldn't be loaded.
@ -1005,7 +1032,7 @@ registrations being ignored.
optional fields in InspectorIssueDetails to convey more specific
information about the kind of issue.
*/
export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue";
export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue"|"SelectElementAccessibilityIssue";
/**
* This struct holds a list of optional fields with additional information
specific to the kind of issue. When adding a new issue code, please also
@ -1033,6 +1060,7 @@ add a new optional field to this type.
propertyRuleIssueDetails?: PropertyRuleIssueDetails;
federatedAuthUserInfoRequestIssueDetails?: FederatedAuthUserInfoRequestIssueDetails;
sharedDictionaryIssueDetails?: SharedDictionaryIssueDetails;
selectElementAccessibilityIssueDetails?: SelectElementAccessibilityIssueDetails;
}
/**
* A unique id for a DevTools inspector issue. Allows other entities (e.g.
@ -1534,7 +1562,7 @@ events afterwards if enabled and recording.
*/
windowState?: WindowState;
}
export type PermissionType = "accessibilityEvents"|"audioCapture"|"backgroundSync"|"backgroundFetch"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"flash"|"geolocation"|"idleDetection"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"protectedMediaIdentifier"|"sensors"|"storageAccess"|"speakerSelection"|"topLevelStorageAccess"|"videoCapture"|"videoCapturePanTiltZoom"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"windowManagement";
export type PermissionType = "ar"|"audioCapture"|"automaticFullscreen"|"backgroundFetch"|"backgroundSync"|"cameraPanTiltZoom"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"geolocation"|"handTracking"|"idleDetection"|"keyboardLock"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"pointerLock"|"protectedMediaIdentifier"|"sensors"|"smartCard"|"speakerSelection"|"storageAccess"|"topLevelStorageAccess"|"videoCapture"|"vr"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"webPrinting"|"windowManagement";
export type PermissionSetting = "granted"|"denied"|"prompt";
/**
* Definition of PermissionDescriptor defined in the Permissions API:
@ -1961,6 +1989,19 @@ inspector" rules), "regular" for regular stylesheets.
*/
matches: RuleMatch[];
}
/**
* CSS style coming from animations with the name of the animation.
*/
export interface CSSAnimationStyle {
/**
* The name of the animation.
*/
name?: string;
/**
* The style coming from the animation.
*/
style: CSSStyle;
}
/**
* Inherited CSS rule collection from ancestor node.
*/
@ -1974,6 +2015,19 @@ inspector" rules), "regular" for regular stylesheets.
*/
matchedCSSRules: RuleMatch[];
}
/**
* Inherited CSS style collection for animated styles from ancestor node.
*/
export interface InheritedAnimatedStyleEntry {
/**
* Styles coming from the animations of the ancestor, if any, in the style inheritance chain.
*/
animationStyles?: CSSAnimationStyle[];
/**
* The style coming from the transitions of the ancestor, if any, in the style inheritance chain.
*/
transitionsStyle?: CSSStyle;
}
/**
* Inherited pseudo element matches from pseudos of an ancestor node.
*/
@ -2897,6 +2951,21 @@ the browser.
}
export type forcePseudoStateReturnValue = {
}
/**
* Ensures that the given node is in its starting-style state.
*/
export type forceStartingStyleParameters = {
/**
* The element id for which to force the starting-style state.
*/
nodeId: DOM.NodeId;
/**
* Boolean indicating if this is on or off.
*/
forced: boolean;
}
export type forceStartingStyleReturnValue = {
}
export type getBackgroundColorsParameters = {
/**
* Id of the node to get background colors for.
@ -2934,6 +3003,46 @@ be ignored (as if the image had failed to load).
*/
computedStyle: CSSComputedStyleProperty[];
}
/**
* Resolve the specified values in the context of the provided element.
For example, a value of '1em' is evaluated according to the computed
'font-size' of the element and a value 'calc(1px + 2px)' will be
resolved to '3px'.
*/
export type resolveValuesParameters = {
/**
* Substitution functions (var()/env()/attr()) and cascade-dependent
keywords (revert/revert-layer) do not work.
*/
values: string[];
/**
* Id of the node in whose context the expression is evaluated
*/
nodeId: DOM.NodeId;
/**
* Only longhands and custom property names are accepted.
*/
propertyName?: string;
/**
* Pseudo element type, only works for pseudo elements that generate
elements in the tree, such as ::before and ::after.
*/
pseudoType?: DOM.PseudoType;
/**
* Pseudo element custom ident.
*/
pseudoIdentifier?: string;
}
export type resolveValuesReturnValue = {
results: string[];
}
export type getLonghandPropertiesParameters = {
shorthandName: string;
value: string;
}
export type getLonghandPropertiesReturnValue = {
longhandProperties: CSSProperty[];
}
/**
* Returns the styles defined inline (explicitly in the "style" attribute and implicitly, using DOM
attributes) for a DOM node identified by `nodeId`.
@ -2951,6 +3060,28 @@ attributes) for a DOM node identified by `nodeId`.
*/
attributesStyle?: CSSStyle;
}
/**
* Returns the styles coming from animations & transitions
including the animation & transition styles coming from inheritance chain.
*/
export type getAnimatedStylesForNodeParameters = {
nodeId: DOM.NodeId;
}
export type getAnimatedStylesForNodeReturnValue = {
/**
* Styles coming from animations.
*/
animationStyles?: CSSAnimationStyle[];
/**
* Style coming from transitions.
*/
transitionsStyle?: CSSStyle;
/**
* Inherited style entries for animationsStyle and transitionsStyle from
the inheritance chain of the element.
*/
inherited?: InheritedAnimatedStyleEntry[];
}
/**
* Returns requested styles for a DOM node identified by `nodeId`.
*/
@ -3603,7 +3734,7 @@ front-end.
/**
* Pseudo element type.
*/
export type PseudoType = "first-line"|"first-letter"|"check"|"before"|"after"|"select-arrow"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"placeholder"|"file-selector-button"|"details-content"|"picker";
export type PseudoType = "first-line"|"first-letter"|"checkmark"|"before"|"after"|"picker-icon"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"placeholder"|"file-selector-button"|"details-content"|"picker";
/**
* Shadow root type.
*/
@ -8616,7 +8747,7 @@ applicable or not known.
/**
* The reason why request was blocked.
*/
export type BlockedReason = "other"|"csp"|"mixed-content"|"origin"|"inspector"|"subresource-filter"|"content-type"|"coep-frame-resource-needs-coep-header"|"coop-sandboxed-iframe-cannot-navigate-to-coop-page"|"corp-not-same-origin"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep"|"corp-not-same-origin-after-defaulted-to-same-origin-by-dip"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip"|"corp-not-same-site";
export type BlockedReason = "other"|"csp"|"mixed-content"|"origin"|"inspector"|"subresource-filter"|"content-type"|"coep-frame-resource-needs-coep-header"|"coop-sandboxed-iframe-cannot-navigate-to-coop-page"|"corp-not-same-origin"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep"|"corp-not-same-origin-after-defaulted-to-same-origin-by-dip"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip"|"corp-not-same-site"|"sri-message-signature-mismatch";
/**
* The reason why request was blocked.
*/
@ -9917,6 +10048,9 @@ are represented by the invalid cookie line string instead of a proper cookie.
blockedCookies: BlockedSetCookieWithReason[];
/**
* Raw response headers as they were received over the wire.
Duplicate headers in the response are represented as a single key with their values
concatentated using `\n` as the separator.
See also `headersText` that contains verbatim text for HTTP/1.*.
*/
headers: Headers;
/**
@ -9962,6 +10096,9 @@ Only one responseReceivedEarlyHints may be fired for eached responseReceived eve
requestId: RequestId;
/**
* Raw response headers as they were received over the wire.
Duplicate headers in the response are represented as a single key with their values
concatentated using `\n` as the separator.
See also `headersText` that contains verbatim text for HTTP/1.*.
*/
headers: Headers;
}
@ -9978,7 +10115,7 @@ or after the response was received.
of the operation already exists und thus, the operation was abort
preemptively (e.g. a cache hit).
*/
status: "Ok"|"InvalidArgument"|"MissingIssuerKeys"|"FailedPrecondition"|"ResourceExhausted"|"AlreadyExists"|"ResourceLimited"|"Unauthorized"|"BadResponse"|"InternalError"|"UnknownError"|"FulfilledLocally";
status: "Ok"|"InvalidArgument"|"MissingIssuerKeys"|"FailedPrecondition"|"ResourceExhausted"|"AlreadyExists"|"ResourceLimited"|"Unauthorized"|"BadResponse"|"InternalError"|"UnknownError"|"FulfilledLocally"|"SiteIssuerLimit";
type: TrustTokenOperationType;
requestId: RequestId;
/**
@ -10672,6 +10809,26 @@ should be omitted for worker targets.
export type loadNetworkResourceReturnValue = {
resource: LoadNetworkResourcePageResult;
}
/**
* Sets Controls for third-party cookie access
Page reload is required before the new cookie bahavior will be observed
*/
export type setCookieControlsParameters = {
/**
* Whether 3pc restriction is enabled.
*/
enableThirdPartyCookieRestriction: boolean;
/**
* Whether 3pc grace period exception should be enabled; false by default.
*/
disableThirdPartyCookieMetadata: boolean;
/**
* Whether 3pc heuristics exceptions should be enabled; false by default.
*/
disableThirdPartyCookieHeuristics: boolean;
}
export type setCookieControlsReturnValue = {
}
}
/**
@ -11545,7 +11702,7 @@ as an ad.
* All Permissions Policy features. This enum should match the one defined
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.
*/
export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"deferred-fetch-minimal"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
/**
* Reason for a permissions policy feature to be disabled.
*/
@ -15519,6 +15676,14 @@ Parts of the URL other than those constituting origin are ignored.
* The initial URL the page will be navigated to. An empty string indicates about:blank.
*/
url: string;
/**
* Frame left origin in DIP (headless chrome only).
*/
left?: number;
/**
* Frame top origin in DIP (headless chrome only).
*/
top?: number;
/**
* Frame width in DIP (headless chrome only).
*/
@ -17151,6 +17316,16 @@ possible for multiple rule sets and links to trigger a single attempt.
ruleSetIds: RuleSetId[];
nodeIds: DOM.BackendNodeId[];
}
/**
* Chrome manages different types of preloads together using a
concept of preloading pipeline. For example, if a site uses a
SpeculationRules for prerender, Chrome first starts a prefetch and
then upgrades it to prerender.
CDP events for them are emitted separately but they share
`PreloadPipelineId`.
*/
export type PreloadPipelineId = string;
/**
* List of FinalStatus reasons for Prerender2.
*/
@ -17198,6 +17373,7 @@ filter out the ones that aren't necessary to the developers.
*/
export type prefetchStatusUpdatedPayload = {
key: PreloadingAttemptKey;
pipelineId: PreloadPipelineId;
/**
* The frame id of the frame initiating prefetch.
*/
@ -17212,6 +17388,7 @@ filter out the ones that aren't necessary to the developers.
*/
export type prerenderStatusUpdatedPayload = {
key: PreloadingAttemptKey;
pipelineId: PreloadPipelineId;
status: PreloadingStatus;
prerenderStatus?: PrerenderFinalStatus;
/**
@ -17922,6 +18099,10 @@ variables as its properties.
* Content hash of the script, SHA-256.
*/
hash: string;
/**
* For Wasm modules, the content of the `build_id` custom section.
*/
buildId: string;
/**
* Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
*/
@ -17996,6 +18177,10 @@ scripts upon enabling debugger.
* Content hash of the script, SHA-256.
*/
hash: string;
/**
* For Wasm modules, the content of the `build_id` custom section.
*/
buildId: string;
/**
* Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
*/
@ -20507,9 +20692,13 @@ Error was thrown.
"CSS.disable": CSS.disableParameters;
"CSS.enable": CSS.enableParameters;
"CSS.forcePseudoState": CSS.forcePseudoStateParameters;
"CSS.forceStartingStyle": CSS.forceStartingStyleParameters;
"CSS.getBackgroundColors": CSS.getBackgroundColorsParameters;
"CSS.getComputedStyleForNode": CSS.getComputedStyleForNodeParameters;
"CSS.resolveValues": CSS.resolveValuesParameters;
"CSS.getLonghandProperties": CSS.getLonghandPropertiesParameters;
"CSS.getInlineStylesForNode": CSS.getInlineStylesForNodeParameters;
"CSS.getAnimatedStylesForNode": CSS.getAnimatedStylesForNodeParameters;
"CSS.getMatchedStylesForNode": CSS.getMatchedStylesForNodeParameters;
"CSS.getMediaQueries": CSS.getMediaQueriesParameters;
"CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeParameters;
@ -20751,6 +20940,7 @@ Error was thrown.
"Network.getSecurityIsolationStatus": Network.getSecurityIsolationStatusParameters;
"Network.enableReportingApi": Network.enableReportingApiParameters;
"Network.loadNetworkResource": Network.loadNetworkResourceParameters;
"Network.setCookieControls": Network.setCookieControlsParameters;
"Overlay.disable": Overlay.disableParameters;
"Overlay.enable": Overlay.enableParameters;
"Overlay.getHighlightObjectForTest": Overlay.getHighlightObjectForTestParameters;
@ -21119,9 +21309,13 @@ Error was thrown.
"CSS.disable": CSS.disableReturnValue;
"CSS.enable": CSS.enableReturnValue;
"CSS.forcePseudoState": CSS.forcePseudoStateReturnValue;
"CSS.forceStartingStyle": CSS.forceStartingStyleReturnValue;
"CSS.getBackgroundColors": CSS.getBackgroundColorsReturnValue;
"CSS.getComputedStyleForNode": CSS.getComputedStyleForNodeReturnValue;
"CSS.resolveValues": CSS.resolveValuesReturnValue;
"CSS.getLonghandProperties": CSS.getLonghandPropertiesReturnValue;
"CSS.getInlineStylesForNode": CSS.getInlineStylesForNodeReturnValue;
"CSS.getAnimatedStylesForNode": CSS.getAnimatedStylesForNodeReturnValue;
"CSS.getMatchedStylesForNode": CSS.getMatchedStylesForNodeReturnValue;
"CSS.getMediaQueries": CSS.getMediaQueriesReturnValue;
"CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeReturnValue;
@ -21363,6 +21557,7 @@ Error was thrown.
"Network.getSecurityIsolationStatus": Network.getSecurityIsolationStatusReturnValue;
"Network.enableReportingApi": Network.enableReportingApiReturnValue;
"Network.loadNetworkResource": Network.loadNetworkResourceReturnValue;
"Network.setCookieControls": Network.setCookieControlsReturnValue;
"Overlay.disable": Overlay.disableReturnValue;
"Overlay.enable": Overlay.enableReturnValue;
"Overlay.getHighlightObjectForTest": Overlay.getHighlightObjectForTestReturnValue;

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-core",
"version": "1.50.0-next",
"version": "1.51.0-next",
"description": "Playwright Component Testing Helpers",
"repository": {
"type": "git",
@ -26,8 +26,8 @@
}
},
"dependencies": {
"playwright-core": "1.50.0-next",
"playwright-core": "1.51.0-next",
"vite": "^5.2.8",
"playwright": "1.50.0-next"
"playwright": "1.51.0-next"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-react",
"version": "1.50.0-next",
"version": "1.51.0-next",
"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.51.0-next",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-react17",
"version": "1.50.0-next",
"version": "1.51.0-next",
"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.51.0-next",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-svelte",
"version": "1.50.0-next",
"version": "1.51.0-next",
"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.51.0-next",
"@sveltejs/vite-plugin-svelte": "^3.0.1"
},
"devDependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-vue",
"version": "1.50.0-next",
"version": "1.51.0-next",
"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.51.0-next",
"@vitejs/plugin-vue": "^5.2.0"
},
"bin": {

View file

@ -1,6 +1,6 @@
{
"name": "playwright-firefox",
"version": "1.50.0-next",
"version": "1.51.0-next",
"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.51.0-next"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/test",
"version": "1.50.0-next",
"version": "1.51.0-next",
"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.51.0-next"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "playwright-webkit",
"version": "1.50.0-next",
"version": "1.51.0-next",
"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.51.0-next"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "playwright",
"version": "1.50.0-next",
"version": "1.51.0-next",
"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.51.0-next"
},
"optionalDependencies": {
"fsevents": "2.3.2"

View file

@ -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) {

View file

@ -19,7 +19,6 @@ import * as path from 'path';
import type { APIRequestContext, BrowserContext, Browser, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core';
import * as playwrightLibrary from 'playwright-core';
import { createGuid, debugMode, addInternalStackPrefix, isString, asLocator, jsonStringifyForceASCII, zones } from 'playwright-core/lib/utils';
import type { ExpectZone } from 'playwright-core/lib/utils';
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test';
import type { TestInfoImpl, TestStepInternal } from './worker/testInfo';
import { rootTestType } from './common/testType';
@ -264,12 +263,12 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
// Some special calls do not get into steps.
if (!testInfo || data.apiName.includes('setTestIdAttribute') || data.apiName === 'tracing.groupEnd')
return;
const expectZone = zones.zoneData<ExpectZone>('expectZone');
if (expectZone) {
const zone = zones.zoneData<TestStepInternal>('stepZone');
if (zone && zone.category === 'expect') {
// Display the internal locator._expect call under the name of the enclosing expect call,
// and connect it to the existing expect step.
data.apiName = expectZone.title;
data.stepId = expectZone.stepId;
data.apiName = zone.title;
data.stepId = zone.stepId;
return;
}
// In the general case, create a step for each api call and connect them through the stepId.

View file

@ -19,7 +19,6 @@ import {
createGuid,
isString,
pollAgainstDeadline } from 'playwright-core/lib/utils';
import type { ExpectZone } from 'playwright-core/lib/utils';
import {
toBeAttached,
toBeChecked,
@ -315,9 +314,10 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
// out all the frames that belong to the test runner from caught runtime errors.
const stackFrames = filteredStackTrace(captureRawStack());
// Enclose toPass in a step to maintain async stacks, toPass matcher is always async.
// toPass and poll matchers can contain other steps, expects and API calls,
// so they behave like a retriable step.
const stepInfo = {
category: 'expect',
category: (matcherName === 'toPass' || this._info.poll) ? 'step' : 'expect',
title: trimLongString(title, 1024),
params: args[0] ? { expected: args[0] } : undefined,
infectParentStepsWithError: this._info.isSoft,
@ -345,11 +345,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
try {
const callback = () => matcher.call(target, ...args);
// toPass and poll matchers can contain other steps, expects and API calls,
// so they behave like a retriable step.
const result = (matcherName === 'toPass' || this._info.poll) ?
zones.run('stepZone', step, callback) :
zones.run<ExpectZone, any>('expectZone', { title, stepId: step.stepId }, callback);
const result = zones.run('stepZone', step, callback);
if (result instanceof Promise)
return result.then(finalizer).catch(reportStepError);
finalizer();

View file

@ -196,20 +196,13 @@ export function toHaveAccessibleDescription(
export function toHaveAccessibleName(
this: ExpectMatcherState,
locator: LocatorEx,
expected: string | RegExp | (string | RegExp)[],
options: { timeout?: number, ignoreCase?: boolean, normalizeWhiteSpace?: boolean } = {}
expected: string | RegExp,
options?: { timeout?: number, ignoreCase?: boolean },
) {
if (Array.isArray(expected)) {
return toEqual.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => {
const expectedText = serializeExpectedTextValues(expected, { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true });
return await locator._expect('to.have.accessible.name.array', { expectedText, isNot, timeout });
}, expected, options);
} else {
return toMatchText.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => {
const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true });
return await locator._expect('to.have.accessible.name', { expectedText, isNot, timeout });
}, expected, options);
}
return toMatchText.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => {
const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true });
return await locator._expect('to.have.accessible.name', { expectedText, isNot, timeout });
}, expected, options);
}
export function toHaveAccessibleErrorMessage(

View file

@ -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;

View file

@ -17,7 +17,6 @@
import fs from 'fs';
import path from 'path';
import { captureRawStack, monotonicTime, zones, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils';
import type { ExpectZone } from 'playwright-core/lib/utils';
import type { TestInfo, TestStatus, FullProject } from '../../types/test';
import type { AttachmentPayload, StepBeginPayload, StepEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc';
import type { TestCase } from '../common/test';
@ -35,7 +34,7 @@ export interface TestStepInternal {
attachmentIndices: number[];
stepId: string;
title: string;
category: 'hook' | 'fixture' | 'test.step' | 'test.step.skip' | 'expect' | 'attach' | string;
category: string;
location?: Location;
boxedStack?: StackFrame[];
steps: TestStepInternal[];
@ -195,7 +194,7 @@ export class TestInfoImpl implements TestInfo {
this._attachmentsPush = this.attachments.push.bind(this.attachments);
this.attachments.push = (...attachments: TestInfo['attachments']) => {
for (const a of attachments)
this._attach(a, this._expectStepId() ?? this._parentStep()?.stepId);
this._attach(a, this._parentStep()?.stepId);
return this.attachments.length;
};
@ -245,10 +244,6 @@ export class TestInfoImpl implements TestInfo {
?? this._findLastStageStep(this._steps); // If no parent step on stack, assume the current stage as parent.
}
private _expectStepId() {
return zones.zoneData<ExpectZone>('expectZone')?.stepId;
}
_addStep(data: Omit<TestStepInternal, 'complete' | 'stepId' | 'steps' | 'attachmentIndices'>, parentStep?: TestStepInternal): TestStepInternal {
const stepId = `${data.category}@${++this._lastStepId}`;

View file

@ -8142,7 +8142,7 @@ interface LocatorAssertions {
* @param name Expected accessible name.
* @param options
*/
toHaveAccessibleName(name: string|RegExp|ReadonlyArray<string|RegExp>, options?: {
toHaveAccessibleName(name: string|RegExp, options?: {
/**
* Whether to perform case-insensitive match.
* [`ignoreCase`](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-have-accessible-name-option-ignore-case)
@ -8207,21 +8207,24 @@ interface LocatorAssertions {
/**
* Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with given CSS classes.
* This needs to be a full match or using a relaxed regular expression.
* When a string is provided, it must fully match the element's `class` attribute. To match individual classes or
* perform partial matches, use a regular expression:
*
* **Usage**
*
* ```html
* <div class='selected row' id='component'></div>
* <div class='middle selected row' id='component'></div>
* ```
*
* ```js
* const locator = page.locator('#component');
* await expect(locator).toHaveClass(/selected/);
* await expect(locator).toHaveClass('selected row');
* await expect(locator).toHaveClass('middle selected row');
* await expect(locator).toHaveClass(/(^|\s)selected(\s|$)/);
* ```
*
* Note that if array is passed as an expected value, entire lists of elements can be asserted:
* When an array is passed, the method asserts that the list of elements located matches the corresponding list of
* expected class values. Each element's class attribute is matched against the corresponding string or regular
* expression in the array:
*
* ```js
* const locator = page.locator('list > .component');
@ -8694,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`.
*/

View file

@ -1772,7 +1772,6 @@ export type BrowserContextPauseResult = void;
export type BrowserContextEnableRecorderParams = {
language?: string,
mode?: 'inspecting' | 'recording',
codegenMode?: 'actions' | 'trace-events',
pauseOnNextStatement?: boolean,
testIdAttributeName?: string,
launchOptions?: any,
@ -1786,7 +1785,6 @@ export type BrowserContextEnableRecorderParams = {
export type BrowserContextEnableRecorderOptions = {
language?: string,
mode?: 'inspecting' | 'recording',
codegenMode?: 'actions' | 'trace-events',
pauseOnNextStatement?: boolean,
testIdAttributeName?: string,
launchOptions?: any,

View file

@ -1200,11 +1200,6 @@ BrowserContext:
literals:
- inspecting
- recording
codegenMode:
type: enum?
literals:
- actions
- trace-events
pauseOnNextStatement: boolean?
testIdAttributeName: string?
launchOptions: json?

View file

@ -1,28 +0,0 @@
<!--
Copyright (c) Microsoft Corporation.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/playwright-logo.svg" type="image/svg+xml">
<title>Playwright Recorder</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/recorder.tsx"></script>
</body>
</html>

View file

@ -6,7 +6,3 @@ ui/
[sw-main.ts]
sw/**
[recorder.tsx]
ui/recorder/**

View file

@ -1,41 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@web/common.css';
import { applyTheme } from '@web/theme';
import '@web/third_party/vscode/codicon.css';
import * as ReactDOM from 'react-dom/client';
import { RecorderView } from './ui/recorder/recorderView';
(async () => {
applyTheme();
if (window.location.protocol !== 'file:') {
if (!navigator.serviceWorker)
throw new Error(`Service workers are not supported.\nMake sure to serve the Recorder (${window.location}) via HTTPS or localhost.`);
navigator.serviceWorker.register('sw.bundle.js');
if (!navigator.serviceWorker.controller) {
await new Promise<void>(f => {
navigator.serviceWorker.oncontrollerchange = () => f();
});
}
// Keep SW running.
setInterval(function() { fetch('ping'); }, 10000);
}
ReactDOM.createRoot(document.querySelector('#root')!).render(<RecorderView />);
})();

View file

@ -70,13 +70,20 @@
flex: none;
}
.action-selector {
.action-parameter {
display: inline;
flex: none;
padding-left: 5px;
}
.action-locator-parameter {
color: var(--vscode-charts-orange);
}
.action-generic-parameter {
color: var(--vscode-charts-purple);
}
.action-url {
display: inline;
flex: none;

View file

@ -19,8 +19,7 @@ import { msToString } from '@web/uiUtils';
import * as React from 'react';
import './actionList.css';
import * as modelUtil from './modelUtil';
import { asLocator } from '@isomorphic/locatorGenerators';
import type { Language } from '@isomorphic/locatorGenerators';
import { asLocator, type Language } from '@isomorphic/locatorGenerators';
import type { TreeState } from '@web/components/treeView';
import { TreeView } from '@web/components/treeView';
import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil';
@ -116,9 +115,10 @@ export const renderAction = (
}) => {
const { sdkLanguage, revealConsole, revealAttachment, isLive, showDuration, showBadges } = options;
const { errors, warnings } = modelUtil.stats(action);
const locator = action.params.selector ? asLocator(sdkLanguage || 'javascript', action.params.selector) : undefined;
const showAttachments = !!action.attachments?.length && !!revealAttachment;
const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript');
let time: string = '';
if (action.endTime)
time = msToString(action.endTime - action.startTime);
@ -129,7 +129,23 @@ export const renderAction = (
return <>
<div className='action-title' title={action.apiName}>
<span>{action.apiName}</span>
{locator && <div className='action-selector' title={locator}>{locator}</div>}
{parameterString &&
(parameterString.type === 'locator' ? (
<>
<span className='action-parameter action-locator-parameter'>
{parameterString.value}
</span>
{parameterString.childDisplayString && (
<span className='action-parameter action-generic-parameter'>
{parameterString.childDisplayString.value}
</span>
)}
</>
) : (
<span className='action-parameter action-generic-parameter'>
{parameterString.value}
</span>
))}
{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>
@ -151,3 +167,154 @@ function excludeOrigin(url: string): string {
return url;
}
}
type ActionParameterDisplayString =
| {
type: 'generic';
value: string;
}
| {
type: 'locator';
value: string;
childDisplayString?: ActionParameterDisplayString;
};
const clockDisplayString = (
action: ActionTraceEvent,
): ActionParameterDisplayString | undefined => {
switch (action.method) {
case 'clockPauseAt':
case 'clockSetFixedTime':
case 'clockSetSystemTime': {
if (
action.params.timeString === undefined &&
action.params.timeNumber === undefined
)
return undefined;
return {
type: 'generic',
value: new Date(
action.params.timeString ?? action.params.timeNumber,
).toLocaleString(undefined, { timeZone: 'UTC' }),
};
}
case 'clockFastForward':
case 'clockRunFor': {
if (
action.params.ticksNumber === undefined &&
action.params.ticksString === undefined
)
return undefined;
return {
type: 'generic',
value: action.params.ticksString ?? `${action.params.ticksNumber}ms`,
};
}
}
return undefined;
};
const keyboardDisplayString = (
action: ActionTraceEvent,
): ActionParameterDisplayString | undefined => {
switch (action.method) {
case 'press':
case 'keyboardPress':
case 'keyboardDown':
case 'keyboardUp': {
if (action.params.key === undefined)
return undefined;
return { type: 'generic', value: action.params.key };
}
case 'type':
case 'fill':
case 'keyboardType':
case 'keyboardInsertText': {
const string = action.params.text ?? action.params.value;
if (string === undefined)
return undefined;
return { type: 'generic', value: `"${string}"` };
}
}
};
const mouseDisplayString = (
action: ActionTraceEvent,
): ActionParameterDisplayString | undefined => {
switch (action.method) {
case 'click':
case 'dblclick':
case 'mouseClick':
case 'mouseMove': {
if (action.params.x === undefined || action.params.y === undefined)
return undefined;
return {
type: 'generic',
value: `(${action.params.x}, ${action.params.y})`,
};
}
case 'mouseWheel': {
if (
action.params.deltaX === undefined ||
action.params.deltaY === undefined
)
return undefined;
return {
type: 'generic',
value: `(${action.params.deltaX}, ${action.params.deltaY})`,
};
}
}
};
const touchscreenDisplayString = (
action: ActionTraceEvent,
): ActionParameterDisplayString | undefined => {
switch (action.method) {
case 'tap': {
if (action.params.x === undefined || action.params.y === undefined)
return undefined;
return {
type: 'generic',
value: `(${action.params.x}, ${action.params.y})`,
};
}
}
};
const actionParameterDisplayString = (
action: ActionTraceEvent,
sdkLanguage: Language,
ignoreLocator: boolean = false,
): ActionParameterDisplayString | undefined => {
const params = action.params;
// Locators have many possible classes, so follow existing logic and use `selector` presence
if (!ignoreLocator && params.selector !== undefined) {
return {
type: 'locator',
value: asLocator(sdkLanguage, params.selector),
childDisplayString: actionParameterDisplayString(
action,
sdkLanguage,
true,
),
};
}
switch (action.class.toLowerCase()) {
case 'browsercontext':
return clockDisplayString(action);
case 'page':
case 'frame':
case 'elementhandle':
return (
keyboardDisplayString(action) ??
mouseDisplayString(action) ??
touchscreenDisplayString(action)
);
}
return undefined;
};

View file

@ -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; }
}

View file

@ -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>;
})}

View file

@ -47,17 +47,28 @@ export const InspectorTab: React.FunctionComponent<{
setIsInspecting(false);
}, [highlightedElement, setHighlightedElement, setIsInspecting]);
return <div className='vbox' style={{ backgroundColor: 'var(--vscode-sideBar-background)' }}>
<div style={{ margin: '10px 0px 10px 10px', color: 'var(--vscode-editorCodeLens-foreground)', flex: 'none' }}>Locator</div>
<div style={{ margin: '0 10px 10px', flex: 'auto' }}>
return <div style={{ flex: 'auto', backgroundColor: 'var(--vscode-sideBar-background)', padding: '0 10px 10px 10px', overflow: 'auto' }}>
<div className='hbox' style={{ lineHeight: '28px', color: 'var(--vscode-editorCodeLens-foreground)' }}>
<div style={{ flex: 'auto' }}>Locator</div>
<ToolbarButton icon='files' title='Copy locator' onClick={() => {
copy(highlightedElement.locator || '');
}}></ToolbarButton>
</div>
<div style={{ height: 50 }}>
<CodeMirrorWrapper text={highlightedElement.locator || ''} language={sdkLanguage} isFocused={true} wrapLines={true} onChange={text => {
// Updating text needs to go first - react can squeeze a render between the state updates.
setHighlightedElement({ ...highlightedElement, locator: text, lastEdited: 'locator' });
setIsInspecting(false);
}} />
</div>
<div style={{ margin: '10px 0px 10px 10px', color: 'var(--vscode-editorCodeLens-foreground)', flex: 'none' }}>Aria</div>
<div style={{ margin: '0 10px 10px', flex: 'auto' }}>
<div className='hbox' style={{ lineHeight: '28px', color: 'var(--vscode-editorCodeLens-foreground)' }}>
<div style={{ flex: 'auto' }}>Aria snapshot</div>
<ToolbarButton icon='files' title='Copy snapshot' onClick={() => {
copy(highlightedElement.ariaSnapshot || '');
}}></ToolbarButton>
</div>
<div style={{ height: 150 }}>
<CodeMirrorWrapper
text={highlightedElement.ariaSnapshot || ''}
language='yaml'
@ -65,10 +76,5 @@ export const InspectorTab: React.FunctionComponent<{
highlight={ariaSnapshotErrors}
onChange={onAriaEditorChange} />
</div>
<div style={{ position: 'absolute', right: 5, top: 5 }}>
<ToolbarButton icon='files' title='Copy locator' onClick={() => {
copy(highlightedElement.locator || '');
}}></ToolbarButton>
</div>
</div>;
};

View file

@ -1,5 +0,0 @@
[*]
@isomorphic/**
@trace/**
@web/**
../**

View file

@ -1,62 +0,0 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import type * as actionTypes from '@recorder/actions';
import { ListView } from '@web/components/listView';
import * as React from 'react';
import '../actionList.css';
import { traceParamsForAction } from '@isomorphic/recorderUtils';
import { asLocator } from '@isomorphic/locatorGenerators';
import type { Language } from '@isomorphic/locatorGenerators';
const ActionList = ListView<actionTypes.ActionInContext>;
export const ActionListView: React.FC<{
sdkLanguage: Language,
actions: actionTypes.ActionInContext[],
selectedAction: actionTypes.ActionInContext | undefined,
onSelectedAction: (action: actionTypes.ActionInContext | undefined) => void,
}> = ({
sdkLanguage,
actions,
selectedAction,
onSelectedAction,
}) => {
const render = React.useCallback((action: actionTypes.ActionInContext) => {
return renderAction(sdkLanguage, action);
}, [sdkLanguage]);
return <div className='vbox'>
<ActionList
name='actions'
items={actions}
selectedItem={selectedAction}
onSelected={onSelectedAction}
render={render} />
</div>;
};
export const renderAction = (sdkLanguage: Language, action: actionTypes.ActionInContext) => {
const { method, apiName, params } = traceParamsForAction(action);
const locator = params.selector ? asLocator(sdkLanguage || 'javascript', params.selector) : undefined;
return <>
<div className='action-title' title={apiName}>
<span>{apiName}</span>
{locator && <div className='action-selector' title={locator}>{locator}</div>}
{method === 'goto' && params.url && <div className='action-url' title={params.url}>{params.url}</div>}
</div>
</>;
};

View file

@ -1,118 +0,0 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import type * as actionTypes from '@recorder/actions';
import type { Mode, Source } from '@recorder/recorderTypes';
import * as React from 'react';
export const BackendContext = React.createContext<Backend | undefined>(undefined);
export const BackendProvider: React.FunctionComponent<React.PropsWithChildren<{
guid: string,
}>> = ({ guid, children }) => {
const [connection, setConnection] = React.useState<Connection | undefined>(undefined);
const [mode, setMode] = React.useState<Mode>('none');
const [actions, setActions] = React.useState<{ actions: actionTypes.ActionInContext[], sources: Source[] }>({ actions: [], sources: [] });
const callbacks = React.useRef({ setMode, setActions });
React.useEffect(() => {
const wsURL = new URL(`../${guid}`, window.location.toString());
wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
const webSocket = new WebSocket(wsURL.toString());
setConnection(new Connection(webSocket, callbacks.current));
return () => {
webSocket.close();
};
}, [guid]);
const backend = React.useMemo(() => {
return connection ? { mode, actions: actions.actions, sources: actions.sources, connection } : undefined;
}, [actions, mode, connection]);
return <BackendContext.Provider value={backend}>
{children}
</BackendContext.Provider>;
};
export type Backend = {
actions: actionTypes.ActionInContext[],
sources: Source[],
connection: Connection,
};
type ConnectionCallbacks = {
setMode: (mode: Mode) => void;
setActions: (data: { actions: actionTypes.ActionInContext[], sources: Source[] }) => void;
};
class Connection {
private _lastId = 0;
private _webSocket: WebSocket;
private _callbacks = new Map<number, { resolve: (arg: any) => void, reject: (arg: Error) => void }>();
private _options: ConnectionCallbacks;
constructor(webSocket: WebSocket, options: ConnectionCallbacks) {
this._webSocket = webSocket;
this._callbacks = new Map();
this._options = options;
this._webSocket.addEventListener('message', event => {
const message = JSON.parse(event.data);
const { id, result, error, method, params } = message;
if (id) {
const callback = this._callbacks.get(id);
if (!callback)
return;
this._callbacks.delete(id);
if (error)
callback.reject(new Error(error));
else
callback.resolve(result);
} else {
this._dispatchEvent(method, params);
}
});
}
setMode(mode: Mode) {
this._sendMessageNoReply('setMode', { mode });
}
private async _sendMessage(method: string, params?: any): Promise<any> {
const id = ++this._lastId;
const message = { id, method, params };
this._webSocket.send(JSON.stringify(message));
return new Promise((resolve, reject) => {
this._callbacks.set(id, { resolve, reject });
});
}
private _sendMessageNoReply(method: string, params?: any) {
this._sendMessage(method, params).catch(() => { });
}
private _dispatchEvent(method: string, params?: any) {
if (method === 'setMode') {
const { mode } = params as { mode: Mode };
this._options.setMode(mode);
}
if (method === 'setActions') {
const { actions, sources } = params as { actions: actionTypes.ActionInContext[], sources: Source[] };
this._options.setActions({ actions: actions.filter(a => a.action.name !== 'openPage' && a.action.name !== 'closePage'), sources });
(window as any).playwrightSourcesEchoForTest = sources;
}
}
}

View file

@ -1,71 +0,0 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { sha1 } from '@web/uiUtils';
import * as React from 'react';
import type { ContextEntry } from '../../types/entries';
import { MultiTraceModel } from '../modelUtil';
export const ModelContext = React.createContext<MultiTraceModel | undefined>(undefined);
export const ModelProvider: React.FunctionComponent<React.PropsWithChildren<{
trace: string,
}>> = ({ trace, children }) => {
const [model, setModel] = React.useState<{ model: MultiTraceModel, sha1: string } | undefined>();
const [counter, setCounter] = React.useState(0);
const pollTimer = React.useRef<NodeJS.Timeout | null>(null);
React.useEffect(() => {
if (pollTimer.current)
clearTimeout(pollTimer.current);
// Start polling running test.
pollTimer.current = setTimeout(async () => {
try {
const result = await loadSingleTraceFile(trace);
if (result.sha1 !== model?.sha1)
setModel(result);
} catch {
setModel(undefined);
} finally {
setCounter(counter + 1);
}
}, 500);
return () => {
if (pollTimer.current)
clearTimeout(pollTimer.current);
};
}, [counter, model, trace]);
return <ModelContext.Provider value={model?.model}>
{children}
</ModelContext.Provider>;
};
async function loadSingleTraceFile(url: string): Promise<{ model: MultiTraceModel, sha1: string }> {
const params = new URLSearchParams();
params.set('trace', url);
params.set('limit', '1');
const response = await fetch(`contexts?${params.toString()}`);
const contextEntries = await response.json() as ContextEntry[];
const tokens: string[] = [];
for (const entry of contextEntries) {
entry.actions.forEach(a => tokens.push(a.type + '@' + a.startTime + '-' + a.endTime));
entry.events.forEach(e => tokens.push(e.type + '@' + e.time));
}
return { model: new MultiTraceModel(contextEntries), sha1: await sha1(tokens.join('|')) };
}

View file

@ -1,15 +0,0 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

View file

@ -1,299 +0,0 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import type * as actionTypes from '@recorder/actions';
import { SourceChooser } from '@web/components/sourceChooser';
import { SplitView } from '@web/components/splitView';
import type { TabbedPaneTabModel } from '@web/components/tabbedPane';
import { TabbedPane } from '@web/components/tabbedPane';
import { Toolbar } from '@web/components/toolbar';
import { ToolbarButton, ToolbarSeparator } from '@web/components/toolbarButton';
import { copy, useSetting } from '@web/uiUtils';
import * as React from 'react';
import { ConsoleTab, useConsoleTabModel } from '../consoleTab';
import type { Boundaries } from '../geometry';
import { InspectorTab } from '../inspectorTab';
import type * as modelUtil from '../modelUtil';
import type { SourceLocation } from '../modelUtil';
import { NetworkTab, useNetworkTabModel } from '../networkTab';
import { collectSnapshots, extendSnapshot, SnapshotView } from '../snapshotTab';
import { SourceTab } from '../sourceTab';
import { ModelContext, ModelProvider } from './modelContext';
import './recorderView.css';
import { ActionListView } from './actionListView';
import { BackendContext, BackendProvider } from './backendContext';
import type { Language } from '@isomorphic/locatorGenerators';
import { SettingsToolbarButton } from '../settingsToolbarButton';
import type { HighlightedElement } from '../snapshotTab';
export const RecorderView: React.FunctionComponent = () => {
const searchParams = new URLSearchParams(window.location.search);
const guid = searchParams.get('ws')!;
const trace = searchParams.get('trace') + '.json';
return <BackendProvider guid={guid}>
<ModelProvider trace={trace}>
<Workbench />
</ModelProvider>
</BackendProvider>;
};
export const Workbench: React.FunctionComponent = () => {
const backend = React.useContext(BackendContext);
const model = React.useContext(ModelContext);
const [fileId, setFileId] = React.useState<string | undefined>();
const [selectedStartTime, setSelectedStartTime] = React.useState<number | undefined>(undefined);
const [isInspecting, setIsInspecting] = React.useState(false);
const [highlightedElementInProperties, setHighlightedElementInProperties] = React.useState<HighlightedElement>({ lastEdited: 'none' });
const [highlightedElementInTrace, setHighlightedElementInTrace] = React.useState<HighlightedElement>({ lastEdited: 'none' });
const [traceCallId, setTraceCallId] = React.useState<string | undefined>();
const setSelectedAction = React.useCallback((action: actionTypes.ActionInContext | undefined) => {
setSelectedStartTime(action?.startTime);
}, []);
const selectedAction = React.useMemo(() => {
return backend?.actions.find(a => a.startTime === selectedStartTime);
}, [backend?.actions, selectedStartTime]);
React.useEffect(() => {
const callId = model?.actions.find(a => a.endTime && a.endTime === selectedAction?.endTime)?.callId;
if (callId)
setTraceCallId(callId);
}, [model, selectedAction]);
const source = React.useMemo(() => backend?.sources.find(s => s.id === fileId) || backend?.sources[0], [backend?.sources, fileId]);
const sourceLocation = React.useMemo(() => {
if (!source)
return undefined;
const sourceLocation: SourceLocation = {
file: '',
line: 0,
column: 0,
source: {
errors: [],
content: source.text
}
};
return sourceLocation;
}, [source]);
const sdkLanguage: Language = source?.language || 'javascript';
const { boundaries } = React.useMemo(() => {
const boundaries = { minimum: model?.startTime || 0, maximum: model?.endTime || 30000 };
if (boundaries.minimum > boundaries.maximum) {
boundaries.minimum = 0;
boundaries.maximum = 30000;
}
// Leave some nice free space on the right hand side.
boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20;
return { boundaries };
}, [model]);
const elementPickedInTrace = React.useCallback((element: HighlightedElement) => {
setHighlightedElementInProperties(element);
setHighlightedElementInTrace({ lastEdited: 'none' });
setIsInspecting(false);
}, []);
const elementTypedInProperties = React.useCallback((element: HighlightedElement) => {
setHighlightedElementInTrace(element);
setHighlightedElementInProperties(element);
}, []);
const actionList = <ActionListView
sdkLanguage={sdkLanguage}
actions={backend?.actions || []}
selectedAction={selectedAction}
onSelectedAction={setSelectedAction}
/>;
const actionsTab: TabbedPaneTabModel = {
id: 'actions',
title: 'Actions',
component: actionList,
};
const toolbar = <Toolbar sidebarBackground>
<div style={{ width: 4 }}></div>
<ToolbarButton icon='inspect' title='Pick locator' toggled={isInspecting} onClick={() => {
setIsInspecting(!isInspecting);
}} />
<ToolbarButton icon='eye' title='Assert visibility' onClick={() => {
}} />
<ToolbarButton icon='whole-word' title='Assert text' onClick={() => {
}} />
<ToolbarButton icon='symbol-constant' title='Assert value' onClick={() => {
}} />
<ToolbarSeparator />
<ToolbarButton icon='files' title='Copy' disabled={!source || !source.text} onClick={() => {
if (source?.text)
copy(source.text);
}}></ToolbarButton>
<div style={{ flex: 'auto' }}></div>
<div>Target:</div>
<SourceChooser fileId={fileId} sources={backend?.sources || []} setFileId={fileId => {
setFileId(fileId);
}} />
<SettingsToolbarButton />
</Toolbar>;
const sidebarTabbedPane = <TabbedPane tabs={[actionsTab]} />;
const traceView = <TraceView
sdkLanguage={sdkLanguage}
callId={traceCallId}
isInspecting={isInspecting}
setIsInspecting={setIsInspecting}
highlightedElement={highlightedElementInTrace}
setHighlightedElement={elementPickedInTrace} />;
const propertiesView = <PropertiesView
sdkLanguage={sdkLanguage}
boundaries={boundaries}
setIsInspecting={setIsInspecting}
highlightedElement={highlightedElementInProperties}
setHighlightedElement={elementTypedInProperties}
sourceLocation={sourceLocation} />;
return <div className='vbox workbench'>
<SplitView
sidebarSize={250}
orientation={'horizontal'}
settingName='recorderActionListSidebar'
sidebarIsFirst
main={<SplitView
sidebarSize={250}
orientation='vertical'
settingName='recorderPropertiesSidebar'
main={<div className='vbox'>
{toolbar}
{traceView}
</div>}
sidebar={propertiesView}
/>}
sidebar={sidebarTabbedPane}
/>
</div>;
};
const PropertiesView: React.FunctionComponent<{
sdkLanguage: Language,
boundaries: Boundaries,
setIsInspecting: (value: boolean) => void,
highlightedElement: HighlightedElement,
setHighlightedElement: (element: HighlightedElement) => void,
sourceLocation: modelUtil.SourceLocation | undefined,
}> = ({
sdkLanguage,
boundaries,
setIsInspecting,
highlightedElement,
setHighlightedElement,
sourceLocation,
}) => {
const model = React.useContext(ModelContext);
const consoleModel = useConsoleTabModel(model, boundaries);
const networkModel = useNetworkTabModel(model, boundaries);
const sourceModel = React.useRef(new Map<string, modelUtil.SourceModel>());
const [selectedPropertiesTab, setSelectedPropertiesTab] = useSetting<string>('recorderPropertiesTab', 'source');
const inspectorTab: TabbedPaneTabModel = {
id: 'inspector',
title: 'Locator',
render: () => <InspectorTab
sdkLanguage={sdkLanguage}
setIsInspecting={setIsInspecting}
highlightedElement={highlightedElement}
setHighlightedElement={setHighlightedElement} />,
};
const sourceTab: TabbedPaneTabModel = {
id: 'source',
title: 'Source',
render: () => <SourceTab
sources={sourceModel.current}
stackFrameLocation={'right'}
fallbackLocation={sourceLocation}
/>
};
const consoleTab: TabbedPaneTabModel = {
id: 'console',
title: 'Console',
count: consoleModel.entries.length,
render: () => <ConsoleTab boundaries={boundaries} consoleModel={consoleModel} />
};
const networkTab: TabbedPaneTabModel = {
id: 'network',
title: 'Network',
count: networkModel.resources.length,
render: () => <NetworkTab boundaries={boundaries} networkModel={networkModel} sdkLanguage={sdkLanguage} />
};
const tabs: TabbedPaneTabModel[] = [
sourceTab,
inspectorTab,
consoleTab,
networkTab,
];
return <TabbedPane
tabs={tabs}
selectedTab={selectedPropertiesTab}
setSelectedTab={setSelectedPropertiesTab}
/>;
};
const TraceView: React.FunctionComponent<{
sdkLanguage: Language,
callId: string | undefined,
isInspecting: boolean;
setIsInspecting: (value: boolean) => void;
highlightedElement: HighlightedElement;
setHighlightedElement: (element: HighlightedElement) => void;
}> = ({
sdkLanguage,
callId,
isInspecting,
setIsInspecting,
highlightedElement,
setHighlightedElement,
}) => {
const model = React.useContext(ModelContext);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [shouldPopulateCanvasFromScreenshot, _] = useSetting('shouldPopulateCanvasFromScreenshot', false);
const action = React.useMemo(() => {
return model?.actions.find(a => a.callId === callId);
}, [model, callId]);
const snapshot = React.useMemo(() => {
const snapshot = collectSnapshots(action);
return snapshot.action || snapshot.after || snapshot.before;
}, [action]);
const snapshotUrls = React.useMemo(() => {
return snapshot ? extendSnapshot(snapshot, shouldPopulateCanvasFromScreenshot) : undefined;
}, [snapshot, shouldPopulateCanvasFromScreenshot]);
return <SnapshotView
sdkLanguage={sdkLanguage}
testIdAttributeName='data-testid'
isInspecting={isInspecting}
setIsInspecting={setIsInspecting}
highlightedElement={highlightedElement}
setHighlightedElement={setHighlightedElement}
snapshotUrls={snapshotUrls} />;
};

View file

@ -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[] = [

View file

@ -46,7 +46,6 @@ export default defineConfig({
input: {
index: path.resolve(__dirname, 'index.html'),
uiMode: path.resolve(__dirname, 'uiMode.html'),
recorder: path.resolve(__dirname, 'recorder.html'),
snapshot: path.resolve(__dirname, 'snapshot.html'),
},
output: {

View file

@ -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];
}

View file

@ -101,7 +101,6 @@ library/inspector/cli-codegen-3.spec.ts cli codegen should generate fram
library/page-clock.spec.ts popup should run time before popup [timeout]
library/page-clock.spec.ts popup should tick after popup [timeout]
library/page-clock.spec.ts popup should tick before popup [timeout]
library/popup.spec.ts should not throttle rAF in the opener page [timeout]
library/popup.spec.ts should not throw when click closes popup [timeout]
library/popup.spec.ts should use viewport size from window features [timeout]
library/trace-viewer.spec.ts should serve css without content-type [timeout]

View file

@ -21,7 +21,6 @@ import * as playwrightLibrary from 'playwright-core';
export type TestModeWorkerOptions = {
mode: TestModeName;
codegenMode: 'trace-events' | 'actions';
};
export type TestModeTestFixtures = {
@ -49,7 +48,6 @@ export const testModeTest = test.extend<TestModeTestFixtures, TestModeWorkerOpti
await run(playwright);
await testMode.teardown();
}, { scope: 'worker' }],
codegenMode: ['actions', { scope: 'worker', option: true }],
toImplInWorkerScope: [async ({ playwright }, use) => {
await use((playwright as any)._toImpl);

View file

@ -313,3 +313,46 @@ it('headless and headful should use same default fonts', async ({ page, browserN
}
await headlessBrowser.close();
});
it('should have the same hyphen rendering on headless and headed', {
annotation: {
type: 'issue',
description: 'https://github.com/microsoft/playwright/issues/33590'
}
}, async ({ browserType, page, headless, server }) => {
const content = `
<!DOCTYPE html>
<html lang="en">
<head>
<style>
.hyphenated {
width: 100px;
hyphens: auto;
text-align: justify;
border: 1px solid black;
}
</style>
</head>
<body>
<div class="hyphenated">
supercalifragilisticexpialidocious
</div>
</body>
</html>
`;
server.setRoute('/hyphenated.html', (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(content);
});
const oppositeBrowser = await browserType.launch({ headless: !headless });
const oppositePage = await oppositeBrowser.newPage();
await oppositePage.goto(server.PREFIX + '/hyphenated.html');
await page.goto(server.PREFIX + '/hyphenated.html');
const [divHeight1, divHeight2] = await Promise.all([
page.evaluate(() => document.querySelector('.hyphenated').getBoundingClientRect().height),
oppositePage.evaluate(() => document.querySelector('.hyphenated').getBoundingClientRect().height),
]);
expect(divHeight1).toBe(divHeight2);
await oppositeBrowser.close();
});

View file

@ -19,7 +19,6 @@ import type { ConsoleMessage } from 'playwright';
test.describe('cli codegen', () => {
test.skip(({ mode }) => mode !== 'default');
test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events');
test('should click', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();
@ -413,7 +412,7 @@ await page.GetByRole(AriaRole.Textbox).PressAsync("Shift+Enter");`);
expect(messages[0].text()).toBe('press');
});
test('should update selected element after pressing Tab', async ({ openRecorder, browserName, codegenMode }) => {
test('should update selected element after pressing Tab', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`

View file

@ -20,7 +20,6 @@ import fs from 'fs';
test.describe('cli codegen', () => {
test.skip(({ mode }) => mode !== 'default');
test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events');
test('should contain open page', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
@ -310,8 +309,7 @@ await page.GetByRole(AriaRole.Button, new() { Name = "click me" }).ClickAsync();
}
});
test('should record open in a new tab with url', async ({ openRecorder, browserName, codegenMode }) => {
test.skip(codegenMode === 'trace-events');
test('should record open in a new tab with url', async ({ openRecorder, browserName }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<a href="about:blank?foo">link</a>`);
@ -453,29 +451,16 @@ await page1.GotoAsync("about:blank?foo");`);
await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/page2.html');`);
});
test('should --save-trace', async ({ runCLI, codegenMode }, testInfo) => {
test.skip(codegenMode === 'trace-events');
const traceFileName = testInfo.outputPath('trace.zip');
const cli = runCLI([`--save-trace=${traceFileName}`], {
autoExitWhen: ' ',
});
await cli.waitForCleanExit();
expect(fs.existsSync(traceFileName)).toBeTruthy();
});
test('should save assets via SIGINT', async ({ runCLI, platform, codegenMode }, testInfo) => {
test.skip(codegenMode === 'trace-events');
test('should save assets via SIGINT', async ({ runCLI, platform }, testInfo) => {
test.skip(platform === 'win32', 'SIGINT not supported on Windows');
const traceFileName = testInfo.outputPath('trace.zip');
const storageFileName = testInfo.outputPath('auth.json');
const harFileName = testInfo.outputPath('har.har');
const cli = runCLI([`--save-trace=${traceFileName}`, `--save-storage=${storageFileName}`, `--save-har=${harFileName}`]);
const cli = runCLI([`--save-storage=${storageFileName}`, `--save-har=${harFileName}`]);
await cli.waitFor(`import { test, expect } from '@playwright/test'`);
await cli.process.kill('SIGINT');
const { exitCode } = await cli.process.exited;
expect(exitCode).toBe(130);
expect(fs.existsSync(traceFileName)).toBeTruthy();
expect(fs.existsSync(storageFileName)).toBeTruthy();
expect(fs.existsSync(harFileName)).toBeTruthy();
});

View file

@ -21,7 +21,6 @@ import type { Page } from '@playwright/test';
test.describe('cli codegen', () => {
test.skip(({ mode }) => mode !== 'default');
test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events');
test('should click locator.first', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();

View file

@ -19,7 +19,6 @@ import { roundBox } from '../../page/pageTest';
test.describe(() => {
test.skip(({ mode }) => mode !== 'default');
test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events');
test('should generate aria snapshot', async ({ openRecorder }) => {
const { recorder } = await openRecorder();

View file

@ -19,7 +19,6 @@ import { roundBox } from '../../page/pageTest';
test.describe(() => {
test.skip(({ mode }) => mode !== 'default');
test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events');
test('should inspect locator', async ({ openRecorder }) => {
const { recorder } = await openRecorder();

View file

@ -67,7 +67,7 @@ export const test = contextTest.extend<CLITestArgs>({
});
},
runCLI: async ({ childProcess, browserName, channel, headless, mode, launchOptions, codegenMode }, run, testInfo) => {
runCLI: async ({ childProcess, browserName, channel, headless, mode, launchOptions }, run, testInfo) => {
testInfo.skip(mode.startsWith('service'));
await run((cliArgs, { autoExitWhen } = {}) => {
@ -78,17 +78,15 @@ export const test = contextTest.extend<CLITestArgs>({
args: cliArgs,
executablePath: launchOptions.executablePath,
autoExitWhen,
codegenMode
});
});
},
openRecorder: async ({ context, recorderPageGetter, codegenMode }, run) => {
openRecorder: async ({ context, recorderPageGetter }, run) => {
await run(async (options?: { testIdAttributeName?: string }) => {
await (context as any)._enableRecorder({
language: 'javascript',
mode: 'recording',
codegenMode,
...options
});
const page = await context.newPage();
@ -235,7 +233,7 @@ export class Recorder {
class CLIMock {
process: TestChildProcess;
constructor(childProcess: CommonFixtures['childProcess'], options: { browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, autoExitWhen: string | undefined, codegenMode?: 'trace-events' | 'actions'}) {
constructor(childProcess: CommonFixtures['childProcess'], options: { browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, autoExitWhen: string | undefined}) {
const nodeArgs = [
'node',
path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'cli.js'),
@ -248,7 +246,6 @@ class CLIMock {
this.process = childProcess({
command: nodeArgs,
env: {
PW_RECORDER_IS_TRACE_VIEWER: options.codegenMode === 'trace-events' ? '1' : undefined,
PWTEST_CLI_AUTO_EXIT_WHEN: options.autoExitWhen,
PWTEST_CLI_IS_UNDER_TEST: '1',
PWTEST_CLI_HEADLESS: options.headless ? '1' : undefined,

View file

@ -147,18 +147,6 @@ for (const browserName of browserNames) {
testDir: path.join(testDir, 'page'),
...projectTemplate,
});
// TODO: figure out reporting to flakiness dashboard (Problem: they get merged, we want to keep them separate)
// config.projects.push({
// name: `${browserName}-codegen-mode-trace`,
// testDir: path.join(testDir, 'library'),
// testMatch: '**/cli-codegen-*.spec.ts',
// ...projectTemplate,
// use: {
// ...projectTemplate.use,
// codegenMode: 'trace-events',
// }
// });
}
export default config;

View file

@ -166,6 +166,61 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
]);
});
test('should show action context on locators and other common actions', async ({
runAndTrace,
page,
}) => {
const traceViewer = await runAndTrace(async () => {
await page.setContent('<input type="text" />');
await page.locator('input').click({ button: 'right' });
await page.getByRole('textbox').click();
await expect(page.locator('input')).toHaveText('');
await page.locator('input').press('Enter');
await page.keyboard.type(
'Hello world this is a very long string what happens when it overflows?',
);
await page.keyboard.press('Control+c');
await page.keyboard.down('Shift');
await page.keyboard.insertText('Hello world');
await page.keyboard.up('Shift');
await page.mouse.move(0, 0);
await page.mouse.down();
await page.mouse.move(100, 200);
await page.mouse.wheel(5, 7);
await page.mouse.up();
await page.clock.fastForward(1000);
await page.clock.fastForward('30:00');
await page.clock.pauseAt(new Date('2020-02-02T00:00:00Z'));
await page.clock.runFor(10);
await page.clock.setFixedTime(new Date('2020-02-02T00:00:00Z'));
await page.clock.setSystemTime(new Date('2020-02-02T00:00:00Z'));
});
await expect(traceViewer.actionTitles).toHaveText([
/page.setContent/,
/locator.clicklocator\('input'\)/,
/locator.clickgetByRole\('textbox'\)/,
/expect.toHaveTextlocator\('input'\)/,
/locator.presslocator\('input'\)Enter/,
/keyboard.type\"Hello world this is a very long string what happens when it overflows\?\"/,
/keyboard.pressControl\+c/,
/keyboard.downShift/,
/keyboard.insertText\"Hello world\"/,
/keyboard.upShift/,
/mouse.move\(0, 0\)/,
/mouse.down/,
/mouse.move\(100, 200\)/,
/mouse.wheel\(5, 7\)/,
/mouse.up/,
/clock.fastForward1000ms/,
/clock.fastForward30:00/,
/clock.pauseAt2\/2\/2020, 12:00:00 AM/,
/clock.runFor10ms/,
/clock.setFixedTime2\/2\/2020, 12:00:00 AM/,
/clock.setSystemTime2\/2\/2020, 12:00:00 AM/,
]);
});
test('should complain about newer version of trace in old viewer', async ({ showTraceViewer, asset }, testInfo) => {
const traceViewer = await showTraceViewer([asset('trace-from-the-future.zip')]);
await expect(traceViewer.page.getByText('The trace was created by a newer version of Playwright and is not supported by this version of the viewer.')).toBeVisible();

View file

@ -436,43 +436,6 @@ test('toHaveAccessibleName', async ({ page }) => {
await expect(page.locator('button')).toHaveAccessibleName('foo bar baz');
});
test('toHaveAccessibleName should accept array of names for multiple elements', async ({ page }) => {
await page.setContent(`
<table>
<tr role="row">
<td role="cell">Cell A1</td>
<td role="cell">Cell B1</td>
<td role="cell">Cell C1</td>
</tr>
<tr role="row">
<td role="cell">Cell A2</td>
<td role="cell">Cell B2</td>
<td role="cell">Cell C2</td>
</tr>
<tr role="row">
<td role="cell">Cell A3</td>
<td role="cell">Cell B3</td>
<td role="cell">Cell C3</td>
</tr>
</table>
`);
await expect(page.getByRole('row')).toHaveAccessibleName([
'Cell A1 Cell B1 Cell C1',
'Cell A2 Cell B2 Cell C2',
'Cell A3 Cell B3 Cell C3',
]);
await expect(page.getByRole('row')).toHaveAccessibleName(['cell a1 cell b1 cell C1',
'cell A2 Cell b2 Cell c2',
'Cell a3 Cell b3 cell C3',], { ignoreCase: true });
await expect(page.getByRole('row')).not.toHaveAccessibleName([
'Cel A4 Cell B4 Cell C4',
'Cell A5 Cell B5 Cell C5',
'Cell A6 Cell B6 Cell C6',
]);
});
test('toHaveAccessibleDescription', async ({ page }) => {
await page.setContent(`
<div role="button" aria-description="Hello"></div>

View file

@ -18,9 +18,10 @@
import { test as it, expect } from './pageTest';
it('should check the box @smoke', async ({ page }) => {
await page.setContent(`<input id='checkbox' type='checkbox'></input>`);
await page.check('input');
expect(await page.evaluate(() => window['checkbox'].checked)).toBe(true);
await page.setContent(`<div class='middle selected row' id='component'></div>`);
const locator = page.locator('#component');
await expect(locator).toHaveClass(/(^|\s)selected(\s|$)/);
await expect(locator).toHaveClass('middle selected row');
});
it('should not check the checked box', async ({ page }) => {

View file

@ -0,0 +1,13 @@
- tree:
- treeitem "[icon-circle-outline] a.test.ts" [expanded]:
- group:
- treeitem "[icon-circle-outline] passes" [selected]:
- button "Run"
- button "Show source"
- button "Watch"
- treeitem "[icon-circle-outline] fails"
- treeitem "[icon-circle-outline] suite"
- treeitem "[icon-circle-outline] b.test.ts" [expanded]:
- group:
- treeitem "[icon-circle-outline] passes"
- treeitem "[icon-circle-outline] fails"

View file

@ -0,0 +1,13 @@
- tree:
- treeitem "[icon-circle-outline] a.test.ts" [expanded]:
- group:
- treeitem "[icon-circle-outline] passes"
- treeitem "[icon-circle-outline] fails" [selected]:
- button "Run"
- button "Show source"
- button "Watch"
- treeitem "[icon-circle-outline] suite"
- treeitem "[icon-circle-outline] b.test.ts" [expanded]:
- group:
- treeitem "[icon-circle-outline] passes"
- treeitem "[icon-circle-outline] fails"

View file

@ -0,0 +1,13 @@
- tree:
- treeitem "[icon-circle-outline] a.test.ts" [expanded]:
- group:
- treeitem "[icon-circle-outline] passes" [selected]:
- button "Run"
- button "Show source"
- button "Watch"
- treeitem "[icon-circle-outline] fails"
- treeitem "[icon-circle-outline] suite"
- treeitem "[icon-circle-outline] b.test.ts" [expanded]:
- group:
- treeitem "[icon-circle-outline] passes"
- treeitem "[icon-circle-outline] fails"

View file

@ -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': `

View file

@ -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();
});

View file

@ -5,16 +5,16 @@
"packages": {
"": {
"dependencies": {
"@playwright/test": "1.49.0-beta-1731772650000"
"@playwright/test": "1.50.0-alpha-2025-01-17"
}
},
"node_modules/@playwright/test": {
"version": "1.49.0-beta-1731772650000",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-beta-1731772650000.tgz",
"integrity": "sha512-0d7DBoGZ23lv1/EkNoFXj5fQ9k3qlYHRE7la68zXihtjTH1DdwEtgdMgXR4UEScF2r/YNXaGRZ7sK/DVu9f6Aw==",
"version": "1.50.0-alpha-2025-01-17",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0-alpha-2025-01-17.tgz",
"integrity": "sha512-fMUwMcP0YE2knged9GJXqv3fpT2xoywTtqYaSzpZmjnNESF+CUUAGY2hHm9/fz/v9ijcjyd62hYFbqS5KeKuHQ==",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.49.0-beta-1731772650000"
"playwright": "1.50.0-alpha-2025-01-17"
},
"bin": {
"playwright": "cli.js"
@ -38,12 +38,12 @@
}
},
"node_modules/playwright": {
"version": "1.49.0-beta-1731772650000",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-beta-1731772650000.tgz",
"integrity": "sha512-+LLjx+DMLjx1qiBtLuURTLV3LmFxvQOSaVp9EDMH/qYpclhsp/W41vNxxZEqf8CIsL0BKHIVQYU+6D3OLnJq8g==",
"version": "1.50.0-alpha-2025-01-17",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0-alpha-2025-01-17.tgz",
"integrity": "sha512-LRavQ9Qu27nHvJ57f+7UDBTAEWhGKV+MS2qLAJpF8HXtfSMVlLK82W9Oba41lCNUzgLoAuFv0wCO/RcHqLz7yQ==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.49.0-beta-1731772650000"
"playwright-core": "1.50.0-alpha-2025-01-17"
},
"bin": {
"playwright": "cli.js"
@ -56,9 +56,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.49.0-beta-1731772650000",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-beta-1731772650000.tgz",
"integrity": "sha512-W1HbioibWPPsazFzU/PL9QzGEGubxizQOyMON8/d7DjOpNBqfzuemNuAsNBXucUEVbUlOOzMuoAEX/iqXUOl6Q==",
"version": "1.50.0-alpha-2025-01-17",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0-alpha-2025-01-17.tgz",
"integrity": "sha512-XkoLZ+7J5ybDq68xSlofPziH1Y8It9LpMisxtBfebjKWbVY8BzctlB1Da9udKDP0oWQPNq4tUnwW0hkeET3lUg==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
@ -70,11 +70,11 @@
},
"dependencies": {
"@playwright/test": {
"version": "1.49.0-beta-1731772650000",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-beta-1731772650000.tgz",
"integrity": "sha512-0d7DBoGZ23lv1/EkNoFXj5fQ9k3qlYHRE7la68zXihtjTH1DdwEtgdMgXR4UEScF2r/YNXaGRZ7sK/DVu9f6Aw==",
"version": "1.50.0-alpha-2025-01-17",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0-alpha-2025-01-17.tgz",
"integrity": "sha512-fMUwMcP0YE2knged9GJXqv3fpT2xoywTtqYaSzpZmjnNESF+CUUAGY2hHm9/fz/v9ijcjyd62hYFbqS5KeKuHQ==",
"requires": {
"playwright": "1.49.0-beta-1731772650000"
"playwright": "1.50.0-alpha-2025-01-17"
}
},
"fsevents": {
@ -84,18 +84,18 @@
"optional": true
},
"playwright": {
"version": "1.49.0-beta-1731772650000",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-beta-1731772650000.tgz",
"integrity": "sha512-+LLjx+DMLjx1qiBtLuURTLV3LmFxvQOSaVp9EDMH/qYpclhsp/W41vNxxZEqf8CIsL0BKHIVQYU+6D3OLnJq8g==",
"version": "1.50.0-alpha-2025-01-17",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0-alpha-2025-01-17.tgz",
"integrity": "sha512-LRavQ9Qu27nHvJ57f+7UDBTAEWhGKV+MS2qLAJpF8HXtfSMVlLK82W9Oba41lCNUzgLoAuFv0wCO/RcHqLz7yQ==",
"requires": {
"fsevents": "2.3.2",
"playwright-core": "1.49.0-beta-1731772650000"
"playwright-core": "1.50.0-alpha-2025-01-17"
}
},
"playwright-core": {
"version": "1.49.0-beta-1731772650000",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-beta-1731772650000.tgz",
"integrity": "sha512-W1HbioibWPPsazFzU/PL9QzGEGubxizQOyMON8/d7DjOpNBqfzuemNuAsNBXucUEVbUlOOzMuoAEX/iqXUOl6Q=="
"version": "1.50.0-alpha-2025-01-17",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0-alpha-2025-01-17.tgz",
"integrity": "sha512-XkoLZ+7J5ybDq68xSlofPziH1Y8It9LpMisxtBfebjKWbVY8BzctlB1Da9udKDP0oWQPNq4tUnwW0hkeET3lUg=="
}
}
}

View file

@ -1,6 +1,6 @@
{
"private": true,
"dependencies": {
"@playwright/test": "1.49.0-beta-1731772650000"
"@playwright/test": "1.50.0-alpha-2025-01-17"
}
}

View file

@ -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': `
@ -616,7 +637,7 @@ test('should not propagate errors from within toPass', async ({ runInlineTest })
expect(result.exitCode).toBe(0);
expect(result.output).toBe(`
hook |Before Hooks
expect |expect.toPass @ a.test.ts:7
step |expect.toPass @ a.test.ts:7
expect | expect.toBe @ a.test.ts:6
expect | error: Error: expect(received).toBe(expected) // Object.is equality
expect | expect.toBe @ a.test.ts:6
@ -643,8 +664,8 @@ test('should show final toPass error', async ({ runInlineTest }) => {
expect(result.exitCode).toBe(1);
expect(stripAnsi(result.output)).toBe(`
hook |Before Hooks
expect |expect.toPass @ a.test.ts:6
expect | error: Error: expect(received).toBe(expected) // Object.is equality
step |expect.toPass @ a.test.ts:6
step | error: Error: expect(received).toBe(expected) // Object.is equality
expect | expect.toBe @ a.test.ts:5
expect | error: Error: expect(received).toBe(expected) // Object.is equality
hook |After Hooks
@ -909,7 +930,7 @@ test('step inside expect.toPass', async ({ runInlineTest }) => {
expect(stripAnsi(result.output)).toBe(`
hook |Before Hooks
test.step |step 1 @ a.test.ts:4
expect | expect.toPass @ a.test.ts:11
step | expect.toPass @ a.test.ts:11
test.step | step 2, attempt: 0 @ a.test.ts:7
test.step | error: Error: expect(received).toBe(expected) // Object.is equality
expect | expect.toBe @ a.test.ts:9
@ -956,7 +977,7 @@ fixture | fixture: context
pw:api | browser.newContext
fixture | fixture: page
pw:api | browserContext.newPage
expect |expect.toPass @ a.test.ts:11
step |expect.toPass @ a.test.ts:11
pw:api | page.goto(about:blank) @ a.test.ts:6
test.step | inner step attempt: 0 @ a.test.ts:7
test.step | error: Error: expect(received).toBe(expected) // Object.is equality
@ -1007,7 +1028,7 @@ fixture | fixture: context
pw:api | browser.newContext
fixture | fixture: page
pw:api | browserContext.newPage
expect |expect.poll.toHaveLength @ a.test.ts:14
step |expect.poll.toHaveLength @ a.test.ts:14
pw:api | page.goto(about:blank) @ a.test.ts:7
test.step | inner step attempt: 0 @ a.test.ts:8
expect | expect.toBe @ a.test.ts:10
@ -1059,7 +1080,7 @@ pw:api | browser.newContext
fixture | fixture: page
pw:api | browserContext.newPage
pw:api |page.setContent @ a.test.ts:4
expect |expect.poll.toBe @ a.test.ts:13
step |expect.poll.toBe @ a.test.ts:13
expect | expect.toHaveText @ a.test.ts:7
test.step | iteration 1 @ a.test.ts:9
expect | expect.toBeVisible @ a.test.ts:10
@ -1565,3 +1586,66 @@ expect |expect.toBe @ a.test.ts:10
hook |After Hooks
`);
});
test('show api calls inside expects', async ({ runInlineTest }) => {
const result = await runInlineTest({
'reporter.ts': stepIndentReporter,
'playwright.config.ts': `module.exports = { reporter: './reporter' };`,
'a.test.ts': `
import { test, expect as baseExpect } from '@playwright/test';
const expect = baseExpect.extend({
async toBeInvisible(locator: Locator) {
try {
await expect.poll(() => locator.isVisible()).toBe(false);
return { name: 'toBeInvisible', pass: true, message: '' };
} catch (e) {
return { name: 'toBeInvisible', pass: false, message: () => 'Expected to be invisible, got visible!' };
}
},
});
test('test', async ({ page }) => {
await page.setContent('<div>hello</div>');
const promise = expect(page.locator('div')).toBeInvisible();
await page.waitForTimeout(1100);
await page.setContent('<div style="display:none">hello</div>');
await promise;
});
`
}, { reporter: '' });
expect(result.exitCode).toBe(0);
expect(result.report.stats.expected).toBe(1);
expect(stripAnsi(result.output)).toBe(`
hook |Before Hooks
fixture | fixture: browser
pw:api | browserType.launch
fixture | fixture: context
pw:api | browser.newContext
fixture | fixture: page
pw:api | browserContext.newPage
pw:api |page.setContent @ a.test.ts:16
expect |expect.toBeInvisible @ a.test.ts:17
step | expect.poll.toBe @ a.test.ts:7
pw:api | locator.isVisible(div) @ a.test.ts:7
expect | expect.toBe @ a.test.ts:7
expect | error: Error: expect(received).toBe(expected) // Object.is equality
pw:api | locator.isVisible(div) @ a.test.ts:7
expect | expect.toBe @ a.test.ts:7
expect | error: Error: expect(received).toBe(expected) // Object.is equality
pw:api | locator.isVisible(div) @ a.test.ts:7
expect | expect.toBe @ a.test.ts:7
expect | error: Error: expect(received).toBe(expected) // Object.is equality
pw:api | locator.isVisible(div) @ a.test.ts:7
expect | expect.toBe @ a.test.ts:7
expect | error: Error: expect(received).toBe(expected) // Object.is equality
pw:api | locator.isVisible(div) @ a.test.ts:7
expect | expect.toBe @ a.test.ts:7
pw:api |page.waitForTimeout @ a.test.ts:18
pw:api |page.setContent @ a.test.ts:19
hook |After Hooks
fixture | fixture: page
fixture | fixture: context
`);
});

View file

@ -164,6 +164,7 @@ test('should traverse up/down', async ({ runUITest }) => {
- treeitem "[icon-circle-outline] fails"
- treeitem "[icon-circle-outline] suite" [expanded=false]
`);
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot();
await page.keyboard.press('ArrowDown');
await expect.poll(dumpTestTree(page)).toContain(`
@ -180,6 +181,7 @@ test('should traverse up/down', async ({ runUITest }) => {
- treeitem "[icon-circle-outline] fails" [selected]
- treeitem "[icon-circle-outline] suite" [expanded=false]
`);
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot();
await page.keyboard.press('ArrowUp');
await expect.poll(dumpTestTree(page)).toContain(`
@ -196,6 +198,7 @@ test('should traverse up/down', async ({ runUITest }) => {
- treeitem "[icon-circle-outline] fails"
- treeitem "[icon-circle-outline] suite" [expanded=false]
`);
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot();
});
test('should expand / collapse groups', async ({ runUITest }) => {