Merge branch 'main' into trace-viewer-hrm

This commit is contained in:
Simon Knott 2024-11-20 09:09:39 +01:00
commit f8e05b0a89
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
81 changed files with 1027 additions and 560 deletions

View file

@ -1,6 +1,6 @@
# 🎭 Playwright # 🎭 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-131.0.6778.33-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-132.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-132.0.6834.6-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-132.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) ## [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 | | | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: | | :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->131.0.6778.33<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Chromium <!-- GEN:chromium-version -->132.0.6834.6<!-- 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: | | WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->132.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox <!-- GEN:firefox-version -->132.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |

View file

@ -206,6 +206,9 @@ Below is the HTML markup and the respective ARIA snapshot:
- link "About" - link "About"
``` ```
### option: Locator.ariaSnapshot.timeout = %%-input-timeout-%%
* since: v1.49
### option: Locator.ariaSnapshot.timeout = %%-input-timeout-js-%% ### option: Locator.ariaSnapshot.timeout = %%-input-timeout-js-%%
* since: v1.49 * since: v1.49

View file

@ -701,7 +701,7 @@ expect(locator).to_be_enabled()
```csharp ```csharp
var locator = Page.Locator("button.submit"); var locator = Page.Locator("button.submit");
await Expect(locator).toBeEnabledAsync(); await Expect(locator).ToBeEnabledAsync();
``` ```
### option: LocatorAssertions.toBeEnabled.enabled ### option: LocatorAssertions.toBeEnabled.enabled
@ -1181,7 +1181,7 @@ expect(locator).to_have_accessible_description("Save results to disk")
```csharp ```csharp
var locator = Page.GetByTestId("save-button"); var locator = Page.GetByTestId("save-button");
await Expect(locator).toHaveAccessibleDescriptionAsync("Save results to disk"); await Expect(locator).ToHaveAccessibleDescriptionAsync("Save results to disk");
``` ```
### param: LocatorAssertions.toHaveAccessibleDescription.description ### param: LocatorAssertions.toHaveAccessibleDescription.description
@ -1231,12 +1231,12 @@ expect(locator).to_have_accessible_name("Save to disk")
```csharp ```csharp
var locator = Page.GetByTestId("save-button"); var locator = Page.GetByTestId("save-button");
await Expect(locator).toHaveAccessibleNameAsync("Save to disk"); await Expect(locator).ToHaveAccessibleNameAsync("Save to disk");
``` ```
### param: LocatorAssertions.toHaveAccessibleName.name ### param: LocatorAssertions.toHaveAccessibleName.name
* since: v1.44 * since: v1.44
- `name` <[string]|[RegExp]> - `name` <[string]|[RegExp]|[Array]<[string]|[RegExp]>>
Expected accessible name. Expected accessible name.
@ -2159,3 +2159,6 @@ assertThat(page.locator("body")).matchesAriaSnapshot("""
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%% ### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%%
* since: v1.49 * since: v1.49
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.49

View file

@ -21,7 +21,7 @@ Playwright tests can be run on any CI provider. This guide covers one way of run
## Introduction ## Introduction
* langs: python, java, csharp * langs: python, java, csharp
Playwright tests can be ran on any CI provider. In this section we will cover running tests on GitHub using GitHub actions. If you would like to see how to configure other CI providers check out our detailed doc on Continuous Integration. Playwright tests can be run on any CI provider. In this section we will cover running tests on GitHub using GitHub actions. If you would like to see how to configure other CI providers check out our detailed doc on Continuous Integration.
#### You will learn #### You will learn
* langs: python, java, csharp * langs: python, java, csharp

View file

@ -164,11 +164,11 @@ await Page.GotoAsync("http://localhost:3333");
await Page.Clock.PauseAtAsync(new DateTime(2024, 2, 2, 10, 0, 0)); await Page.Clock.PauseAtAsync(new DateTime(2024, 2, 2, 10, 0, 0));
// Assert the page state. // Assert the page state.
await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:00:00 AM"); await Expect(Page.GetByTestId("current-time")).ToHaveTextAsync("2/2/2024, 10:00:00 AM");
// Close the laptop lid again and open it at 10:30am. // Close the laptop lid again and open it at 10:30am.
await Page.Clock.FastForwardAsync("30:00"); await Page.Clock.FastForwardAsync("30:00");
await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:30:00 AM"); await Expect(Page.GetByTestId("current-time")).ToHaveTextAsync("2/2/2024, 10:30:00 AM");
``` ```
## Test inactivity monitoring ## Test inactivity monitoring

View file

@ -80,7 +80,7 @@ The `tests` folder contains a basic example test to help you get started with te
## Running the Example Test ## Running the Example Test
By default tests will be run on all 3 browsers, chromium, firefox and webkit using 3 workers. This can be configured in the [playwright.config file](./test-configuration.md). Tests are run in headless mode meaning no browser will open up when running the tests. Results of the tests and test logs will be shown in the terminal. By default tests will be run on all 3 browsers, Chromium, Firefox and WebKit using 3 workers. This can be configured in the [playwright.config file](./test-configuration.md). Tests are run in headless mode meaning no browser will open up when running the tests. Results of the tests and test logs will be shown in the terminal.
<Tabs <Tabs
defaultValue="npm" defaultValue="npm"

View file

@ -788,7 +788,7 @@ All the same methods are also available on [Locator], [FrameLocator] and [Frame]
- [`method: LocatorAssertions.toHaveAttribute`] with an empty value does not match missing attribute anymore. For example, the following snippet will succeed when `button` **does not** have a `disabled` attribute. - [`method: LocatorAssertions.toHaveAttribute`] with an empty value does not match missing attribute anymore. For example, the following snippet will succeed when `button` **does not** have a `disabled` attribute.
```csharp ```csharp
await Expect(Page.GetByRole(AriaRole.Button)).ToHaveAttribute("disabled", ""); await Expect(Page.GetByRole(AriaRole.Button)).ToHaveAttributeAsync("disabled", "");
``` ```
### Browser Versions ### Browser Versions

View file

@ -33,7 +33,7 @@ npx playwright test --ui
![UI Mode](https://github.com/microsoft/playwright/assets/13063165/c5b501cc-4f5d-485a-87cc-66044c651786) ![UI Mode](https://github.com/microsoft/playwright/assets/13063165/c5b501cc-4f5d-485a-87cc-66044c651786)
Check out or [detailed guide on UI Mode](./test-ui-mode.md) to learn more about it's features. Check out or [detailed guide on UI Mode](./test-ui-mode.md) to learn more about its features.
### Run tests in headed mode ### Run tests in headed mode
@ -112,11 +112,11 @@ npx playwright test --ui
![showing errors in ui mode](https://github.com/microsoft/playwright/assets/13063165/ffca2fd1-5349-41fb-ade9-ace143bb2c58) ![showing errors in ui mode](https://github.com/microsoft/playwright/assets/13063165/ffca2fd1-5349-41fb-ade9-ace143bb2c58)
While debugging you can use the Pick Locator button to select an element on the page and see the locator that Playwright would use to find that element. You can also edit the locator in the locator playground and see it highlighting live on the Browser window. Use the Copy Locator button to copy the locator to your clipboard and then paste it into you test. While debugging you can use the Pick Locator button to select an element on the page and see the locator that Playwright would use to find that element. You can also edit the locator in the locator playground and see it highlighting live on the Browser window. Use the Copy Locator button to copy the locator to your clipboard and then paste it into your test.
![pick locator in ui mode](https://github.com/microsoft/playwright/assets/13063165/9e7eeb84-bd26-4010-8614-75e24b56c716) ![pick locator in ui mode](https://github.com/microsoft/playwright/assets/13063165/9e7eeb84-bd26-4010-8614-75e24b56c716)
Check out our [detailed guide on UI Mode](./test-ui-mode.md) to learn more about it's features. Check out our [detailed guide on UI Mode](./test-ui-mode.md) to learn more about its features.
### Debug tests with the Playwright Inspector ### Debug tests with the Playwright Inspector

View file

@ -45,7 +45,7 @@ npx playwright test --trace on
## Opening the HTML report ## Opening the HTML report
The HTML report shows you a report of all your tests that have been ran and on which browsers as well as how long they took. Tests can be filtered by passed tests, failed, flakey or skipped tests. You can also search for a particular test. Clicking on a test will open the detailed view where you can see more information on your tests such as the errors, the test steps and the trace. The HTML report shows you a report of all your tests that have been run and on which browsers as well as how long they took. Tests can be filtered by passed tests, failed, flaky or skipped tests. You can also search for a particular test. Clicking on a test will open the detailed view where you can see more information on your tests such as the errors, the test steps and the trace.
```bash ```bash
npx playwright show-report npx playwright show-report

7
package-lock.json generated
View file

@ -3002,10 +3002,11 @@
} }
}, },
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",
"shebang-command": "^2.0.0", "shebang-command": "^2.0.0",

View file

@ -20,6 +20,7 @@ import './colors.css';
import './common.css'; import './common.css';
import * as icons from './icons'; import * as icons from './icons';
import { clsx } from '@web/uiUtils'; import { clsx } from '@web/uiUtils';
import { useAnchor } from './links';
export const Chip: React.FC<{ export const Chip: React.FC<{
header: JSX.Element | string, header: JSX.Element | string,
@ -28,10 +29,9 @@ export const Chip: React.FC<{
setExpanded?: (expanded: boolean) => void, setExpanded?: (expanded: boolean) => void,
children?: any, children?: any,
dataTestId?: string, dataTestId?: string,
targetRef?: React.RefObject<HTMLDivElement>, }> = ({ header, expanded, setExpanded, children, noInsets, dataTestId }) => {
}> = ({ header, expanded, setExpanded, children, noInsets, dataTestId, targetRef }) => {
const id = React.useId(); const id = React.useId();
return <div className='chip' data-testid={dataTestId} ref={targetRef}> return <div className='chip' data-testid={dataTestId}>
<div <div
role='button' role='button'
aria-expanded={!!expanded} aria-expanded={!!expanded}
@ -53,16 +53,17 @@ export const AutoChip: React.FC<{
noInsets?: boolean, noInsets?: boolean,
children?: any, children?: any,
dataTestId?: string, dataTestId?: string,
targetRef?: React.RefObject<HTMLDivElement>, revealOnAnchorId?: string,
}> = ({ header, initialExpanded, noInsets, children, dataTestId, targetRef }) => { }> = ({ header, initialExpanded, noInsets, children, dataTestId, revealOnAnchorId }) => {
const [expanded, setExpanded] = React.useState(initialExpanded || initialExpanded === undefined); const [expanded, setExpanded] = React.useState(initialExpanded ?? true);
const onReveal = React.useCallback(() => setExpanded(true), []);
useAnchor(revealOnAnchorId, onReveal);
return <Chip return <Chip
header={header} header={header}
expanded={expanded} expanded={expanded}
setExpanded={setExpanded} setExpanded={setExpanded}
noInsets={noInsets} noInsets={noInsets}
dataTestId={dataTestId} dataTestId={dataTestId}
targetRef={targetRef}
> >
{children} {children}
</Chip>; </Chip>;

View file

@ -113,3 +113,32 @@ export function generateTraceUrl(traces: TestAttachment[]) {
} }
const kMissingContentType = 'x-playwright/missing'; const kMissingContentType = 'x-playwright/missing';
type AnchorID = string | ((id: string | null) => boolean) | undefined;
export function useAnchor(id: AnchorID, onReveal: () => void) {
React.useEffect(() => {
if (typeof id === 'undefined')
return;
const listener = () => {
const params = new URLSearchParams(window.location.hash.slice(1));
const anchor = params.get('anchor');
const isRevealed = typeof id === 'function' ? id(anchor) : anchor === id;
if (isRevealed)
onReveal();
};
window.addEventListener('popstate', listener);
return () => window.removeEventListener('popstate', listener);
}, [id, onReveal]);
}
export function Anchor({ id, children }: React.PropsWithChildren<{ id: AnchorID }>) {
const ref = React.useRef<HTMLDivElement>(null);
const onAnchorReveal = React.useCallback(() => {
requestAnimationFrame(() => ref.current?.scrollIntoView({ block: 'start', inline: 'start' }));
}, []);
useAnchor(id, onAnchorReveal);
return <div ref={ref}>{children}</div>;
}

View file

@ -101,7 +101,6 @@ const TestCaseViewLoader: React.FC<{
const searchParams = React.useContext(SearchParamsContext); const searchParams = React.useContext(SearchParamsContext);
const [test, setTest] = React.useState<TestCase | undefined>(); const [test, setTest] = React.useState<TestCase | undefined>();
const testId = searchParams.get('testId'); const testId = searchParams.get('testId');
const anchor = (searchParams.get('anchor') || '') as 'video' | 'diff' | '';
const run = +(searchParams.get('run') || '0'); const run = +(searchParams.get('run') || '0');
const { prev, next } = React.useMemo(() => { const { prev, next } = React.useMemo(() => {
@ -133,7 +132,6 @@ const TestCaseViewLoader: React.FC<{
next={next} next={next}
prev={prev} prev={prev}
test={test} test={test}
anchor={anchor}
run={run} run={run}
/>; />;
}; };

View file

@ -63,7 +63,7 @@ const testCase: TestCase = {
}; };
test('should render test case', async ({ mount }) => { test('should render test case', async ({ mount }) => {
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={testCase} prev={undefined} next={undefined} run={0} anchor=''></TestCaseView>); const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={testCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible(); await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible();
await expect(component.getByText('Hidden annotation')).toBeHidden(); await expect(component.getByText('Hidden annotation')).toBeHidden();
await component.getByText('Annotations').click(); await component.getByText('Annotations').click();
@ -79,7 +79,7 @@ test('should render test case', async ({ mount }) => {
test('should render copy buttons for annotations', async ({ mount, page, context }) => { test('should render copy buttons for annotations', async ({ mount, page, context }) => {
await context.grantPermissions(['clipboard-read', 'clipboard-write']); await context.grantPermissions(['clipboard-read', 'clipboard-write']);
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={testCase} prev={undefined} next={undefined} run={0} anchor=''></TestCaseView>); const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={testCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible(); await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible();
await component.getByText('Annotation text', { exact: false }).first().hover(); await component.getByText('Annotation text', { exact: false }).first().hover();
await expect(component.locator('.test-case-annotation').getByLabel('Copy to clipboard').first()).toBeVisible(); await expect(component.locator('.test-case-annotation').getByLabel('Copy to clipboard').first()).toBeVisible();
@ -108,7 +108,7 @@ const annotationLinkRenderingTestCase: TestCase = {
}; };
test('should correctly render links in annotations', async ({ mount }) => { test('should correctly render links in annotations', async ({ mount }) => {
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={annotationLinkRenderingTestCase} prev={undefined} next={undefined} run={0} anchor=''></TestCaseView>); const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={annotationLinkRenderingTestCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
const firstLink = await component.getByText('https://playwright.dev/docs/intro').first(); const firstLink = await component.getByText('https://playwright.dev/docs/intro').first();
await expect(firstLink).toBeVisible(); await expect(firstLink).toBeVisible();
@ -181,7 +181,7 @@ const testCaseSummary: TestCaseSummary = {
test('should correctly render links in attachments', async ({ mount }) => { test('should correctly render links in attachments', async ({ mount }) => {
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} prev={undefined} next={undefined} run={0} anchor=''></TestCaseView>); const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
await component.getByText('first attachment').click(); await component.getByText('first attachment').click();
const body = await component.getByText('The body with https://playwright.dev/docs/intro link'); const body = await component.getByText('The body with https://playwright.dev/docs/intro link');
await expect(body).toBeVisible(); await expect(body).toBeVisible();
@ -194,7 +194,7 @@ test('should correctly render links in attachments', async ({ mount }) => {
}); });
test('should correctly render links in attachment name', async ({ mount }) => { test('should correctly render links in attachment name', async ({ mount }) => {
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} prev={undefined} next={undefined} run={0} anchor=''></TestCaseView>); const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
const link = component.getByText('attachment with inline link').locator('a'); const link = component.getByText('attachment with inline link').locator('a');
await expect(link).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/31284'); await expect(link).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/31284');
await expect(link).toHaveText('https://github.com/microsoft/playwright/issues/31284'); await expect(link).toHaveText('https://github.com/microsoft/playwright/issues/31284');
@ -204,7 +204,7 @@ test('should correctly render links in attachment name', async ({ mount }) => {
}); });
test('should correctly render prev and next', async ({ mount }) => { test('should correctly render prev and next', async ({ mount }) => {
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} prev={testCaseSummary} next={testCaseSummary} run={0} anchor=''></TestCaseView>); const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} prev={testCaseSummary} next={testCaseSummary} run={0}></TestCaseView>);
await expect(component).toMatchAriaSnapshot(` await expect(component).toMatchAriaSnapshot(`
- text: group - text: group
- link "« previous" - link "« previous"

View file

@ -33,9 +33,8 @@ export const TestCaseView: React.FC<{
test: TestCase | undefined, test: TestCase | undefined,
next: TestCaseSummary | undefined, next: TestCaseSummary | undefined,
prev: TestCaseSummary | undefined, prev: TestCaseSummary | undefined,
anchor: 'video' | 'diff' | '',
run: number, run: number,
}> = ({ projectNames, test, run, anchor, next, prev }) => { }> = ({ projectNames, test, run, next, prev }) => {
const [selectedResultIndex, setSelectedResultIndex] = React.useState(run); const [selectedResultIndex, setSelectedResultIndex] = React.useState(run);
const searchParams = React.useContext(SearchParamsContext); const searchParams = React.useContext(SearchParamsContext);
const filterParam = searchParams.has('q') ? '&q=' + searchParams.get('q') : ''; const filterParam = searchParams.has('q') ? '&q=' + searchParams.get('q') : '';
@ -79,7 +78,7 @@ export const TestCaseView: React.FC<{
test.results.map((result, index) => ({ test.results.map((result, index) => ({
id: String(index), id: String(index),
title: <div style={{ display: 'flex', alignItems: 'center' }}>{statusIcon(result.status)} {retryLabel(index)}</div>, title: <div style={{ display: 'flex', alignItems: 'center' }}>{statusIcon(result.status)} {retryLabel(index)}</div>,
render: () => <TestResultView test={test!} result={result} anchor={anchor}></TestResultView> render: () => <TestResultView test={test!} result={result} />
})) || []} selectedTab={String(selectedResultIndex)} setSelectedTab={id => setSelectedResultIndex(+id)} />} })) || []} selectedTab={String(selectedResultIndex)} setSelectedTab={id => setSelectedResultIndex(+id)} />}
</div>; </div>;
}; };

View file

@ -25,7 +25,7 @@ export const TestErrorView: React.FC<{
testId?: string; testId?: string;
}> = ({ error, testId }) => { }> = ({ error, testId }) => {
const html = React.useMemo(() => ansiErrorToHtml(error), [error]); const html = React.useMemo(() => ansiErrorToHtml(error), [error]);
return <div className='test-error-view test-error-text' data-testId={testId} dangerouslySetInnerHTML={{ __html: html || '' }}></div>; return <div className='test-error-view test-error-text' data-testid={testId} dangerouslySetInnerHTML={{ __html: html || '' }}></div>;
}; };
export const TestScreenshotErrorView: React.FC<{ export const TestScreenshotErrorView: React.FC<{

View file

@ -75,12 +75,12 @@ function imageDiffBadge(test: TestCaseSummary): JSX.Element | undefined {
const resultWithImageDiff = test.results.find(result => result.attachments.some(attachment => { const resultWithImageDiff = test.results.find(result => result.attachments.some(attachment => {
return attachment.contentType.startsWith('image/') && !!attachment.name.match(/-(expected|actual|diff)/); return attachment.contentType.startsWith('image/') && !!attachment.name.match(/-(expected|actual|diff)/);
})); }));
return resultWithImageDiff ? <Link href={`#?testId=${test.testId}&anchor=diff&run=${test.results.indexOf(resultWithImageDiff)}`} title='View images' className='test-file-badge'>{image()}</Link> : undefined; return resultWithImageDiff ? <Link href={`#?testId=${test.testId}&anchor=diff-0&run=${test.results.indexOf(resultWithImageDiff)}`} title='View images' className='test-file-badge'>{image()}</Link> : undefined;
} }
function videoBadge(test: TestCaseSummary): JSX.Element | undefined { function videoBadge(test: TestCaseSummary): JSX.Element | undefined {
const resultWithVideo = test.results.find(result => result.attachments.some(attachment => attachment.name === 'video')); const resultWithVideo = test.results.find(result => result.attachments.some(attachment => attachment.name === 'video'));
return resultWithVideo ? <Link href={`#?testId=${test.testId}&anchor=video&run=${test.results.indexOf(resultWithVideo)}`} title='View video' className='test-file-badge'>{video()}</Link> : undefined; return resultWithVideo ? <Link href={`#?testId=${test.testId}&anchor=videos&run=${test.results.indexOf(resultWithVideo)}`} title='View video' className='test-file-badge'>{video()}</Link> : undefined;
} }
function traceBadge(test: TestCaseSummary): JSX.Element | undefined { function traceBadge(test: TestCaseSummary): JSX.Element | undefined {

View file

@ -20,7 +20,7 @@ import { TreeItem } from './treeItem';
import { msToString } from './utils'; import { msToString } from './utils';
import { AutoChip } from './chip'; import { AutoChip } from './chip';
import { traceImage } from './images'; import { traceImage } from './images';
import { AttachmentLink, generateTraceUrl } from './links'; import { Anchor, AttachmentLink, generateTraceUrl } from './links';
import { statusIcon } from './statusIcon'; import { statusIcon } from './statusIcon';
import type { ImageDiff } from '@web/shared/imageDiffView'; import type { ImageDiff } from '@web/shared/imageDiffView';
import { ImageDiffView } from '@web/shared/imageDiffView'; import { ImageDiffView } from '@web/shared/imageDiffView';
@ -64,9 +64,7 @@ function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiff[] {
export const TestResultView: React.FC<{ export const TestResultView: React.FC<{
test: TestCase, test: TestCase,
result: TestResult, result: TestResult,
anchor: 'video' | 'diff' | '', }> = ({ result }) => {
}> = ({ result, anchor }) => {
const { screenshots, videos, traces, otherAttachments, diffs, errors, htmls } = React.useMemo(() => { const { screenshots, videos, traces, otherAttachments, diffs, errors, htmls } = React.useMemo(() => {
const attachments = result?.attachments || []; const attachments = result?.attachments || [];
const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/'))); const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/')));
@ -80,20 +78,6 @@ export const TestResultView: React.FC<{
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, errors, htmls }; return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, errors, htmls };
}, [result]); }, [result]);
const videoRef = React.useRef<HTMLDivElement>(null);
const imageDiffRef = React.useRef<HTMLDivElement>(null);
const [scrolled, setScrolled] = React.useState(false);
React.useEffect(() => {
if (scrolled)
return;
setScrolled(true);
if (anchor === 'video')
videoRef.current?.scrollIntoView({ block: 'start', inline: 'start' });
if (anchor === 'diff')
imageDiffRef.current?.scrollIntoView({ block: 'start', inline: 'start' });
}, [scrolled, anchor, setScrolled, videoRef]);
return <div className='test-result'> return <div className='test-result'>
{!!errors.length && <AutoChip header='Errors'> {!!errors.length && <AutoChip header='Errors'>
{errors.map((error, index) => { {errors.map((error, index) => {
@ -107,9 +91,11 @@ export const TestResultView: React.FC<{
</AutoChip>} </AutoChip>}
{diffs.map((diff, index) => {diffs.map((diff, index) =>
<AutoChip key={`diff-${index}`} dataTestId='test-results-image-diff' header={`Image mismatch: ${diff.name}`} targetRef={imageDiffRef}> <Anchor key={`diff-${index}`} id={`diff-${index}`}>
<ImageDiffView key='image-diff' diff={diff}></ImageDiffView> <AutoChip dataTestId='test-results-image-diff' header={`Image mismatch: ${diff.name}`} revealOnAnchorId={`diff-${index}`}>
<ImageDiffView diff={diff}/>
</AutoChip> </AutoChip>
</Anchor>
)} )}
{!!screenshots.length && <AutoChip header='Screenshots'> {!!screenshots.length && <AutoChip header='Screenshots'>
@ -123,23 +109,23 @@ export const TestResultView: React.FC<{
})} })}
</AutoChip>} </AutoChip>}
{!!traces.length && <AutoChip header='Traces'> {!!traces.length && <Anchor id='traces'><AutoChip header='Traces' revealOnAnchorId='traces'>
{<div> {<div>
<a href={generateTraceUrl(traces)}> <a href={generateTraceUrl(traces)}>
<img className='screenshot' src={traceImage} style={{ width: 192, height: 117, marginLeft: 20 }} /> <img className='screenshot' src={traceImage} style={{ width: 192, height: 117, marginLeft: 20 }} />
</a> </a>
{traces.map((a, i) => <AttachmentLink key={`trace-${i}`} attachment={a} linkName={traces.length === 1 ? 'trace' : `trace-${i + 1}`}></AttachmentLink>)} {traces.map((a, i) => <AttachmentLink key={`trace-${i}`} attachment={a} linkName={traces.length === 1 ? 'trace' : `trace-${i + 1}`}></AttachmentLink>)}
</div>} </div>}
</AutoChip>} </AutoChip></Anchor>}
{!!videos.length && <AutoChip header='Videos' targetRef={videoRef}> {!!videos.length && <Anchor id='videos'><AutoChip header='Videos' revealOnAnchorId='videos'>
{videos.map((a, i) => <div key={`video-${i}`}> {videos.map((a, i) => <div key={`video-${i}`}>
<video controls> <video controls>
<source src={a.path} type={a.contentType}/> <source src={a.path} type={a.contentType}/>
</video> </video>
<AttachmentLink attachment={a}></AttachmentLink> <AttachmentLink attachment={a}></AttachmentLink>
</div>)} </div>)}
</AutoChip>} </AutoChip></Anchor>}
{!!(otherAttachments.size + htmls.length) && <AutoChip header='Attachments'> {!!(otherAttachments.size + htmls.length) && <AutoChip header='Attachments'>
{[...htmls].map((a, i) => ( {[...htmls].map((a, i) => (

View file

@ -3,21 +3,21 @@
"browsers": [ "browsers": [
{ {
"name": "chromium", "name": "chromium",
"revision": "1148", "revision": "1149",
"installByDefault": true, "installByDefault": true,
"browserVersion": "131.0.6778.33" "browserVersion": "132.0.6834.6"
}, },
{ {
"name": "chromium-headless-shell", "name": "chromium-headless-shell",
"revision": "1148", "revision": "1149",
"installByDefault": true, "installByDefault": true,
"browserVersion": "131.0.6778.33" "browserVersion": "132.0.6834.6"
}, },
{ {
"name": "chromium-tip-of-tree", "name": "chromium-tip-of-tree",
"revision": "1277", "revision": "1279",
"installByDefault": false, "installByDefault": false,
"browserVersion": "132.0.6834.0" "browserVersion": "133.0.6846.0"
}, },
{ {
"name": "firefox", "name": "firefox",
@ -27,9 +27,9 @@
}, },
{ {
"name": "firefox-beta", "name": "firefox-beta",
"revision": "1465", "revision": "1466",
"installByDefault": false, "installByDefault": false,
"browserVersion": "132.0b8" "browserVersion": "133.0b9"
}, },
{ {
"name": "webkit", "name": "webkit",

View file

@ -97,6 +97,7 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.RequestInitializer) { constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.RequestInitializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
this.markAsInternalType();
this._redirectedFrom = Request.fromNullable(initializer.redirectedFrom); this._redirectedFrom = Request.fromNullable(initializer.redirectedFrom);
if (this._redirectedFrom) if (this._redirectedFrom)
this._redirectedFrom._redirectedTo = this; this._redirectedFrom._redirectedTo = this;
@ -645,6 +646,7 @@ export class Response extends ChannelOwner<channels.ResponseChannel> implements
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.ResponseInitializer) { constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.ResponseInitializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
this.markAsInternalType();
this._provisionalHeaders = new RawHeaders(initializer.headers); this._provisionalHeaders = new RawHeaders(initializer.headers);
this._request = Request.from(this._initializer.request); this._request = Request.from(this._initializer.request);
Object.assign(this._request._timing, this._initializer.timing); Object.assign(this._request._timing, this._initializer.timing);

View file

@ -112,7 +112,7 @@ export module Protocol {
- from 'checked' to 'selected': states which apply to widgets - from 'checked' to 'selected': states which apply to widgets
- from 'activedescendant' to 'owns' - relationships between elements other than parent/child/sibling. - from 'activedescendant' to 'owns' - relationships between elements other than parent/child/sibling.
*/ */
export type AXPropertyName = "busy"|"disabled"|"editable"|"focusable"|"focused"|"hidden"|"hiddenRoot"|"invalid"|"keyshortcuts"|"settable"|"roledescription"|"live"|"atomic"|"relevant"|"root"|"autocomplete"|"hasPopup"|"level"|"multiselectable"|"orientation"|"multiline"|"readonly"|"required"|"valuemin"|"valuemax"|"valuetext"|"checked"|"expanded"|"modal"|"pressed"|"selected"|"activedescendant"|"controls"|"describedby"|"details"|"errormessage"|"flowto"|"labelledby"|"owns"|"url"; export type AXPropertyName = "actions"|"busy"|"disabled"|"editable"|"focusable"|"focused"|"hidden"|"hiddenRoot"|"invalid"|"keyshortcuts"|"settable"|"roledescription"|"live"|"atomic"|"relevant"|"root"|"autocomplete"|"hasPopup"|"level"|"multiselectable"|"orientation"|"multiline"|"readonly"|"required"|"valuemin"|"valuemax"|"valuetext"|"checked"|"expanded"|"modal"|"pressed"|"selected"|"activedescendant"|"controls"|"describedby"|"details"|"errormessage"|"flowto"|"labelledby"|"owns"|"url";
/** /**
* A node in the accessibility tree. * A node in the accessibility tree.
*/ */
@ -694,7 +694,7 @@ percentage [0 - 100] for scroll driven animations
export interface AffectedFrame { export interface AffectedFrame {
frameId: Page.FrameId; frameId: Page.FrameId;
} }
export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout"; 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 CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"|"WarnDeprecationTrialMetadata"|"WarnThirdPartyCookieHeuristic";
export type CookieOperation = "SetCookie"|"ReadCookie"; export type CookieOperation = "SetCookie"|"ReadCookie";
/** /**
@ -2183,12 +2183,17 @@ The array enumerates @scope at-rules starting with the innermost one, going outw
* The array keeps the types of ancestor CSSRules from the innermost going outwards. * The array keeps the types of ancestor CSSRules from the innermost going outwards.
*/ */
ruleTypes?: CSSRuleType[]; ruleTypes?: CSSRuleType[];
/**
* @starting-style CSS at-rule array.
The array enumerates @starting-style at-rules starting with the innermost one, going outwards.
*/
startingStyles?: CSSStartingStyle[];
} }
/** /**
* Enum indicating the type of a CSS rule, used to represent the order of a style rule's ancestors. * Enum indicating the type of a CSS rule, used to represent the order of a style rule's ancestors.
This list only contains rule types that are collected during the ancestor rule collection. This list only contains rule types that are collected during the ancestor rule collection.
*/ */
export type CSSRuleType = "MediaRule"|"SupportsRule"|"ContainerRule"|"LayerRule"|"ScopeRule"|"StyleRule"; export type CSSRuleType = "MediaRule"|"SupportsRule"|"ContainerRule"|"LayerRule"|"ScopeRule"|"StyleRule"|"StartingStyleRule";
/** /**
* CSS coverage information. * CSS coverage information.
*/ */
@ -2424,6 +2429,10 @@ available).
* Optional logical axes queried for the container. * Optional logical axes queried for the container.
*/ */
logicalAxes?: DOM.LogicalAxes; logicalAxes?: DOM.LogicalAxes;
/**
* true if the query contains scroll-state() queries.
*/
queriesScrollState?: boolean;
} }
/** /**
* CSS Supports at-rule descriptor. * CSS Supports at-rule descriptor.
@ -2475,6 +2484,20 @@ available).
text: string; text: string;
/** /**
* The associated rule header range in the enclosing stylesheet (if * The associated rule header range in the enclosing stylesheet (if
available).
*/
range?: SourceRange;
/**
* Identifier of the stylesheet containing this object (if exists).
*/
styleSheetId?: StyleSheetId;
}
/**
* CSS Starting Style at-rule descriptor.
*/
export interface CSSStartingStyle {
/**
* The associated rule header range in the enclosing stylesheet (if
available). available).
*/ */
range?: SourceRange; range?: SourceRange;
@ -2779,6 +2802,12 @@ resized.) The current implementation considers only viewport-dependent media fea
*/ */
styleSheetId: StyleSheetId; styleSheetId: StyleSheetId;
} }
export type computedStyleUpdatedPayload = {
/**
* The node id that has updated computed styles.
*/
nodeId: DOM.NodeId;
}
/** /**
* Inserts a new rule with the given `ruleText` in a stylesheet with given `styleSheetId`, at the * Inserts a new rule with the given `ruleText` in a stylesheet with given `styleSheetId`, at the
@ -3039,6 +3068,19 @@ returns an array of locations of the CSS selector in the style sheet.
export type getLocationForSelectorReturnValue = { export type getLocationForSelectorReturnValue = {
ranges: SourceRange[]; ranges: SourceRange[];
} }
/**
* Starts tracking the given node for the computed style updates
and whenever the computed style is updated for node, it queues
a `computedStyleUpdated` event with throttling.
There can only be 1 node tracked for computed style updates
so passing a new node id removes tracking from the previous node.
Pass `undefined` to disable tracking.
*/
export type trackComputedStyleUpdatesForNodeParameters = {
nodeId?: DOM.NodeId;
}
export type trackComputedStyleUpdatesForNodeReturnValue = {
}
/** /**
* Starts tracking the given computed styles for updates. The specified array of properties * Starts tracking the given computed styles for updates. The specified array of properties
replaces the one previously specified. Pass empty array to disable tracking. replaces the one previously specified. Pass empty array to disable tracking.
@ -3561,7 +3603,7 @@ front-end.
/** /**
* Pseudo element type. * Pseudo element type.
*/ */
export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"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"|"select-fallback-button"|"select-fallback-button-text"|"picker"; 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";
/** /**
* Shadow root type. * Shadow root type.
*/ */
@ -4876,15 +4918,17 @@ $x functions).
} }
/** /**
* Returns the query container of the given node based on container query * Returns the query container of the given node based on container query
conditions: containerName, physical, and logical axes. If no axes are conditions: containerName, physical and logical axes, and whether it queries
provided, the style container is returned, which is the direct parent or the scroll-state. If no axes are provided and queriesScrollState is false, the
closest element with a matching container-name. style container is returned, which is the direct parent or the closest
element with a matching container-name.
*/ */
export type getContainerForNodeParameters = { export type getContainerForNodeParameters = {
nodeId: NodeId; nodeId: NodeId;
containerName?: string; containerName?: string;
physicalAxes?: PhysicalAxes; physicalAxes?: PhysicalAxes;
logicalAxes?: LogicalAxes; logicalAxes?: LogicalAxes;
queriesScrollState?: boolean;
} }
export type getContainerForNodeReturnValue = { export type getContainerForNodeReturnValue = {
/** /**
@ -8255,7 +8299,9 @@ file, data and other requests and responses, their headers, bodies, timing, etc.
*/ */
export type LoaderId = string; export type LoaderId = string;
/** /**
* Unique request identifier. * Unique network request identifier.
Note that this does not identify individual HTTP requests that are part of
a network request.
*/ */
export type RequestId = string; export type RequestId = string;
/** /**
@ -8830,6 +8876,7 @@ If the opcode isn't 1, then payloadData is a base64 encoded string representing
type: "parser"|"script"|"preload"|"SignedExchange"|"preflight"|"other"; type: "parser"|"script"|"preload"|"SignedExchange"|"preflight"|"other";
/** /**
* Initiator JavaScript stack trace, set for Script only. * Initiator JavaScript stack trace, set for Script only.
Requires the Debugger domain to be enabled.
*/ */
stack?: Runtime.StackTrace; stack?: Runtime.StackTrace;
/** /**
@ -8944,7 +8991,7 @@ This is a temporary ability and it will be removed in the future.
/** /**
* Types of reasons why a cookie may not be sent with a request. * Types of reasons why a cookie may not be sent with a request.
*/ */
export type CookieBlockedReason = "SecureOnly"|"NotOnPath"|"DomainMismatch"|"SameSiteStrict"|"SameSiteLax"|"SameSiteUnspecifiedTreatedAsLax"|"SameSiteNoneInsecure"|"UserPreferences"|"ThirdPartyPhaseout"|"ThirdPartyBlockedInFirstPartySet"|"UnknownError"|"SchemefulSameSiteStrict"|"SchemefulSameSiteLax"|"SchemefulSameSiteUnspecifiedTreatedAsLax"|"SamePartyFromCrossPartyContext"|"NameValuePairExceedsMaxSize"; export type CookieBlockedReason = "SecureOnly"|"NotOnPath"|"DomainMismatch"|"SameSiteStrict"|"SameSiteLax"|"SameSiteUnspecifiedTreatedAsLax"|"SameSiteNoneInsecure"|"UserPreferences"|"ThirdPartyPhaseout"|"ThirdPartyBlockedInFirstPartySet"|"UnknownError"|"SchemefulSameSiteStrict"|"SchemefulSameSiteLax"|"SchemefulSameSiteUnspecifiedTreatedAsLax"|"SamePartyFromCrossPartyContext"|"NameValuePairExceedsMaxSize"|"PortMismatch"|"SchemeMismatch";
/** /**
* Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request. * Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request.
*/ */
@ -11498,7 +11545,7 @@ as an ad.
* All Permissions Policy features. This enum should match the one defined * All Permissions Policy features. This enum should match the one defined
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5. 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"|"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"|"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. * Reason for a permissions policy feature to be disabled.
*/ */
@ -12384,7 +12431,8 @@ the page execution. Execution can be resumed via calling Page.handleJavaScriptDi
defaultPrompt?: string; defaultPrompt?: string;
} }
/** /**
* Fired for top level page lifecycle events such as navigation, load, paint, etc. * Fired for lifecycle events (navigation, load, paint, etc) in the current
target (including local frames).
*/ */
export type lifecycleEventPayload = { export type lifecycleEventPayload = {
/** /**
@ -14339,6 +14387,7 @@ int
destinationLimitPriority: SignedInt64AsBase10; destinationLimitPriority: SignedInt64AsBase10;
aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig; aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig;
scopesData?: AttributionScopesData; scopesData?: AttributionScopesData;
maxEventLevelReports: number;
} }
export type AttributionReportingSourceRegistrationResult = "success"|"internalError"|"insufficientSourceCapacity"|"insufficientUniqueDestinationCapacity"|"excessiveReportingOrigins"|"prohibitedByBrowserPolicy"|"successNoised"|"destinationReportingLimitReached"|"destinationGlobalLimitReached"|"destinationBothLimitsReached"|"reportingOriginsPerSiteLimitReached"|"exceedsMaxChannelCapacity"|"exceedsMaxScopesChannelCapacity"|"exceedsMaxTriggerStateCardinality"|"exceedsMaxEventStatesLimit"|"destinationPerDayReportingLimitReached"; export type AttributionReportingSourceRegistrationResult = "success"|"internalError"|"insufficientSourceCapacity"|"insufficientUniqueDestinationCapacity"|"excessiveReportingOrigins"|"prohibitedByBrowserPolicy"|"successNoised"|"destinationReportingLimitReached"|"destinationGlobalLimitReached"|"destinationBothLimitsReached"|"reportingOriginsPerSiteLimitReached"|"exceedsMaxChannelCapacity"|"exceedsMaxScopesChannelCapacity"|"exceedsMaxTriggerStateCardinality"|"exceedsMaxEventStatesLimit"|"destinationPerDayReportingLimitReached";
export type AttributionReportingSourceRegistrationTimeConfig = "include"|"exclude"; export type AttributionReportingSourceRegistrationTimeConfig = "include"|"exclude";
@ -14386,7 +14435,7 @@ int
scopes: string[]; scopes: string[];
} }
export type AttributionReportingEventLevelResult = "success"|"successDroppedLowerPriority"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"deduplicated"|"excessiveAttributions"|"priorityTooLow"|"neverAttributedSource"|"excessiveReportingOrigins"|"noMatchingSourceFilterData"|"prohibitedByBrowserPolicy"|"noMatchingConfigurations"|"excessiveReports"|"falselyAttributedSource"|"reportWindowPassed"|"notRegistered"|"reportWindowNotStarted"|"noMatchingTriggerData"; export type AttributionReportingEventLevelResult = "success"|"successDroppedLowerPriority"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"deduplicated"|"excessiveAttributions"|"priorityTooLow"|"neverAttributedSource"|"excessiveReportingOrigins"|"noMatchingSourceFilterData"|"prohibitedByBrowserPolicy"|"noMatchingConfigurations"|"excessiveReports"|"falselyAttributedSource"|"reportWindowPassed"|"notRegistered"|"reportWindowNotStarted"|"noMatchingTriggerData";
export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports"; export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"insufficientNamedBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports";
/** /**
* A single Related Website Set object. * A single Related Website Set object.
*/ */
@ -15920,6 +15969,8 @@ are ignored.
export module Fetch { export module Fetch {
/** /**
* Unique request identifier. * Unique request identifier.
Note that this does not identify individual HTTP requests that are part of
a network request.
*/ */
export type RequestId = string; export type RequestId = string;
/** /**
@ -16302,7 +16353,7 @@ https://webaudio.github.io/web-audio-api/
/** /**
* Enum of AudioContextState from the spec * Enum of AudioContextState from the spec
*/ */
export type ContextState = "suspended"|"running"|"closed"; export type ContextState = "suspended"|"running"|"closed"|"interrupted";
/** /**
* Enum of AudioNode types * Enum of AudioNode types
*/ */
@ -20213,6 +20264,7 @@ Error was thrown.
"CSS.styleSheetAdded": CSS.styleSheetAddedPayload; "CSS.styleSheetAdded": CSS.styleSheetAddedPayload;
"CSS.styleSheetChanged": CSS.styleSheetChangedPayload; "CSS.styleSheetChanged": CSS.styleSheetChangedPayload;
"CSS.styleSheetRemoved": CSS.styleSheetRemovedPayload; "CSS.styleSheetRemoved": CSS.styleSheetRemovedPayload;
"CSS.computedStyleUpdated": CSS.computedStyleUpdatedPayload;
"Cast.sinksUpdated": Cast.sinksUpdatedPayload; "Cast.sinksUpdated": Cast.sinksUpdatedPayload;
"Cast.issueUpdated": Cast.issueUpdatedPayload; "Cast.issueUpdated": Cast.issueUpdatedPayload;
"DOM.attributeModified": DOM.attributeModifiedPayload; "DOM.attributeModified": DOM.attributeModifiedPayload;
@ -20464,6 +20516,7 @@ Error was thrown.
"CSS.getStyleSheetText": CSS.getStyleSheetTextParameters; "CSS.getStyleSheetText": CSS.getStyleSheetTextParameters;
"CSS.getLayersForNode": CSS.getLayersForNodeParameters; "CSS.getLayersForNode": CSS.getLayersForNodeParameters;
"CSS.getLocationForSelector": CSS.getLocationForSelectorParameters; "CSS.getLocationForSelector": CSS.getLocationForSelectorParameters;
"CSS.trackComputedStyleUpdatesForNode": CSS.trackComputedStyleUpdatesForNodeParameters;
"CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesParameters; "CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesParameters;
"CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesParameters; "CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesParameters;
"CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeParameters; "CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeParameters;
@ -21075,6 +21128,7 @@ Error was thrown.
"CSS.getStyleSheetText": CSS.getStyleSheetTextReturnValue; "CSS.getStyleSheetText": CSS.getStyleSheetTextReturnValue;
"CSS.getLayersForNode": CSS.getLayersForNodeReturnValue; "CSS.getLayersForNode": CSS.getLayersForNodeReturnValue;
"CSS.getLocationForSelector": CSS.getLocationForSelectorReturnValue; "CSS.getLocationForSelector": CSS.getLocationForSelectorReturnValue;
"CSS.trackComputedStyleUpdatesForNode": CSS.trackComputedStyleUpdatesForNodeReturnValue;
"CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesReturnValue; "CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesReturnValue;
"CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesReturnValue; "CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesReturnValue;
"CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeReturnValue; "CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeReturnValue;

View file

@ -117,8 +117,10 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
const assertion = action.value ? `toHaveValue(${quote(action.value)})` : `toBeEmpty()`; const assertion = action.value ? `toHaveValue(${quote(action.value)})` : `toBeEmpty()`;
return `${this._isTest ? '' : '// '}await expect(${subject}.${this._asLocator(action.selector)}).${assertion};`; return `${this._isTest ? '' : '// '}await expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
} }
case 'assertSnapshot': case 'assertSnapshot': {
return `${this._isTest ? '' : '// '}await expect(${subject}.${this._asLocator(action.selector)}).toMatchAriaSnapshot(${quoteMultiline(action.snapshot)});`; const commentIfNeeded = this._isTest ? '' : '// ';
return `${commentIfNeeded}await expect(${subject}.${this._asLocator(action.selector)}).toMatchAriaSnapshot(${quoteMultiline(action.snapshot, `${commentIfNeeded} `)});`;
}
} }
} }

View file

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

View file

@ -296,7 +296,8 @@ export class FrameManager {
if (request._documentId) if (request._documentId)
frame.setPendingDocument({ documentId: request._documentId, request }); frame.setPendingDocument({ documentId: request._documentId, request });
if (request._isFavicon) { if (request._isFavicon) {
route?.continue({ isFallback: true }).catch(() => {}); // Abort favicon requests to avoid network access in case of interception.
route?.abort('aborted').catch(() => {});
return; return;
} }
this._page.emitOnContext(BrowserContext.Events.Request, request); this._page.emitOnContext(BrowserContext.Events.Request, request);

View file

@ -20,15 +20,36 @@ import { escapeRegExp, longestCommonSubstring } from '@isomorphic/stringUtils';
import { yamlEscapeKeyIfNeeded, yamlEscapeValueIfNeeded } from './yaml'; import { yamlEscapeKeyIfNeeded, yamlEscapeValueIfNeeded } from './yaml';
import type { AriaProps, AriaRole, AriaTemplateNode, AriaTemplateRoleNode, AriaTemplateTextNode } from '@isomorphic/ariaSnapshot'; import type { AriaProps, AriaRole, AriaTemplateNode, AriaTemplateRoleNode, AriaTemplateTextNode } from '@isomorphic/ariaSnapshot';
type AriaNode = AriaProps & { export type AriaNode = AriaProps & {
role: AriaRole | 'fragment'; role: AriaRole | 'fragment';
name: string; name: string;
children: (AriaNode | string)[]; children: (AriaNode | string)[];
element: Element; element: Element;
}; };
export function generateAriaTree(rootElement: Element): AriaNode { export type AriaSnapshot = {
root: AriaNode;
elements: Map<number, Element>;
ids: Map<Element, number>;
};
export function generateAriaTree(rootElement: Element): AriaSnapshot {
const visited = new Set<Node>(); const visited = new Set<Node>();
const snapshot: AriaSnapshot = {
root: { role: 'fragment', name: '', children: [], element: rootElement },
elements: new Map<number, Element>(),
ids: new Map<Element, number>(),
};
const addElement = (element: Element) => {
const id = snapshot.elements.size + 1;
snapshot.elements.set(id, element);
snapshot.ids.set(element, id);
};
addElement(rootElement);
const visit = (ariaNode: AriaNode, node: Node) => { const visit = (ariaNode: AriaNode, node: Node) => {
if (visited.has(node)) if (visited.has(node))
return; return;
@ -58,6 +79,7 @@ export function generateAriaTree(rootElement: Element): AriaNode {
} }
} }
addElement(element);
const childAriaNode = toAriaNode(element); const childAriaNode = toAriaNode(element);
if (childAriaNode) if (childAriaNode)
ariaNode.children.push(childAriaNode); ariaNode.children.push(childAriaNode);
@ -100,15 +122,14 @@ export function generateAriaTree(rootElement: Element): AriaNode {
} }
roleUtils.beginAriaCaches(); roleUtils.beginAriaCaches();
const ariaRoot: AriaNode = { role: 'fragment', name: '', children: [], element: rootElement };
try { try {
visit(ariaRoot, rootElement); visit(snapshot.root, rootElement);
} finally { } finally {
roleUtils.endAriaCaches(); roleUtils.endAriaCaches();
} }
normalizeStringChildren(ariaRoot); normalizeStringChildren(snapshot.root);
return ariaRoot; return snapshot;
} }
function toAriaNode(element: Element): AriaNode | null { function toAriaNode(element: Element): AriaNode | null {
@ -143,10 +164,6 @@ function toAriaNode(element: Element): AriaNode | null {
return result; return result;
} }
export function renderedAriaTree(rootElement: Element, options?: { mode?: 'raw' | 'regex' }): string {
return renderAriaTree(generateAriaTree(rootElement), options);
}
function normalizeStringChildren(rootA11yNode: AriaNode) { function normalizeStringChildren(rootA11yNode: AriaNode) {
const flushChildren = (buffer: string[], normalizedChildren: (AriaNode | string)[]) => { const flushChildren = (buffer: string[], normalizedChildren: (AriaNode | string)[]) => {
if (!buffer.length) if (!buffer.length)
@ -203,7 +220,7 @@ export type MatcherReceived = {
}; };
export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: AriaNode[], received: MatcherReceived } { export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: AriaNode[], received: MatcherReceived } {
const root = generateAriaTree(rootElement); const root = generateAriaTree(rootElement).root;
const matches = matchesNodeDeep(root, template, false); const matches = matchesNodeDeep(root, template, false);
return { return {
matches, matches,
@ -215,7 +232,7 @@ export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode
} }
export function getAllByAria(rootElement: Element, template: AriaTemplateNode): Element[] { export function getAllByAria(rootElement: Element, template: AriaTemplateNode): Element[] {
const root = generateAriaTree(rootElement); const root = generateAriaTree(rootElement).root;
const matches = matchesNodeDeep(root, template, true); const matches = matchesNodeDeep(root, template, true);
return matches.map(n => n.element); return matches.map(n => n.element);
} }
@ -285,7 +302,7 @@ function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode, collectAll:
return results; return results;
} }
export function renderAriaTree(ariaNode: AriaNode, options?: { mode?: 'raw' | 'regex' }): string { export function renderAriaTree(ariaNode: AriaNode, options?: { mode?: 'raw' | 'regex', ids?: Map<Element, number> }): string {
const lines: string[] = []; const lines: string[] = [];
const includeText = options?.mode === 'regex' ? textContributesInfo : () => true; const includeText = options?.mode === 'regex' ? textContributesInfo : () => true;
const renderString = options?.mode === 'regex' ? convertToBestGuessRegex : (str: string) => str; const renderString = options?.mode === 'regex' ? convertToBestGuessRegex : (str: string) => str;
@ -324,6 +341,11 @@ export function renderAriaTree(ariaNode: AriaNode, options?: { mode?: 'raw' | 'r
key += ` [pressed]`; key += ` [pressed]`;
if (ariaNode.selected === true) if (ariaNode.selected === true)
key += ` [selected]`; key += ` [selected]`;
if (options?.ids) {
const id = options?.ids.get(ariaNode.element);
if (id)
key += ` [id=${id}]`;
}
const escapedKey = indent + '- ' + yamlEscapeKeyIfNeeded(key); const escapedKey = indent + '- ' + yamlEscapeKeyIfNeeded(key);
if (!ariaNode.children.length) { if (!ariaNode.children.length) {

View file

@ -90,7 +90,8 @@ export class Highlight {
} }
install() { install() {
if (!this._injectedScript.document.documentElement.contains(this._glassPaneElement)) // NOTE: document.documentElement can be null: https://github.com/microsoft/TypeScript/issues/50078
if (this._injectedScript.document.documentElement && !this._injectedScript.document.documentElement.contains(this._glassPaneElement))
this._injectedScript.document.documentElement.appendChild(this._glassPaneElement); this._injectedScript.document.documentElement.appendChild(this._glassPaneElement);
} }

View file

@ -34,7 +34,8 @@ import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } fr
import { asLocator } from '../../utils/isomorphic/locatorGenerators'; import { asLocator } from '../../utils/isomorphic/locatorGenerators';
import type { Language } from '../../utils/isomorphic/locatorGenerators'; import type { Language } from '../../utils/isomorphic/locatorGenerators';
import { cacheNormalizedWhitespaces, normalizeWhiteSpace, trimStringWithEllipsis } from '../../utils/isomorphic/stringUtils'; import { cacheNormalizedWhitespaces, normalizeWhiteSpace, trimStringWithEllipsis } from '../../utils/isomorphic/stringUtils';
import { matchesAriaTree, renderedAriaTree, getAllByAria } from './ariaSnapshot'; import { matchesAriaTree, getAllByAria, generateAriaTree, renderAriaTree } from './ariaSnapshot';
import type { AriaNode, AriaSnapshot } from './ariaSnapshot';
import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot'; import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot';
import { parseYamlTemplate } from '@isomorphic/ariaSnapshot'; import { parseYamlTemplate } from '@isomorphic/ariaSnapshot';
@ -215,10 +216,27 @@ export class InjectedScript {
return new Set<Element>(result.map(r => r.element)); return new Set<Element>(result.map(r => r.element));
} }
ariaSnapshot(node: Node, options?: { mode?: 'raw' | 'regex' }): string { ariaSnapshot(node: Node, options?: { mode?: 'raw' | 'regex', id?: boolean }): string {
if (node.nodeType !== Node.ELEMENT_NODE) if (node.nodeType !== Node.ELEMENT_NODE)
throw this.createStacklessError('Can only capture aria snapshot of Element nodes.'); throw this.createStacklessError('Can only capture aria snapshot of Element nodes.');
return renderedAriaTree(node as Element, options); const ariaSnapshot = generateAriaTree(node as Element);
return renderAriaTree(ariaSnapshot.root, options);
}
ariaSnapshotAsObject(node: Node): AriaSnapshot {
return generateAriaTree(node as Element);
}
ariaSnapshotElement(snapshot: AriaSnapshot, elementId: number): Element | null {
return snapshot.elements.get(elementId) || null;
}
renderAriaTree(ariaNode: AriaNode, options?: { mode?: 'raw' | 'regex', id?: boolean}): string {
return renderAriaTree(ariaNode, options);
}
renderAriaSnapshotWithIds(ariaSnapshot: AriaSnapshot): string {
return renderAriaTree(ariaSnapshot.root, { ids: ariaSnapshot.ids });
} }
getAllByAria(document: Document, template: AriaTemplateNode): Element[] { getAllByAria(document: Document, template: AriaTemplateNode): Element[] {
@ -1336,6 +1354,8 @@ export class InjectedScript {
received = elements.map(e => options.useInnerText ? (e as HTMLElement).innerText : elementText(new Map(), e).full); received = elements.map(e => options.useInnerText ? (e as HTMLElement).innerText : elementText(new Map(), e).full);
else if (expression === 'to.have.class.array') else if (expression === 'to.have.class.array')
received = elements.map(e => e.classList.toString()); 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) { if (received && options.expectedText) {
// "To match an array" is "to contain an array" + "equal length" // "To match an array" is "to contain an array" + "equal length"

View file

@ -492,10 +492,11 @@ class RecordActionTool implements RecorderTool {
return; return;
const result = activeElement ? this._recorder.injectedScript.generateSelector(activeElement, { testIdAttributeName: this._recorder.state.testIdAttributeName }) : null; const result = activeElement ? this._recorder.injectedScript.generateSelector(activeElement, { testIdAttributeName: this._recorder.state.testIdAttributeName }) : null;
this._activeModel = result && result.selector ? result : null; this._activeModel = result && result.selector ? result : null;
if (userGesture) if (userGesture) {
this._hoveredElement = activeElement as HTMLElement | null; this._hoveredElement = activeElement as HTMLElement | null;
this._updateModelForHoveredElement(); this._updateModelForHoveredElement();
} }
}
private _shouldIgnoreMouseEvent(event: MouseEvent): boolean { private _shouldIgnoreMouseEvent(event: MouseEvent): boolean {
const target = this._recorder.deepEventTarget(event); const target = this._recorder.deepEventTarget(event);
@ -589,6 +590,8 @@ class RecordActionTool implements RecorderTool {
} }
private _updateModelForHoveredElement() { private _updateModelForHoveredElement() {
if (this._performingActions.size)
return;
if (!this._hoveredElement || !this._hoveredElement.isConnected) { if (!this._hoveredElement || !this._hoveredElement.isConnected) {
this._hoveredModel = null; this._hoveredModel = null;
this._hoveredElement = null; this._hoveredElement = null;
@ -1018,7 +1021,7 @@ export class Recorder {
private _listeners: (() => void)[] = []; private _listeners: (() => void)[] = [];
private _currentTool: RecorderTool; private _currentTool: RecorderTool;
private _tools: Record<Mode, RecorderTool>; private _tools: Record<Mode, RecorderTool>;
private _actionSelectorModel: HighlightModel | null = null; private _lastHighlightedSelector: string | undefined = undefined;
private _lastHighlightedAriaTemplateJSON: string = 'undefined'; private _lastHighlightedAriaTemplateJSON: string = 'undefined';
readonly highlight: Highlight; readonly highlight: Highlight;
readonly overlay: Overlay | undefined; readonly overlay: Overlay | undefined;
@ -1129,12 +1132,12 @@ export class Recorder {
this._switchCurrentTool(); this._switchCurrentTool();
this.overlay?.setUIState(state); this.overlay?.setUIState(state);
// Race or scroll. let highlight: HighlightModel | 'clear' | 'noop' = 'noop';
if (this._actionSelectorModel?.selector && !this._actionSelectorModel?.elements.length && !this._lastHighlightedAriaTemplateJSON) if (state.actionSelector !== this._lastHighlightedSelector) {
this._actionSelectorModel = null; this._lastHighlightedSelector = state.actionSelector;
const model = state.actionSelector ? querySelector(this.injectedScript, state.actionSelector, this.document) : null;
if (state.actionSelector && state.actionSelector !== this._actionSelectorModel?.selector) highlight = model?.elements.length ? model : 'clear';
this._actionSelectorModel = querySelector(this.injectedScript, state.actionSelector, this.document); }
const ariaTemplateJSON = JSON.stringify(state.ariaTemplate); const ariaTemplateJSON = JSON.stringify(state.ariaTemplate);
if (this._lastHighlightedAriaTemplateJSON !== ariaTemplateJSON) { if (this._lastHighlightedAriaTemplateJSON !== ariaTemplateJSON) {
@ -1142,16 +1145,15 @@ export class Recorder {
const template = state.ariaTemplate ? this.injectedScript.utils.parseYamlTemplate(state.ariaTemplate) : undefined; const template = state.ariaTemplate ? this.injectedScript.utils.parseYamlTemplate(state.ariaTemplate) : undefined;
const elements = template ? this.injectedScript.getAllByAria(this.document, template) : []; const elements = template ? this.injectedScript.getAllByAria(this.document, template) : [];
if (elements.length) if (elements.length)
this._actionSelectorModel = { elements }; highlight = { elements };
else else
this._actionSelectorModel = null; highlight = 'clear';
} }
if (!state.actionSelector && !state.ariaTemplate) if (highlight === 'clear')
this._actionSelectorModel = null; this.clearHighlight();
else if (highlight !== 'noop')
if (this.state.mode === 'none' || this.state.mode === 'standby') this.updateHighlight(highlight, false);
this.updateHighlight(this._actionSelectorModel, false);
} }
clearHighlight() { clearHighlight() {
@ -1266,6 +1268,8 @@ export class Recorder {
private _onScroll(event: Event) { private _onScroll(event: Event) {
if (!event.isTrusted) if (!event.isTrusted)
return; return;
this._lastHighlightedSelector = undefined;
this._lastHighlightedAriaTemplateJSON = 'undefined';
this.highlight.hideActionPoint(); this.highlight.hideActionPoint();
this._currentTool.onScroll?.(event); this._currentTool.onScroll?.(event);
} }

View file

@ -383,7 +383,11 @@ export function getAriaLabelledByElements(element: Element): Element[] | null {
const ref = element.getAttribute('aria-labelledby'); const ref = element.getAttribute('aria-labelledby');
if (ref === null) if (ref === null)
return null; return null;
return getIdRefs(element, ref); const refs = getIdRefs(element, ref);
// step 2b:
// "if the current node has an aria-labelledby attribute that contains at least one valid IDREF"
// Therefore, if none of the refs match an element, we consider aria-labelledby to be missing.
return refs.length ? refs : null;
} }
function allowsNameFromContent(role: string, targetDescendant: boolean) { function allowsNameFromContent(role: string, targetDescendant: boolean) {

View file

@ -43,12 +43,12 @@ export async function launchApp(browserType: BrowserType, options: {
} }
const context = await browserType.launchPersistentContext(serverSideCallMetadata(), '', { const context = await browserType.launchPersistentContext(serverSideCallMetadata(), '', {
channel: !options.persistentContextOptions?.executablePath ? findChromiumChannel(options.sdkLanguage) : undefined,
noDefaultViewport: true,
ignoreDefaultArgs: ['--enable-automation'], ignoreDefaultArgs: ['--enable-automation'],
colorScheme: 'no-override',
acceptDownloads: isUnderTest() ? 'accept' : 'internal-browser-default',
...options?.persistentContextOptions, ...options?.persistentContextOptions,
channel: options.persistentContextOptions?.channel ?? (!options.persistentContextOptions?.executablePath ? findChromiumChannel(options.sdkLanguage) : undefined),
noDefaultViewport: options.persistentContextOptions?.noDefaultViewport ?? true,
acceptDownloads: options?.persistentContextOptions?.acceptDownloads ?? (isUnderTest() ? 'accept' : 'internal-browser-default'),
colorScheme: options?.persistentContextOptions?.colorScheme ?? 'no-override',
args, args,
}); });
const [page] = context.pages(); const [page] = context.pages();

View file

@ -132,6 +132,10 @@ export class Recorder implements InstrumentationListener, IRecorder {
this._contextRecorder.clearScript(); this._contextRecorder.clearScript();
return; return;
} }
if (data.event === 'runTask') {
this._contextRecorder.runTask(data.params.task);
return;
}
}); });
await Promise.all([ await Promise.all([

View file

@ -0,0 +1,184 @@
/**
* 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 { WebSocketTransport } from '../transport';
import type { ConnectionTransport, ProtocolResponse } from '../transport';
export type ChatMessage = {
content: string;
user: 'user' | 'assistant';
};
export class Chat {
private _history: ChatMessage[] = [];
private _connectionPromise: Promise<Connection> | undefined;
private _chatSinks = new Map<string, (chunk: string) => void>();
private _wsEndpoint: string;
constructor(wsEndpoint: string) {
this._wsEndpoint = wsEndpoint;
}
clearHistory() {
this._history = [];
}
async post<T>(prompt: string): Promise<T | null> {
await this._append('user', prompt);
let text = await asString(await this._post());
if (text.startsWith('```json') && text.endsWith('```'))
text = text.substring('```json'.length, text.length - '```'.length);
for (let i = 0; i < 3; ++i) {
try {
return JSON.parse(text);
} catch (e) {
await this._append('user', String(e));
}
}
throw new Error('Failed to parse response: ' + text);
}
private async _append(user: ChatMessage['user'], content: string) {
this._history.push({ user, content });
}
private async _connection(): Promise<Connection> {
if (!this._connectionPromise) {
this._connectionPromise = WebSocketTransport.connect(undefined, this._wsEndpoint).then(transport => {
return new Connection(transport, (method, params) => this._dispatchEvent(method, params), () => {});
});
}
return this._connectionPromise;
}
private _dispatchEvent(method: string, params: any) {
if (method === 'chatChunk') {
const { chatId, chunk } = params;
const chunkSink = this._chatSinks.get(chatId)!;
chunkSink(chunk);
if (!chunk)
this._chatSinks.delete(chatId);
}
}
private async _post(): Promise<AsyncIterable<string>> {
const connection = await this._connection();
const result = await connection.send('chat', { history: this._history });
const { chatId } = result;
const { iterable, addChunk } = iterablePump();
this._chatSinks.set(chatId, addChunk);
return iterable;
}
}
export async function asString(stream: AsyncIterable<string>): Promise<string> {
let result = '';
for await (const chunk of stream)
result += chunk;
return result;
}
type ChunkIterator = {
iterable: AsyncIterable<string>;
addChunk: (chunk: string) => void;
};
function iterablePump(): ChunkIterator {
let controller: ReadableStreamDefaultController<string>;
const stream = new ReadableStream<string>({ start: c => controller = c });
const iterable = (async function* () {
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done)
break;
yield value!;
}
})();
return {
iterable,
addChunk: chunk => {
if (chunk)
controller.enqueue(chunk);
else
controller.close();
}
};
}
class Connection {
private readonly _transport: ConnectionTransport;
private _lastId = 0;
private _closed = false;
private _pending = new Map<number, { resolve: (result: any) => void; reject: (error: any) => void; }>();
private _onEvent: (method: string, params: any) => void;
private _onClose: () => void;
constructor(transport: ConnectionTransport, onEvent: (method: string, params: any) => void, onClose: () => void) {
this._transport = transport;
this._onEvent = onEvent;
this._onClose = onClose;
this._transport.onmessage = this._dispatchMessage.bind(this);
this._transport.onclose = this._close.bind(this);
}
send(method: string, params: any): Promise<any> {
const id = this._lastId++;
const message = { id, method, params };
this._transport.send(message);
return new Promise((resolve, reject) => {
this._pending.set(id, { resolve, reject });
});
}
private _dispatchMessage(message: ProtocolResponse) {
if (message.id === undefined) {
this._onEvent(message.method!, message.params);
return;
}
const callback = this._pending.get(message.id);
this._pending.delete(message.id);
if (!callback)
return;
if (message.error) {
callback.reject(new Error(message.error.message));
return;
}
callback.resolve(message.result);
}
_close() {
this._closed = true;
this._transport.onmessage = undefined;
this._transport.onclose = undefined;
for (const { reject } of this._pending.values())
reject(new Error('Connection closed'));
this._onClose();
}
isClosed() {
return this._closed;
}
close() {
if (!this._closed)
this._transport.close();
}
}

View file

@ -208,6 +208,10 @@ export class ContextRecorder extends EventEmitter {
} }
} }
runTask(task: string): void {
// TODO: implement
}
private _describeMainFrame(page: Page): actions.FrameDescription { private _describeMainFrame(page: Page): actions.FrameDescription {
return { return {
pageAlias: this._pageAliases.get(page)!, pageAlias: this._pageAliases.get(page)!,

View file

@ -88,7 +88,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
}); });
const mainFrame = this._page.mainFrame(); const mainFrame = this._page.mainFrame();
await mainFrame.goto(serverSideCallMetadata(), 'https://playwright/index.html'); await mainFrame.goto(serverSideCallMetadata(), process.env.PW_HMR ? 'http://localhost:44225' : 'https://playwright/index.html');
} }
static factory(context: BrowserContext): IRecorderAppFactory { static factory(context: BrowserContext): IRecorderAppFactory {

View file

@ -36,7 +36,7 @@ export async function downloadBrowserWithProgressBar(title: string, browserDirec
const zipPath = path.join(os.tmpdir(), downloadFileName); const zipPath = path.join(os.tmpdir(), downloadFileName);
try { try {
const retryCount = 3; const retryCount = 5;
for (let attempt = 1; attempt <= retryCount; ++attempt) { for (let attempt = 1; attempt <= retryCount; ++attempt) {
debugLogger.log('install', `downloading ${title} - attempt #${attempt}`); debugLogger.log('install', `downloading ${title} - attempt #${attempt}`);
const url = downloadURLs[(attempt - 1) % downloadURLs.length]; const url = downloadURLs[(attempt - 1) % downloadURLs.length];

View file

@ -37,16 +37,23 @@ const PACKAGE_PATH = path.join(__dirname, '..', '..', '..');
const BIN_PATH = path.join(__dirname, '..', '..', '..', 'bin'); const BIN_PATH = path.join(__dirname, '..', '..', '..', 'bin');
const PLAYWRIGHT_CDN_MIRRORS = [ const PLAYWRIGHT_CDN_MIRRORS = [
'https://playwright.azureedge.net/dbazure/download/playwright', // ESRP CDN
'https://playwright.download.prss.microsoft.com/dbazure/download/playwright', // Directly hit ESRP CDN
// Old endpoints which hit the Storage Bucket directly:
'https://playwright.azureedge.net', 'https://playwright.azureedge.net',
'https://playwright-akamai.azureedge.net', 'https://playwright-akamai.azureedge.net', // Actually Edgio which will be retired Q4 2025.
'https://playwright-verizon.azureedge.net', 'https://playwright-verizon.azureedge.net', // Actually Edgio which will be retired Q4 2025.
]; ];
if (process.env.PW_TEST_CDN_THAT_SHOULD_WORK) { if (process.env.PW_TEST_CDN_THAT_SHOULD_WORK) {
for (let i = 0; i < PLAYWRIGHT_CDN_MIRRORS.length; i++) { for (let i = 0; i < PLAYWRIGHT_CDN_MIRRORS.length; i++) {
const cdn = PLAYWRIGHT_CDN_MIRRORS[i]; const cdn = PLAYWRIGHT_CDN_MIRRORS[i];
if (cdn !== process.env.PW_TEST_CDN_THAT_SHOULD_WORK) if (cdn !== process.env.PW_TEST_CDN_THAT_SHOULD_WORK) {
PLAYWRIGHT_CDN_MIRRORS[i] = cdn + '.does-not-resolve.playwright.dev'; const parsedCDN = new URL(cdn);
parsedCDN.hostname = parsedCDN.hostname + '.does-not-resolve.playwright.dev';
PLAYWRIGHT_CDN_MIRRORS[i] = parsedCDN.toString();
}
} }
} }

View file

@ -125,11 +125,11 @@ export async function installRootRedirect(server: HttpServer, traceUrls: string[
for (const reporter of options.reporter || []) for (const reporter of options.reporter || [])
params.append('reporter', reporter); params.append('reporter', reporter);
params.set('server', server.urlPrefix('precise'));
let baseUrl = '.'; let baseUrl = '.';
if (process.env.PW_HMR) if (process.env.PW_HMR) {
baseUrl = 'http://localhost:44223'; // port is hardcoded in build.js baseUrl = 'http://localhost:44223'; // port is hardcoded in build.js
params.set('server', server.urlPrefix('precise') + '/trace/');
}
const urlPath = `${baseUrl}/trace/${options.webApp || 'index.html'}?${params.toString()}`; const urlPath = `${baseUrl}/trace/${options.webApp || 'index.html'}?${params.toString()}`;
server.routePath('/', (_, response) => { server.routePath('/', (_, response) => {

View file

@ -29,7 +29,8 @@ const debugLoggerColorMap = {
'channel': 33, // blue 'channel': 33, // blue
'server': 45, // cyan 'server': 45, // cyan
'server:channel': 34, // green 'server:channel': 34, // green
'server:metadata': 33, // blue 'server:metadata': 33, // blue,
'recorder': 45, // cyan
}; };
export type LogName = keyof typeof debugLoggerColorMap; export type LogName = keyof typeof debugLoggerColorMap;

View file

@ -112,7 +112,7 @@ export module Protocol {
- from 'checked' to 'selected': states which apply to widgets - from 'checked' to 'selected': states which apply to widgets
- from 'activedescendant' to 'owns' - relationships between elements other than parent/child/sibling. - from 'activedescendant' to 'owns' - relationships between elements other than parent/child/sibling.
*/ */
export type AXPropertyName = "busy"|"disabled"|"editable"|"focusable"|"focused"|"hidden"|"hiddenRoot"|"invalid"|"keyshortcuts"|"settable"|"roledescription"|"live"|"atomic"|"relevant"|"root"|"autocomplete"|"hasPopup"|"level"|"multiselectable"|"orientation"|"multiline"|"readonly"|"required"|"valuemin"|"valuemax"|"valuetext"|"checked"|"expanded"|"modal"|"pressed"|"selected"|"activedescendant"|"controls"|"describedby"|"details"|"errormessage"|"flowto"|"labelledby"|"owns"|"url"; export type AXPropertyName = "actions"|"busy"|"disabled"|"editable"|"focusable"|"focused"|"hidden"|"hiddenRoot"|"invalid"|"keyshortcuts"|"settable"|"roledescription"|"live"|"atomic"|"relevant"|"root"|"autocomplete"|"hasPopup"|"level"|"multiselectable"|"orientation"|"multiline"|"readonly"|"required"|"valuemin"|"valuemax"|"valuetext"|"checked"|"expanded"|"modal"|"pressed"|"selected"|"activedescendant"|"controls"|"describedby"|"details"|"errormessage"|"flowto"|"labelledby"|"owns"|"url";
/** /**
* A node in the accessibility tree. * A node in the accessibility tree.
*/ */
@ -694,7 +694,7 @@ percentage [0 - 100] for scroll driven animations
export interface AffectedFrame { export interface AffectedFrame {
frameId: Page.FrameId; frameId: Page.FrameId;
} }
export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout"; 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 CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"|"WarnDeprecationTrialMetadata"|"WarnThirdPartyCookieHeuristic";
export type CookieOperation = "SetCookie"|"ReadCookie"; export type CookieOperation = "SetCookie"|"ReadCookie";
/** /**
@ -2183,12 +2183,17 @@ The array enumerates @scope at-rules starting with the innermost one, going outw
* The array keeps the types of ancestor CSSRules from the innermost going outwards. * The array keeps the types of ancestor CSSRules from the innermost going outwards.
*/ */
ruleTypes?: CSSRuleType[]; ruleTypes?: CSSRuleType[];
/**
* @starting-style CSS at-rule array.
The array enumerates @starting-style at-rules starting with the innermost one, going outwards.
*/
startingStyles?: CSSStartingStyle[];
} }
/** /**
* Enum indicating the type of a CSS rule, used to represent the order of a style rule's ancestors. * Enum indicating the type of a CSS rule, used to represent the order of a style rule's ancestors.
This list only contains rule types that are collected during the ancestor rule collection. This list only contains rule types that are collected during the ancestor rule collection.
*/ */
export type CSSRuleType = "MediaRule"|"SupportsRule"|"ContainerRule"|"LayerRule"|"ScopeRule"|"StyleRule"; export type CSSRuleType = "MediaRule"|"SupportsRule"|"ContainerRule"|"LayerRule"|"ScopeRule"|"StyleRule"|"StartingStyleRule";
/** /**
* CSS coverage information. * CSS coverage information.
*/ */
@ -2424,6 +2429,10 @@ available).
* Optional logical axes queried for the container. * Optional logical axes queried for the container.
*/ */
logicalAxes?: DOM.LogicalAxes; logicalAxes?: DOM.LogicalAxes;
/**
* true if the query contains scroll-state() queries.
*/
queriesScrollState?: boolean;
} }
/** /**
* CSS Supports at-rule descriptor. * CSS Supports at-rule descriptor.
@ -2475,6 +2484,20 @@ available).
text: string; text: string;
/** /**
* The associated rule header range in the enclosing stylesheet (if * The associated rule header range in the enclosing stylesheet (if
available).
*/
range?: SourceRange;
/**
* Identifier of the stylesheet containing this object (if exists).
*/
styleSheetId?: StyleSheetId;
}
/**
* CSS Starting Style at-rule descriptor.
*/
export interface CSSStartingStyle {
/**
* The associated rule header range in the enclosing stylesheet (if
available). available).
*/ */
range?: SourceRange; range?: SourceRange;
@ -2779,6 +2802,12 @@ resized.) The current implementation considers only viewport-dependent media fea
*/ */
styleSheetId: StyleSheetId; styleSheetId: StyleSheetId;
} }
export type computedStyleUpdatedPayload = {
/**
* The node id that has updated computed styles.
*/
nodeId: DOM.NodeId;
}
/** /**
* Inserts a new rule with the given `ruleText` in a stylesheet with given `styleSheetId`, at the * Inserts a new rule with the given `ruleText` in a stylesheet with given `styleSheetId`, at the
@ -3039,6 +3068,19 @@ returns an array of locations of the CSS selector in the style sheet.
export type getLocationForSelectorReturnValue = { export type getLocationForSelectorReturnValue = {
ranges: SourceRange[]; ranges: SourceRange[];
} }
/**
* Starts tracking the given node for the computed style updates
and whenever the computed style is updated for node, it queues
a `computedStyleUpdated` event with throttling.
There can only be 1 node tracked for computed style updates
so passing a new node id removes tracking from the previous node.
Pass `undefined` to disable tracking.
*/
export type trackComputedStyleUpdatesForNodeParameters = {
nodeId?: DOM.NodeId;
}
export type trackComputedStyleUpdatesForNodeReturnValue = {
}
/** /**
* Starts tracking the given computed styles for updates. The specified array of properties * Starts tracking the given computed styles for updates. The specified array of properties
replaces the one previously specified. Pass empty array to disable tracking. replaces the one previously specified. Pass empty array to disable tracking.
@ -3561,7 +3603,7 @@ front-end.
/** /**
* Pseudo element type. * Pseudo element type.
*/ */
export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"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"|"select-fallback-button"|"select-fallback-button-text"|"picker"; 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";
/** /**
* Shadow root type. * Shadow root type.
*/ */
@ -4876,15 +4918,17 @@ $x functions).
} }
/** /**
* Returns the query container of the given node based on container query * Returns the query container of the given node based on container query
conditions: containerName, physical, and logical axes. If no axes are conditions: containerName, physical and logical axes, and whether it queries
provided, the style container is returned, which is the direct parent or the scroll-state. If no axes are provided and queriesScrollState is false, the
closest element with a matching container-name. style container is returned, which is the direct parent or the closest
element with a matching container-name.
*/ */
export type getContainerForNodeParameters = { export type getContainerForNodeParameters = {
nodeId: NodeId; nodeId: NodeId;
containerName?: string; containerName?: string;
physicalAxes?: PhysicalAxes; physicalAxes?: PhysicalAxes;
logicalAxes?: LogicalAxes; logicalAxes?: LogicalAxes;
queriesScrollState?: boolean;
} }
export type getContainerForNodeReturnValue = { export type getContainerForNodeReturnValue = {
/** /**
@ -8255,7 +8299,9 @@ file, data and other requests and responses, their headers, bodies, timing, etc.
*/ */
export type LoaderId = string; export type LoaderId = string;
/** /**
* Unique request identifier. * Unique network request identifier.
Note that this does not identify individual HTTP requests that are part of
a network request.
*/ */
export type RequestId = string; export type RequestId = string;
/** /**
@ -8830,6 +8876,7 @@ If the opcode isn't 1, then payloadData is a base64 encoded string representing
type: "parser"|"script"|"preload"|"SignedExchange"|"preflight"|"other"; type: "parser"|"script"|"preload"|"SignedExchange"|"preflight"|"other";
/** /**
* Initiator JavaScript stack trace, set for Script only. * Initiator JavaScript stack trace, set for Script only.
Requires the Debugger domain to be enabled.
*/ */
stack?: Runtime.StackTrace; stack?: Runtime.StackTrace;
/** /**
@ -8944,7 +8991,7 @@ This is a temporary ability and it will be removed in the future.
/** /**
* Types of reasons why a cookie may not be sent with a request. * Types of reasons why a cookie may not be sent with a request.
*/ */
export type CookieBlockedReason = "SecureOnly"|"NotOnPath"|"DomainMismatch"|"SameSiteStrict"|"SameSiteLax"|"SameSiteUnspecifiedTreatedAsLax"|"SameSiteNoneInsecure"|"UserPreferences"|"ThirdPartyPhaseout"|"ThirdPartyBlockedInFirstPartySet"|"UnknownError"|"SchemefulSameSiteStrict"|"SchemefulSameSiteLax"|"SchemefulSameSiteUnspecifiedTreatedAsLax"|"SamePartyFromCrossPartyContext"|"NameValuePairExceedsMaxSize"; export type CookieBlockedReason = "SecureOnly"|"NotOnPath"|"DomainMismatch"|"SameSiteStrict"|"SameSiteLax"|"SameSiteUnspecifiedTreatedAsLax"|"SameSiteNoneInsecure"|"UserPreferences"|"ThirdPartyPhaseout"|"ThirdPartyBlockedInFirstPartySet"|"UnknownError"|"SchemefulSameSiteStrict"|"SchemefulSameSiteLax"|"SchemefulSameSiteUnspecifiedTreatedAsLax"|"SamePartyFromCrossPartyContext"|"NameValuePairExceedsMaxSize"|"PortMismatch"|"SchemeMismatch";
/** /**
* Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request. * Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request.
*/ */
@ -11498,7 +11545,7 @@ as an ad.
* All Permissions Policy features. This enum should match the one defined * All Permissions Policy features. This enum should match the one defined
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5. 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"|"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"|"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. * Reason for a permissions policy feature to be disabled.
*/ */
@ -12384,7 +12431,8 @@ the page execution. Execution can be resumed via calling Page.handleJavaScriptDi
defaultPrompt?: string; defaultPrompt?: string;
} }
/** /**
* Fired for top level page lifecycle events such as navigation, load, paint, etc. * Fired for lifecycle events (navigation, load, paint, etc) in the current
target (including local frames).
*/ */
export type lifecycleEventPayload = { export type lifecycleEventPayload = {
/** /**
@ -14339,6 +14387,7 @@ int
destinationLimitPriority: SignedInt64AsBase10; destinationLimitPriority: SignedInt64AsBase10;
aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig; aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig;
scopesData?: AttributionScopesData; scopesData?: AttributionScopesData;
maxEventLevelReports: number;
} }
export type AttributionReportingSourceRegistrationResult = "success"|"internalError"|"insufficientSourceCapacity"|"insufficientUniqueDestinationCapacity"|"excessiveReportingOrigins"|"prohibitedByBrowserPolicy"|"successNoised"|"destinationReportingLimitReached"|"destinationGlobalLimitReached"|"destinationBothLimitsReached"|"reportingOriginsPerSiteLimitReached"|"exceedsMaxChannelCapacity"|"exceedsMaxScopesChannelCapacity"|"exceedsMaxTriggerStateCardinality"|"exceedsMaxEventStatesLimit"|"destinationPerDayReportingLimitReached"; export type AttributionReportingSourceRegistrationResult = "success"|"internalError"|"insufficientSourceCapacity"|"insufficientUniqueDestinationCapacity"|"excessiveReportingOrigins"|"prohibitedByBrowserPolicy"|"successNoised"|"destinationReportingLimitReached"|"destinationGlobalLimitReached"|"destinationBothLimitsReached"|"reportingOriginsPerSiteLimitReached"|"exceedsMaxChannelCapacity"|"exceedsMaxScopesChannelCapacity"|"exceedsMaxTriggerStateCardinality"|"exceedsMaxEventStatesLimit"|"destinationPerDayReportingLimitReached";
export type AttributionReportingSourceRegistrationTimeConfig = "include"|"exclude"; export type AttributionReportingSourceRegistrationTimeConfig = "include"|"exclude";
@ -14386,7 +14435,7 @@ int
scopes: string[]; scopes: string[];
} }
export type AttributionReportingEventLevelResult = "success"|"successDroppedLowerPriority"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"deduplicated"|"excessiveAttributions"|"priorityTooLow"|"neverAttributedSource"|"excessiveReportingOrigins"|"noMatchingSourceFilterData"|"prohibitedByBrowserPolicy"|"noMatchingConfigurations"|"excessiveReports"|"falselyAttributedSource"|"reportWindowPassed"|"notRegistered"|"reportWindowNotStarted"|"noMatchingTriggerData"; export type AttributionReportingEventLevelResult = "success"|"successDroppedLowerPriority"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"deduplicated"|"excessiveAttributions"|"priorityTooLow"|"neverAttributedSource"|"excessiveReportingOrigins"|"noMatchingSourceFilterData"|"prohibitedByBrowserPolicy"|"noMatchingConfigurations"|"excessiveReports"|"falselyAttributedSource"|"reportWindowPassed"|"notRegistered"|"reportWindowNotStarted"|"noMatchingTriggerData";
export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports"; export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"insufficientNamedBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports";
/** /**
* A single Related Website Set object. * A single Related Website Set object.
*/ */
@ -15920,6 +15969,8 @@ are ignored.
export module Fetch { export module Fetch {
/** /**
* Unique request identifier. * Unique request identifier.
Note that this does not identify individual HTTP requests that are part of
a network request.
*/ */
export type RequestId = string; export type RequestId = string;
/** /**
@ -16302,7 +16353,7 @@ https://webaudio.github.io/web-audio-api/
/** /**
* Enum of AudioContextState from the spec * Enum of AudioContextState from the spec
*/ */
export type ContextState = "suspended"|"running"|"closed"; export type ContextState = "suspended"|"running"|"closed"|"interrupted";
/** /**
* Enum of AudioNode types * Enum of AudioNode types
*/ */
@ -20213,6 +20264,7 @@ Error was thrown.
"CSS.styleSheetAdded": CSS.styleSheetAddedPayload; "CSS.styleSheetAdded": CSS.styleSheetAddedPayload;
"CSS.styleSheetChanged": CSS.styleSheetChangedPayload; "CSS.styleSheetChanged": CSS.styleSheetChangedPayload;
"CSS.styleSheetRemoved": CSS.styleSheetRemovedPayload; "CSS.styleSheetRemoved": CSS.styleSheetRemovedPayload;
"CSS.computedStyleUpdated": CSS.computedStyleUpdatedPayload;
"Cast.sinksUpdated": Cast.sinksUpdatedPayload; "Cast.sinksUpdated": Cast.sinksUpdatedPayload;
"Cast.issueUpdated": Cast.issueUpdatedPayload; "Cast.issueUpdated": Cast.issueUpdatedPayload;
"DOM.attributeModified": DOM.attributeModifiedPayload; "DOM.attributeModified": DOM.attributeModifiedPayload;
@ -20464,6 +20516,7 @@ Error was thrown.
"CSS.getStyleSheetText": CSS.getStyleSheetTextParameters; "CSS.getStyleSheetText": CSS.getStyleSheetTextParameters;
"CSS.getLayersForNode": CSS.getLayersForNodeParameters; "CSS.getLayersForNode": CSS.getLayersForNodeParameters;
"CSS.getLocationForSelector": CSS.getLocationForSelectorParameters; "CSS.getLocationForSelector": CSS.getLocationForSelectorParameters;
"CSS.trackComputedStyleUpdatesForNode": CSS.trackComputedStyleUpdatesForNodeParameters;
"CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesParameters; "CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesParameters;
"CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesParameters; "CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesParameters;
"CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeParameters; "CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeParameters;
@ -21075,6 +21128,7 @@ Error was thrown.
"CSS.getStyleSheetText": CSS.getStyleSheetTextReturnValue; "CSS.getStyleSheetText": CSS.getStyleSheetTextReturnValue;
"CSS.getLayersForNode": CSS.getLayersForNodeReturnValue; "CSS.getLayersForNode": CSS.getLayersForNodeReturnValue;
"CSS.getLocationForSelector": CSS.getLocationForSelectorReturnValue; "CSS.getLocationForSelector": CSS.getLocationForSelectorReturnValue;
"CSS.trackComputedStyleUpdatesForNode": CSS.trackComputedStyleUpdatesForNodeReturnValue;
"CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesReturnValue; "CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesReturnValue;
"CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesReturnValue; "CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesReturnValue;
"CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeReturnValue; "CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeReturnValue;

View file

@ -618,9 +618,10 @@ class ArtifactsRecorder {
if (captureScreenshots) if (captureScreenshots)
await this._screenshotOnTestFailure(); await this._screenshotOnTestFailure();
const leftoverContexts: BrowserContext[] = []; let leftoverContexts: BrowserContext[] = [];
for (const browserType of [this._playwright.chromium, this._playwright.firefox, this._playwright.webkit]) for (const browserType of [this._playwright.chromium, this._playwright.firefox, this._playwright.webkit])
leftoverContexts.push(...(browserType as any)._contexts); leftoverContexts.push(...(browserType as any)._contexts);
leftoverContexts = leftoverContexts.filter(context => !this._reusedContexts.has(context));
const leftoverApiRequests: APIRequestContext[] = Array.from((this._playwright.request as any)._contexts as Set<APIRequestContext>); const leftoverApiRequests: APIRequestContext[] = Array.from((this._playwright.request as any)._contexts as Set<APIRequestContext>);
// Collect traces/screenshots for remaining contexts. // Collect traces/screenshots for remaining contexts.

View file

@ -181,7 +181,7 @@ export function toHaveAccessibleDescription(
options?: { timeout?: number, ignoreCase?: boolean }, options?: { timeout?: number, ignoreCase?: boolean },
) { ) {
return toMatchText.call(this, 'toHaveAccessibleDescription', locator, 'Locator', async (isNot, timeout) => { return toMatchText.call(this, 'toHaveAccessibleDescription', locator, 'Locator', async (isNot, timeout) => {
const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true });
return await locator._expect('to.have.accessible.description', { expectedText, isNot, timeout }); return await locator._expect('to.have.accessible.description', { expectedText, isNot, timeout });
}, expected, options); }, expected, options);
} }
@ -189,14 +189,21 @@ export function toHaveAccessibleDescription(
export function toHaveAccessibleName( export function toHaveAccessibleName(
this: ExpectMatcherState, this: ExpectMatcherState,
locator: LocatorEx, locator: LocatorEx,
expected: string | RegExp, expected: string | RegExp | (string | RegExp)[],
options?: { timeout?: number, ignoreCase?: boolean }, options: { timeout?: number, ignoreCase?: boolean, normalizeWhiteSpace?: 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) => { return toMatchText.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => {
const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true });
return await locator._expect('to.have.accessible.name', { expectedText, isNot, timeout }); return await locator._expect('to.have.accessible.name', { expectedText, isNot, timeout });
}, expected, options); }, expected, options);
} }
}
export function toHaveAttribute( export function toHaveAttribute(
this: ExpectMatcherState, this: ExpectMatcherState,

View file

@ -126,7 +126,6 @@ function unshift(snapshot: string): string {
const match = line.match(/^(\s*)/); const match = line.match(/^(\s*)/);
if (match && match[1].length < whitespacePrefixLength) if (match && match[1].length < whitespacePrefixLength)
whitespacePrefixLength = match[1].length; whitespacePrefixLength = match[1].length;
break;
} }
return lines.filter(t => t.trim()).map(line => line.substring(whitespacePrefixLength)).join('\n'); return lines.filter(t => t.trim()).map(line => line.substring(whitespacePrefixLength)).join('\n');
} }

View file

@ -83,6 +83,10 @@ export async function applySuggestedRebaselines(config: FullConfigInternal, repo
const indent = lines[matcher.loc!.start.line - 1].match(/^\s*/)![0]; const indent = lines[matcher.loc!.start.line - 1].match(/^\s*/)![0];
const newText = replacement.code.replace(/\{indent\}/g, indent); const newText = replacement.code.replace(/\{indent\}/g, indent);
ranges.push({ start: matcher.start!, end: node.end!, oldText: source.substring(matcher.start!, node.end!), newText }); ranges.push({ start: matcher.start!, end: node.end!, oldText: source.substring(matcher.start!, node.end!), newText });
// We can have multiple, hopefully equal, replacements for the same location,
// for example when a single test runs multiple times because of projects or retries.
// Do not apply multiple replacements for the same assertion.
break;
} }
} }
}); });

View file

@ -7899,7 +7899,7 @@ interface LocatorAssertions {
* @param name Expected accessible name. * @param name Expected accessible name.
* @param options * @param options
*/ */
toHaveAccessibleName(name: string|RegExp, options?: { toHaveAccessibleName(name: string|RegExp|ReadonlyArray<string|RegExp>, options?: {
/** /**
* Whether to perform case-insensitive match. * Whether to perform case-insensitive match.
* [`ignoreCase`](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-have-accessible-name-option-ignore-case) * [`ignoreCase`](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-have-accessible-name-option-ignore-case)

View file

@ -27,7 +27,7 @@ import { CallLogView } from './callLog';
import './recorder.css'; import './recorder.css';
import { asLocator } from '@isomorphic/locatorGenerators'; import { asLocator } from '@isomorphic/locatorGenerators';
import { toggleTheme } from '@web/theme'; import { toggleTheme } from '@web/theme';
import { copy } from '@web/uiUtils'; import { copy, useSetting } from '@web/uiUtils';
import yaml from 'yaml'; import yaml from 'yaml';
import { parseAriaKey } from '@isomorphic/ariaSnapshot'; import { parseAriaKey } from '@isomorphic/ariaSnapshot';
import type { AriaKeyError, ParsedYaml } from '@isomorphic/ariaSnapshot'; import type { AriaKeyError, ParsedYaml } from '@isomorphic/ariaSnapshot';
@ -47,7 +47,7 @@ export const Recorder: React.FC<RecorderProps> = ({
}) => { }) => {
const [selectedFileId, setSelectedFileId] = React.useState<string | undefined>(); const [selectedFileId, setSelectedFileId] = React.useState<string | undefined>();
const [runningFileId, setRunningFileId] = React.useState<string | undefined>(); const [runningFileId, setRunningFileId] = React.useState<string | undefined>();
const [selectedTab, setSelectedTab] = React.useState<string>('log'); const [selectedTab, setSelectedTab] = useSetting<string>('recorderPropertiesTab', 'log');
const [ariaSnapshot, setAriaSnapshot] = React.useState<string | undefined>(); const [ariaSnapshot, setAriaSnapshot] = React.useState<string | undefined>();
const [ariaSnapshotErrors, setAriaSnapshotErrors] = React.useState<SourceHighlight[]>(); const [ariaSnapshotErrors, setAriaSnapshotErrors] = React.useState<SourceHighlight[]>();
@ -67,6 +67,7 @@ export const Recorder: React.FC<RecorderProps> = ({
const language = source.language; const language = source.language;
setLocator(asLocator(language, elementInfo.selector)); setLocator(asLocator(language, elementInfo.selector));
setAriaSnapshot(elementInfo.ariaSnapshot); setAriaSnapshot(elementInfo.ariaSnapshot);
setAriaSnapshotErrors([]);
if (userGesture && selectedTab !== 'locator' && selectedTab !== 'aria') if (userGesture && selectedTab !== 'locator' && selectedTab !== 'aria')
setSelectedTab('locator'); setSelectedTab('locator');
@ -122,9 +123,6 @@ export const Recorder: React.FC<RecorderProps> = ({
if (!errors.length) if (!errors.length)
window.dispatch({ event: 'highlightRequested', params: { ariaTemplate: fragment } }); window.dispatch({ event: 'highlightRequested', params: { ariaTemplate: fragment } });
}, [mode]); }, [mode]);
const isRecording = mode === 'recording' || mode === 'recording-inspecting';
const locatorPlaceholder = isRecording ? '// Unavailable while recording' : (locator ? undefined : '// Pick element or type locator');
const ariaPlaceholder = isRecording ? '# Unavailable while recording' : (ariaSnapshot ? undefined : '# Pick element or type snapshot');
return <div className='recorder'> return <div className='recorder'>
<Toolbar> <Toolbar>
@ -191,7 +189,7 @@ export const Recorder: React.FC<RecorderProps> = ({
{ {
id: 'locator', id: 'locator',
title: 'Locator', title: 'Locator',
render: () => <CodeMirrorWrapper text={locatorPlaceholder || locator} language={source.language} readOnly={isRecording} focusOnChange={true} onChange={onEditorChange} wrapLines={true} /> render: () => <CodeMirrorWrapper text={locator} placeholder='Type locator to inspect' language={source.language} focusOnChange={true} onChange={onEditorChange} wrapLines={true} />
}, },
{ {
id: 'log', id: 'log',
@ -200,8 +198,8 @@ export const Recorder: React.FC<RecorderProps> = ({
}, },
{ {
id: 'aria', id: 'aria',
title: 'Aria snapshot', title: 'Aria',
render: () => <CodeMirrorWrapper text={ariaPlaceholder || ariaSnapshot || ''} language={'yaml'} readOnly={isRecording} onChange={onAriaEditorChange} highlight={ariaSnapshotErrors} wrapLines={true} /> render: () => <CodeMirrorWrapper text={ariaSnapshot || ''} placeholder='Type aria template to match' language={'yaml'} onChange={onAriaEditorChange} highlight={ariaSnapshotErrors} wrapLines={true} />
}, },
]} ]}
selectedTab={selectedTab} selectedTab={selectedTab}

View file

@ -1,27 +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">
<title>Playwright Trace Viewer for VS Code</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/embedded.tsx"></script>
</body>
</html>

View file

@ -1,60 +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 { EmbeddedWorkbenchLoader } from './ui/embeddedWorkbenchLoader';
(async () => {
applyTheme();
// workaround to send keystrokes back to vscode webview to keep triggering key bindings there
const handleKeyEvent = (e: KeyboardEvent) => {
if (!e.isTrusted)
return;
window.parent?.postMessage({
type: e.type,
key: e.key,
keyCode: e.keyCode,
code: e.code,
shiftKey: e.shiftKey,
altKey: e.altKey,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
repeat: e.repeat,
}, '*');
};
window.addEventListener('keydown', handleKeyEvent);
window.addEventListener('keyup', handleKeyEvent);
if (window.location.protocol !== 'file:') {
if (!navigator.serviceWorker)
throw new Error(`Service workers are not supported.\nMake sure to serve the Trace Viewer (${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(<EmbeddedWorkbenchLoader />);
})();

View file

@ -43,12 +43,9 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, client:
const clientId = client?.id ?? ''; const clientId = client?.id ?? '';
let data = clientIdToTraceUrls.get(clientId); let data = clientIdToTraceUrls.get(clientId);
if (!data) { if (!data) {
let traceViewerServerBaseUrl = self.registration.scope; let traceViewerServerBaseUrl = new URL('../', client?.url ?? self.registration.scope);
if (client?.url) { if (traceViewerServerBaseUrl.searchParams.has('server'))
const clientUrl = new URL(client.url); traceViewerServerBaseUrl = new URL(traceViewerServerBaseUrl.searchParams.get('server')!, traceViewerServerBaseUrl);
if (clientUrl.searchParams.has('server'))
traceViewerServerBaseUrl = clientUrl.searchParams.get('server')!;
}
data = { limit, traceUrls: new Set(), traceViewerServer: new TraceViewerServer(traceViewerServerBaseUrl) }; data = { limit, traceUrls: new Set(), traceViewerServer: new TraceViewerServer(traceViewerServerBaseUrl) };
clientIdToTraceUrls.set(clientId, data); clientIdToTraceUrls.set(clientId, data);

View file

@ -146,7 +146,7 @@ function formatUrl(trace: string, server: TraceViewerServer) {
} }
export class TraceViewerServer { export class TraceViewerServer {
constructor(private readonly baseUrl: string) {} constructor(private readonly baseUrl: URL) {}
getFileURL(path: string): URL { getFileURL(path: string): URL {
const url = new URL('trace/file', this.baseUrl); const url = new URL('trace/file', this.baseUrl);

View file

@ -1,68 +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.
*/
.empty-state {
display: flex;
align-items: center;
justify-content: center;
flex: auto;
flex-direction: column;
background-color: var(--vscode-editor-background);
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 100;
line-height: 24px;
}
body .empty-state {
background: rgba(255, 255, 255, 0.8);
}
body.dark-mode .empty-state {
background: rgba(0, 0, 0, 0.8);
}
.empty-state .title {
font-size: 24px;
font-weight: bold;
margin-bottom: 30px;
}
.progress {
flex: none;
width: 100%;
height: 3px;
z-index: 10;
}
.inner-progress {
background-color: var(--vscode-progressBar-background);
height: 100%;
}
.workbench-loader {
contain: size;
}
/* Limit to a reasonable minimum viewport */
html, body {
min-width: 550px;
min-height: 450px;
overflow: auto;
}

View file

@ -1,102 +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 * as React from 'react';
import type { ContextEntry } from '../types/entries';
import { MultiTraceModel } from './modelUtil';
import './embeddedWorkbenchLoader.css';
import { Workbench } from './workbench';
import { currentTheme, toggleTheme } from '@web/theme';
import type { SourceLocation } from './modelUtil';
function openPage(url: string, target?: string) {
if (url)
window.parent!.postMessage({ method: 'openExternal', params: { url, target } }, '*');
}
function openSourceLocation({ file, line, column }: SourceLocation) {
window.parent!.postMessage({ method: 'openSourceLocation', params: { file, line, column } }, '*');
}
export const EmbeddedWorkbenchLoader: React.FunctionComponent = () => {
const [traceURLs, setTraceURLs] = React.useState<string[]>([]);
const [model, setModel] = React.useState<MultiTraceModel>(emptyModel);
const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 });
const [processingErrorMessage, setProcessingErrorMessage] = React.useState<string | null>(null);
React.useEffect(() => {
window.addEventListener('message', async ({ data: { method, params } }) => {
if (method === 'loadTraceRequested') {
setTraceURLs(params.traceUrl ? [params.traceUrl] : []);
setProcessingErrorMessage(null);
} else if (method === 'applyTheme') {
if (currentTheme() !== params.theme)
toggleTheme();
}
});
// notify vscode that it is now listening to its messages
window.parent!.postMessage({ type: 'loaded' }, '*');
}, []);
React.useEffect(() => {
(async () => {
if (traceURLs.length) {
const swListener = (event: any) => {
if (event.data.method === 'progress')
setProgress(event.data.params);
};
navigator.serviceWorker.addEventListener('message', swListener);
setProgress({ done: 0, total: 1 });
const contextEntries: ContextEntry[] = [];
for (let i = 0; i < traceURLs.length; i++) {
const url = traceURLs[i];
const params = new URLSearchParams();
params.set('trace', url);
params.set('limit', String(traceURLs.length));
const response = await fetch(`contexts?${params.toString()}`);
if (!response.ok) {
setProcessingErrorMessage((await response.json()).error);
return;
}
contextEntries.push(...(await response.json()));
}
navigator.serviceWorker.removeEventListener('message', swListener);
const model = new MultiTraceModel(contextEntries);
setProgress({ done: 0, total: 0 });
setModel(model);
} else {
setModel(emptyModel);
}
})();
}, [traceURLs]);
React.useEffect(() => {
if (processingErrorMessage)
window.parent?.postMessage({ method: 'showErrorMessage', params: { message: processingErrorMessage } }, '*');
}, [processingErrorMessage]);
return <div className='vbox workbench-loader'>
<div className='progress'>
<div className='inner-progress' style={{ width: progress.total ? (100 * progress.done / progress.total) + '%' : 0 }}></div>
</div>
<Workbench model={model} openPage={openPage} onOpenExternally={openSourceLocation} />
{!traceURLs.length && <div className='empty-state'>
<div className='title'>Select test to see the trace</div>
</div>}
</div>;
};
export const emptyModel = new MultiTraceModel([]);

View file

@ -30,7 +30,7 @@ export const SettingsView: React.FunctionComponent<{
{settings.map(({ value, set, title }) => { {settings.map(({ value, set, title }) => {
return <div key={title} className='setting'> return <div key={title} className='setting'>
<label> <label>
<input type='checkbox' checked={value} onClick={() => set(!value)}/> <input type='checkbox' checked={value} onChange={() => set(!value)}/>
{title} {title}
</label> </label>
</div>; </div>;

View file

@ -40,8 +40,7 @@ export const SnapshotTabsView: React.FunctionComponent<{
setIsInspecting: (isInspecting: boolean) => void, setIsInspecting: (isInspecting: boolean) => void,
highlightedLocator: string, highlightedLocator: string,
setHighlightedLocator: (locator: string) => void, setHighlightedLocator: (locator: string) => void,
openPage?: (url: string, target?: string) => Window | any, }> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator }) => {
}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator, openPage }) => {
const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action'); const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action');
const snapshots = React.useMemo(() => { const snapshots = React.useMemo(() => {
@ -66,9 +65,7 @@ export const SnapshotTabsView: React.FunctionComponent<{
})} })}
<div style={{ flex: 'auto' }}></div> <div style={{ flex: 'auto' }}></div>
<ToolbarButton icon='link-external' title='Open snapshot in a new tab' disabled={!snapshotUrls?.popoutUrl} onClick={() => { <ToolbarButton icon='link-external' title='Open snapshot in a new tab' disabled={!snapshotUrls?.popoutUrl} onClick={() => {
if (!openPage) const win = window.open(snapshotUrls?.popoutUrl || '', '_blank');
openPage = window.open;
const win = openPage(snapshotUrls?.popoutUrl || '', '_blank');
win?.addEventListener('DOMContentLoaded', () => { win?.addEventListener('DOMContentLoaded', () => {
const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []); const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []);
new ConsoleAPI(injectedScript); new ConsoleAPI(injectedScript);

View file

@ -62,7 +62,7 @@ export const FiltersView: React.FC<{
{[...statusFilters.entries()].map(([status, value]) => { {[...statusFilters.entries()].map(([status, value]) => {
return <div className='filter-entry' key={status} role='listitem'> return <div className='filter-entry' key={status} role='listitem'>
<label> <label>
<input type='checkbox' checked={value} onClick={() => { <input type='checkbox' checked={value} onChange={() => {
const copy = new Map(statusFilters); const copy = new Map(statusFilters);
copy.set(status, !copy.get(status)); copy.set(status, !copy.get(status));
setStatusFilters(copy); setStatusFilters(copy);
@ -76,7 +76,7 @@ export const FiltersView: React.FC<{
{[...projectFilters.entries()].map(([projectName, value]) => { {[...projectFilters.entries()].map(([projectName, value]) => {
return <div className='filter-entry' key={projectName} role='listitem'> return <div className='filter-entry' key={projectName} role='listitem'>
<label> <label>
<input type='checkbox' checked={value} onClick={() => { <input type='checkbox' checked={value} onChange={() => {
const copy = new Map(projectFilters); const copy = new Map(projectFilters);
copy.set(projectName, !copy.get(projectName)); copy.set(projectName, !copy.get(projectName));
setProjectFilters(copy); setProjectFilters(copy);

View file

@ -99,11 +99,17 @@ export const TestListView: React.FC<{
setSelectedTreeItemId(selectedTreeItem.id); setSelectedTreeItemId(selectedTreeItem.id);
}, [runningState, setSelectedTreeItemId, testTree, collapseAllCount, setCollapseAllCount, requestedCollapseAllCount, expandAllCount, setExpandAllCount, requestedExpandAllCount, treeState, setTreeState]); }, [runningState, setSelectedTreeItemId, testTree, collapseAllCount, setCollapseAllCount, requestedCollapseAllCount, expandAllCount, setExpandAllCount, requestedExpandAllCount, treeState, setTreeState]);
// Compute selected item. // Compute selected item
const { selectedTreeItem } = React.useMemo(() => { const selectedTreeItem = React.useMemo(() => {
if (!selectedTreeItemId)
return undefined;
return testTree.treeItemById(selectedTreeItemId);
}, [selectedTreeItemId, testTree]);
// Handle selection effects separately
React.useEffect(() => {
if (!testModel) if (!testModel)
return { selectedTreeItem: undefined }; return;
const selectedTreeItem = selectedTreeItemId ? testTree.treeItemById(selectedTreeItemId) : undefined;
const testFile = itemLocation(selectedTreeItem, testModel); const testFile = itemLocation(selectedTreeItem, testModel);
let selectedTest: reporterTypes.TestCase | undefined; let selectedTest: reporterTypes.TestCase | undefined;
if (selectedTreeItem?.kind === 'test') if (selectedTreeItem?.kind === 'test')
@ -111,8 +117,7 @@ export const TestListView: React.FC<{
else if (selectedTreeItem?.kind === 'case' && selectedTreeItem.tests.length === 1) else if (selectedTreeItem?.kind === 'case' && selectedTreeItem.tests.length === 1)
selectedTest = selectedTreeItem.tests[0]; selectedTest = selectedTreeItem.tests[0];
onItemSelected({ treeItem: selectedTreeItem, testCase: selectedTest, testFile }); onItemSelected({ treeItem: selectedTreeItem, testCase: selectedTest, testFile });
return { selectedTreeItem }; }, [testModel, selectedTreeItem, onItemSelected]);
}, [onItemSelected, selectedTreeItemId, testModel, testTree]);
// Update watch all. // Update watch all.
React.useEffect(() => { React.useEffect(() => {

View file

@ -47,8 +47,10 @@ const xtermDataSource: XtermDataSource = {
}; };
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
const guid = searchParams.get('ws'); let testServerBaseUrl = new URL('../', window.location.href);
const wsURL = new URL(`/${guid}`, searchParams.get('server') ?? window.location.toString()); if (testServerBaseUrl.searchParams.has('server'))
testServerBaseUrl = new URL(testServerBaseUrl.searchParams.get('server')!, testServerBaseUrl);
const wsURL = new URL(searchParams.get('ws')!, testServerBaseUrl);
wsURL.protocol = (wsURL.protocol === 'https:' ? 'wss:' : 'ws:'); wsURL.protocol = (wsURL.protocol === 'https:' ? 'wss:' : 'ws:');
const queryParams = { const queryParams = {
args: searchParams.getAll('arg'), args: searchParams.getAll('arg'),
@ -109,7 +111,10 @@ export const UIModeView: React.FC<{}> = ({
const inputRef = React.useRef<HTMLInputElement>(null); const inputRef = React.useRef<HTMLInputElement>(null);
const reloadTests = React.useCallback(() => { const reloadTests = React.useCallback(() => {
setTestServerConnection(new TestServerConnection(new WebSocketTestServerTransport(wsURL))); setTestServerConnection(prevConnection => {
prevConnection?.close();
return new TestServerConnection(new WebSocketTestServerTransport(wsURL));
});
}, []); }, []);
// Load tests on startup. // Load tests on startup.
@ -224,7 +229,7 @@ export const UIModeView: React.FC<{}> = ({
newFilter.set(projectSuite.title, !!selectedProjects?.includes(projectSuite.title)); newFilter.set(projectSuite.title, !!selectedProjects?.includes(projectSuite.title));
} }
if (!selectedProjects && newFilter.size && ![...newFilter.values()].includes(true)) if (!selectedProjects && newFilter.size && ![...newFilter.values()].includes(true))
newFilter.set(newFilter.entries().next().value[0], true); newFilter.set(newFilter.entries().next().value![0], true);
if (projectFilters.size !== newFilter.size || [...projectFilters].some(([k, v]) => newFilter.get(k) !== v)) if (projectFilters.size !== newFilter.size || [...projectFilters].some(([k, v]) => newFilter.get(k) !== v))
setProjectFilters(newFilter); setProjectFilters(newFilter);
}, [projectFilters, testModel]); }, [projectFilters, testModel]);

View file

@ -53,10 +53,9 @@ export const Workbench: React.FunctionComponent<{
status?: UITestStatus, status?: UITestStatus,
annotations?: { type: string; description?: string; }[]; annotations?: { type: string; description?: string; }[];
inert?: boolean, inert?: boolean,
openPage?: (url: string, target?: string) => Window | any,
onOpenExternally?: (location: modelUtil.SourceLocation) => void, onOpenExternally?: (location: modelUtil.SourceLocation) => void,
revealSource?: boolean, revealSource?: boolean,
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, openPage, onOpenExternally, revealSource }) => { }> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, onOpenExternally, revealSource }) => {
const [selectedCallId, setSelectedCallId] = React.useState<string | undefined>(undefined); const [selectedCallId, setSelectedCallId] = React.useState<string | undefined>(undefined);
const [revealedError, setRevealedError] = React.useState<ErrorDescription | undefined>(undefined); const [revealedError, setRevealedError] = React.useState<ErrorDescription | undefined>(undefined);
const [revealedAttachment, setRevealedAttachment] = React.useState<AfterActionTraceEventAttachment | undefined>(undefined); const [revealedAttachment, setRevealedAttachment] = React.useState<AfterActionTraceEventAttachment | undefined>(undefined);
@ -344,8 +343,7 @@ export const Workbench: React.FunctionComponent<{
isInspecting={isInspecting} isInspecting={isInspecting}
setIsInspecting={setIsInspecting} setIsInspecting={setIsInspecting}
highlightedLocator={highlightedLocator} highlightedLocator={highlightedLocator}
setHighlightedLocator={locatorPicked} setHighlightedLocator={locatorPicked} />}
openPage={openPage} />}
sidebar={ sidebar={
<TabbedPane <TabbedPane
tabs={[actionsTab, metadataTab]} tabs={[actionsTab, metadataTab]}

View file

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

View file

@ -24,6 +24,7 @@ import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/python/python'; import 'codemirror/mode/python/python';
import 'codemirror/mode/clike/clike'; import 'codemirror/mode/clike/clike';
import 'codemirror/mode/markdown/markdown'; import 'codemirror/mode/markdown/markdown';
import 'codemirror/addon/display/placeholder';
import 'codemirror/addon/mode/simple'; import 'codemirror/addon/mode/simple';
import 'codemirror/mode/yaml/yaml'; import 'codemirror/mode/yaml/yaml';

View file

@ -181,3 +181,7 @@ body.dark-mode .CodeMirror span.cm-type {
text-decoration-color: var(--vscode-errorForeground); text-decoration-color: var(--vscode-errorForeground);
text-decoration-style: wavy; text-decoration-style: wavy;
} }
.CodeMirror-placeholder {
color: var(--vscode-input-placeholderForeground) !important;
}

View file

@ -46,6 +46,7 @@ export interface SourceProps {
wrapLines?: boolean; wrapLines?: boolean;
onChange?: (text: string) => void; onChange?: (text: string) => void;
dataTestId?: string; dataTestId?: string;
placeholder?: string;
} }
export const CodeMirrorWrapper: React.FC<SourceProps> = ({ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
@ -62,6 +63,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
wrapLines, wrapLines,
onChange, onChange,
dataTestId, dataTestId,
placeholder,
}) => { }) => {
const [measure, codemirrorElement] = useMeasure<HTMLDivElement>(); const [measure, codemirrorElement] = useMeasure<HTMLDivElement>();
const [modulePromise] = React.useState<Promise<CodeMirror>>(import('./codeMirrorModule').then(m => m.default)); const [modulePromise] = React.useState<Promise<CodeMirror>>(import('./codeMirrorModule').then(m => m.default));
@ -89,7 +91,8 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
&& mode === codemirrorRef.current.cm.getOption('mode') && mode === codemirrorRef.current.cm.getOption('mode')
&& !!readOnly === codemirrorRef.current.cm.getOption('readOnly') && !!readOnly === codemirrorRef.current.cm.getOption('readOnly')
&& lineNumbers === codemirrorRef.current.cm.getOption('lineNumbers') && lineNumbers === codemirrorRef.current.cm.getOption('lineNumbers')
&& wrapLines === codemirrorRef.current.cm.getOption('lineWrapping')) { && wrapLines === codemirrorRef.current.cm.getOption('lineWrapping')
&& placeholder === codemirrorRef.current.cm.getOption('placeholder')) {
// No need to re-create codemirror. // No need to re-create codemirror.
return; return;
} }
@ -102,6 +105,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
readOnly: !!readOnly, readOnly: !!readOnly,
lineNumbers, lineNumbers,
lineWrapping: wrapLines, lineWrapping: wrapLines,
placeholder,
}); });
codemirrorRef.current = { cm }; codemirrorRef.current = { cm };
if (isFocused) if (isFocused)
@ -109,7 +113,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
setCodemirror(cm); setCodemirror(cm);
return cm; return cm;
})(); })();
}, [modulePromise, codemirror, codemirrorElement, language, mimeType, linkify, lineNumbers, wrapLines, readOnly, isFocused]); }, [modulePromise, codemirror, codemirrorElement, language, mimeType, linkify, lineNumbers, wrapLines, readOnly, isFocused, placeholder]);
React.useEffect(() => { React.useEffect(() => {
if (codemirrorRef.current) if (codemirrorRef.current)

View file

@ -19,6 +19,8 @@ import net from 'net';
import type { AddressInfo } from 'net'; import type { AddressInfo } from 'net';
const CDNS = [ const CDNS = [
'https://playwright.azureedge.net/dbazure/download/playwright', // ESRP
'https://playwright.download.prss.microsoft.com/dbazure/download/playwright', // ESRP Fallback
'https://playwright.azureedge.net', 'https://playwright.azureedge.net',
'https://playwright-akamai.azureedge.net', 'https://playwright-akamai.azureedge.net',
'https://playwright-verizon.azureedge.net', 'https://playwright-verizon.azureedge.net',
@ -90,8 +92,8 @@ test(`npx playwright install should not hang when CDN closes the connection`, as
}, },
expectToExitWithError: true expectToExitWithError: true
}); });
expect(retryCount).toBe(3); expect(retryCount).toBe(5);
expect([...result.matchAll(/Download failed: server closed connection/g)]).toHaveLength(3); expect([...result.matchAll(/Download failed: server closed connection/g)]).toHaveLength(5);
} finally { } finally {
await new Promise(resolve => server.close(resolve)); await new Promise(resolve => server.close(resolve));
} }
@ -120,8 +122,8 @@ test(`npx playwright install should not hang when CDN TCP connection stalls`, as
}, },
expectToExitWithError: true expectToExitWithError: true
}); });
expect(retryCount).toBe(3); expect(retryCount).toBe(5);
expect([...result.matchAll(/timed out after/g)]).toHaveLength(3); expect([...result.matchAll(/timed out after/g)]).toHaveLength(5);
} finally { } finally {
for (const socket of socketsToDestroy) for (const socket of socketsToDestroy)
socket.destroy(); socket.destroy();

View file

@ -408,3 +408,23 @@ it('should be able to render avif images', {
})); }));
expect(await page.evaluate(() => (window as any).error)).toBe(undefined); expect(await page.evaluate(() => (window as any).error)).toBe(undefined);
}); });
it('should not crash when clicking a label with a <input type="file"/>', {
annotation: {
type: 'issue',
description: 'https://github.com/microsoft/playwright/issues/33257'
}
}, async ({ page }) => {
await page.setContent(`
<form>
<label>
A second file
<input type="file" />
</label>
</form>
`);
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByText('A second file').click();
const fileChooser = await fileChooserPromise;
expect(fileChooser.page()).toBe(page);
});

View file

@ -31,7 +31,8 @@ type Fixtures = {
}; };
const test = baseTest.extend<Fixtures>({ const test = baseTest.extend<Fixtures>({
wsEndpoint: async ({ }, use) => { wsEndpoint: async ({ headless }, use) => {
if (headless)
process.env.PW_DEBUG_CONTROLLER_HEADLESS = '1'; process.env.PW_DEBUG_CONTROLLER_HEADLESS = '1';
const server = new PlaywrightServer({ mode: 'extension', path: '/' + createGuid(), maxConnections: Number.MAX_VALUE, enableSocksProxy: false }); const server = new PlaywrightServer({ mode: 'extension', path: '/' + createGuid(), maxConnections: Number.MAX_VALUE, enableSocksProxy: false });
const wsEndpoint = await server.listen(); const wsEndpoint = await server.listen();

View file

@ -402,17 +402,6 @@ await page1.GotoAsync("about:blank?foo");`);
await expect.poll(() => messages).toEqual(['mousedown', 'mouseup', 'click']); await expect.poll(() => messages).toEqual(['mousedown', 'mouseup', 'click']);
}); });
test('should update hover model on action', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name='updated'"></input>`);
const [models] = await Promise.all([
recorder.waitForActionPerformed(),
page.click('input')
]);
expect(models.hovered).toBe('#checkbox');
});
test('should reset hover model on action when element detaches', async ({ openRecorder }) => { test('should reset hover model on action when element detaches', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder(); const { page, recorder } = await openRecorder();

View file

@ -64,11 +64,10 @@ test.describe(() => {
test('should inspect aria snapshot', async ({ openRecorder }) => { test('should inspect aria snapshot', async ({ openRecorder }) => {
const { recorder } = await openRecorder(); const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<main><button>Submit</button></main>`); await recorder.setContentAndWait(`<main><button>Submit</button></main>`);
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
await recorder.page.click('x-pw-tool-item.pick-locator'); await recorder.page.click('x-pw-tool-item.pick-locator');
await recorder.page.hover('button'); await recorder.page.hover('button');
await recorder.trustedClick(); await recorder.trustedClick();
await recorder.recorderPage.getByRole('tab', { name: 'Aria snapshot ' }).click(); await recorder.recorderPage.getByRole('tab', { name: 'Aria' }).click();
await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(` await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
- textbox - textbox
- text: '- button "Submit"' - text: '- button "Submit"'
@ -85,12 +84,11 @@ test.describe(() => {
const submitButton = recorder.page.getByRole('button', { name: 'Submit' }); const submitButton = recorder.page.getByRole('button', { name: 'Submit' });
const cancelButton = recorder.page.getByRole('button', { name: 'Cancel' }); const cancelButton = recorder.page.getByRole('button', { name: 'Cancel' });
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
await recorder.page.click('x-pw-tool-item.pick-locator'); await recorder.page.click('x-pw-tool-item.pick-locator');
await submitButton.hover(); await submitButton.hover();
await recorder.trustedClick(); await recorder.trustedClick();
await recorder.recorderPage.getByRole('tab', { name: 'Aria snapshot ' }).click(); await recorder.recorderPage.getByRole('tab', { name: 'Aria' }).click();
await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(` await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
- text: '- button "Submit"' - text: '- button "Submit"'
`); `);
@ -128,13 +126,12 @@ test.describe(() => {
</main>`); </main>`);
const submitButton = recorder.page.getByRole('button', { name: 'Submit' }); const submitButton = recorder.page.getByRole('button', { name: 'Submit' });
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
await recorder.page.click('x-pw-tool-item.pick-locator'); await recorder.page.click('x-pw-tool-item.pick-locator');
await submitButton.hover(); await submitButton.hover();
await recorder.trustedClick(); await recorder.trustedClick();
await recorder.recorderPage.getByRole('tab', { name: 'Aria snapshot ' }).click(); await recorder.recorderPage.getByRole('tab', { name: 'Aria' }).click();
await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(` await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
- text: '- button "Submit"' - text: '- button "Submit"'
`); `);
@ -144,4 +141,35 @@ test.describe(() => {
// 3 highlighted tokens. // 3 highlighted tokens.
await expect(recorder.recorderPage.locator('.source-line-error-underline')).toHaveCount(3); await expect(recorder.recorderPage.locator('.source-line-error-underline')).toHaveCount(3);
}); });
test('should generate valid javascript with multiline snapshot assertion', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
// set width and height to 100% to ensure click is outside of the list
await recorder.setContentAndWait(`<body style="width:100%;height:100%"><ul><li>item 1</li><li>item 2</li></ul></body>`);
await recorder.page.click('x-pw-tool-item.snapshot');
await recorder.page.hover('body');
await recorder.trustedClick();
// playwright tests assertions are uncommented
await expect.poll(() =>
recorder.text('Playwright Test')).toContain([
` await expect(page.locator('body')).toMatchAriaSnapshot(\``,
` - list:`,
` - listitem: item 1`,
` - listitem: item 2`,
` \`);`,
].join('\n'));
// non-test javascript has commented assertions
await expect.poll(() =>
recorder.text('JavaScript')).toContain([
` // await expect(page.locator('body')).toMatchAriaSnapshot(\``,
` // - list:`,
` // - listitem: item 1`,
` // - listitem: item 2`,
` // \`);`,
].join('\n'));
});
}); });

View file

@ -0,0 +1,69 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './inspectorTest';
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();
await recorder.setContentAndWait(`<main><button>Submit</button></main>`);
await recorder.page.click('x-pw-tool-item.pick-locator');
await recorder.page.hover('button');
await recorder.trustedClick();
await recorder.recorderPage.getByRole('tab', { name: 'Locator' }).click();
await expect(recorder.recorderPage.locator('.tab-locator .CodeMirror')).toMatchAriaSnapshot(`
- text: "getByRole('button', { name: 'Submit' })"
`);
});
test('should update locator highlight', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<main>
<button>Submit</button>
<button>Cancel</button>
</main>`);
const submitButton = recorder.page.getByRole('button', { name: 'Submit' });
const cancelButton = recorder.page.getByRole('button', { name: 'Cancel' });
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
await recorder.page.click('x-pw-tool-item.pick-locator');
await submitButton.hover();
await recorder.trustedClick();
await recorder.recorderPage.getByRole('tab', { name: 'Locator' }).click();
await expect(recorder.recorderPage.locator('.tab-locator .CodeMirror')).toMatchAriaSnapshot(`
- text: "getByRole('button', { name: 'Submit' })"
`);
await recorder.recorderPage.locator('.tab-locator .CodeMirror').click();
for (let i = 0; i < `Submit' })`.length; i++)
await recorder.recorderPage.keyboard.press('Backspace');
{
// Different button.
await recorder.recorderPage.locator('.tab-locator .CodeMirror').pressSequentially(`Cancel' })`);
await expect(recorder.page.locator('x-pw-highlight')).toBeVisible();
const box1 = roundBox(await cancelButton.boundingBox());
const box2 = roundBox(await recorder.page.locator('x-pw-highlight').boundingBox());
expect(box1).toEqual(box2);
}
});
});

View file

@ -495,6 +495,16 @@ test('should not include hidden pseudo into accessible name', async ({ page }) =
expect.soft(await getNameAndRole(page, 'a')).toEqual({ role: 'link', name: 'hello hello' }); expect.soft(await getNameAndRole(page, 'a')).toEqual({ role: 'link', name: 'hello hello' });
}); });
test('should ignore invalid aria-labelledby', async ({ page }) => {
await page.setContent(`
<label>
<span>Text here</span>
<input type=text aria-labelledby="does-not-exist">
</label>
`);
expect.soft(await getNameAndRole(page, 'input')).toEqual({ role: 'textbox', name: 'Text here' });
});
function toArray(x: any): any[] { function toArray(x: any): any[] {
return Array.isArray(x) ? x : [x]; return Array.isArray(x) ? x : [x];
} }

View file

@ -1444,6 +1444,24 @@ test('should not record route actions', {
]); ]);
}); });
test('should not record network actions', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33558' },
}, async ({ page, runAndTrace, server }) => {
const traceViewer = await runAndTrace(async () => {
page.on('request', async request => {
await request.allHeaders();
});
page.on('response', async response => {
await response.text();
});
await page.goto(server.EMPTY_PAGE);
});
await expect(traceViewer.actionTitles).toHaveText([
/page.goto.*empty.html/,
]);
});
test('should show baseURL in metadata pane', { test('should show baseURL in metadata pane', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31847' }, annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31847' },
}, async ({ showTraceViewer }) => { }, async ({ showTraceViewer }) => {

View file

@ -431,8 +431,48 @@ test('toHaveAccessibleName', async ({ page }) => {
await expect(page.locator('div')).toHaveAccessibleName(/ell\w/); await expect(page.locator('div')).toHaveAccessibleName(/ell\w/);
await expect(page.locator('div')).not.toHaveAccessibleName(/hello/); await expect(page.locator('div')).not.toHaveAccessibleName(/hello/);
await expect(page.locator('div')).toHaveAccessibleName(/hello/, { ignoreCase: true }); await expect(page.locator('div')).toHaveAccessibleName(/hello/, { ignoreCase: true });
await page.setContent(`<button>foo&nbsp;bar\nbaz</button>`);
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 }) => { test('toHaveAccessibleDescription', async ({ page }) => {
await page.setContent(` await page.setContent(`
<div role="button" aria-description="Hello"></div> <div role="button" aria-description="Hello"></div>
@ -443,6 +483,12 @@ test('toHaveAccessibleDescription', async ({ page }) => {
await expect(page.locator('div')).toHaveAccessibleDescription(/ell\w/); await expect(page.locator('div')).toHaveAccessibleDescription(/ell\w/);
await expect(page.locator('div')).not.toHaveAccessibleDescription(/hello/); await expect(page.locator('div')).not.toHaveAccessibleDescription(/hello/);
await expect(page.locator('div')).toHaveAccessibleDescription(/hello/, { ignoreCase: true }); await expect(page.locator('div')).toHaveAccessibleDescription(/hello/, { ignoreCase: true });
await page.setContent(`
<div role="button" aria-describedby="desc"></div>
<span id="desc">foo&nbsp;bar\nbaz</span>
`);
await expect(page.locator('div')).toHaveAccessibleDescription('foo bar baz');
}); });
test('toHaveRole', async ({ page }) => { test('toHaveRole', async ({ page }) => {

View file

@ -27,7 +27,6 @@ function unshift(snapshot: string): string {
const match = line.match(/^(\s*)/); const match = line.match(/^(\s*)/);
if (match && match[1].length < whitespacePrefixLength) if (match && match[1].length < whitespacePrefixLength)
whitespacePrefixLength = match[1].length; whitespacePrefixLength = match[1].length;
break;
} }
return lines.filter(t => t.trim()).map(line => line.substring(whitespacePrefixLength)).join('\n'); return lines.filter(t => t.trim()).map(line => line.substring(whitespacePrefixLength)).join('\n');
} }

View file

@ -318,3 +318,25 @@ it('request.postData is not null when fetching FormData with a Blob', {
expect(postData).toContain('Content-Disposition: form-data; name="file"; filename="blob"'); expect(postData).toContain('Content-Disposition: form-data; name="file"; filename="blob"');
expect(postData).toContain('\r\nhello\r\n'); expect(postData).toContain('\r\nhello\r\n');
}); });
it('should abort favicon requests if interception is enabled', async ({ page, server, browserName }) => {
let requestCount = 0;
server.setRoute('/favicon.ico', (req, res) => {
++requestCount;
res.setHeader('content-type', 'text/plain');
res.end('my content');
});
// Intercept all requests.
await page.route('**/*', async route => {
await route.fulfill({
status: 200,
body: 'Hello, world!',
});
});
await page.goto(server.EMPTY_PAGE);
const response = await page.evaluate(() => fetch('/favicon.ico').then(r => r.text()).catch(e => 'load failed'));
expect(response).toBe('load failed');
// Browsers can send favicon requests in the background.
await new Promise(f => setTimeout(f, 1000));
expect(requestCount).toBe(0);
});

View file

@ -659,3 +659,17 @@ test('should parse attributes', async ({ page }) => {
`); `);
} }
}); });
test('should not unshift actual template text', async ({ page }) => {
await page.setContent(`
<h1>title</h1>
<h1>title 2</h1>
`);
const error = await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading "title" [level=1]
- heading "title 2" [level=1]
`, { timeout: 1000 }).catch(e => e);
expect(stripAnsi(error.message)).toContain(`
- heading "title" [level=1]
- heading "title 2" [level=1]`);
});

View file

@ -96,6 +96,14 @@ test('should reuse context with trace if mode=when-possible', async ({ runInline
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
let lastContextGuid; let lastContextGuid;
test.beforeAll(async () => {
console.log('fromBeforeAll');
});
test.afterAll(async () => {
console.log('fromAfterAll');
});
test('one', async ({ context, page }) => { test('one', async ({ context, page }) => {
lastContextGuid = context._guid; lastContextGuid = context._guid;
await page.setContent('<button>Click</button>'); await page.setContent('<button>Click</button>');
@ -113,10 +121,13 @@ test('should reuse context with trace if mode=when-possible', async ({ runInline
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
expect(result.passed).toBe(2); expect(result.passed).toBe(2);
expect(result.output).toContain('fromBeforeAll');
expect(result.output).toContain('fromAfterAll');
const trace1 = await parseTrace(testInfo.outputPath('test-results', 'reuse-one', 'trace.zip')); const trace1 = await parseTrace(testInfo.outputPath('test-results', 'reuse-one', 'trace.zip'));
expect(trace1.actionTree).toEqual([ expect(trace1.actionTree).toEqual([
'Before Hooks', 'Before Hooks',
' beforeAll hook',
' fixture: browser', ' fixture: browser',
' browserType.launch', ' browserType.launch',
' fixture: context', ' fixture: context',
@ -143,6 +154,7 @@ test('should reuse context with trace if mode=when-possible', async ({ runInline
'After Hooks', 'After Hooks',
' fixture: page', ' fixture: page',
' fixture: context', ' fixture: context',
' afterAll hook',
]); ]);
expect(trace2.traceModel.storage().snapshotsForTest().length).toBeGreaterThan(0); expect(trace2.traceModel.storage().snapshotsForTest().length).toBeGreaterThan(0);
}); });

View file

@ -5,15 +5,16 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"@playwright/test": "1.49.0-alpha-2024-10-30" "@playwright/test": "1.49.0-beta-1731772650000"
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.49.0-alpha-2024-10-30", "version": "1.49.0-beta-1731772650000",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-30.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-beta-1731772650000.tgz",
"integrity": "sha512-7pq4a+eDCkp6VmGGpr6KanL0gQ2SunC9dAjtP+VZLobdaY0ZL7XkmD2rL8UNANF2AkmKdOf+GmTS+wZ42qhvLg==", "integrity": "sha512-0d7DBoGZ23lv1/EkNoFXj5fQ9k3qlYHRE7la68zXihtjTH1DdwEtgdMgXR4UEScF2r/YNXaGRZ7sK/DVu9f6Aw==",
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.49.0-alpha-2024-10-30" "playwright": "1.49.0-beta-1731772650000"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -27,6 +28,7 @@
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
@ -36,11 +38,12 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.49.0-alpha-2024-10-30", "version": "1.49.0-beta-1731772650000",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-30.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-beta-1731772650000.tgz",
"integrity": "sha512-OJ++0IaaTyBHZuPMi7kNZ/ssyRvN4Fkh7NCpYBRyfPL8H90bEVwDe7j4Ab79HMBLxUZMg7D7aRIlimmYmVdbpQ==", "integrity": "sha512-+LLjx+DMLjx1qiBtLuURTLV3LmFxvQOSaVp9EDMH/qYpclhsp/W41vNxxZEqf8CIsL0BKHIVQYU+6D3OLnJq8g==",
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.49.0-alpha-2024-10-30" "playwright-core": "1.49.0-beta-1731772650000"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -53,9 +56,10 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.49.0-alpha-2024-10-30", "version": "1.49.0-beta-1731772650000",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-30.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-beta-1731772650000.tgz",
"integrity": "sha512-T1KDI5SQPqzVIahMOpCX7GE2Slv/5KEM+gSnj5mQZDi57Z8Ij5xnGz6ZX4KBdDrmkBRHLrRM4ijXfH1Q7zNkEg==", "integrity": "sha512-W1HbioibWPPsazFzU/PL9QzGEGubxizQOyMON8/d7DjOpNBqfzuemNuAsNBXucUEVbUlOOzMuoAEX/iqXUOl6Q==",
"license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
}, },
@ -66,11 +70,11 @@
}, },
"dependencies": { "dependencies": {
"@playwright/test": { "@playwright/test": {
"version": "1.49.0-alpha-2024-10-30", "version": "1.49.0-beta-1731772650000",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-30.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-beta-1731772650000.tgz",
"integrity": "sha512-7pq4a+eDCkp6VmGGpr6KanL0gQ2SunC9dAjtP+VZLobdaY0ZL7XkmD2rL8UNANF2AkmKdOf+GmTS+wZ42qhvLg==", "integrity": "sha512-0d7DBoGZ23lv1/EkNoFXj5fQ9k3qlYHRE7la68zXihtjTH1DdwEtgdMgXR4UEScF2r/YNXaGRZ7sK/DVu9f6Aw==",
"requires": { "requires": {
"playwright": "1.49.0-alpha-2024-10-30" "playwright": "1.49.0-beta-1731772650000"
} }
}, },
"fsevents": { "fsevents": {
@ -80,18 +84,18 @@
"optional": true "optional": true
}, },
"playwright": { "playwright": {
"version": "1.49.0-alpha-2024-10-30", "version": "1.49.0-beta-1731772650000",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-30.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-beta-1731772650000.tgz",
"integrity": "sha512-OJ++0IaaTyBHZuPMi7kNZ/ssyRvN4Fkh7NCpYBRyfPL8H90bEVwDe7j4Ab79HMBLxUZMg7D7aRIlimmYmVdbpQ==", "integrity": "sha512-+LLjx+DMLjx1qiBtLuURTLV3LmFxvQOSaVp9EDMH/qYpclhsp/W41vNxxZEqf8CIsL0BKHIVQYU+6D3OLnJq8g==",
"requires": { "requires": {
"fsevents": "2.3.2", "fsevents": "2.3.2",
"playwright-core": "1.49.0-alpha-2024-10-30" "playwright-core": "1.49.0-beta-1731772650000"
} }
}, },
"playwright-core": { "playwright-core": {
"version": "1.49.0-alpha-2024-10-30", "version": "1.49.0-beta-1731772650000",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-30.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-beta-1731772650000.tgz",
"integrity": "sha512-T1KDI5SQPqzVIahMOpCX7GE2Slv/5KEM+gSnj5mQZDi57Z8Ij5xnGz6ZX4KBdDrmkBRHLrRM4ijXfH1Q7zNkEg==" "integrity": "sha512-W1HbioibWPPsazFzU/PL9QzGEGubxizQOyMON8/d7DjOpNBqfzuemNuAsNBXucUEVbUlOOzMuoAEX/iqXUOl6Q=="
} }
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"private": true, "private": true,
"dependencies": { "dependencies": {
"@playwright/test": "1.49.0-alpha-2024-10-30" "@playwright/test": "1.49.0-beta-1731772650000"
} }
} }

View file

@ -1406,9 +1406,7 @@ pw:api |page.setContent @ a.test.ts:5
test.step |custom step @ a.test.ts:6 test.step |custom step @ a.test.ts:6
pw:api | page.waitForResponse @ a.test.ts:7 pw:api | page.waitForResponse @ a.test.ts:7
pw:api | page.click(div) @ a.test.ts:13 pw:api | page.click(div) @ a.test.ts:13
pw:api | response.text @ a.test.ts:8
expect | expect.toBeTruthy @ a.test.ts:9 expect | expect.toBeTruthy @ a.test.ts:9
pw:api | response.text @ a.test.ts:15
expect |expect.toBe @ a.test.ts:17 expect |expect.toBe @ a.test.ts:17
hook |After Hooks hook |After Hooks
fixture | fixture: page fixture | fixture: page
@ -1416,6 +1414,42 @@ fixture | fixture: context
`); `);
}); });
test('reading network request / response should not be listed as step', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33558' }
}, async ({ runInlineTest, server }) => {
const result = await runInlineTest({
'reporter.ts': stepIndentReporter,
'playwright.config.ts': `module.exports = { reporter: './reporter' };`,
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('waitForResponse step nesting', async ({ page }) => {
page.on('request', async request => {
await request.allHeaders();
});
page.on('response', async response => {
await response.text();
});
await page.goto('${server.EMPTY_PAGE}');
});
`
}, { reporter: '', workers: 1, timeout: 3000 });
expect(result.exitCode).toBe(0);
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.goto(${server.EMPTY_PAGE}) @ a.test.ts:10
hook |After Hooks
fixture | fixture: page
fixture | fixture: context
`);
});
test('calls from page.route callback should be under its parent step', { test('calls from page.route callback should be under its parent step', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33186' } annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33186' }
}, async ({ runInlineTest, server }) => { }, async ({ runInlineTest, server }) => {

View file

@ -775,3 +775,26 @@ test('should respect --ignore-snapshots option', {
- treeitem ${/\[icon-check\] snapshot/} - treeitem ${/\[icon-check\] snapshot/}
`); `);
}); });
test('should not leak websocket connections', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33641' }
}, async ({ runUITest }) => {
const { page } = await runUITest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('test', async () => {});
`,
});
const [ws1] = await Promise.all([
page.waitForEvent('websocket'),
page.getByTitle('Reload').click(),
]);
await Promise.all([
page.waitForEvent('websocket'),
page.getByTitle('Reload').click(),
]);
await expect.poll(() => ws1.isClosed()).toBe(true);
});

View file

@ -24,12 +24,15 @@ function trimPatch(patch: string) {
return patch.split('\n').map(line => line.trimEnd()).join('\n'); return patch.split('\n').map(line => line.trimEnd()).join('\n');
} }
test('should update snapshot with the update-snapshots flag', async ({ runInlineTest }, testInfo) => { test('should update snapshot with the update-snapshots flag with multiple projects', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({ const result = await runInlineTest({
'playwright.config.ts': `
export default { projects: [{ name: 'p1' }, { name: 'p2' }] };
`,
'a.spec.ts': ` 'a.spec.ts': `
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
test('test', async ({ page }) => { test('test', async ({ page }) => {
await page.setContent(\`<h1>hello</h1>\`); await page.setContent(\`<h1>hello</h1><h2>bye</h2>\`);
await expect(page.locator('body')).toMatchAriaSnapshot(\` await expect(page.locator('body')).toMatchAriaSnapshot(\`
- heading "world" - heading "world"
\`); \`);
@ -43,12 +46,13 @@ test('should update snapshot with the update-snapshots flag', async ({ runInline
expect(trimPatch(data)).toBe(`diff --git a/a.spec.ts b/a.spec.ts expect(trimPatch(data)).toBe(`diff --git a/a.spec.ts b/a.spec.ts
--- a/a.spec.ts --- a/a.spec.ts
+++ b/a.spec.ts +++ b/a.spec.ts
@@ -3,7 +3,7 @@ @@ -3,7 +3,8 @@
test('test', async ({ page }) => { test('test', async ({ page }) => {
await page.setContent(\`<h1>hello</h1>\`); await page.setContent(\`<h1>hello</h1><h2>bye</h2>\`);
await expect(page.locator('body')).toMatchAriaSnapshot(\` await expect(page.locator('body')).toMatchAriaSnapshot(\`
- - heading "world" - - heading "world"
+ - heading "hello" [level=1] + - heading "hello" [level=1]
+ - heading "bye" [level=2]
\`); \`);
}); });

View file

@ -291,6 +291,25 @@ steps.push({
concurrent: watchMode, // feeds into trace-viewer's `public` directory, so it needs to be finished before trace-viewer build starts concurrent: watchMode, // feeds into trace-viewer's `public` directory, so it needs to be finished before trace-viewer build starts
}); });
if (watchMode) {
// the build above outputs into `packages/trace-viewer/public`, where the `vite build` for `packages/trace-viewer` is supposed to pick it up.
// there's a bug in `vite build --watch` though where the public dir is only copied over initially, but its not watched.
// to work around this, we run a second watch build of the service worker into the final output.
// bug: https://github.com/vitejs/vite/issues/18655
steps.push({
command: 'npx',
args: [
'vite', '--config', 'vite.sw.config.ts',
'build', '--watch', '--minify=false',
'--outDir', path.join(__dirname, '..', '..', 'packages', 'playwright-core', 'lib', 'vite', 'traceViewer'),
'--emptyOutDir=false'
],
shell: true,
cwd: path.join(__dirname, '..', '..', 'packages', 'trace-viewer'),
concurrent: true
});
}
// Build/watch web packages. // Build/watch web packages.
for (const webPackage of ['html-reporter', 'recorder', 'trace-viewer']) { for (const webPackage of ['html-reporter', 'recorder', 'trace-viewer']) {
steps.push({ steps.push({
@ -323,6 +342,13 @@ if (watchMode) {
cwd: path.join(__dirname, '..', '..', 'packages', 'html-reporter'), cwd: path.join(__dirname, '..', '..', 'packages', 'html-reporter'),
concurrent: true, concurrent: true,
}); });
steps.push({
command: 'npx',
args: ['vite', '--port', '44225'],
shell: true,
cwd: path.join(__dirname, '..', '..', 'packages', 'recorder'),
concurrent: true,
});
} }
// Generate injected. // Generate injected.

View file

@ -109,7 +109,7 @@ Example:
// 5. Generate types. // 5. Generate types.
console.log('\nGenerating protocol types...'); console.log('\nGenerating protocol types...');
const executablePath = registry.findExecutable(browserName).executablePathOrDie(); const executablePath = registry.findExecutable(browserName).executablePathOrDie();
await protocolGenerator.generateProtocol(browserName, executablePath).catch(console.warn); await protocolGenerator.generateProtocol(browserName, executablePath);
// 6. Update docs. // 6. Update docs.
console.log('\nUpdating documentation...'); console.log('\nUpdating documentation...');