This page covers the HTML reporter system in @playwright/test: the Node.js backend that collects test results and writes the report to disk, the static HTTP server used to serve it, and the React frontend application that renders the report in a browser. For the base reporter interface and terminal reporters, see Built-in Reporters. For blob-based sharded report merging, see Blob Reporter and Report Merging.
The HTML reporter produces a self-contained directory (playwright-report/ by default) containing an index.html, a zip-encoded data bundle embedded within it, and a data/ folder of attachment files. When served over HTTP, the React frontend decodes the data bundle and renders an interactive test results browser.
The system has two distinct parts:
| Part | Location | Role |
|---|---|---|
HtmlReporter | packages/playwright/src/reporters/html.ts | ReporterV2 implementation; coordinates build |
HtmlBuilder | packages/playwright/src/reporters/html.ts | Serializes test data, copies app assets |
startHtmlReportServer | packages/playwright/src/reporters/html.ts | Serves the report folder over HTTP |
| React frontend | packages/html-reporter/src/ | Renders the interactive report UI |
gitCommitInfoPlugin | packages/playwright/src/plugins/gitCommitInfoPlugin.ts | Injects git/CI metadata into config.metadata |
HtmlReporter implements the ReporterV2 interface. Its lifecycle maps directly to the test runner's event callbacks:
Architecture: HtmlReporter build flow
Sources: packages/playwright/src/reporters/html.ts56-186
HtmlReporter._resolveOptions() merges reporter config options with environment variable overrides:
| Option | Env Variable | Default |
|---|---|---|
outputFolder | PLAYWRIGHT_HTML_OUTPUT_DIR (or legacy PLAYWRIGHT_HTML_REPORT) | playwright-report/ |
open | PLAYWRIGHT_HTML_OPEN (or legacy PW_TEST_HTML_REPORT_OPEN) | on-failure |
attachmentsBaseURL | PLAYWRIGHT_HTML_ATTACHMENTS_BASE_URL | data/ |
host | PLAYWRIGHT_HTML_HOST | localhost |
port | PLAYWRIGHT_HTML_PORT | 9323 (preferred) |
title | PLAYWRIGHT_HTML_TITLE | (none) |
noSnippets | PLAYWRIGHT_HTML_NO_SNIPPETS | false |
noCopyPrompt | PLAYWRIGHT_HTML_NO_COPY_PROMPT | false |
The open option accepts always, never, or on-failure. The report auto-opens only when process.stdin.isTTY is true and not running on CI.
Sources: packages/playwright/src/reporters/html.ts112-121 packages/playwright/src/reporters/html.ts167-185
HtmlBuilder.build() walks the project/file/test suite tree and produces two kinds of data:
report.json — the HTMLReport object containing aggregate stats, file summaries (TestFileSummary[]), project names, top-level errors, and shard machine timings.<fileId>.json — one per test file, containing the full TestFile object with all TestCase results and steps.Both are written into an in-memory zip (yazl.ZipFile). After all data is added, _writeReportData() appends the zip as a base64-encoded <script id="playwrightReportBase64"> tag to index.html. This makes the report fully self-contained.
Architecture: HtmlBuilder data model
Sources: packages/html-reporter/src/types.d.ts1-130 packages/playwright/src/reporters/html.ts251-366
HtmlBuilder._serializeAttachments() handles three cases:
data/<sha1>.<ext>. The attachment path is replaced with attachmentsBaseURL + sha1.data/<sha1>.<ext> similarly.stdout, stderr): ANSI escape codes are stripped and consecutive same-name entries are merged.If attachmentsBaseURL is set to a remote URL (e.g., https://some-url.com/), attachment references point to the remote host instead of the local data/ folder.
Sources: packages/playwright/src/reporters/html.ts456-526
When noSnippets is not set, createSnippets() reads each source file referenced by test steps and generates syntax-highlighted code snippet strings using codeFrameColumns from Babel. These are stored in TestStep.snippet and rendered inline in the test step tree.
Similarly, createErrorCodeframe() generates a code frame for test errors that have a source location.
Sources: packages/playwright/src/reporters/html.ts689-740
dedupeSteps() merges consecutive identical steps (same category, title, and source location) that have no errors into a single DedupedStep with a count > 1. This reduces noise for loops.
Sources: packages/playwright/src/reporters/html.ts668-687
startHtmlReportServer(folder) creates an HttpServer that serves the report folder. Requests to /trace/file?path=<absolute> are delegated to server.serveFile() for serving trace files from arbitrary paths. All other requests map to files within the report folder.
showHTMLReport() starts the server, prints the URL, and opens it in the browser using the open package.
Sources: packages/playwright/src/reporters/html.ts210-249
The HTML reporter frontend lives in packages/html-reporter/src/. It is a Vite-built React SPA. On load, it decodes window.playwrightReportBase64 (the base64 zip injected by HtmlBuilder) and uses it as its data source via the LoadedReport abstraction.
Navigation is hash-based. The URL hash encodes search parameters (#?key=value). ReportView defines three routes via the Route component and predicate functions:
| Predicate | Condition | View |
|---|---|---|
testFilesRoutePredicate | no testId, no speedboard | TestFilesView |
testCaseRoutePredicate | testId present | TestCaseView loaded via TestCaseViewLoader |
speedboardRoutePredicate | speedboard present, no testId | Speedboard |
The navigate() function in links.tsx uses history.pushState + dispatching a popstate event. useSearchParams() reads from a React context updated on popstate.
Sources: packages/html-reporter/src/reportView.tsx38-159 packages/html-reporter/src/links.tsx30-34 packages/html-reporter/src/links.tsx155-172
Architecture: Frontend component tree
Sources: packages/html-reporter/src/reportView.tsx packages/html-reporter/src/testFilesView.tsx packages/html-reporter/src/testCaseView.tsx packages/html-reporter/src/testResultView.tsx packages/html-reporter/src/speedboard.tsx
GlobalFilterView renders the status filter bar and the search input. StatsNavView renders the "All / Passed / Failed / Flaky / Skipped / ⏱ / ⚙" navigation items. Clicking a status filter sets s:<status> in the URL query. Ctrl+click appends rather than replaces filters.
The settings gear opens a Dialog with theme and "Merge files" toggle options. "Merge files" groups tests by their top-level describe block across files.
Sources: packages/html-reporter/src/headerView.tsx46-175
TestFilesHeader shows overall duration, start time, project name (if single project), filter counts, and a "Metadata" toggle. TestFilesView renders a list of TestFileView chips, each collapsible. By default, files auto-expand if the total visible test count stays below 200.
TestCaseListView renders individual test rows with:
#?testId=<id>Sources: packages/html-reporter/src/testFilesView.tsx packages/html-reporter/src/testFileView.tsx
TestCaseView displays a single test's full detail. It shows:
test.path.join(' › '))TraceLink)TabbedPane with one tab per test result (Run, Retry #1, …)TestResultViewKeyboard navigation is available: ArrowLeft/ArrowRight move to previous/next test in the filtered list.
Sources: packages/html-reporter/src/testCaseView.tsx
TestResultView renders a single result attempt. It partitions result.attachments into groups and renders each as a collapsible AutoChip:
| Section | Content |
|---|---|
| Errors | Error messages with optional TestScreenshotErrorView for screenshot diffs |
| Test Steps | StepTreeItem tree with duration, status icons, code snippets |
| Image mismatches | ImageDiffView with Diff/Actual/Expected/Side-by-side/Slider tabs |
| Screenshots | Inline <img> tags for image attachments |
| Traces | Trace preview thumbnail linking to the Trace Viewer |
| Videos | <video> elements |
| Attachments | Download/expand links for other attachments |
| Executed in Worker #N | TestCaseListView of all tests run in the same worker |
StepTreeItem is a recursive TreeItem that shows step title, location, duration, and a link to any step attachment. It lazy-loads children.
The Copy prompt button (controlled by noCopyPrompt) assembles a structured AI debugging prompt from test name, location, errors, stdout, and stderr.
Sources: packages/html-reporter/src/testResultView.tsx
Activated via #?speedboard in the URL. Speedboard renders two sections:
Shards: A GanttChart of shard execution timelines, drawn as an SVG with time-axis ticks. Data comes from HTMLReport.machines[], which HtmlBuilder populates from MachineData collected via onReportConfigure/onReportEnd.SlowestTests: A TestFileView-based list of tests sorted by descending duration, paginated in groups of 50.Sources: packages/html-reporter/src/speedboard.tsx packages/html-reporter/src/gantt.tsx
Filter.parse(expression) tokenizes the filter string and classifies tokens:
| Token prefix | Field |
|---|---|
p:<name> | project name |
s:<status> | outcome (passed, failed, flaky, skipped) |
@<tag> | test tag |
annot:<type>=<value> | annotation |
| (plain text) | full-text search across title, path, file, project, tags |
Tokens can be negated with !. Ctrl+click on nav items appends tokens; a normal click replaces the same-prefix token. Filter.matches(test) returns false for skipped tests by default (unless s:skipped is explicitly set).
filterWithQuery() in filter.ts handles the URL manipulation for filter changes.
Sources: packages/html-reporter/src/filter.ts
gitCommitInfoPlugin is a TestRunnerPlugin registered by addGitCommitInfoPlugin() at runner startup. In its setup() hook it detects the CI environment and populates config.metadata with:
| Key | Type | Source |
|---|---|---|
ci | CIInfo | Detected from env vars (GitHub Actions, GitLab CI, Jenkins) |
gitCommit | GitCommitInfo | git log -1 output (hash, subject, author, committer, branch) |
gitDiff | string | git diff output, up to 100,000 characters |
gitCommit and gitDiff are only collected when captureGitInfo.commit / captureGitInfo.diff is enabled, or when CI is detected and these options are not explicitly disabled.
Git operations have a 3-second timeout (GIT_OPERATIONS_TIMEOUT_MS).
MetadataView (in metadataView.tsx) renders this metadata on the report's main page. It displays PR title and link, author info, and commit timestamp when gitCommit is present. Arbitrary config.metadata keys (excluding ci, gitCommit, gitDiff, actualWorkers) are shown if show-metadata-other is in the URL query.
Architecture: gitCommitInfoPlugin data flow
Sources: packages/playwright/src/plugins/gitCommitInfoPlugin.ts packages/html-reporter/src/metadataView.tsx packages/playwright/src/isomorphic/types.d.ts
After a report is built, the output folder has this layout:
playwright-report/
├── index.html # App shell + embedded base64 zip <script>
├── data/
│ ├── <sha1>.png # Attachment files (screenshots, etc.)
│ └── <sha1>.webm
└── trace/ # Only present if any trace attachments exist
├── index.html
└── assets/
The zip embedded in index.html contains:
report.json — the HTMLReport aggregate object<fileId>.json — one TestFile per test fileLoadedReport in the frontend decodes this zip on load and caches per-file entries fetched lazily.
Sources: packages/playwright/src/reporters/html.ts340-378
The frontend uses a URL anchor system for deep-linking to specific attachments within a test result. The anchor search parameter holds an anchor ID like attachment-0 or attachment-trace. useAnchor() and useIsAnchored() in links.tsx react to the current anchor and trigger scroll-into-view or expansion of the relevant AutoChip. AttachmentLink registers itself with useAnchor and flashes when its attachment is anchored.
testResultHref() builds a full #?testId=...&run=...&anchor=... URL for linking from step trees and file list badges to specific attachment sections.
Refresh this wiki
This wiki was recently refreshed. Please wait 2 days to refresh again.