coverage.json reference (schema version 1)
This page is the field-by-field reference for coverage.json, the machine-readable result of a run. The reporter and the playwright-ui-coverage report CLI write coverage.json next to index.html in the report folder (ui-coverage-report/ by default). It's the integration point for CI gates, dashboards, baselines and PR comments.
- TypeScript types:
import type { CoverageReport } from 'playwright-ui-coverage'(source: src/coverage-report.ts). - JSON Schema (draft 2020-12): schema/coverage.schema.json, shipped in the package and importable as
playwright-ui-coverage/coverage.schema.json. - The raw data folder (
data/ui-coverage-data.json,formatVersion) is a different file: input for rebuilding the report, not part of this contract.
Compatibility
schemaVersionis an integer, currently1. It changes only on breaking changes: a field removed or renamed, its type changed, or its meaning changed.New fields can be added in any release without a version change. Readers must ignore fields they don't know. The JSON Schema doesn't set
additionalProperties: falsefor that reason.Fields marked internal (
"x-internal": truein the schema) are written for the HTML report and may change in any release. In v1 that'sviews[].screenshots.Some values are heuristics whose content may change between releases while the field stays:
suggestedLocatortext,a11yHints[].messagewording, the set ofa11yHints[].ruleids, elementlabels.Check the version before reading:
jsif (report.schemaVersion !== 1) throw new Error(`unsupported coverage.json schemaVersion ${report.schemaVersion}`);
What is counted (the denominator)
Coverage is measured in units: interactive elements and link units.
coverage = (testedElementsCount + testedLinksCount)
/ (testedElementsCount + untestedElementsCount + testedLinksCount + untestedLinksCount) × 100rounded to one decimal; 100 when there are no units.
- An element is an interactive element of a view (see Which elements count), identified by its
keywithin the view. Elements sharing a key (a "Delete" button on every row) are one unit, whateverinstancessays, and so is a group (see Element grouping): the "Add to cart" button of every product card, adata-uicov-group. A group is tested when any member is. - An element is tested when a counted test interacted with it in that view. By default only each test's last passing attempt counts (
countFailedTests: truecounts every attempt). Elements touched only by attempts that didn't pass are untested and also counted infailingOnlyElementsCount. - A link unit is the set of
<a href>elements of a view that lead to one other view (destination URLs mapped to views by the same function as page URLs,normalizeUrl), identified by the destination view's name (key). It is tested when a counted test visited the destination view, by any path. An<a>in a link unit is not also counted as an element. - Links that stay elements (tested when clicked): links within the same view (
#fragment,#, the page itself or another page of the same view),javascript:and empty hrefs, links inside iframes, all links in component tests (viewBy: 'file'), and links to other origins (unlesslinks: { external: 'ignore' }, which drops them).links: falsemakes every link an element, as in reports written before link units. - Untested views (
untestedViews): destination views of untested links that no recorded test visited. They are not inviewsand don't add units of their own; their incoming links are the untested link units. a11yHintsCountis informational and never affectscoverage.- The overall summary sums the views' counts;
coverageis computed from the sums (not an average of the views' percents).
Top level: CoverageReport
| Field | Type | |
|---|---|---|
schemaVersion | 1 | See Compatibility. |
generatedAt | string | ISO 8601 time the report was built. |
generator | { name, version } | name is "playwright-ui-coverage", version the package version that wrote the file. |
summary | ReportSummary | Overall counts. |
views | ViewCoverage[] | One per view, sorted by name. |
untestedViews | UntestedView[] | Destination views of untested links that no recorded test visited, sorted by name. |
tests | TestRef[] | Every test with recorded data, sorted by title. |
thresholds | ThresholdCheck[] | Results of the threshold option; empty when it isn't set. |
insights | { mostRepeated } | See Insights. |
playwrightReport | { path, attachmentsBaseURL }, optional | Playwright's HTML report, when linked: path is its folder relative to the folder of coverage.json (/ separators), attachmentsBaseURL where tests[].trace files are, relative to path. |
config | object, optional | The UI coverage config that built the report: source (file: the config file relative to the working directory, or null; inline: keys set in the reporter options), profile (name or null) and resolved (the merged config; informational, its keys follow the config file, not this schema's versioning). |
baseline | BaselineCheck, optional | Result of the baseline option; absent when it isn't set. |
summary: ReportSummary / CoverageCounts
views[].summary is a CoverageCounts; the top-level summary is a ReportSummary, the same plus viewCount and untestedViewCount.
| Field | Type | |
|---|---|---|
viewCount | integer | Top level only: number of entries in views. |
untestedViewCount | integer | Top level only: number of entries in untestedViews. |
coverage | number | Percent, 0–100, one decimal. See the denominator. |
testedElementsCount | integer | |
untestedElementsCount | integer | Includes failingOnlyElementsCount. |
testedLinksCount | integer | Link units whose destination view a counted test visited. |
untestedLinksCount | integer | Link units whose destination view no counted test visited. |
failingOnlyElementsCount | integer | Untested elements that only attempts that didn't pass touched. |
a11yHintsCount | integer | Accessibility hints on these elements. |
views[]: ViewCoverage
A view is a group of page URLs: named by the first matching views pattern (/users/:username), else by normalizeUrl or the automatic rules (/users/42?tab=x → /users/:id); URLs excluded by viewFilters are in no view. Or, with viewBy: 'file' (component tests), a spec file path.
| Field | Type | |
|---|---|---|
name | string | As shown in the report and matched by threshold.views. |
reportUrl | string | This view in the HTML report, relative to the folder of coverage.json: index.html# + encodeURIComponent(name). |
summary | CoverageCounts | |
elements | ElementCoverage[] | Interactive elements, not the <a> elements of links. Untested first, then by label. |
links | LinkCoverage[] | Link units: this view's links grouped by destination view. Untested first, then by key. |
screenshots | ViewScreenshot[] | Internal. The screenshots the HTML report shows: { file, dpr, testId?, boxes: [{ key, x, y, w, h }] }, file relative to the folder of coverage.json, boxes in CSS pixels. Which screenshots are picked depends on timing. |
views[].elements[]: ElementCoverage
(view name, key) identifies an element across runs, as long as the identity rules (significantAttributes, attributeFilters, …) don't change. This is the identity to use for baselines and run-to-run comparisons.
| Field | Type | |
|---|---|---|
key | string | Structural selector, unique within the view. Inside an iframe or open shadow root, prefixed by the host's key and >> (iframe[data-testid="payments"] >> [data-testid="card"], promo-banner >> button:nth-of-type(2)). |
label | string | role "name", with (+N variants) when instances sharing the key had different labels. For display. |
tag | string | Lowercase tag name; ? for an element only seen in an interaction, never in an inventory. |
instances | integer | Max number of instances seen at once. |
group | { name?, reason, members }, optional | Set when the element stands for several. reason: markup (data-uicov-group), config (elementGroups), same-key (instances sharing the key), row (the same column of a table or grid), repeated (the same control in repeated structures) or label (a <label> merged into its control); more may be added. name: the group's name, explicit groups only. members: distinct elements (keys before grouping) in it. Absent for a single element. |
tested | boolean | |
testIds | string[] | Ids (into tests) of the tests whose interactions counted. Empty when untested. |
failingTestIds | string[] | Ids of tests that touched it only in attempts that didn't pass. Non-empty with tested: false = "failing tests only". |
interactionCounts | Record<string, integer> | Counted interactions by type, e.g. { "click": 3, "input": 1 }. Empty when untested. Sum it for a total. |
role | string, optional | Recorded ARIA role. |
name | string, optional | Recorded accessible name. |
nameFrom | string, optional | Where name came from: aria-labelledby, aria-label, label, placeholder, title, alt, value, text or name. |
attrs | Record<string, string>, optional | Identifying attributes (id, name, role, type, aria-label, href, tabindex, contenteditable, data-*, your significantAttributes). |
frames | string[], optional | Keys of the <iframe> elements the element is inside, outermost first; each is a prefix of key. Absent in the top document. |
suggestedLocator | string, optional | One line of Playwright code that interacts with the element. Heuristic. |
a11yHints | { rule, message }[], optional | Absent when there are none. rule is one of no-name, placeholder-only, duplicate-name, not-focusable, no-role; more may be added. |
Optional fields are absent (not null) when unknown, e.g. for data recorded by older versions.
views[].links[]: LinkCoverage
The links of a view that go to one other view: one unit. (view name, key) identifies it across runs (baselines list untested ones under untestedLinks).
| Field | Type | |
|---|---|---|
key | string | The destination view's name, e.g. /users/:id. Unique within the view. |
label | string | link "name" of the first referrer by label, with (+N more) when several <a> elements go there. For display. |
tested | boolean | A counted test visited the destination view (not necessarily through these links). |
destinations | string[] | Distinct destination URLs, sorted, at most 20: path, query and hash when on the page's origin, else the full URL. |
destinationCount | integer | Number of distinct destination URLs (may exceed destinations.length). |
referrers | ElementCoverage[] | The <a> elements of the view that link there, with suggestedLocator etc. Their tested/testIds say whether a test interacted with them: informational, not part of coverage. |
untestedViews[]: UntestedView
| Field | Type | |
|---|---|---|
name | string | View name, from the destination URL the same way page URLs are named. |
destinations | string[] | Distinct destination URLs, sorted, at most 20. |
destinationCount | integer | Number of distinct destination URLs. |
referrers | { view, label, keys }[] | The views linking to it (view, a name in views), with their link unit's label and the <a> elements' keys (views[].links[].referrers[].key). |
A view seen only in attempts that don't count (failing tests) is in views, so it isn't an untested view, but links to it are untested.
tests[]: TestRef
| Field | Type | |
|---|---|---|
id | string | TestCase.id: stable across runs for the same file, title and project. |
title | string | Spec file and title path: users.spec.ts › add a user. |
project | string, optional | Playwright project name. |
passed | boolean | Whether the attempt the report used passed (had the expected status, so test.fail() tests pass when they fail). |
retry | integer | TestResult.retry of that attempt: the last passing one, else the last one. |
trace | string, optional | That attempt's trace file in the HTML report's attachments folder, when linked: <playwrightReport.path>/<attachmentsBaseURL><trace>. |
thresholds[]: ThresholdCheck
| Field | Type | |
|---|---|---|
scope | "overall" | "view" | |
view | string, when scope is view | The view name; for a pattern that matched no view, the pattern. |
pattern | string, optional | The threshold.views glob that set this requirement. |
required | number | Required coverage. |
actual | number | null | Actual coverage; null when the view wasn't in this run. |
status | "passed" | "failed" | "missing" | missing only warns; the run fails when any check is failed. |
baseline: BaselineCheck
Present when the baseline reporter option is set (and not mode: 'off'). Units are identified by (view, kind, key).
| Field | Type | |
|---|---|---|
file | string | The baseline file, relative to the folder of coverage.json (/ separators). |
mode | "check" | "update" | |
status | "passed" | "failed" | "missing" | "updated" | failed: newGaps is non-empty and the run failed. missing: no baseline file yet (warning only). updated: mode: 'update' rewrote the file; never fails. |
nextFile | string, optional | Check mode: where the current state was written (<file> → <name>.next.json), relative like file. |
allowNewViews | boolean | Whether untested units of views not in the baseline were allowed. |
newGaps | { view, kind, key, label, newView }[] | Untested units the baseline doesn't list, by view, kind, key. kind is element or link; newView is true when the whole view is new. In update mode: the units added to the file. |
resolved | { view, kind, key, reason, label? }[] | Units the baseline lists as untested that aren't any more: reason tested (with label) or removed (no longer in the view). |
addedViews | string[] | Views not in the baseline (renames excluded). |
removedViews | string[] | Baseline views not in this run (renames excluded). Never fail. |
renamedViews | { from, to }[] | A baseline view that disappeared and a new view with similar untested keys (Jaccard index ≥ 0.5), compared as one. |
The baseline file itself (ui-coverage-baseline.json) is { "baselineVersion": 1, "views": { "<view>": { "untestedElements": [keys], "untestedLinks"?: [keys] } } }, keys sorted.
Insights
insights.mostRepeated: up to 10 elements used by at least 2 tests, most first. Each is { view, key, label, testCount, interactionCount, testShare }, where testShare is testCount as a percent of tests.length. These are often setup steps repeated in every test.
Using it in a script
A custom gate, e.g. failing when any view under /admin is below 60%:
import fs from 'node:fs';
const report = JSON.parse(fs.readFileSync('ui-coverage-report/coverage.json', 'utf8'));
if (report.schemaVersion !== 1) throw new Error(`unsupported coverage.json schemaVersion ${report.schemaVersion}`);
const low = report.views.filter((v) => v.name.startsWith('/admin') && v.summary.coverage < 60);
if (low.length) {
console.error(low.map((v) => `${v.name}: ${v.summary.coverage}%`).join('\n'));
process.exit(1);
}For the common cases there are built-in gates: thresholds and the baseline.
Example
{
"schemaVersion": 1,
"generatedAt": "2026-01-01T12:00:00.000Z",
"generator": { "name": "playwright-ui-coverage", "version": "0.1.0" },
"summary": {
"viewCount": 1,
"coverage": 50,
"testedElementsCount": 1,
"untestedElementsCount": 1,
"testedLinksCount": 0,
"untestedLinksCount": 0,
"failingOnlyElementsCount": 0,
"a11yHintsCount": 0
},
"views": [
{
"name": "/users/:id",
"reportUrl": "index.html#%2Fusers%2F%3Aid",
"summary": {
"coverage": 50,
"testedElementsCount": 1,
"untestedElementsCount": 1,
"testedLinksCount": 0,
"untestedLinksCount": 0,
"failingOnlyElementsCount": 0,
"a11yHintsCount": 0
},
"elements": [
{
"key": "[data-action=\"delete-user\"]",
"label": "button \"Delete\"",
"tag": "button",
"instances": 1,
"tested": false,
"testIds": [],
"failingTestIds": [],
"interactionCounts": {},
"role": "button",
"name": "Delete",
"nameFrom": "text",
"attrs": { "data-action": "delete-user" },
"suggestedLocator": "await page.getByRole('button', { name: 'Delete' }).click();"
},
{
"key": "main > div:nth-of-type(1) > a",
"label": "link \"← Back to users\"",
"tag": "a",
"instances": 1,
"tested": true,
"testIds": ["7d08affc2ce055c80dfd-96667847379d7d7361ae"],
"failingTestIds": [],
"interactionCounts": { "click": 1 },
"role": "link",
"name": "← Back to users",
"nameFrom": "text",
"attrs": { "href": "/users" }
}
],
"screenshots": []
}
],
"tests": [
{ "id": "7d08affc2ce055c80dfd-96667847379d7d7361ae", "title": "users.spec.ts › open a user", "project": "chromium", "passed": true, "retry": 0 }
],
"thresholds": [{ "scope": "overall", "required": 30, "actual": 50, "status": "passed" }],
"insights": { "mostRepeated": [] }
}Changes from the unversioned format (before schema version 1)
| Before | Version 1 |
|---|---|
total, tested, percent, failingOnly, a11yHints at the top level | summary.{testedElementsCount, untestedElementsCount, coverage, failingOnlyElementsCount, a11yHintsCount}, plus viewCount and link counts |
routes[] with route, total, tested, percent, failingOnly, a11yHints | views[] with name, reportUrl and summary |
element testedBy / testedByFailing (titles) | testIds / failingTestIds (always present; titles in tests) |
element interactionTotal, testCount | removed: sum interactionCounts, use testIds.length |
screenshots[].source, .test (title) | screenshots[].file, .testId (internal) |
tests[].run | tests[].retry, plus passed |
htmlReport | playwrightReport |
mostRepeated[] with route, tests, interactions, share | insights.mostRepeated[] with view, testCount, interactionCount, testShare |
thresholds[] only when configured, with target, passed | always present, with scope and view; use status |