Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# 🎭 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-145.0.7632.18-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-146.0.1-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-26.0-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-informational)](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-145.0.7632.26-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-146.0.1-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-26.0-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord)

## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)

Playwright is a framework for Web Testing and Automation. It allows testing [Chromium](https://www.chromium.org/Home)<sup>1</sup>, [Firefox](https://www.mozilla.org/en-US/firefox/new/) and [WebKit](https://webkit.org/) with a single API. Playwright is built to enable cross-browser web automation that is **ever-green**, **capable**, **reliable**, and **fast**.

| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium<sup>1</sup> <!-- GEN:chromium-version -->145.0.7632.18<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium<sup>1</sup> <!-- GEN:chromium-version -->145.0.7632.26<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->26.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->146.0.1<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"build-android-driver": "./utils/build_android_driver.sh",
"innerloop": "playwright run-server --reuse-browser",
"playwright-cli": "node packages/playwright/lib/mcp/terminal/cli.js",
"test-playwright-cli": "playwright test --config=tests/mcp/playwright.config.ts --project=chrome cli.spec.ts",
"test-playwright-cli": "playwright test --config=tests/mcp/playwright.config.ts --project=chrome cli-",
"playwright-cli-readme": "node utils/generate_cli_help.js --readme"
},
"workspaces": [
Expand Down
12 changes: 8 additions & 4 deletions packages/html-reporter/src/chip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ export const Chip: React.FC<{
expanded?: boolean,
noInsets?: boolean,
setExpanded?: (expanded: boolean) => void,
children?: any,
children?: React.ReactNode,
body?: () => React.ReactNode | undefined,
dataTestId?: string,
}> = ({ header, footer, expanded, setExpanded, children, noInsets, dataTestId }) => {
}> = ({ header, footer, expanded, setExpanded, children, noInsets, body, dataTestId }) => {
const id = React.useId();
return <div className='chip' data-testid={dataTestId}>
<div
Expand All @@ -45,6 +46,7 @@ export const Chip: React.FC<{
</div>
{(!setExpanded || expanded) && <div id={id} role='region' className={clsx('chip-body', noInsets && 'chip-body-no-insets')}>
{children}
{body && body()}
{footer && <div className='chip-footer'>{footer}</div>}
</div>}
</div>;
Expand All @@ -54,10 +56,11 @@ export const AutoChip: React.FC<{
header: React.JSX.Element | string,
initialExpanded?: boolean,
noInsets?: boolean,
children?: any,
children?: React.ReactNode,
body?: () => React.ReactNode | undefined,
dataTestId?: string,
revealOnAnchorId?: AnchorID,
}> = ({ header, initialExpanded, noInsets, children, dataTestId, revealOnAnchorId }) => {
}> = ({ header, initialExpanded, noInsets, children, body, dataTestId, revealOnAnchorId }) => {
const [expanded, setExpanded] = React.useState(initialExpanded ?? true);
const onReveal = React.useCallback(() => setExpanded(true), []);
useAnchor(revealOnAnchorId, onReveal);
Expand All @@ -66,6 +69,7 @@ export const AutoChip: React.FC<{
expanded={expanded}
setExpanded={setExpanded}
noInsets={noInsets}
body={body}
dataTestId={dataTestId}
>
{children}
Expand Down
5 changes: 1 addition & 4 deletions packages/html-reporter/src/reportView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,9 @@ const TestCaseViewLoader: React.FC<{
</div>;
}

const { projectNames, metadata, options } = report.json();
return <div className='test-case-column'>
<TestCaseView
projectNames={projectNames}
testRunMetadata={metadata}
options={options}
report={report}
next={next}
prev={prev}
test={test}
Expand Down
14 changes: 6 additions & 8 deletions packages/html-reporter/src/testCaseView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import type { TestAnnotation } from '@playwright/test';
import type { HTMLReportOptions, TestCase, TestCaseSummary } from './types';
import type { TestCase, TestCaseSummary } from './types';
import * as React from 'react';
import { TabbedPane } from './tabbedPane';
import { AutoChip } from './chip';
Expand All @@ -29,18 +29,16 @@ import { msToString } from './utils';
import { clsx } from '@web/uiUtils';
import { CopyToClipboardContainer } from './copyToClipboard';
import { HeaderView } from './headerView';
import type { MetadataWithCommitInfo } from '@playwright/isomorphic/types';
import { ProjectAndTagLabelsView } from './labels';
import type { LoadedReport } from './loadedReport';

export const TestCaseView: React.FC<{
projectNames: string[],
report: LoadedReport,
test: TestCase,
testRunMetadata: MetadataWithCommitInfo | undefined,
next: TestCaseSummary | undefined,
prev: TestCaseSummary | undefined,
run: number,
options?: HTMLReportOptions,
}> = ({ projectNames, test, testRunMetadata, run, next, prev, options }) => {
}> = ({ report, test, run, next, prev }) => {
const [selectedResultIndex, setSelectedResultIndex] = React.useState(run);
const searchParams = useSearchParams();

Expand All @@ -66,7 +64,7 @@ export const TestCaseView: React.FC<{
<TraceLink test={test} trailingSeparator={true} />
<div className='test-case-duration'>{msToString(test.duration)}</div>
</div>
<ProjectAndTagLabelsView style={{ marginLeft: '6px' }} projectNames={projectNames} activeProjectName={test.projectName} otherLabels={test.tags} />
<ProjectAndTagLabelsView style={{ marginLeft: '6px' }} projectNames={report.json().projectNames} activeProjectName={test.projectName} otherLabels={test.tags} />
{/* If there are no results, display test annotations. Otherwise test annotations will be displayed alongside runtime annotations in individual result pane */}
{test.results.length === 0 && visibleTestAnnotations.length !== 0 && <AutoChip header='Annotations' dataTestId='test-case-annotations'>
{visibleTestAnnotations.map((annotation, index) => <TestCaseAnnotationView key={index} annotation={annotation} />)}
Expand All @@ -84,7 +82,7 @@ export const TestCaseView: React.FC<{
{!!visibleAnnotations.length && <AutoChip header='Annotations' dataTestId='test-case-annotations'>
{visibleAnnotations.map((annotation, index) => <TestCaseAnnotationView key={index} annotation={annotation} />)}
</AutoChip>}
<TestResultView test={test!} result={result} testRunMetadata={testRunMetadata} options={options} />
<TestResultView test={test!} result={result} report={report} />
</>;
},
})) || []} selectedTab={String(selectedResultIndex)} setSelectedTab={id => setSelectedResultIndex(+id)} />
Expand Down
2 changes: 1 addition & 1 deletion packages/html-reporter/src/testFileView.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
text-overflow: ellipsis;
}

.test-file-test:hover {
.test-file-test-selected, .test-file-test:hover {
background-color: var(--color-canvas-subtle);
}

Expand Down
35 changes: 25 additions & 10 deletions packages/html-reporter/src/testFileView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,13 @@ import { video, image } from './icons';
import { clsx } from '@web/uiUtils';
import { ProjectAndTagLabelsView } from './labels';

export const TestFileView: React.FC<React.PropsWithChildren<{
export const TestFileView: React.FC<{
file: TestFileSummary;
projectNames: string[];
isFileExpanded?: (fileId: string) => boolean;
setFileExpanded?: (fileId: string, expanded: boolean) => void;
footer?: React.JSX.Element | string;
}>> = ({ file, projectNames, isFileExpanded, setFileExpanded, footer }) => {
const searchParams = useSearchParams();
}> = ({ file, projectNames, isFileExpanded, setFileExpanded, footer }) => {
return <Chip
expanded={isFileExpanded ? isFileExpanded(file.fileId) : undefined}
noInsets={true}
Expand All @@ -42,15 +41,31 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
</span>}
footer={footer}
>
{file.tests.map(test =>
<div key={`test-${test.testId}`} className={clsx('test-file-test', 'test-file-test-outcome-' + test.outcome)}>
<TestCaseListView tests={file.tests} projectNames={projectNames} />
</Chip>;
};

export const TestCaseListView: React.FC<{
tests: TestCaseSummary[];
projectNames: string[];
runs?: number[];
selectedTestId?: string;
}> = ({ tests, projectNames, runs, selectedTestId }) => {
const searchParams = useSearchParams();
return <div role='list'>
{tests.map((test, index) => {
const run = runs?.[index];
const result = run !== undefined ? test.results[run] : undefined;
const href = testResultHref({ test, result }, searchParams);
const selected = selectedTestId === test.testId;
return <div key={`test-${test.testId}`} className={clsx('test-file-test', 'test-file-test-outcome-' + test.outcome, selected && 'test-file-test-selected')} role='listitem' aria-current={selected}>
<div className='hbox' style={{ alignItems: 'flex-start' }}>
<div className='hbox'>
<span className='test-file-test-status-icon'>
{statusIcon(test.outcome)}
</span>
<span>
<Link href={testResultHref({ test }, searchParams)} title={[...test.path, test.title].join(' › ')}>
<Link href={href} title={[...test.path, test.title].join(' › ')}>
<span className='test-file-title'>{[...test.path, test.title].join(' › ')}</span>
</Link>
<ProjectAndTagLabelsView style={{ marginLeft: '6px' }} projectNames={projectNames} activeProjectName={test.projectName} otherLabels={test.tags} />
Expand All @@ -60,17 +75,17 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
</div>
<div className='test-file-details-row'>
<div className='test-file-details-row-items'>
<Link href={testResultHref({ test }, searchParams)} title={[...test.path, test.title].join(' › ')} className='test-file-path-link'>
<Link href={href} title={[...test.path, test.title].join(' › ')} className='test-file-path-link'>
<span className='test-file-path'>{test.location.file}:{test.location.line}</span>
</Link>
<ImageDiffBadge test={test} />
<VideoBadge test={test} />
<TraceLink test={test} dim={true} />
</div>
</div>
</div>
)}
</Chip>;
</div>;
})}
</div>;
};

function ImageDiffBadge({ test }: { test: TestCaseSummary }) {
Expand Down
56 changes: 48 additions & 8 deletions packages/html-reporter/src/testResultView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
limitations under the License.
*/

import type { HTMLReportOptions, TestAttachment, TestCase, TestResult, TestStep } from './types';
import type { TestAttachment, TestCase, TestCaseSummary, TestResult, TestStep } from './types';
import * as React from 'react';
import { TreeItem } from './treeItem';
import { formatUrl, msToString } from './utils';
Expand All @@ -29,7 +29,8 @@ import * as icons from './icons';
import './testResultView.css';
import { useAsyncMemo } from '@web/uiUtils';
import { copyPrompt } from '@web/shared/prompts';
import type { MetadataWithCommitInfo } from '@playwright/isomorphic/types';
import type { LoadedReport } from './loadedReport';
import { TestCaseListView } from './testFileView';

interface ImageDiffWithAnchors extends ImageDiff {
anchors: string[];
Expand Down Expand Up @@ -71,11 +72,10 @@ function groupImageDiffs(screenshots: Set<TestAttachment>, result: TestResult):
}

export const TestResultView: React.FC<{
report: LoadedReport,
test: TestCase,
result: TestResult,
testRunMetadata: MetadataWithCommitInfo | undefined,
options?: HTMLReportOptions,
}> = ({ test, result, testRunMetadata, options }) => {
}> = ({ report, test, result }) => {
const { screenshots, videos, traces, otherAttachments, diffs, errors, otherAttachmentAnchors, screenshotAnchors, errorContext } = React.useMemo(() => {
const attachments = result.attachments.filter(a => !a.name.startsWith('_'));
const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/')));
Expand All @@ -92,7 +92,7 @@ export const TestResultView: React.FC<{
}, [result]);

const prompt = useAsyncMemo(async () => {
if (options?.noCopyPrompt)
if (report.json().options?.noCopyPrompt)
return undefined;

const stdoutAttachment = result.attachments.find(a => a.name === 'stdout');
Expand All @@ -105,14 +105,14 @@ export const TestResultView: React.FC<{
`- Name: ${test.path.join(' >> ')} >> ${test.title}`,
`- Location: ${test.location.file}:${test.location.line}:${test.location.column}`
].join('\n'),
metadata: testRunMetadata,
metadata: report.json().metadata,
errorContext: errorContext?.path ? await fetch(errorContext.path!).then(r => r.text()) : errorContext?.body,
errors: result.errors,
buildCodeFrame: async error => error.codeframe,
stdout,
stderr,
});
}, [test, errorContext, testRunMetadata, result], undefined);
}, [test, errorContext, report, result], undefined);

return <div className='test-result'>
{!!errors.length && <AutoChip header='Errors'>
Expand Down Expand Up @@ -177,6 +177,16 @@ export const TestResultView: React.FC<{
</Anchor>
)}
</AutoChip>}

<AutoChip header={`Executed in Worker #${result.workerIndex}`} dataTestId='worker-test-list' initialExpanded={false} noInsets={true} body={() => {
const list = buildWorkerLists(report).get(result.workerIndex) || { tests: [], runs: [] };
return <TestCaseListView
tests={list.tests}
runs={list.runs}
projectNames={report.json().projectNames}
selectedTestId={test.testId}
/>;
}}/>
</div>;
};

Expand Down Expand Up @@ -216,3 +226,33 @@ const StepTreeItem: React.FC<{
return snippet.concat(steps);
} : undefined} depth={depth}/>;
};

