@setsquare/review-sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +57 -0
- package/README.md +338 -0
- package/THIRD-PARTY-NOTICES.md +61 -0
- package/dist/autoscan.d.ts +25 -0
- package/dist/autoscan.d.ts.map +1 -0
- package/dist/autoscan.js +332 -0
- package/dist/autoscan.js.map +1 -0
- package/dist/axe.d.ts +96 -0
- package/dist/axe.d.ts.map +1 -0
- package/dist/axe.js +210 -0
- package/dist/axe.js.map +1 -0
- package/dist/bin/setsquare.d.ts +3 -0
- package/dist/bin/setsquare.d.ts.map +1 -0
- package/dist/bin/setsquare.js +21 -0
- package/dist/bin/setsquare.js.map +1 -0
- package/dist/brand.d.ts +84 -0
- package/dist/brand.d.ts.map +1 -0
- package/dist/brand.js +335 -0
- package/dist/brand.js.map +1 -0
- package/dist/checks/dynamic.d.ts +57 -0
- package/dist/checks/dynamic.d.ts.map +1 -0
- package/dist/checks/dynamic.js +184 -0
- package/dist/checks/dynamic.js.map +1 -0
- package/dist/checks/rule-pack.d.ts +34 -0
- package/dist/checks/rule-pack.d.ts.map +1 -0
- package/dist/checks/rule-pack.js +270 -0
- package/dist/checks/rule-pack.js.map +1 -0
- package/dist/checks/versions.d.ts +28 -0
- package/dist/checks/versions.d.ts.map +1 -0
- package/dist/checks/versions.js +45 -0
- package/dist/checks/versions.js.map +1 -0
- package/dist/ci.d.ts +37 -0
- package/dist/ci.d.ts.map +1 -0
- package/dist/ci.js +199 -0
- package/dist/ci.js.map +1 -0
- package/dist/contract.d.ts +334 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +35 -0
- package/dist/contract.js.map +1 -0
- package/dist/dedupe.d.ts +79 -0
- package/dist/dedupe.d.ts.map +1 -0
- package/dist/dedupe.js +102 -0
- package/dist/dedupe.js.map +1 -0
- package/dist/document.d.ts +29 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +94 -0
- package/dist/document.js.map +1 -0
- package/dist/envelope.d.ts +124 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +337 -0
- package/dist/envelope.js.map +1 -0
- package/dist/escape.d.ts +14 -0
- package/dist/escape.d.ts.map +1 -0
- package/dist/escape.js +20 -0
- package/dist/escape.js.map +1 -0
- package/dist/findings.d.ts +47 -0
- package/dist/findings.d.ts.map +1 -0
- package/dist/findings.js +229 -0
- package/dist/findings.js.map +1 -0
- package/dist/fixture.d.ts +7 -0
- package/dist/fixture.d.ts.map +1 -0
- package/dist/fixture.js +237 -0
- package/dist/fixture.js.map +1 -0
- package/dist/group.d.ts +214 -0
- package/dist/group.d.ts.map +1 -0
- package/dist/group.js +709 -0
- package/dist/group.js.map +1 -0
- package/dist/html-report.d.ts +68 -0
- package/dist/html-report.d.ts.map +1 -0
- package/dist/html-report.js +649 -0
- package/dist/html-report.js.map +1 -0
- package/dist/human.d.ts +65 -0
- package/dist/human.d.ts.map +1 -0
- package/dist/human.js +111 -0
- package/dist/human.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/infer.d.ts +65 -0
- package/dist/infer.d.ts.map +1 -0
- package/dist/infer.js +67 -0
- package/dist/infer.js.map +1 -0
- package/dist/insights.d.ts +140 -0
- package/dist/insights.d.ts.map +1 -0
- package/dist/insights.js +483 -0
- package/dist/insights.js.map +1 -0
- package/dist/install.d.ts +8 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +114 -0
- package/dist/install.js.map +1 -0
- package/dist/links.d.ts +45 -0
- package/dist/links.d.ts.map +1 -0
- package/dist/links.js +45 -0
- package/dist/links.js.map +1 -0
- package/dist/mask.d.ts +85 -0
- package/dist/mask.d.ts.map +1 -0
- package/dist/mask.js +98 -0
- package/dist/mask.js.map +1 -0
- package/dist/observe.d.ts +51 -0
- package/dist/observe.d.ts.map +1 -0
- package/dist/observe.js +233 -0
- package/dist/observe.js.map +1 -0
- package/dist/options.d.ts +249 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +185 -0
- package/dist/options.js.map +1 -0
- package/dist/output.d.ts +113 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +231 -0
- package/dist/output.js.map +1 -0
- package/dist/perf.d.ts +11 -0
- package/dist/perf.d.ts.map +1 -0
- package/dist/perf.js +150 -0
- package/dist/perf.js.map +1 -0
- package/dist/reading-level.d.ts +73 -0
- package/dist/reading-level.d.ts.map +1 -0
- package/dist/reading-level.js +189 -0
- package/dist/reading-level.js.map +1 -0
- package/dist/reporter.d.ts +94 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +709 -0
- package/dist/reporter.js.map +1 -0
- package/dist/rules.d.ts +55 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +77 -0
- package/dist/rules.js.map +1 -0
- package/dist/runtime.d.ts +32 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +67 -0
- package/dist/runtime.js.map +1 -0
- package/dist/sanitise.d.ts +116 -0
- package/dist/sanitise.d.ts.map +1 -0
- package/dist/sanitise.js +247 -0
- package/dist/sanitise.js.map +1 -0
- package/dist/scan.d.ts +60 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +246 -0
- package/dist/scan.js.map +1 -0
- package/dist/screens.d.ts +98 -0
- package/dist/screens.d.ts.map +1 -0
- package/dist/screens.js +301 -0
- package/dist/screens.js.map +1 -0
- package/dist/settle.d.ts +65 -0
- package/dist/settle.d.ts.map +1 -0
- package/dist/settle.js +162 -0
- package/dist/settle.js.map +1 -0
- package/dist/shots.d.ts +109 -0
- package/dist/shots.d.ts.map +1 -0
- package/dist/shots.js +113 -0
- package/dist/shots.js.map +1 -0
- package/dist/style.d.ts +47 -0
- package/dist/style.d.ts.map +1 -0
- package/dist/style.js +154 -0
- package/dist/style.js.map +1 -0
- package/dist/suite.d.ts +218 -0
- package/dist/suite.d.ts.map +1 -0
- package/dist/suite.js +219 -0
- package/dist/suite.js.map +1 -0
- package/dist/summary.d.ts +268 -0
- package/dist/summary.d.ts.map +1 -0
- package/dist/summary.js +1308 -0
- package/dist/summary.js.map +1 -0
- package/dist/unscannable.d.ts +75 -0
- package/dist/unscannable.d.ts.map +1 -0
- package/dist/unscannable.js +111 -0
- package/dist/unscannable.js.map +1 -0
- package/dist/versions.d.ts +13 -0
- package/dist/versions.d.ts.map +1 -0
- package/dist/versions.js +39 -0
- package/dist/versions.js.map +1 -0
- package/dist/wcag-map.d.ts +19 -0
- package/dist/wcag-map.d.ts.map +1 -0
- package/dist/wcag-map.js +52 -0
- package/dist/wcag-map.js.map +1 -0
- package/dist/with-setsquare.d.ts +26 -0
- package/dist/with-setsquare.d.ts.map +1 -0
- package/dist/with-setsquare.js +70 -0
- package/dist/with-setsquare.js.map +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import { BRAND_CSS, MASTHEAD_CSS, masthead } from './brand.js';
|
|
2
|
+
import { esc } from './escape.js';
|
|
3
|
+
import { humanInstant, shortCommit } from './human.js';
|
|
4
|
+
import { insightBlocks } from './insights.js';
|
|
5
|
+
import { renderScreensSection, SCREENS_CSS } from './screens.js';
|
|
6
|
+
import { conformanceGrid, conformanceSummary, ruleSurfaceShare, stateCoverage, } from '@setsquare/schema/conformance';
|
|
7
|
+
import { evidenceQuality, groupFindings, perfRows, surfaceRows, testOutcomes, totals, } from './summary.js';
|
|
8
|
+
const STATE_WORD = {
|
|
9
|
+
failed: 'FAIL',
|
|
10
|
+
cantTell: 'CHECK',
|
|
11
|
+
rulesPassed: 'OK',
|
|
12
|
+
inapplicable: 'N/A',
|
|
13
|
+
untested: '—',
|
|
14
|
+
};
|
|
15
|
+
const STATE_LABEL = {
|
|
16
|
+
failed: 'failed',
|
|
17
|
+
cantTell: 'needs a person',
|
|
18
|
+
rulesPassed: 'every automated rule passed',
|
|
19
|
+
inapplicable: 'checks ran; nothing to check on this surface',
|
|
20
|
+
untested: 'untested',
|
|
21
|
+
};
|
|
22
|
+
function n(value) {
|
|
23
|
+
return value.toLocaleString('en-GB');
|
|
24
|
+
}
|
|
25
|
+
function percent(affected, total) {
|
|
26
|
+
if (total === 0 || affected === 0)
|
|
27
|
+
return '0%';
|
|
28
|
+
const rounded = Math.round((affected / total) * 100);
|
|
29
|
+
return rounded === 0 ? '<1%' : `${rounded}%`;
|
|
30
|
+
}
|
|
31
|
+
/** Why a merged group is partial, in one plain sentence. */
|
|
32
|
+
function partialReason(group) {
|
|
33
|
+
if (group.missing.length > 0) {
|
|
34
|
+
return `${group.missing.length} invocation${group.missing.length === 1 ? '' : 's'} of run group “${group.id}” began but ${group.missing.length === 1 ? 'was' : 'were'} never merged.`;
|
|
35
|
+
}
|
|
36
|
+
if (group.stale.length > 0) {
|
|
37
|
+
return `${group.stale.length} invocation${group.stale.length === 1 ? '' : 's'} of run group “${group.id}” ${group.stale.length === 1 ? 'was' : 'were'} excluded as stale.`;
|
|
38
|
+
}
|
|
39
|
+
if (group.expectedParts !== null && group.completed !== group.expectedParts) {
|
|
40
|
+
return `${group.completed} of ${group.expectedParts} declared invocations of run group “${group.id}” have merged.`;
|
|
41
|
+
}
|
|
42
|
+
return `An invocation of run group “${group.id}” was cut short before it finished.`;
|
|
43
|
+
}
|
|
44
|
+
/** `href` is allowed only for URLs that came out of the envelope itself. */
|
|
45
|
+
function helpLink(url) {
|
|
46
|
+
if (!url || !url.startsWith('https://'))
|
|
47
|
+
return '';
|
|
48
|
+
return ` <a href="${esc(url)}" rel="noopener">rule guidance</a>`;
|
|
49
|
+
}
|
|
50
|
+
export function renderHtmlReport(input) {
|
|
51
|
+
/*
|
|
52
|
+
* The P1b blocks, from the same wording functions the terminal uses.
|
|
53
|
+
*
|
|
54
|
+
* A definition list rather than a table: these are figure/label pairs with
|
|
55
|
+
* prose attached, not tabular data, and a table would promise columns that do
|
|
56
|
+
* not line up across blocks. Each block's notes carry the caveats that stop a
|
|
57
|
+
* reader drawing the wrong conclusion — "counts are per engine", "time spent
|
|
58
|
+
* not time added" — so they are rendered, never dropped as decoration.
|
|
59
|
+
*/
|
|
60
|
+
const insightSections = insightBlocks(input.envelope, {
|
|
61
|
+
...(input.declaredAxes ? { declaredAxes: input.declaredAxes } : {}),
|
|
62
|
+
...(input.unmeasuredAxes ? { unmeasuredAxes: input.unmeasuredAxes } : {}),
|
|
63
|
+
...(input.suite ? { suite: input.suite } : {}),
|
|
64
|
+
...(input.overhead ? { overhead: input.overhead } : {}),
|
|
65
|
+
})
|
|
66
|
+
.map((block, index) => {
|
|
67
|
+
const id = `h-insight-${index}`;
|
|
68
|
+
const rows = block.figures
|
|
69
|
+
.map((figure) => ` <div><dt>${esc(String(figure.value))}</dt>` +
|
|
70
|
+
`<dd>${esc(figure.label)}` +
|
|
71
|
+
(figure.notes && figure.notes.length > 0
|
|
72
|
+
? `<p class="muted">${figure.notes.map((note) => esc(note)).join('<br />')}</p>`
|
|
73
|
+
: '') +
|
|
74
|
+
`</dd></div>`)
|
|
75
|
+
.join('\n');
|
|
76
|
+
const notes = block.notes.map((note) => ` <p class="muted">${esc(note)}</p>`).join('\n');
|
|
77
|
+
return `
|
|
78
|
+
<section aria-labelledby="${id}">
|
|
79
|
+
<h2 id="${id}">${esc(block.heading)}</h2>
|
|
80
|
+
<dl class="insight">
|
|
81
|
+
${rows}
|
|
82
|
+
</dl>
|
|
83
|
+
${notes}
|
|
84
|
+
</section>`;
|
|
85
|
+
})
|
|
86
|
+
.join('\n');
|
|
87
|
+
const { envelope } = input;
|
|
88
|
+
const summary = conformanceSummary(envelope, {
|
|
89
|
+
...(input.criterionRules ? { criterionRules: input.criterionRules } : {}),
|
|
90
|
+
});
|
|
91
|
+
const grid = conformanceGrid(envelope);
|
|
92
|
+
const count = totals(envelope);
|
|
93
|
+
const tests = testOutcomes(envelope);
|
|
94
|
+
const quality = evidenceQuality(envelope);
|
|
95
|
+
const groups = groupFindings(envelope);
|
|
96
|
+
const rows = surfaceRows(envelope);
|
|
97
|
+
const perf = perfRows(envelope);
|
|
98
|
+
const failingRules = new Set(envelope.scans.flatMap((scan) => scan.findings.filter((f) => f.status === 'violation').map((f) => f.ruleId))).size;
|
|
99
|
+
const verdictLine = () => {
|
|
100
|
+
if (count.scans === 0) {
|
|
101
|
+
return 'Nothing was scanned. This is not a clean result — it means nothing was looked at.';
|
|
102
|
+
}
|
|
103
|
+
const { verdict } = summary;
|
|
104
|
+
if (verdict.failuresDetectedAt) {
|
|
105
|
+
const fixes = verdict.nextFixes.map((r) => `${r.criterion} ${r.name}`).join(', ');
|
|
106
|
+
return (`Failures detected at Level ${verdict.failuresDetectedAt}. ` +
|
|
107
|
+
`To clear them, fix: ${esc(fixes)}. Automated checks cover only part of WCAG.`);
|
|
108
|
+
}
|
|
109
|
+
if (verdict.noFailuresDetectedAt) {
|
|
110
|
+
return (`No Level ${verdict.noFailuresDetectedAt} violations detected across ` +
|
|
111
|
+
`${n(summary.surfacesScanned)} surface${summary.surfacesScanned === 1 ? '' : 's'} (automated checks only).`);
|
|
112
|
+
}
|
|
113
|
+
return 'No conformance evidence in this run.';
|
|
114
|
+
};
|
|
115
|
+
const ladder = summary.levels
|
|
116
|
+
.map((level) => {
|
|
117
|
+
let state;
|
|
118
|
+
let cls;
|
|
119
|
+
if (!level.assessed) {
|
|
120
|
+
state = 'not assessed';
|
|
121
|
+
cls = 'muted';
|
|
122
|
+
}
|
|
123
|
+
else if (level.failing.length > 0) {
|
|
124
|
+
state = `failures detected — ${level.failing.length === 1 ? 'one criterion' : `${level.failing.length} criteria`}`;
|
|
125
|
+
cls = 'bad';
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
state = 'no failures detected in what was scanned';
|
|
129
|
+
cls = 'good';
|
|
130
|
+
}
|
|
131
|
+
const scans = level.assessedScans
|
|
132
|
+
? ` <span class="muted">(assessed on ${level.assessedScans.aaa} of ${level.assessedScans.total} scans)</span>`
|
|
133
|
+
: '';
|
|
134
|
+
return `<li><span class="level">${level.level}</span> <span class="${cls}">${state}</span>${scans}</li>`;
|
|
135
|
+
})
|
|
136
|
+
.join('\n ');
|
|
137
|
+
/*
|
|
138
|
+
* The WCAG-EM-shaped scope statement: what was scanned, in which states, when,
|
|
139
|
+
* by what. The first page of any audit, and what lets a person staple this
|
|
140
|
+
* behind a manual sample instead of arguing with it.
|
|
141
|
+
*/
|
|
142
|
+
const coverageRows = stateCoverage(envelope);
|
|
143
|
+
const scopeRows = coverageRows
|
|
144
|
+
.map((row) => `<tr><th scope="row">${esc(row.surface)}</th><td>${row.states.map(esc).join(', ')}${row.defaultOnly ? ' <span class="muted">(default state only)</span>' : ''}</td></tr>`)
|
|
145
|
+
.join('\n ');
|
|
146
|
+
const defaultOnly = coverageRows.filter((row) => row.defaultOnly).length;
|
|
147
|
+
const stateNote = defaultOnly > 0 && coverageRows.length > 0
|
|
148
|
+
? `<p class="muted">${defaultOnly} of ${coverageRows.length} surfaces have evidence in their default
|
|
149
|
+
state only. Error, loading and empty states are where regressions hide —
|
|
150
|
+
<code>setsquare.scan(page, surface, { state })</code> captures them.</p>`
|
|
151
|
+
: '';
|
|
152
|
+
const targets = [...new Set(envelope.scans.map((scan) => scan.wcagTarget))].sort().join(', ') || 'AA';
|
|
153
|
+
/* The grid: criteria × surfaces, real table, state words in every cell. */
|
|
154
|
+
const gridHead = grid.surfaces
|
|
155
|
+
.map((surface, i) => `<th scope="col"><span class="idx">${i + 1}</span> ${esc(surface)}</th>`)
|
|
156
|
+
.join('');
|
|
157
|
+
const gridBody = grid.rows
|
|
158
|
+
.map((row) => {
|
|
159
|
+
const cells = row.cells
|
|
160
|
+
.map((cell) => `<td class="s-${cell.state}">${STATE_WORD[cell.state]}</td>`)
|
|
161
|
+
.join('');
|
|
162
|
+
const removed = row.removedIn22
|
|
163
|
+
? ' <span class="muted">(removed in WCAG 2.2 — counts toward no level)</span>'
|
|
164
|
+
: '';
|
|
165
|
+
return `<tr><th scope="row">${row.criterion} ${esc(row.name)} <span class="level">${row.removedIn22 ? '—' : row.level}</span>${removed}</th>${cells}</tr>`;
|
|
166
|
+
})
|
|
167
|
+
.join('\n ');
|
|
168
|
+
const rollupDetail = (rollup) => {
|
|
169
|
+
const parts = [];
|
|
170
|
+
if (rollup.failedSurfaces.length > 0) {
|
|
171
|
+
parts.push(`<p><strong>Failed</strong> on ${rollup.failedSurfaces.length} of ${rollup.totalSurfaces} surfaces ` +
|
|
172
|
+
`(${percent(rollup.failedSurfaces.length, rollup.totalSurfaces)}): ${rollup.failedSurfaces.map(esc).join(', ')}</p>`);
|
|
173
|
+
}
|
|
174
|
+
if (rollup.cantTellSurfaces.length > 0) {
|
|
175
|
+
parts.push(`<p><strong>Needs a person</strong> on: ${rollup.cantTellSurfaces.map(esc).join(', ')}</p>`);
|
|
176
|
+
}
|
|
177
|
+
if (rollup.rulesPassedSurfaces.length > 0) {
|
|
178
|
+
parts.push(`<p><strong>Every automated rule passed</strong> on: ${rollup.rulesPassedSurfaces.map(esc).join(', ')}</p>`);
|
|
179
|
+
}
|
|
180
|
+
if (rollup.inapplicableSurfaces.length > 0) {
|
|
181
|
+
parts.push(`<p><strong>Nothing to check</strong> (checks ran, no such content) on: ${rollup.inapplicableSurfaces.map(esc).join(', ')}</p>`);
|
|
182
|
+
}
|
|
183
|
+
if (rollup.failingRules.length > 0) {
|
|
184
|
+
parts.push(`<p>Failing rules: <code>${rollup.failingRules.map(esc).join('</code>, <code>')}</code></p>`);
|
|
185
|
+
}
|
|
186
|
+
const passing = rollup.rules.filter((rule) => !rollup.failingRules.includes(rule));
|
|
187
|
+
if (passing.length > 0) {
|
|
188
|
+
parts.push(`<p class="muted">Other rules with evidence here: <code>${passing.map(esc).join('</code>, <code>')}</code></p>`);
|
|
189
|
+
}
|
|
190
|
+
return parts.join('\n ');
|
|
191
|
+
};
|
|
192
|
+
/*
|
|
193
|
+
* Grouped by outcome, not by criterion number.
|
|
194
|
+
*
|
|
195
|
+
* Numeric order put a failure at 1.4.3 twenty rows below a pass at 1.1.1, so
|
|
196
|
+
* reading the failures meant reading everything. Nobody does that. The order
|
|
197
|
+
* below is the order people actually care in — what is broken, what needs a
|
|
198
|
+
* human, what held, what did not apply — with numeric order kept INSIDE each
|
|
199
|
+
* group so a known criterion is still findable.
|
|
200
|
+
*
|
|
201
|
+
* Criteria removed in 2.2 come last: they count toward no level, so they are
|
|
202
|
+
* the least actionable thing on the page.
|
|
203
|
+
*/
|
|
204
|
+
const byNumber = (a, b) => a.criterion.localeCompare(b.criterion, 'en', { numeric: true });
|
|
205
|
+
const outcomeGroups = [
|
|
206
|
+
{
|
|
207
|
+
heading: 'Failures',
|
|
208
|
+
note: 'An automated check failed on at least one surface.',
|
|
209
|
+
rollups: summary.levels.flatMap((level) => level.failing).sort(byNumber),
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
heading: 'Needs a person',
|
|
213
|
+
note: 'A check ran and could not decide. These are not passes.',
|
|
214
|
+
rollups: summary.levels.flatMap((level) => level.cantTell).sort(byNumber),
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
heading: 'Every automated rule passed',
|
|
218
|
+
note: 'Evidence that the automated rules held here — not criterion conformance.',
|
|
219
|
+
rollups: summary.levels.flatMap((level) => level.rulesPassed).sort(byNumber),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
heading: 'Nothing to check',
|
|
223
|
+
note: 'The checks ran and these pages contain nothing the criterion regulates.',
|
|
224
|
+
rollups: summary.levels.flatMap((level) => level.inapplicable).sort(byNumber),
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
heading: 'Removed in WCAG 2.2',
|
|
228
|
+
note: 'Kept for the record. These count toward no level.',
|
|
229
|
+
rollups: [...summary.removed].sort(byNumber),
|
|
230
|
+
},
|
|
231
|
+
];
|
|
232
|
+
const criterionDetail = (rollup) => `
|
|
233
|
+
<details${rollup.state === 'failed' ? ' open' : ''}>
|
|
234
|
+
<summary><span class="s-${rollup.state} pill">${STATE_WORD[rollup.state]}</span> ${rollup.criterion} ${esc(rollup.name)} <span class="level">${rollup.removedIn22 ? 'removed in 2.2' : `Level ${rollup.level}`}</span></summary>
|
|
235
|
+
<div>
|
|
236
|
+
${rollupDetail(rollup)}
|
|
237
|
+
</div>
|
|
238
|
+
</details>`;
|
|
239
|
+
const criteriaDetails = outcomeGroups
|
|
240
|
+
.filter((group) => group.rollups.length > 0)
|
|
241
|
+
.map((group) => `
|
|
242
|
+
<h3 class="outcome">${group.heading} <span class="count">${n(group.rollups.length)}</span></h3>
|
|
243
|
+
<p class="muted outcome-note">${group.note}</p>
|
|
244
|
+
${group.rollups.map(criterionDetail).join('\n')}`)
|
|
245
|
+
.join('\n');
|
|
246
|
+
/* Coverage ledger, with the notRun/notAutomatable split when the map is present. */
|
|
247
|
+
const countedLevels = summary.levels.filter((level) => level.level !== 'AAA' || level.assessed);
|
|
248
|
+
const rulesPassedCount = countedLevels.reduce((sum, level) => sum + level.rulesPassed.length, 0);
|
|
249
|
+
const cantTellCount = countedLevels.reduce((sum, level) => sum + level.cantTell.length, 0);
|
|
250
|
+
const inapplicableRollups = countedLevels.flatMap((level) => level.inapplicable);
|
|
251
|
+
const untested = countedLevels.flatMap((level) => level.untested);
|
|
252
|
+
const coverage = [];
|
|
253
|
+
if (rulesPassedCount > 0) {
|
|
254
|
+
coverage.push(`<li><strong>${rulesPassedCount}</strong> criteria: every automated rule passed — evidence, not criterion conformance</li>`);
|
|
255
|
+
}
|
|
256
|
+
if (cantTellCount > 0)
|
|
257
|
+
coverage.push(`<li><strong>${cantTellCount}</strong> criteria need a person</li>`);
|
|
258
|
+
if (inapplicableRollups.length > 0) {
|
|
259
|
+
coverage.push(`<li><strong>${inapplicableRollups.length}</strong> criteria: checks ran, nothing to check on these pages — ` +
|
|
260
|
+
`${inapplicableRollups.map((r) => r.criterion).join(', ')}</li>`);
|
|
261
|
+
}
|
|
262
|
+
if (untested.length > 0) {
|
|
263
|
+
if (input.criterionRules) {
|
|
264
|
+
const noCheck = untested.filter((r) => r.untestedReason === 'notAutomatable');
|
|
265
|
+
const notRun = untested.filter((r) => r.untestedReason === 'notRun');
|
|
266
|
+
if (noCheck.length > 0) {
|
|
267
|
+
coverage.push(`<li><strong>${noCheck.length}</strong> criteria have no automated check in this engine: ` +
|
|
268
|
+
`${noCheck.map((r) => r.criterion).join(', ')}</li>`);
|
|
269
|
+
}
|
|
270
|
+
if (notRun.length > 0) {
|
|
271
|
+
coverage.push(`<li><strong>${notRun.length}</strong> criteria have checks that did not run here: ` +
|
|
272
|
+
`${notRun.map((r) => r.criterion).join(', ')}</li>`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
coverage.push(`<li><strong>${untested.length}</strong> criteria not automatically checked in this run</li>`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const surfacesBody = rows
|
|
280
|
+
.map((row) => {
|
|
281
|
+
const state = row.violations > 0 ? 'failed' : row.incomplete > 0 ? 'cantTell' : 'rulesPassed';
|
|
282
|
+
const word = row.violations > 0 ? 'FAIL' : row.incomplete > 0 ? 'CHECK' : 'pass';
|
|
283
|
+
return (`<tr><th scope="row">${esc(row.label)}</th>` +
|
|
284
|
+
`<td class="s-${state}">${word}</td>` +
|
|
285
|
+
`<td class="num">${n(row.violations)}</td>` +
|
|
286
|
+
`<td class="num">${n(row.incomplete)}</td>` +
|
|
287
|
+
`<td>${row.rules.map((rule) => `<code>${esc(rule)}</code>`).join(', ')}</td></tr>`);
|
|
288
|
+
})
|
|
289
|
+
.join('\n ');
|
|
290
|
+
const ruleSections = (status) => groups
|
|
291
|
+
.filter((group) => group.status === status)
|
|
292
|
+
.map((group) => {
|
|
293
|
+
const share = ruleSurfaceShare(envelope, group.ruleId);
|
|
294
|
+
const spread = status === 'violation'
|
|
295
|
+
? `on ${share.affected} of ${share.total} surfaces (${percent(share.affected, share.total)})`
|
|
296
|
+
: `on ${group.contexts.length} surface${group.contexts.length === 1 ? '' : 's'}`;
|
|
297
|
+
return `
|
|
298
|
+
<details>
|
|
299
|
+
<summary><code>${esc(group.ruleId)}</code> — ${esc(group.impact)}, ${n(group.nodes)} element${group.nodes === 1 ? '' : 's'} ${spread}${group.criteria.length > 0 ? ` — WCAG ${group.criteria.map(esc).join(', ')}` : ''}</summary>
|
|
300
|
+
<div>
|
|
301
|
+
${group.failureSummary ? `<p>${esc(group.failureSummary)}</p>` : ''}
|
|
302
|
+
${group.selectors.length > 0 ? `<p>Where: <code>${group.selectors.map(esc).join('</code>, <code>')}</code>${group.selectorsTruncated ? ' <span class="muted">(sample — every selector is in the embedded run data)</span>' : ''}</p>` : ''}
|
|
303
|
+
<p>${esc(group.contexts.join(', '))}${helpLink(group.helpUrl)}</p>
|
|
304
|
+
</div>
|
|
305
|
+
</details>`;
|
|
306
|
+
})
|
|
307
|
+
.join('\n');
|
|
308
|
+
const perfBody = perf
|
|
309
|
+
.map((row) => `<tr><th scope="row">${esc(row.label)}</th>` +
|
|
310
|
+
`<td class="num">${row.lcpMs !== undefined ? `${n(Math.round(row.lcpMs))}ms` : '—'}</td>` +
|
|
311
|
+
`<td class="num">${row.cls !== undefined ? row.cls.toFixed(2) : '—'}</td>` +
|
|
312
|
+
`<td class="num">${row.loadMs !== undefined ? `${n(Math.round(row.loadMs))}ms` : '—'}</td>` +
|
|
313
|
+
`<td class="num">${row.ttfbMs !== undefined ? `${n(Math.round(row.ttfbMs))}ms` : '—'}</td></tr>`)
|
|
314
|
+
.join('\n ');
|
|
315
|
+
const testsLine = envelope.results.length > 0
|
|
316
|
+
? `Playwright: ${tests.passed} passed` +
|
|
317
|
+
(tests.failed > 0 ? `, ${tests.failed} failed` : '') +
|
|
318
|
+
(tests.flaky > 0 ? `, ${tests.flaky} flaky` : '') +
|
|
319
|
+
(tests.skipped > 0 ? `, ${tests.skipped} skipped` : '') +
|
|
320
|
+
'.'
|
|
321
|
+
: '';
|
|
322
|
+
/*
|
|
323
|
+
* The group banner comes before every number in the document: a report over
|
|
324
|
+
* a half-merged group that leads with its conformance verdict is a report
|
|
325
|
+
* that will be quoted without the half that changes what it means.
|
|
326
|
+
*/
|
|
327
|
+
const group = input.group;
|
|
328
|
+
const groupBanner = group
|
|
329
|
+
? group.coverage === 'complete'
|
|
330
|
+
? ''
|
|
331
|
+
: `
|
|
332
|
+
<p class="verdict ${group.coverage === 'partial' ? 'bad' : ''}">${group.coverage === 'partial'
|
|
333
|
+
? `<strong>Partial run.</strong> ${esc(partialReason(group))} Evidence from what is missing is absent, not clean — do not treat this artifact as a baseline.`
|
|
334
|
+
: `<strong>Coverage unconfirmed.</strong> ${n(group.completed)} invocation${group.completed === 1 ? '' : 's'} of run group “${esc(group.id)}” merged; whether more were intended was not declared (set <code>SETSQUARE_RUN_PARTS</code>). Treat this artifact as current, not necessarily final.`}</p>`
|
|
335
|
+
: '';
|
|
336
|
+
const groupSection = group
|
|
337
|
+
? `
|
|
338
|
+
<section aria-labelledby="h-group">
|
|
339
|
+
<h2 id="h-group">Run group</h2>
|
|
340
|
+
<p class="muted">This is the merged evidence of run group “${esc(group.id)}” — one logical run spanning
|
|
341
|
+
${n(group.completed)} Playwright invocation${group.completed === 1 ? '' : 's'}${group.expectedParts !== null ? ` of ${n(group.expectedParts)} declared` : ''}. Test ids are prefixed with each invocation's label.</p>
|
|
342
|
+
<div class="tablewrap">
|
|
343
|
+
<table>
|
|
344
|
+
<thead><tr><th scope="col">Invocation</th><th scope="col">Lane</th><th scope="col">Started</th><th scope="col">Duration</th><th scope="col">Playwright</th><th scope="col">Tests</th><th scope="col">Scans</th></tr></thead>
|
|
345
|
+
<tbody>
|
|
346
|
+
${group.parts
|
|
347
|
+
.map((part) => `<tr><th scope="row">${esc(part.label)}</th><td>${esc(part.lane ?? '—')}</td><td>${esc(part.startedAt)}</td><td class="num">${n(Math.round(part.durationMs / 1000))}s</td><td>${esc(part.playwrightStatus)}</td><td class="num">${n(part.tests)}</td><td class="num">${n(part.scans)}</td></tr>`)
|
|
348
|
+
.join('\n ')}
|
|
349
|
+
</tbody>
|
|
350
|
+
</table>
|
|
351
|
+
</div>${group.missing.length > 0
|
|
352
|
+
? `
|
|
353
|
+
<p class="bad">${n(group.missing.length)} invocation${group.missing.length === 1 ? '' : 's'} began but ${group.missing.length === 1 ? 'was' : 'were'} never merged: <code>${group.missing.map(esc).join('</code>, <code>')}</code>.</p>`
|
|
354
|
+
: ''}
|
|
355
|
+
</section>`
|
|
356
|
+
: '';
|
|
357
|
+
/*
|
|
358
|
+
* The screens block sits after the evidence and before the provenance: it is
|
|
359
|
+
* the part of this document a reviewer scans rather than reads, and putting it
|
|
360
|
+
* above the conformance verdict would let a page of pictures answer a question
|
|
361
|
+
* about WCAG that pictures cannot answer.
|
|
362
|
+
*/
|
|
363
|
+
const screensSection = input.screens && input.screens.rows.length > 0
|
|
364
|
+
? `\n${renderScreensSection(input.screens)}`
|
|
365
|
+
: '';
|
|
366
|
+
const warningsBlock = input.warnings.length > 0
|
|
367
|
+
? `
|
|
368
|
+
<section aria-labelledby="h-warnings">
|
|
369
|
+
<h2 id="h-warnings">Worth knowing</h2>
|
|
370
|
+
<ul>
|
|
371
|
+
${input.warnings.map((warning) => `<li>${esc(warning)}</li>`).join('\n ')}
|
|
372
|
+
</ul>
|
|
373
|
+
</section>`
|
|
374
|
+
: '';
|
|
375
|
+
// `</script>`-proofing: `<` never appears raw inside the data block.
|
|
376
|
+
const embedded = JSON.stringify(envelope).replaceAll('<', '\\u003c');
|
|
377
|
+
return `<!doctype html>
|
|
378
|
+
<html lang="en">
|
|
379
|
+
<head>
|
|
380
|
+
<meta charset="utf-8">
|
|
381
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
382
|
+
<meta name="color-scheme" content="light dark">
|
|
383
|
+
<title>Setsquare — accessibility evidence, ${esc(humanInstant(envelope.generatedAt))}</title>
|
|
384
|
+
<style>
|
|
385
|
+
${BRAND_CSS}
|
|
386
|
+
/*
|
|
387
|
+
* The page's own names, mapped onto brand roles rather than renaming every
|
|
388
|
+
* rule below: one block to read, one place to change.
|
|
389
|
+
*
|
|
390
|
+
* The status roles lost their tinted BACKGROUNDS in the process, deliberately.
|
|
391
|
+
* tokens.css verifies each status colour as text on a surface and as a fill
|
|
392
|
+
* under its own ink pair; it has no tints, and inventing a pale red would mean
|
|
393
|
+
* shipping a contrast ratio nobody measured into an accessibility report. So a
|
|
394
|
+
* failing cell is bold danger-coloured TEXT, and the solid fill is kept for the
|
|
395
|
+
* pills, where role-under-its-own-ink is a measured pair.
|
|
396
|
+
*
|
|
397
|
+
* The faint --none-fg collapses into --ss-ink-muted: the palette has no second,
|
|
398
|
+
* fainter grey that clears 7:1, and the untested glyph is already distinct by
|
|
399
|
+
* content.
|
|
400
|
+
*
|
|
401
|
+
* (No backticks in this comment. It sits inside a template literal, so one
|
|
402
|
+
* would end the string and the parse error would surface pages away.)
|
|
403
|
+
*/
|
|
404
|
+
:root {
|
|
405
|
+
--bg: var(--ss-surface); --fg: var(--ss-ink); --muted: var(--ss-ink-muted);
|
|
406
|
+
--line: var(--ss-border); --code-bg: var(--ss-surface-raised);
|
|
407
|
+
--bad-fg: var(--ss-danger); --warn-fg: var(--ss-warning); --good-fg: var(--ss-success);
|
|
408
|
+
--none-fg: var(--ss-ink-muted);
|
|
409
|
+
}
|
|
410
|
+
${MASTHEAD_CSS}
|
|
411
|
+
* { box-sizing: border-box; }
|
|
412
|
+
body {
|
|
413
|
+
margin: 0 auto; padding: 2rem 1.5rem 4rem; max-width: 72rem;
|
|
414
|
+
background: var(--bg); color: var(--fg);
|
|
415
|
+
font: 16px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
416
|
+
}
|
|
417
|
+
h1 { font-size: 1.6rem; margin: 0 0 .25rem; }
|
|
418
|
+
h2 { font-size: 1.2rem; margin: 2.5rem 0 .75rem; }
|
|
419
|
+
a { color: inherit; }
|
|
420
|
+
:focus-visible { outline: 3px solid var(--fg); outline-offset: 2px; }
|
|
421
|
+
code { background: var(--code-bg); padding: .1em .35em; border-radius: 4px; font-size: .875em; overflow-wrap: anywhere; }
|
|
422
|
+
.muted { color: var(--muted); }
|
|
423
|
+
.verdict { font-size: 1.1rem; margin: 1rem 0; padding: 1rem; border: 1px solid var(--line); border-radius: 8px; }
|
|
424
|
+
ul.ladder { list-style: none; padding: 0; margin: .5rem 0; }
|
|
425
|
+
ul.ladder li { padding: .2rem 0; }
|
|
426
|
+
.level { display: inline-block; min-width: 3ch; font-weight: 700; }
|
|
427
|
+
.good { color: var(--good-fg); }
|
|
428
|
+
.bad { color: var(--bad-fg); font-weight: 600; }
|
|
429
|
+
.figures { display: flex; flex-wrap: wrap; gap: 1.5rem; margin: 1.5rem 0; }
|
|
430
|
+
.figures div { border: 1px solid var(--line); border-radius: 8px; padding: .75rem 1rem; }
|
|
431
|
+
.figures strong { display: block; font-size: 1.4rem; }
|
|
432
|
+
/*
|
|
433
|
+
* The P1b insight blocks. A definition list, not the .figures stat strip:
|
|
434
|
+
* these rows carry prose under each figure, and a flex row of boxes would
|
|
435
|
+
* squeeze the caveats into columns nobody reads. The caveats are the point.
|
|
436
|
+
*/
|
|
437
|
+
.insight { margin: 1rem 0; }
|
|
438
|
+
.insight > div { display: grid; grid-template-columns: 5rem 1fr; gap: .25rem 1rem; padding: .5rem 0; border-top: 1px solid var(--line); }
|
|
439
|
+
.insight > div:first-child { border-top: 0; }
|
|
440
|
+
.insight dt { font-weight: 600; text-align: right; font-variant-numeric: tabular-nums; }
|
|
441
|
+
.insight dd { margin: 0; }
|
|
442
|
+
.insight dd p { margin: .25rem 0 0; }
|
|
443
|
+
@media (max-width: 34rem) {
|
|
444
|
+
/* One column below the fold width: a 5rem gutter beside wrapping prose stops
|
|
445
|
+
being a gutter and starts being a squeeze. */
|
|
446
|
+
.insight > div { grid-template-columns: 1fr; }
|
|
447
|
+
.insight dt { text-align: left; }
|
|
448
|
+
}
|
|
449
|
+
.tablewrap { overflow-x: auto; }
|
|
450
|
+
table { border-collapse: collapse; width: 100%; margin: .75rem 0; }
|
|
451
|
+
th, td { border: 1px solid var(--line); padding: .4rem .6rem; text-align: left; vertical-align: top; }
|
|
452
|
+
|
|
453
|
+
/*
|
|
454
|
+
* Row banding, and the sticky criterion column.
|
|
455
|
+
*
|
|
456
|
+
* Neither is required by WCAG. Rows are already delineated by a 1px border in
|
|
457
|
+
* --ss-border, which tokens.css measures at 3.00:1 and is therefore at the
|
|
458
|
+
* 1.4.11 non-text-contrast threshold; and state is carried by the WORD in each
|
|
459
|
+
* cell, not by its colour, which is what 1.4.1 actually asks. Both of these are
|
|
460
|
+
* readability, and they are worth doing anyway.
|
|
461
|
+
*
|
|
462
|
+
* The bands are the two surfaces the palette already verifies, alternating —
|
|
463
|
+
* no new colour, so nothing new to measure. Light bands #ffffff against
|
|
464
|
+
* #f2f2f2, dark #0a0a0a against #1a1a1a, and tokens.css records each theme's
|
|
465
|
+
* ratios against its WORSE case, so the better surface cannot fail: ink is
|
|
466
|
+
* 18.76:1 / 17.40:1 and every status colour clears 7:1 on both.
|
|
467
|
+
*
|
|
468
|
+
* NB: no backticks anywhere in this style block — it is inside a template
|
|
469
|
+
* literal, so one ends the string and the parse error lands pages away.
|
|
470
|
+
*
|
|
471
|
+
* The band travels as a custom property rather than a background on the row,
|
|
472
|
+
* because the sticky cell has to paint the SAME band or it shows the wrong
|
|
473
|
+
* colour the moment it detaches.
|
|
474
|
+
*/
|
|
475
|
+
tr { --row-bg: var(--ss-surface); }
|
|
476
|
+
tbody tr:nth-child(even) { --row-bg: var(--ss-surface-raised); }
|
|
477
|
+
th, td { background: var(--row-bg); }
|
|
478
|
+
|
|
479
|
+
/*
|
|
480
|
+
* The grid is sixteen surfaces wide inside a horizontal scroller, and what you
|
|
481
|
+
* lose scrolling right is not which row you are on — the borders handle that —
|
|
482
|
+
* it is WHICH CRITERION, because the row header has left the viewport. Pinning
|
|
483
|
+
* it is the difference between a readable table and a wall of FAIL.
|
|
484
|
+
*
|
|
485
|
+
* A box-shadow draws the right edge rather than border-right: under
|
|
486
|
+
* border-collapse: collapse the border belongs to the table, not the cell, so
|
|
487
|
+
* it does not travel with a sticky cell and the pinned column loses its edge.
|
|
488
|
+
*/
|
|
489
|
+
.grid th[scope="row"], .grid thead th:first-child {
|
|
490
|
+
position: sticky; left: 0; z-index: 1;
|
|
491
|
+
box-shadow: 1px 0 0 var(--line);
|
|
492
|
+
}
|
|
493
|
+
/*
|
|
494
|
+
* The header cell sits above the body cells it shares a column with, so that
|
|
495
|
+
* when the table is ALSO scrolled down the two never fight over the corner.
|
|
496
|
+
* Pinning the row headers without pinning this one was the first version, and
|
|
497
|
+
* it looked broken in exactly the way you would expect: the surface headers
|
|
498
|
+
* slid underneath the criterion labels.
|
|
499
|
+
*/
|
|
500
|
+
.grid thead th:first-child { z-index: 2; }
|
|
501
|
+
th[scope="col"] .idx { color: var(--muted); font-weight: 400; }
|
|
502
|
+
td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
503
|
+
td.s-failed { color: var(--bad-fg); font-weight: 700; }
|
|
504
|
+
td.s-cantTell { color: var(--warn-fg); font-weight: 600; }
|
|
505
|
+
td.s-rulesPassed { color: var(--good-fg); }
|
|
506
|
+
td.s-inapplicable { color: var(--muted); }
|
|
507
|
+
/* Pills are the one solid fill: a role under its own -ink is a measured pair. */
|
|
508
|
+
.pill.s-failed { background: var(--ss-danger); color: var(--ss-danger-ink); font-weight: 700; }
|
|
509
|
+
.pill.s-cantTell { background: var(--ss-warning); color: var(--ss-warning-ink); font-weight: 600; }
|
|
510
|
+
.pill.s-rulesPassed { background: var(--ss-success); color: var(--ss-success-ink); }
|
|
511
|
+
.pill.s-inapplicable { background: var(--ss-surface-raised); color: var(--muted); border: 1px solid var(--line); }
|
|
512
|
+
td.s-untested { color: var(--none-fg); }
|
|
513
|
+
h3.outcome { font-size: 1rem; margin: 1.75rem 0 .1rem; display: flex; align-items: baseline; gap: .5rem; }
|
|
514
|
+
h3.outcome .count { font-weight: 400; color: var(--muted); font-size: .875rem; }
|
|
515
|
+
.outcome-note { margin: 0 0 .5rem; font-size: .875rem; }
|
|
516
|
+
.pill { display: inline-block; min-width: 4.5ch; text-align: center; padding: 0 .4em; border-radius: 4px; }
|
|
517
|
+
details { border: 1px solid var(--line); border-radius: 8px; padding: .5rem .75rem; margin: .5rem 0; }
|
|
518
|
+
details > div { margin-top: .5rem; }
|
|
519
|
+
summary { cursor: pointer; }
|
|
520
|
+
footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); color: var(--muted); font-size: .875rem; }
|
|
521
|
+
footer code { font-size: .8rem; }
|
|
522
|
+
.ok { color: var(--good-fg); }
|
|
523
|
+
${SCREENS_CSS}
|
|
524
|
+
</style>
|
|
525
|
+
</head>
|
|
526
|
+
<body>
|
|
527
|
+
${masthead('Setsquare — accessibility evidence',
|
|
528
|
+
/*
|
|
529
|
+
* The friendly instant is visible; the exact one stays in the `datetime`
|
|
530
|
+
* attribute, so a machine reading this page still gets milliseconds and a
|
|
531
|
+
* person reading it gets a date. The commit is shortened for display only —
|
|
532
|
+
* the full SHA is in run.json, which is what anything automated reads.
|
|
533
|
+
*/
|
|
534
|
+
`<time datetime="${esc(envelope.generatedAt)}">${esc(humanInstant(envelope.generatedAt))}</time>` +
|
|
535
|
+
` · ${esc(envelope.source.branch)} @ ${esc(shortCommit(envelope.source.commit))}` +
|
|
536
|
+
`${testsLine ? ` · ${esc(testsLine)}` : ''}`)}${groupBanner}
|
|
537
|
+
<main>
|
|
538
|
+
<section aria-labelledby="h-verdict">
|
|
539
|
+
<h2 id="h-verdict">Conformance — WCAG 2.2 <span class="muted">(includes every 2.0 and 2.1 criterion)</span></h2>
|
|
540
|
+
<p class="verdict">${verdictLine()}</p>
|
|
541
|
+
<ul class="ladder">
|
|
542
|
+
${ladder}
|
|
543
|
+
</ul>
|
|
544
|
+
<ul>
|
|
545
|
+
${coverage.join('\n ')}
|
|
546
|
+
</ul>
|
|
547
|
+
</section>
|
|
548
|
+
|
|
549
|
+
<section aria-labelledby="h-figures">
|
|
550
|
+
<h2 id="h-figures">This run</h2>
|
|
551
|
+
<div class="figures">
|
|
552
|
+
<div><strong>${n(count.surfaces)}</strong> surfaces scanned${count.themes > 0 ? ` in ${count.themes} theme${count.themes === 1 ? '' : 's'}` : ''}</div>
|
|
553
|
+
<div><strong>${n(failingRules)}</strong> rules failing, on ${n(count.violations)} elements</div>
|
|
554
|
+
<div><strong>${n(count.incomplete)}</strong> elements need a person</div>
|
|
555
|
+
<div><strong>${n(count.passes)}</strong> checks passed across ${n(quality.rulesAssessed)} rules</div>
|
|
556
|
+
</div>
|
|
557
|
+
</section>
|
|
558
|
+
|
|
559
|
+
<section aria-labelledby="h-grid">
|
|
560
|
+
<h2 id="h-grid">Conformance grid</h2>
|
|
561
|
+
<p class="muted">Criteria with evidence, per surface. A surface counts as failing if it fails in any scanned
|
|
562
|
+
state or theme. <strong>OK</strong> means every automated rule for the criterion passed there — evidence,
|
|
563
|
+
not criterion conformance; <strong>CHECK</strong> means automation could not decide; <strong>N/A</strong> means
|
|
564
|
+
the checks ran and the surface contains nothing the criterion regulates; <strong>—</strong> means
|
|
565
|
+
nothing looked.</p>
|
|
566
|
+
<div class="tablewrap">
|
|
567
|
+
<table class="grid">
|
|
568
|
+
<caption class="muted">WCAG success criteria × surfaces</caption>
|
|
569
|
+
<thead><tr><th scope="col">Criterion</th>${gridHead}</tr></thead>
|
|
570
|
+
<tbody>
|
|
571
|
+
${gridBody}
|
|
572
|
+
</tbody>
|
|
573
|
+
</table>
|
|
574
|
+
</div>
|
|
575
|
+
${criteriaDetails}
|
|
576
|
+
</section>
|
|
577
|
+
|
|
578
|
+
<section aria-labelledby="h-scope">
|
|
579
|
+
<h2 id="h-scope">Scope</h2>
|
|
580
|
+
<p>Assessed against WCAG 2.2 ${esc(targets)} with automated checks only, on the surfaces and states below.
|
|
581
|
+
Anything not listed was not looked at.</p>
|
|
582
|
+
<div class="tablewrap">
|
|
583
|
+
<table>
|
|
584
|
+
<thead><tr><th scope="col">Surface</th><th scope="col">States scanned</th></tr></thead>
|
|
585
|
+
<tbody>
|
|
586
|
+
${scopeRows}
|
|
587
|
+
</tbody>
|
|
588
|
+
</table>
|
|
589
|
+
</div>
|
|
590
|
+
${stateNote}
|
|
591
|
+
</section>
|
|
592
|
+
|
|
593
|
+
<section aria-labelledby="h-surfaces">
|
|
594
|
+
<h2 id="h-surfaces">Surfaces</h2>
|
|
595
|
+
<div class="tablewrap">
|
|
596
|
+
<table>
|
|
597
|
+
<thead><tr><th scope="col">Surface</th><th scope="col">State</th><th scope="col">Failing elements</th><th scope="col">Needs review</th><th scope="col">Rules failing</th></tr></thead>
|
|
598
|
+
<tbody>
|
|
599
|
+
${surfacesBody}
|
|
600
|
+
</tbody>
|
|
601
|
+
</table>
|
|
602
|
+
</div>
|
|
603
|
+
</section>
|
|
604
|
+
|
|
605
|
+
<section aria-labelledby="h-rules">
|
|
606
|
+
<h2 id="h-rules">Rules failing</h2>
|
|
607
|
+
${ruleSections('violation') || '<p class="muted">None.</p>'}
|
|
608
|
+
</section>
|
|
609
|
+
|
|
610
|
+
<section aria-labelledby="h-review">
|
|
611
|
+
<h2 id="h-review">Needs review</h2>
|
|
612
|
+
<p class="muted">Automation could not decide these — usually contrast over an image or a gradient.
|
|
613
|
+
Not passes and not failures; a person has to look.</p>
|
|
614
|
+
${ruleSections('incomplete') || '<p class="muted">None.</p>'}
|
|
615
|
+
</section>
|
|
616
|
+
${perf.length > 0
|
|
617
|
+
? `
|
|
618
|
+
<section aria-labelledby="h-perf">
|
|
619
|
+
<h2 id="h-perf">Performance <span class="muted">(CI lab, not real users)</span></h2>
|
|
620
|
+
<div class="tablewrap">
|
|
621
|
+
<table>
|
|
622
|
+
<thead><tr><th scope="col">Page load</th><th scope="col">LCP</th><th scope="col">CLS</th><th scope="col">Load</th><th scope="col">TTFB</th></tr></thead>
|
|
623
|
+
<tbody>
|
|
624
|
+
${perfBody}
|
|
625
|
+
</tbody>
|
|
626
|
+
</table>
|
|
627
|
+
</div>
|
|
628
|
+
<p class="muted">— means the engine did not report it: Firefox and WebKit implement neither LCP nor CLS.
|
|
629
|
+
Absent is not zero.</p>
|
|
630
|
+
</section>`
|
|
631
|
+
: ''}
|
|
632
|
+
${insightSections}
|
|
633
|
+
${screensSection}
|
|
634
|
+
${groupSection}
|
|
635
|
+
${warningsBlock}
|
|
636
|
+
</main>
|
|
637
|
+
<footer>
|
|
638
|
+
<p>Generated by Setsquare SDK ${esc(envelope.source.sdkVersion)} · axe-core ${esc(envelope.source.axeVersion)} ·
|
|
639
|
+
ruleset <code>${esc(envelope.source.rulesetDigest)}</code> · ${esc(envelope.source.runSource)} run.</p>
|
|
640
|
+
<p>This report summarises automated rule results per scanned surface. It is not a WCAG conformance claim:
|
|
641
|
+
WCAG conformance is defined per full page and complete process and includes requirements (non-interference,
|
|
642
|
+
accessibility support, conforming alternate versions) that automated scanning cannot evaluate.</p>
|
|
643
|
+
</footer>
|
|
644
|
+
<script type="application/json" id="setsquare-run">${embedded}</script>
|
|
645
|
+
</body>
|
|
646
|
+
</html>
|
|
647
|
+
`;
|
|
648
|
+
}
|
|
649
|
+
//# sourceMappingURL=html-report.js.map
|