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
[![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)
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| | 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: |
| 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"
```
### option: Locator.ariaSnapshot.timeout = %%-input-timeout-%%
* since: v1.49
### option: Locator.ariaSnapshot.timeout = %%-input-timeout-js-%%
* since: v1.49

View file

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

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
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
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.
```csharp
await Expect(Page.GetByRole(AriaRole.Button)).ToHaveAttribute("disabled", "");
await Expect(Page.GetByRole(AriaRole.Button)).ToHaveAttributeAsync("disabled", "");
```
### 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)
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
@ -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)
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)
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

View file

@ -45,7 +45,7 @@ npx playwright test --trace on
## 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
npx playwright show-report

7
package-lock.json generated
View file

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

View file

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

View file

@ -113,3 +113,32 @@ export function generateTraceUrl(traces: TestAttachment[]) {
}
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 [test, setTest] = React.useState<TestCase | undefined>();
const testId = searchParams.get('testId');
const anchor = (searchParams.get('anchor') || '') as 'video' | 'diff' | '';
const run = +(searchParams.get('run') || '0');
const { prev, next } = React.useMemo(() => {
@ -133,7 +132,6 @@ const TestCaseViewLoader: React.FC<{
next={next}
prev={prev}
test={test}
anchor={anchor}
run={run}
/>;
};

View file

@ -63,7 +63,7 @@ const testCase: TestCase = {
};
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('Hidden annotation')).toBeHidden();
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 }) => {
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 component.getByText('Annotation text', { exact: false }).first().hover();
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 }) => {
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();
await expect(firstLink).toBeVisible();
@ -181,7 +181,7 @@ const testCaseSummary: TestCaseSummary = {
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();
const body = await component.getByText('The body with https://playwright.dev/docs/intro link');
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 }) => {
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');
await expect(link).toHaveAttribute('href', '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 }) => {
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(`
- text: group
- link "« previous"

View file

@ -33,9 +33,8 @@ export const TestCaseView: React.FC<{
test: TestCase | undefined,
next: TestCaseSummary | undefined,
prev: TestCaseSummary | undefined,
anchor: 'video' | 'diff' | '',
run: number,
}> = ({ projectNames, test, run, anchor, next, prev }) => {
}> = ({ projectNames, test, run, next, prev }) => {
const [selectedResultIndex, setSelectedResultIndex] = React.useState(run);
const searchParams = React.useContext(SearchParamsContext);
const filterParam = searchParams.has('q') ? '&q=' + searchParams.get('q') : '';
@ -79,7 +78,7 @@ export const TestCaseView: React.FC<{
test.results.map((result, index) => ({
id: String(index),
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)} />}
</div>;
};

View file

@ -25,7 +25,7 @@ export const TestErrorView: React.FC<{
testId?: string;
}> = ({ error, testId }) => {
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<{

View file

@ -75,12 +75,12 @@ function imageDiffBadge(test: TestCaseSummary): JSX.Element | undefined {
const resultWithImageDiff = test.results.find(result => result.attachments.some(attachment => {
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 {
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 {

View file

@ -20,7 +20,7 @@ import { TreeItem } from './treeItem';
import { msToString } from './utils';
import { AutoChip } from './chip';
import { traceImage } from './images';
import { AttachmentLink, generateTraceUrl } from './links';
import { Anchor, AttachmentLink, generateTraceUrl } from './links';
import { statusIcon } from './statusIcon';
import type { ImageDiff } 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<{
test: TestCase,
result: TestResult,
anchor: 'video' | 'diff' | '',
}> = ({ result, anchor }) => {
}> = ({ result }) => {
const { screenshots, videos, traces, otherAttachments, diffs, errors, htmls } = React.useMemo(() => {
const attachments = result?.attachments || [];
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 };
}, [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'>
{!!errors.length && <AutoChip header='Errors'>
{errors.map((error, index) => {
@ -107,9 +91,11 @@ export const TestResultView: React.FC<{
</AutoChip>}
{diffs.map((diff, index) =>
<AutoChip key={`diff-${index}`} dataTestId='test-results-image-diff' header={`Image mismatch: ${diff.name}`} targetRef={imageDiffRef}>
<ImageDiffView key='image-diff' diff={diff}></ImageDiffView>
</AutoChip>
<Anchor key={`diff-${index}`} id={`diff-${index}`}>
<AutoChip dataTestId='test-results-image-diff' header={`Image mismatch: ${diff.name}`} revealOnAnchorId={`diff-${index}`}>
<ImageDiffView diff={diff}/>
</AutoChip>
</Anchor>
)}
{!!screenshots.length && <AutoChip header='Screenshots'>
@ -123,23 +109,23 @@ export const TestResultView: React.FC<{
})}
</AutoChip>}
{!!traces.length && <AutoChip header='Traces'>
{!!traces.length && <Anchor id='traces'><AutoChip header='Traces' revealOnAnchorId='traces'>
{<div>
<a href={generateTraceUrl(traces)}>
<img className='screenshot' src={traceImage} style={{ width: 192, height: 117, marginLeft: 20 }} />
</a>
{traces.map((a, i) => <AttachmentLink key={`trace-${i}`} attachment={a} linkName={traces.length === 1 ? 'trace' : `trace-${i + 1}`}></AttachmentLink>)}
</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}`}>
<video controls>
<source src={a.path} type={a.contentType}/>
</video>
<AttachmentLink attachment={a}></AttachmentLink>
</div>)}
</AutoChip>}
</AutoChip></Anchor>}
{!!(otherAttachments.size + htmls.length) && <AutoChip header='Attachments'>
{[...htmls].map((a, i) => (

View file

@ -3,21 +3,21 @@
"browsers": [
{
"name": "chromium",
"revision": "1148",
"revision": "1149",
"installByDefault": true,
"browserVersion": "131.0.6778.33"
"browserVersion": "132.0.6834.6"
},
{
"name": "chromium-headless-shell",
"revision": "1148",
"revision": "1149",
"installByDefault": true,
"browserVersion": "131.0.6778.33"
"browserVersion": "132.0.6834.6"
},
{
"name": "chromium-tip-of-tree",
"revision": "1277",
"revision": "1279",
"installByDefault": false,
"browserVersion": "132.0.6834.0"
"browserVersion": "133.0.6846.0"
},
{
"name": "firefox",
@ -27,9 +27,9 @@
},
{
"name": "firefox-beta",
"revision": "1465",
"revision": "1466",
"installByDefault": false,
"browserVersion": "132.0b8"
"browserVersion": "133.0b9"
},
{
"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) {
super(parent, type, guid, initializer);
this.markAsInternalType();
this._redirectedFrom = Request.fromNullable(initializer.redirectedFrom);
if (this._redirectedFrom)
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) {
super(parent, type, guid, initializer);
this.markAsInternalType();
this._provisionalHeaders = new RawHeaders(initializer.headers);
this._request = Request.from(this._initializer.request);
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 '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.
*/
@ -694,7 +694,7 @@ percentage [0 - 100] for scroll driven animations
export interface AffectedFrame {
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 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.
*/
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.
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.
*/
@ -2424,6 +2429,10 @@ available).
* Optional logical axes queried for the container.
*/
logicalAxes?: DOM.LogicalAxes;
/**
* true if the query contains scroll-state() queries.
*/
queriesScrollState?: boolean;
}
/**
* CSS Supports at-rule descriptor.
@ -2475,6 +2484,20 @@ available).
text: string;
/**
* 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).
*/
range?: SourceRange;
@ -2779,6 +2802,12 @@ resized.) The current implementation considers only viewport-dependent media fea
*/
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
@ -3039,6 +3068,19 @@ returns an array of locations of the CSS selector in the style sheet.
export type getLocationForSelectorReturnValue = {
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
replaces the one previously specified. Pass empty array to disable tracking.
@ -3561,7 +3603,7 @@ front-end.
/**
* 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.
*/
@ -4876,15 +4918,17 @@ $x functions).
}
/**
* Returns the query container of the given node based on container query
conditions: containerName, physical, and logical axes. If no axes are
provided, the style container is returned, which is the direct parent or the
closest element with a matching container-name.
conditions: containerName, physical and logical axes, and whether it queries
scroll-state. If no axes are provided and queriesScrollState is false, the
style container is returned, which is the direct parent or the closest
element with a matching container-name.
*/
export type getContainerForNodeParameters = {
nodeId: NodeId;
containerName?: string;
physicalAxes?: PhysicalAxes;
logicalAxes?: LogicalAxes;
queriesScrollState?: boolean;
}
export type getContainerForNodeReturnValue = {
/**
@ -8255,7 +8299,9 @@ file, data and other requests and responses, their headers, bodies, timing, etc.
*/
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;
/**
@ -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";
/**
* Initiator JavaScript stack trace, set for Script only.
Requires the Debugger domain to be enabled.
*/
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.
*/
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.
*/
@ -11498,7 +11545,7 @@ as an ad.
* All Permissions Policy features. This enum should match the one defined
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.
*/
export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"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.
*/
@ -12384,7 +12431,8 @@ the page execution. Execution can be resumed via calling Page.handleJavaScriptDi
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 = {
/**
@ -14339,6 +14387,7 @@ int
destinationLimitPriority: SignedInt64AsBase10;
aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig;
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 AttributionReportingSourceRegistrationTimeConfig = "include"|"exclude";
@ -14386,7 +14435,7 @@ int
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 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.
*/
@ -15920,6 +15969,8 @@ are ignored.
export module Fetch {
/**
* Unique request identifier.
Note that this does not identify individual HTTP requests that are part of
a network request.
*/
export type RequestId = string;
/**
@ -16302,7 +16353,7 @@ https://webaudio.github.io/web-audio-api/
/**
* Enum of AudioContextState from the spec
*/
export type ContextState = "suspended"|"running"|"closed";
export type ContextState = "suspended"|"running"|"closed"|"interrupted";
/**
* Enum of AudioNode types
*/
@ -20213,6 +20264,7 @@ Error was thrown.
"CSS.styleSheetAdded": CSS.styleSheetAddedPayload;
"CSS.styleSheetChanged": CSS.styleSheetChangedPayload;
"CSS.styleSheetRemoved": CSS.styleSheetRemovedPayload;
"CSS.computedStyleUpdated": CSS.computedStyleUpdatedPayload;
"Cast.sinksUpdated": Cast.sinksUpdatedPayload;
"Cast.issueUpdated": Cast.issueUpdatedPayload;
"DOM.attributeModified": DOM.attributeModifiedPayload;
@ -20464,6 +20516,7 @@ Error was thrown.
"CSS.getStyleSheetText": CSS.getStyleSheetTextParameters;
"CSS.getLayersForNode": CSS.getLayersForNodeParameters;
"CSS.getLocationForSelector": CSS.getLocationForSelectorParameters;
"CSS.trackComputedStyleUpdatesForNode": CSS.trackComputedStyleUpdatesForNodeParameters;
"CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesParameters;
"CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesParameters;
"CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeParameters;
@ -21075,6 +21128,7 @@ Error was thrown.
"CSS.getStyleSheetText": CSS.getStyleSheetTextReturnValue;
"CSS.getLayersForNode": CSS.getLayersForNodeReturnValue;
"CSS.getLocationForSelector": CSS.getLocationForSelectorReturnValue;
"CSS.trackComputedStyleUpdatesForNode": CSS.trackComputedStyleUpdatesForNodeReturnValue;
"CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesReturnValue;
"CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesReturnValue;
"CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeReturnValue;

View file

@ -117,8 +117,10 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
const assertion = action.value ? `toHaveValue(${quote(action.value)})` : `toBeEmpty()`;
return `${this._isTest ? '' : '// '}await expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
}
case 'assertSnapshot':
return `${this._isTest ? '' : '// '}await expect(${subject}.${this._asLocator(action.selector)}).toMatchAriaSnapshot(${quoteMultiline(action.snapshot)});`;
case 'assertSnapshot': {
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"
},
"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": {
"width": 360,
"height": 640
@ -121,7 +121,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 640,
"height": 360
@ -132,7 +132,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 360,
"height": 740
@ -143,7 +143,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 740,
"height": 360
@ -154,7 +154,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 320,
"height": 658
@ -165,7 +165,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+ landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 658,
"height": 320
@ -176,7 +176,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 712,
"height": 1138
@ -187,7 +187,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 1138,
"height": 712
@ -1098,7 +1098,7 @@
"defaultBrowserType": "webkit"
},
"LG Optimus L70": {
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/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": {
"width": 384,
"height": 640
@ -1109,7 +1109,7 @@
"defaultBrowserType": "chromium"
},
"LG Optimus L70 landscape": {
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/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": {
"width": 640,
"height": 384
@ -1120,7 +1120,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 550": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 640,
"height": 360
@ -1131,7 +1131,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 550 landscape": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 360,
"height": 640
@ -1142,7 +1142,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 950": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 360,
"height": 640
@ -1153,7 +1153,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 950 landscape": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 640,
"height": 360
@ -1164,7 +1164,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 800,
"height": 1280
@ -1175,7 +1175,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 1280,
"height": 800
@ -1186,7 +1186,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 384,
"height": 640
@ -1197,7 +1197,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 640,
"height": 384
@ -1208,7 +1208,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 360,
"height": 640
@ -1219,7 +1219,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 640,
"height": 360
@ -1230,7 +1230,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 412,
"height": 732
@ -1241,7 +1241,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 732,
"height": 412
@ -1252,7 +1252,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 412,
"height": 732
@ -1263,7 +1263,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 732,
"height": 412
@ -1274,7 +1274,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 412,
"height": 732
@ -1285,7 +1285,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 732,
"height": 412
@ -1296,7 +1296,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 600,
"height": 960
@ -1307,7 +1307,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 960,
"height": 600
@ -1362,7 +1362,7 @@
"defaultBrowserType": "webkit"
},
"Pixel 2": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 411,
"height": 731
@ -1373,7 +1373,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 731,
"height": 411
@ -1384,7 +1384,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 XL": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 411,
"height": 823
@ -1395,7 +1395,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 XL landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 823,
"height": 411
@ -1406,7 +1406,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 393,
"height": 786
@ -1417,7 +1417,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 786,
"height": 393
@ -1428,7 +1428,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 353,
"height": 745
@ -1439,7 +1439,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 745,
"height": 353
@ -1450,7 +1450,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G)": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 412,
"height": 892
@ -1465,7 +1465,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G) landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"height": 892,
"width": 412
@ -1480,7 +1480,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 393,
"height": 851
@ -1495,7 +1495,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 851,
"height": 393
@ -1510,7 +1510,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 412,
"height": 915
@ -1525,7 +1525,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 915,
"height": 412
@ -1540,7 +1540,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 360,
"height": 640
@ -1551,7 +1551,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 640,
"height": 360
@ -1562,7 +1562,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Chrome HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 1792,
"height": 1120
@ -1577,7 +1577,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 1792,
"height": 1120
@ -1622,7 +1622,7 @@
"defaultBrowserType": "webkit"
},
"Desktop Chrome": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 1920,
"height": 1080
@ -1637,7 +1637,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
"width": 1920,
"height": 1080

View file

@ -296,7 +296,8 @@ export class FrameManager {
if (request._documentId)
frame.setPendingDocument({ documentId: request._documentId, request });
if (request._isFavicon) {
route?.continue({ isFallback: true }).catch(() => {});
// Abort favicon requests to avoid network access in case of interception.
route?.abort('aborted').catch(() => {});
return;
}
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 type { AriaProps, AriaRole, AriaTemplateNode, AriaTemplateRoleNode, AriaTemplateTextNode } from '@isomorphic/ariaSnapshot';
type AriaNode = AriaProps & {
export type AriaNode = AriaProps & {
role: AriaRole | 'fragment';
name: string;
children: (AriaNode | string)[];
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 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) => {
if (visited.has(node))
return;
@ -58,6 +79,7 @@ export function generateAriaTree(rootElement: Element): AriaNode {
}
}
addElement(element);
const childAriaNode = toAriaNode(element);
if (childAriaNode)
ariaNode.children.push(childAriaNode);
@ -100,15 +122,14 @@ export function generateAriaTree(rootElement: Element): AriaNode {
}
roleUtils.beginAriaCaches();
const ariaRoot: AriaNode = { role: 'fragment', name: '', children: [], element: rootElement };
try {
visit(ariaRoot, rootElement);
visit(snapshot.root, rootElement);
} finally {
roleUtils.endAriaCaches();
}
normalizeStringChildren(ariaRoot);
return ariaRoot;
normalizeStringChildren(snapshot.root);
return snapshot;
}
function toAriaNode(element: Element): AriaNode | null {
@ -143,10 +164,6 @@ function toAriaNode(element: Element): AriaNode | null {
return result;
}
export function renderedAriaTree(rootElement: Element, options?: { mode?: 'raw' | 'regex' }): string {
return renderAriaTree(generateAriaTree(rootElement), options);
}
function normalizeStringChildren(rootA11yNode: AriaNode) {
const flushChildren = (buffer: string[], normalizedChildren: (AriaNode | string)[]) => {
if (!buffer.length)
@ -203,7 +220,7 @@ export type 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);
return {
matches,
@ -215,7 +232,7 @@ export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode
}
export function getAllByAria(rootElement: Element, template: AriaTemplateNode): Element[] {
const root = generateAriaTree(rootElement);
const root = generateAriaTree(rootElement).root;
const matches = matchesNodeDeep(root, template, true);
return matches.map(n => n.element);
}
@ -285,7 +302,7 @@ function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode, collectAll:
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 includeText = options?.mode === 'regex' ? textContributesInfo : () => true;
const renderString = options?.mode === 'regex' ? convertToBestGuessRegex : (str: string) => str;
@ -324,6 +341,11 @@ export function renderAriaTree(ariaNode: AriaNode, options?: { mode?: 'raw' | 'r
key += ` [pressed]`;
if (ariaNode.selected === true)
key += ` [selected]`;
if (options?.ids) {
const id = options?.ids.get(ariaNode.element);
if (id)
key += ` [id=${id}]`;
}
const escapedKey = indent + '- ' + yamlEscapeKeyIfNeeded(key);
if (!ariaNode.children.length) {

View file

@ -90,7 +90,8 @@ export class Highlight {
}
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);
}

View file

@ -34,7 +34,8 @@ import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } fr
import { asLocator } from '../../utils/isomorphic/locatorGenerators';
import type { Language } from '../../utils/isomorphic/locatorGenerators';
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 { parseYamlTemplate } from '@isomorphic/ariaSnapshot';
@ -215,10 +216,27 @@ export class InjectedScript {
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)
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[] {
@ -1336,6 +1354,8 @@ export class InjectedScript {
received = elements.map(e => options.useInnerText ? (e as HTMLElement).innerText : elementText(new Map(), e).full);
else if (expression === 'to.have.class.array')
received = elements.map(e => e.classList.toString());
else if (expression === 'to.have.accessible.name.array')
received = elements.map(e => getElementAccessibleName(e, false));
if (received && options.expectedText) {
// "To match an array" is "to contain an array" + "equal length"

View file

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

View file

@ -383,7 +383,11 @@ export function getAriaLabelledByElements(element: Element): Element[] | null {
const ref = element.getAttribute('aria-labelledby');
if (ref === 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) {

View file

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

View file

@ -132,6 +132,10 @@ export class Recorder implements InstrumentationListener, IRecorder {
this._contextRecorder.clearScript();
return;
}
if (data.event === 'runTask') {
this._contextRecorder.runTask(data.params.task);
return;
}
});
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 {
return {
pageAlias: this._pageAliases.get(page)!,

View file

@ -88,7 +88,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
});
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 {

View file

@ -36,7 +36,7 @@ export async function downloadBrowserWithProgressBar(title: string, browserDirec
const zipPath = path.join(os.tmpdir(), downloadFileName);
try {
const retryCount = 3;
const retryCount = 5;
for (let attempt = 1; attempt <= retryCount; ++attempt) {
debugLogger.log('install', `downloading ${title} - attempt #${attempt}`);
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 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-akamai.azureedge.net',
'https://playwright-verizon.azureedge.net',
'https://playwright-akamai.azureedge.net', // Actually Edgio which will be retired Q4 2025.
'https://playwright-verizon.azureedge.net', // Actually Edgio which will be retired Q4 2025.
];
if (process.env.PW_TEST_CDN_THAT_SHOULD_WORK) {
for (let i = 0; i < PLAYWRIGHT_CDN_MIRRORS.length; i++) {
const cdn = PLAYWRIGHT_CDN_MIRRORS[i];
if (cdn !== process.env.PW_TEST_CDN_THAT_SHOULD_WORK)
PLAYWRIGHT_CDN_MIRRORS[i] = cdn + '.does-not-resolve.playwright.dev';
if (cdn !== process.env.PW_TEST_CDN_THAT_SHOULD_WORK) {
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 || [])
params.append('reporter', reporter);
params.set('server', server.urlPrefix('precise'));
let baseUrl = '.';
if (process.env.PW_HMR)
if (process.env.PW_HMR) {
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()}`;
server.routePath('/', (_, response) => {

View file

@ -29,7 +29,8 @@ const debugLoggerColorMap = {
'channel': 33, // blue
'server': 45, // cyan
'server:channel': 34, // green
'server:metadata': 33, // blue
'server:metadata': 33, // blue,
'recorder': 45, // cyan
};
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 '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.
*/
@ -694,7 +694,7 @@ percentage [0 - 100] for scroll driven animations
export interface AffectedFrame {
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 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.
*/
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.
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.
*/
@ -2424,6 +2429,10 @@ available).
* Optional logical axes queried for the container.
*/
logicalAxes?: DOM.LogicalAxes;
/**
* true if the query contains scroll-state() queries.
*/
queriesScrollState?: boolean;
}
/**
* CSS Supports at-rule descriptor.
@ -2475,6 +2484,20 @@ available).
text: string;
/**
* 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).
*/
range?: SourceRange;
@ -2779,6 +2802,12 @@ resized.) The current implementation considers only viewport-dependent media fea
*/
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
@ -3039,6 +3068,19 @@ returns an array of locations of the CSS selector in the style sheet.
export type getLocationForSelectorReturnValue = {
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
replaces the one previously specified. Pass empty array to disable tracking.
@ -3561,7 +3603,7 @@ front-end.
/**
* 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.
*/
@ -4876,15 +4918,17 @@ $x functions).
}
/**
* Returns the query container of the given node based on container query
conditions: containerName, physical, and logical axes. If no axes are
provided, the style container is returned, which is the direct parent or the
closest element with a matching container-name.
conditions: containerName, physical and logical axes, and whether it queries
scroll-state. If no axes are provided and queriesScrollState is false, the
style container is returned, which is the direct parent or the closest
element with a matching container-name.
*/
export type getContainerForNodeParameters = {
nodeId: NodeId;
containerName?: string;
physicalAxes?: PhysicalAxes;
logicalAxes?: LogicalAxes;
queriesScrollState?: boolean;
}
export type getContainerForNodeReturnValue = {
/**
@ -8255,7 +8299,9 @@ file, data and other requests and responses, their headers, bodies, timing, etc.
*/
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;
/**
@ -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";
/**
* Initiator JavaScript stack trace, set for Script only.
Requires the Debugger domain to be enabled.
*/
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.
*/
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.
*/
@ -11498,7 +11545,7 @@ as an ad.
* All Permissions Policy features. This enum should match the one defined
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.
*/
export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"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.
*/
@ -12384,7 +12431,8 @@ the page execution. Execution can be resumed via calling Page.handleJavaScriptDi
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 = {
/**
@ -14339,6 +14387,7 @@ int
destinationLimitPriority: SignedInt64AsBase10;
aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig;
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 AttributionReportingSourceRegistrationTimeConfig = "include"|"exclude";
@ -14386,7 +14435,7 @@ int
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 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.
*/
@ -15920,6 +15969,8 @@ are ignored.
export module Fetch {
/**
* Unique request identifier.
Note that this does not identify individual HTTP requests that are part of
a network request.
*/
export type RequestId = string;
/**
@ -16302,7 +16353,7 @@ https://webaudio.github.io/web-audio-api/
/**
* Enum of AudioContextState from the spec
*/
export type ContextState = "suspended"|"running"|"closed";
export type ContextState = "suspended"|"running"|"closed"|"interrupted";
/**
* Enum of AudioNode types
*/
@ -20213,6 +20264,7 @@ Error was thrown.
"CSS.styleSheetAdded": CSS.styleSheetAddedPayload;
"CSS.styleSheetChanged": CSS.styleSheetChangedPayload;
"CSS.styleSheetRemoved": CSS.styleSheetRemovedPayload;
"CSS.computedStyleUpdated": CSS.computedStyleUpdatedPayload;
"Cast.sinksUpdated": Cast.sinksUpdatedPayload;
"Cast.issueUpdated": Cast.issueUpdatedPayload;
"DOM.attributeModified": DOM.attributeModifiedPayload;
@ -20464,6 +20516,7 @@ Error was thrown.
"CSS.getStyleSheetText": CSS.getStyleSheetTextParameters;
"CSS.getLayersForNode": CSS.getLayersForNodeParameters;
"CSS.getLocationForSelector": CSS.getLocationForSelectorParameters;
"CSS.trackComputedStyleUpdatesForNode": CSS.trackComputedStyleUpdatesForNodeParameters;
"CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesParameters;
"CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesParameters;
"CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeParameters;
@ -21075,6 +21128,7 @@ Error was thrown.
"CSS.getStyleSheetText": CSS.getStyleSheetTextReturnValue;
"CSS.getLayersForNode": CSS.getLayersForNodeReturnValue;
"CSS.getLocationForSelector": CSS.getLocationForSelectorReturnValue;
"CSS.trackComputedStyleUpdatesForNode": CSS.trackComputedStyleUpdatesForNodeReturnValue;
"CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesReturnValue;
"CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesReturnValue;
"CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeReturnValue;

View file

@ -618,9 +618,10 @@ class ArtifactsRecorder {
if (captureScreenshots)
await this._screenshotOnTestFailure();
const leftoverContexts: BrowserContext[] = [];
let leftoverContexts: BrowserContext[] = [];
for (const browserType of [this._playwright.chromium, this._playwright.firefox, this._playwright.webkit])
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>);
// Collect traces/screenshots for remaining contexts.

View file

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

View file

@ -126,7 +126,6 @@ function unshift(snapshot: string): string {
const match = line.match(/^(\s*)/);
if (match && match[1].length < whitespacePrefixLength)
whitespacePrefixLength = match[1].length;
break;
}
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 newText = replacement.code.replace(/\{indent\}/g, indent);
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 options
*/
toHaveAccessibleName(name: string|RegExp, options?: {
toHaveAccessibleName(name: string|RegExp|ReadonlyArray<string|RegExp>, options?: {
/**
* Whether to perform case-insensitive match.
* [`ignoreCase`](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-have-accessible-name-option-ignore-case)

View file

@ -27,7 +27,7 @@ import { CallLogView } from './callLog';
import './recorder.css';
import { asLocator } from '@isomorphic/locatorGenerators';
import { toggleTheme } from '@web/theme';
import { copy } from '@web/uiUtils';
import { copy, useSetting } from '@web/uiUtils';
import yaml from 'yaml';
import { parseAriaKey } 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 [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 [ariaSnapshotErrors, setAriaSnapshotErrors] = React.useState<SourceHighlight[]>();
@ -67,6 +67,7 @@ export const Recorder: React.FC<RecorderProps> = ({
const language = source.language;
setLocator(asLocator(language, elementInfo.selector));
setAriaSnapshot(elementInfo.ariaSnapshot);
setAriaSnapshotErrors([]);
if (userGesture && selectedTab !== 'locator' && selectedTab !== 'aria')
setSelectedTab('locator');
@ -122,9 +123,6 @@ export const Recorder: React.FC<RecorderProps> = ({
if (!errors.length)
window.dispatch({ event: 'highlightRequested', params: { ariaTemplate: fragment } });
}, [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'>
<Toolbar>
@ -191,7 +189,7 @@ export const Recorder: React.FC<RecorderProps> = ({
{
id: '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',
@ -200,8 +198,8 @@ export const Recorder: React.FC<RecorderProps> = ({
},
{
id: 'aria',
title: 'Aria snapshot',
render: () => <CodeMirrorWrapper text={ariaPlaceholder || ariaSnapshot || ''} language={'yaml'} readOnly={isRecording} onChange={onAriaEditorChange} highlight={ariaSnapshotErrors} wrapLines={true} />
title: 'Aria',
render: () => <CodeMirrorWrapper text={ariaSnapshot || ''} placeholder='Type aria template to match' language={'yaml'} onChange={onAriaEditorChange} highlight={ariaSnapshotErrors} wrapLines={true} />
},
]}
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 ?? '';
let data = clientIdToTraceUrls.get(clientId);
if (!data) {
let traceViewerServerBaseUrl = self.registration.scope;
if (client?.url) {
const clientUrl = new URL(client.url);
if (clientUrl.searchParams.has('server'))
traceViewerServerBaseUrl = clientUrl.searchParams.get('server')!;
}
let traceViewerServerBaseUrl = new URL('../', client?.url ?? self.registration.scope);
if (traceViewerServerBaseUrl.searchParams.has('server'))
traceViewerServerBaseUrl = new URL(traceViewerServerBaseUrl.searchParams.get('server')!, traceViewerServerBaseUrl);
data = { limit, traceUrls: new Set(), traceViewerServer: new TraceViewerServer(traceViewerServerBaseUrl) };
clientIdToTraceUrls.set(clientId, data);

View file

@ -146,7 +146,7 @@ function formatUrl(trace: string, server: TraceViewerServer) {
}
export class TraceViewerServer {
constructor(private readonly baseUrl: string) {}
constructor(private readonly baseUrl: URL) {}
getFileURL(path: string): URL {
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 }) => {
return <div key={title} className='setting'>
<label>
<input type='checkbox' checked={value} onClick={() => set(!value)}/>
<input type='checkbox' checked={value} onChange={() => set(!value)}/>
{title}
</label>
</div>;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -46,7 +46,6 @@ export default defineConfig({
input: {
index: path.resolve(__dirname, 'index.html'),
uiMode: path.resolve(__dirname, 'uiMode.html'),
embedded: path.resolve(__dirname, 'embedded.html'),
recorder: path.resolve(__dirname, 'recorder.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/clike/clike';
import 'codemirror/mode/markdown/markdown';
import 'codemirror/addon/display/placeholder';
import 'codemirror/addon/mode/simple';
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-style: wavy;
}
.CodeMirror-placeholder {
color: var(--vscode-input-placeholderForeground) !important;
}

View file

@ -46,6 +46,7 @@ export interface SourceProps {
wrapLines?: boolean;
onChange?: (text: string) => void;
dataTestId?: string;
placeholder?: string;
}
export const CodeMirrorWrapper: React.FC<SourceProps> = ({
@ -62,6 +63,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
wrapLines,
onChange,
dataTestId,
placeholder,
}) => {
const [measure, codemirrorElement] = useMeasure<HTMLDivElement>();
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')
&& !!readOnly === codemirrorRef.current.cm.getOption('readOnly')
&& 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.
return;
}
@ -102,6 +105,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
readOnly: !!readOnly,
lineNumbers,
lineWrapping: wrapLines,
placeholder,
});
codemirrorRef.current = { cm };
if (isFocused)
@ -109,7 +113,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
setCodemirror(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(() => {
if (codemirrorRef.current)

View file

@ -19,6 +19,8 @@ import net from 'net';
import type { AddressInfo } from 'net';
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-akamai.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
});
expect(retryCount).toBe(3);
expect([...result.matchAll(/Download failed: server closed connection/g)]).toHaveLength(3);
expect(retryCount).toBe(5);
expect([...result.matchAll(/Download failed: server closed connection/g)]).toHaveLength(5);
} finally {
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
});
expect(retryCount).toBe(3);
expect([...result.matchAll(/timed out after/g)]).toHaveLength(3);
expect(retryCount).toBe(5);
expect([...result.matchAll(/timed out after/g)]).toHaveLength(5);
} finally {
for (const socket of socketsToDestroy)
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);
});
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,8 +31,9 @@ type Fixtures = {
};
const test = baseTest.extend<Fixtures>({
wsEndpoint: async ({ }, use) => {
process.env.PW_DEBUG_CONTROLLER_HEADLESS = '1';
wsEndpoint: async ({ headless }, use) => {
if (headless)
process.env.PW_DEBUG_CONTROLLER_HEADLESS = '1';
const server = new PlaywrightServer({ mode: 'extension', path: '/' + createGuid(), maxConnections: Number.MAX_VALUE, enableSocksProxy: false });
const wsEndpoint = await server.listen();
await use(wsEndpoint);

View file

@ -402,17 +402,6 @@ await page1.GotoAsync("about:blank?foo");`);
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 }) => {
const { page, recorder } = await openRecorder();

View file

@ -64,11 +64,10 @@ test.describe(() => {
test('should inspect aria snapshot', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
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.hover('button');
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(`
- textbox
- text: '- button "Submit"'
@ -85,12 +84,11 @@ test.describe(() => {
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: 'Aria snapshot ' }).click();
await recorder.recorderPage.getByRole('tab', { name: 'Aria' }).click();
await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
- text: '- button "Submit"'
`);
@ -128,13 +126,12 @@ test.describe(() => {
</main>`);
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 submitButton.hover();
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(`
- text: '- button "Submit"'
`);
@ -144,4 +141,35 @@ test.describe(() => {
// 3 highlighted tokens.
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' });
});
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[] {
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', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31847' },
}, 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')).not.toHaveAccessibleName(/hello/);
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 }) => {
await page.setContent(`
<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')).not.toHaveAccessibleDescription(/hello/);
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 }) => {

View file

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

View file

@ -317,4 +317,26 @@ it('request.postData is not null when fetching FormData with a Blob', {
const postData = await postDataPromise;
expect(postData).toContain('Content-Disposition: form-data; name="file"; filename="blob"');
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';
let lastContextGuid;
test.beforeAll(async () => {
console.log('fromBeforeAll');
});
test.afterAll(async () => {
console.log('fromAfterAll');
});
test('one', async ({ context, page }) => {
lastContextGuid = context._guid;
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.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'));
expect(trace1.actionTree).toEqual([
'Before Hooks',
' beforeAll hook',
' fixture: browser',
' browserType.launch',
' fixture: context',
@ -143,6 +154,7 @@ test('should reuse context with trace if mode=when-possible', async ({ runInline
'After Hooks',
' fixture: page',
' fixture: context',
' afterAll hook',
]);
expect(trace2.traceModel.storage().snapshotsForTest().length).toBeGreaterThan(0);
});

View file

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

View file

@ -1,6 +1,6 @@
{
"private": true,
"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
pw:api | page.waitForResponse @ a.test.ts:7
pw:api | page.click(div) @ a.test.ts:13
pw:api | response.text @ a.test.ts:8
expect | expect.toBeTruthy @ a.test.ts:9
pw:api | response.text @ a.test.ts:15
expect |expect.toBe @ a.test.ts:17
hook |After Hooks
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', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33186' }
}, async ({ runInlineTest, server }) => {

View file

@ -775,3 +775,26 @@ test('should respect --ignore-snapshots option', {
- 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');
}
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({
'playwright.config.ts': `
export default { projects: [{ name: 'p1' }, { name: 'p2' }] };
`,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
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(\`
- 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
--- a/a.spec.ts
+++ b/a.spec.ts
@@ -3,7 +3,7 @@
@@ -3,7 +3,8 @@
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(\`
- - heading "world"
+ - 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
});
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.
for (const webPackage of ['html-reporter', 'recorder', 'trace-viewer']) {
steps.push({
@ -323,6 +342,13 @@ if (watchMode) {
cwd: path.join(__dirname, '..', '..', 'packages', 'html-reporter'),
concurrent: true,
});
steps.push({
command: 'npx',
args: ['vite', '--port', '44225'],
shell: true,
cwd: path.join(__dirname, '..', '..', 'packages', 'recorder'),
concurrent: true,
});
}
// Generate injected.

View file

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