type WorkerLists = Map<number, { tests: TestCaseSummary[], runs: number[] }>;
const kWorkerListsSymbol = Symbol('workerLists');

function buildWorkerLists(report: LoadedReport): WorkerLists {
let data: WorkerLists | undefined = (report as any)[kWorkerListsSymbol];
if (!data) {
const lists = new Map<number, { test: TestCaseSummary, time: number, run: number }[]>();
for (const file of report.json().files) {
for (const test of file.tests) {
for (let index = 0; index < test.results.length; index++) {
let list = lists.get(test.results[index].workerIndex);
if (!list) {
list = [];
lists.set(test.results[index].workerIndex, list);
}
list.push({ test, time: new Date(test.results[index].startTime).valueOf(), run: index });
}
}
}

data = new Map();
for (const [workerIndex, list] of lists) {
list.sort((a, b) => a.time - b.time);
data.set(workerIndex, { tests: list.map(t => t.test), runs: list.map(t => t.run) });
}
(report as any)[kWorkerListsSymbol] = data;
}
return data;
}
3 changes: 3 additions & 0 deletions packages/html-reporter/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export type TestCaseSummary = {

export type TestResultSummary = {
attachments: { name: string, contentType: string, path?: string }[];
startTime: string;
workerIndex: number;
};

export type TestCase = Omit<TestCaseSummary, 'results'> & {
Expand All @@ -110,6 +112,7 @@ export type TestResult = {
attachments: TestAttachment[];
status: 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interrupted';
annotations: TestAnnotation[];
workerIndex: number;
};

export type TestStep = {
Expand Down
8 changes: 4 additions & 4 deletions packages/playwright-core/browsers.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
"browsers": [
{
"name": "chromium",
"revision": "1209",
"revision": "1210",
"installByDefault": true,
"browserVersion": "145.0.7632.18",
"browserVersion": "145.0.7632.26",
"title": "Chrome for Testing"
},
{
"name": "chromium-headless-shell",
"revision": "1209",
"revision": "1210",
"installByDefault": true,
"browserVersion": "145.0.7632.18",
"browserVersion": "145.0.7632.26",
"title": "Chrome Headless Shell"
},
{
Expand Down
Loading
Loading