@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
package/dist/sanitise.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { MAX_CONTEXT_LABEL_LENGTH, MAX_FAILURE_SUMMARY_LENGTH, MAX_SELECTORS_PER_FINDING, MAX_SELECTOR_LENGTH, MAX_URL_PATTERN_LENGTH, hostileCharacters, } from '@setsquare/schema/limits';
|
|
2
|
+
/**
|
|
3
|
+
* Sanitisation at source.
|
|
4
|
+
*
|
|
5
|
+
* The trust posture the platform sells rests on this file: "PII-safe by
|
|
6
|
+
* construction" has to be a property of the SDK, not a promise about the
|
|
7
|
+
* platform, because the platform can only be careful with what it receives.
|
|
8
|
+
* Everything here runs in the customer's own process, before a byte crosses any
|
|
9
|
+
* boundary — including the boundary into Playwright's own HTML report and trace,
|
|
10
|
+
* which is why this is the one thing the fixture does rather than the reporter.
|
|
11
|
+
*
|
|
12
|
+
* Every function is pure. No I/O, no page access, no state. That is what lets the
|
|
13
|
+
* fixture call them without becoming stateful, and what lets them be tested
|
|
14
|
+
* against genuinely awful inputs rather than mocked ones.
|
|
15
|
+
*
|
|
16
|
+
* The limits come from `packages/schema` rather than being restated here. A cap
|
|
17
|
+
* that drifted from the contract would surface as rejected payloads in a
|
|
18
|
+
* customer's CI, which is the worst possible place to discover it.
|
|
19
|
+
*
|
|
20
|
+
* What this file deliberately does NOT do: neutralise injection. `<script>` and
|
|
21
|
+
* `](evil)` survive intact, because they are legitimate content in a CSS
|
|
22
|
+
* selector and because destroying evidence to feel safer is a bad trade. Output
|
|
23
|
+
* encoding belongs to each sink — the platform UI, PR-comment markdown, exports,
|
|
24
|
+
* MCP payloads — and each has to do its own.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Remove characters that exist only to deceive.
|
|
28
|
+
*
|
|
29
|
+
* Zero-width characters make two visually identical selectors unequal, which
|
|
30
|
+
* breaks dedupe and clustering silently. Bidi overrides make a rendered line read
|
|
31
|
+
* backwards. Control characters break logs and terminals. None has any business
|
|
32
|
+
* in a CSS selector, so removal loses nothing real.
|
|
33
|
+
*/
|
|
34
|
+
export function stripHostileCharacters(value) {
|
|
35
|
+
return value.replace(hostileCharacters(), '');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Cap a string's length, marking the cut.
|
|
39
|
+
*
|
|
40
|
+
* The ellipsis matters: a silently shortened selector looks like a selector that
|
|
41
|
+
* was always that short, and someone will eventually try to use it to find the
|
|
42
|
+
* element. One character of honesty prevents that.
|
|
43
|
+
*/
|
|
44
|
+
export function capLength(value, max) {
|
|
45
|
+
if (value.length <= max)
|
|
46
|
+
return value;
|
|
47
|
+
return `${value.slice(0, max - 1)}…`;
|
|
48
|
+
}
|
|
49
|
+
/** Strip, then cap. The order matters — stripping can only shorten. */
|
|
50
|
+
function clean(value, max) {
|
|
51
|
+
return capLength(stripHostileCharacters(value), max);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Reduce a finding's selectors to a representative, bounded set.
|
|
55
|
+
*
|
|
56
|
+
* Deduplicated before capping, deliberately. The hostile fixture emits 617
|
|
57
|
+
* selectors of which 600 are near-identical `span:nth-child(N)`; taking the first
|
|
58
|
+
* ten raw would return ten variations of the same thing and describe nothing.
|
|
59
|
+
* Deduplicating first means the cap spends its budget on distinct shapes.
|
|
60
|
+
*
|
|
61
|
+
* Note that dedupe happens AFTER stripping, which is the only order that works:
|
|
62
|
+
* two selectors differing solely by a zero-width character are the same selector
|
|
63
|
+
* wearing a disguise, and comparing them before stripping would keep both.
|
|
64
|
+
*/
|
|
65
|
+
export function sanitiseSelectors(raw, max = MAX_SELECTORS_PER_FINDING) {
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
const selectors = [];
|
|
68
|
+
let shortened = false;
|
|
69
|
+
for (const candidate of raw) {
|
|
70
|
+
const stripped = stripHostileCharacters(candidate);
|
|
71
|
+
if (stripped.length === 0)
|
|
72
|
+
continue;
|
|
73
|
+
const capped = capLength(stripped, MAX_SELECTOR_LENGTH);
|
|
74
|
+
if (capped !== stripped)
|
|
75
|
+
shortened = true;
|
|
76
|
+
if (seen.has(capped))
|
|
77
|
+
continue;
|
|
78
|
+
seen.add(capped);
|
|
79
|
+
if (selectors.length < max)
|
|
80
|
+
selectors.push(capped);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
selectors,
|
|
84
|
+
selectorsTruncated: shortened || seen.size > selectors.length,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** Axe's prose, bounded. */
|
|
88
|
+
export function sanitiseFailureSummary(value) {
|
|
89
|
+
if (!value)
|
|
90
|
+
return undefined;
|
|
91
|
+
const cleaned = clean(value, MAX_FAILURE_SUMMARY_LENGTH);
|
|
92
|
+
return cleaned.length > 0 ? cleaned : undefined;
|
|
93
|
+
}
|
|
94
|
+
/** Theme, persona and state labels — short, and page-derived often enough to matter. */
|
|
95
|
+
export function sanitiseLabel(value) {
|
|
96
|
+
if (!value)
|
|
97
|
+
return undefined;
|
|
98
|
+
const cleaned = clean(value, MAX_CONTEXT_LABEL_LENGTH);
|
|
99
|
+
return cleaned.length > 0 ? cleaned : undefined;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Test identifiers and titles.
|
|
103
|
+
*
|
|
104
|
+
* A deliberately different treatment from page-derived strings, and the
|
|
105
|
+
* difference is worth stating because it looks inconsistent otherwise.
|
|
106
|
+
*
|
|
107
|
+
* Test titles are AUTHORED BY THE CUSTOMER'S DEVELOPERS, not derived from a
|
|
108
|
+
* scanned page. They are not attacker-influenceable in the way a class name is,
|
|
109
|
+
* and they are the only human-readable handle the runs list has — so identifiers
|
|
110
|
+
* inside them are left alone. `the item editor loads for 8fa1c0de` stays exactly
|
|
111
|
+
* that, because redacting it would produce a run list of indistinguishable rows.
|
|
112
|
+
*
|
|
113
|
+
* They can still carry personal data: a data-driven test titled with a candidate's
|
|
114
|
+
* email is entirely ordinary. That is accepted rather than solved, and it is
|
|
115
|
+
* consistent with the posture already recorded in the dev plan — run envelopes
|
|
116
|
+
* structurally carry personal data of customer staff (commit author names and
|
|
117
|
+
* emails, annotation identities), which is exactly why Setsquare designs as a
|
|
118
|
+
* processor rather than pretending the risk away.
|
|
119
|
+
*
|
|
120
|
+
* What IS applied is character sanitisation, because a bidi override in a test
|
|
121
|
+
* title breaks a terminal, a log and a grid cell just as thoroughly as one in a
|
|
122
|
+
* selector, whoever wrote it.
|
|
123
|
+
*/
|
|
124
|
+
export function sanitiseTestText(value, max) {
|
|
125
|
+
return clean(value, max);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Turn a URL into a route pattern, dropping everything that identifies a visit.
|
|
129
|
+
*
|
|
130
|
+
* This is sanitisation, not inference, and the distinction is worth being precise
|
|
131
|
+
* about because it decides which stage owns it. Replacing `/items/42/edit` with
|
|
132
|
+
* `/items/:id/edit` removes a record identifier; replacing
|
|
133
|
+
* `/reset-password/8f3a…` removes a live credential. Naming that pattern
|
|
134
|
+
* `item-editor` is inference, and belongs with surface resolution.
|
|
135
|
+
*
|
|
136
|
+
* Three things are removed unconditionally:
|
|
137
|
+
*
|
|
138
|
+
* - the origin, because the schema has no `url` field and the surface is the
|
|
139
|
+
* path, not the host
|
|
140
|
+
* - the query string, which is where tokens, emails and search terms live
|
|
141
|
+
* - the fragment, for the same reason
|
|
142
|
+
*
|
|
143
|
+
* And then any segment that looks like an identifier rather than a name is
|
|
144
|
+
* replaced. The test is deliberately conservative in the SAFE direction: a
|
|
145
|
+
* word-like segment is kept, and anything else becomes `:id`. Over-redacting
|
|
146
|
+
* costs a slightly coarser grid row; under-redacting ships someone's password
|
|
147
|
+
* reset token to a server.
|
|
148
|
+
*/
|
|
149
|
+
export function toUrlPattern(url) {
|
|
150
|
+
let path;
|
|
151
|
+
try {
|
|
152
|
+
// A relative URL still parses against a base we then discard, so a caller
|
|
153
|
+
// does not have to know which kind it holds.
|
|
154
|
+
path = new URL(url, 'http://localhost').pathname;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return { pattern: '/', redacted: false };
|
|
158
|
+
}
|
|
159
|
+
if (path === '' || path === '/')
|
|
160
|
+
return { pattern: '/', redacted: false };
|
|
161
|
+
let redacted = false;
|
|
162
|
+
const segments = path.split('/').map((segment) => {
|
|
163
|
+
if (segment === '')
|
|
164
|
+
return segment;
|
|
165
|
+
const decoded = decodeSegment(segment);
|
|
166
|
+
if (looksLikeIdentifier(decoded)) {
|
|
167
|
+
redacted = true;
|
|
168
|
+
return ':id';
|
|
169
|
+
}
|
|
170
|
+
return stripHostileCharacters(decoded);
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
pattern: capLength(segments.join('/'), MAX_URL_PATTERN_LENGTH) || '/',
|
|
174
|
+
redacted,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** Percent-encoding hides the shape of a segment; decode before judging it. */
|
|
178
|
+
function decodeSegment(segment) {
|
|
179
|
+
try {
|
|
180
|
+
return decodeURIComponent(segment);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return segment;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
187
|
+
const ULID = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
188
|
+
const ALL_DIGITS = /^\d+$/;
|
|
189
|
+
const LONG_HEX = /^[0-9a-f]{8,}$/i;
|
|
190
|
+
/** `job`, `v1`, `2fa` — lowercase words, optionally with digits. */
|
|
191
|
+
const LOWER_WORD = /^[a-z0-9]+$/;
|
|
192
|
+
/** `API`, `EU` — acronyms do appear in routes. */
|
|
193
|
+
const UPPER_WORD = /^[A-Z0-9]+$/;
|
|
194
|
+
/** `Settings` — capitalised, as some frameworks route. */
|
|
195
|
+
const CAPITALISED_WORD = /^[A-Z][a-z0-9]*$/;
|
|
196
|
+
/** Longer than any plausible route word, before separators are considered. */
|
|
197
|
+
const MAX_NAME_PART = 24;
|
|
198
|
+
/** A letters-and-digits run this long with no separator is a token, not a name. */
|
|
199
|
+
const TOKENISH_MIXED_LENGTH = 12;
|
|
200
|
+
/**
|
|
201
|
+
* Is one separator-delimited part of a segment a name rather than a token?
|
|
202
|
+
*
|
|
203
|
+
* The load-bearing signal is CASE. Route segments are lowercase, kebab-case,
|
|
204
|
+
* occasionally capitalised or an acronym — they do not mix cases inside a single
|
|
205
|
+
* unseparated run. Tokens do, constantly: `aGVsbG8gd29ybGQK` is alphanumeric and
|
|
206
|
+
* starts with a letter, which an earlier version of this function accepted as a
|
|
207
|
+
* word and passed straight through. Base64 is the common case and it was the
|
|
208
|
+
* case that broke it.
|
|
209
|
+
*/
|
|
210
|
+
function partIsNameLike(part) {
|
|
211
|
+
if (part.length === 0 || part.length > MAX_NAME_PART)
|
|
212
|
+
return false;
|
|
213
|
+
if (!(LOWER_WORD.test(part) || UPPER_WORD.test(part) || CAPITALISED_WORD.test(part))) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
// `2fa` is fine; `abc123def456ghi` is a token wearing lowercase.
|
|
217
|
+
const mixesLettersAndDigits = /[a-zA-Z]/.test(part) && /\d/.test(part);
|
|
218
|
+
return !(mixesLettersAndDigits && part.length >= TOKENISH_MIXED_LENGTH);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Is this path segment an identifier rather than a name?
|
|
222
|
+
*
|
|
223
|
+
* Ordered so the cheap certain cases decide first, and biased toward redaction:
|
|
224
|
+
* the final rule KEEPS only what looks like a name, so novel token formats are
|
|
225
|
+
* redacted by default rather than by enumeration. A redaction list that has to
|
|
226
|
+
* predict every id format is a redaction list that eventually misses one — and
|
|
227
|
+
* the thing it misses is a live credential in someone's URL.
|
|
228
|
+
*/
|
|
229
|
+
function looksLikeIdentifier(segment) {
|
|
230
|
+
if (segment.length === 0)
|
|
231
|
+
return false;
|
|
232
|
+
if (ALL_DIGITS.test(segment))
|
|
233
|
+
return true;
|
|
234
|
+
if (UUID.test(segment))
|
|
235
|
+
return true;
|
|
236
|
+
if (ULID.test(segment))
|
|
237
|
+
return true;
|
|
238
|
+
if (LONG_HEX.test(segment))
|
|
239
|
+
return true;
|
|
240
|
+
// An email address in a path. Rare, and unmistakably personal data.
|
|
241
|
+
if (segment.includes('@'))
|
|
242
|
+
return true;
|
|
243
|
+
// Every separator-delimited part must look like a name. One token-shaped part
|
|
244
|
+
// is enough to redact the whole segment: `Xj9__aB4-zQ` is not a route.
|
|
245
|
+
return !segment.split(/[-_.]/).every(partIsNameLike);
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=sanitise.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitise.js","sourceRoot":"","sources":["../src/sanitise.ts"],"names":[],"mappings":"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"}
|
package/dist/scan.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type Page } from '@playwright/test';
|
|
2
|
+
import { type WcagTarget } from './rules.js';
|
|
3
|
+
import { toUrlPattern } from './sanitise.js';
|
|
4
|
+
/**
|
|
5
|
+
* Level 2: the explicit scan API.
|
|
6
|
+
*
|
|
7
|
+
* Auto-scan gets a grid for free from a suite nobody edited. This is for the
|
|
8
|
+
* states auto-scan cannot reach on its own — a modal that is open, a form showing
|
|
9
|
+
* validation errors, a theme the suite toggles deliberately — where the customer
|
|
10
|
+
* knows something we could never infer and says so.
|
|
11
|
+
*
|
|
12
|
+
* The signature takes `page` explicitly rather than arriving as a Playwright
|
|
13
|
+
* fixture, and that is a deliberate trade. A fixture would read marginally better
|
|
14
|
+
* (`async ({ page, setsquare }) => …`) but would need TypeScript declaration merging
|
|
15
|
+
* to type at all, since the customer imports `test` from `@playwright/test` and
|
|
16
|
+
* not from us. Passing `page` costs one argument, types perfectly, matches what
|
|
17
|
+
* `@axe-core/playwright` already trained everyone to expect — and calling this at
|
|
18
|
+
* all is a test edit by definition, so the "zero test rewrites" promise (which is
|
|
19
|
+
* about auto-scan) is untouched.
|
|
20
|
+
*/
|
|
21
|
+
export interface ScanOptions {
|
|
22
|
+
/** The named state being evidenced: `validation-errors`, `menu-open`. */
|
|
23
|
+
state?: string;
|
|
24
|
+
/** Overrides whatever the DOM says. A customer's statement outranks our sniffing. */
|
|
25
|
+
theme?: string;
|
|
26
|
+
/** Overrides the project-name mapping for this scan. */
|
|
27
|
+
persona?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* What a scan found, returned to the caller.
|
|
31
|
+
*
|
|
32
|
+
* SDK-computed facts only, and no comparison of any kind — there is nothing here
|
|
33
|
+
* to compare against, locally, ever. Returned rather than kept private so a
|
|
34
|
+
* customer can assert on their own conformance if they want to; P5's `setsquare
|
|
35
|
+
* verify` is the supported way to do that, but nothing here should stop someone
|
|
36
|
+
* writing `expect(result.violations).toBe(0)` today.
|
|
37
|
+
*/
|
|
38
|
+
export interface ScanSummary {
|
|
39
|
+
surface: string;
|
|
40
|
+
/** The conformance level this scan was assessed against. */
|
|
41
|
+
wcagTarget: WcagTarget;
|
|
42
|
+
violations: number;
|
|
43
|
+
incomplete: number;
|
|
44
|
+
passes: number;
|
|
45
|
+
/** False when the scan could not run. The SDK never throws into a customer's test. */
|
|
46
|
+
scanned: boolean;
|
|
47
|
+
}
|
|
48
|
+
declare function scan(page: Page, surface: string, options?: ScanOptions): Promise<ScanSummary>;
|
|
49
|
+
/**
|
|
50
|
+
* The explicit API surface.
|
|
51
|
+
*
|
|
52
|
+
* An object rather than a bare function so the shape has room to grow — P5 adds
|
|
53
|
+
* `setsquare.verify()` — without the import line changing under anyone.
|
|
54
|
+
*/
|
|
55
|
+
export declare const setsquare: {
|
|
56
|
+
scan: typeof scan;
|
|
57
|
+
};
|
|
58
|
+
/** Exported for the reporter's surface normalisation; the same function scans use. */
|
|
59
|
+
export { toUrlPattern };
|
|
60
|
+
//# sourceMappingURL=scan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAEA,OAAO,EAAQ,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAmBnD,OAAO,EAAW,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAEtD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,WAAW;IAC1B,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qFAAqF;IACrF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,UAAU,EAAE,UAAU,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,sFAAsF;IACtF,OAAO,EAAE,OAAO,CAAC;CAClB;AAuCD,iBAAe,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,WAAW,CAAC,CAoMhG;AAQD;;;;;GAKG;AACH,eAAO,MAAM,SAAS;;CAAW,CAAC;AAElC,sFAAsF;AACtF,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
package/dist/scan.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { test } from '@playwright/test';
|
|
3
|
+
import { engineVersion, rulesetDigest, runAxe } from './axe.js';
|
|
4
|
+
import { SCAN_RECORD_ATTACHMENT, SCAN_RECORD_VERSION, } from './contract.js';
|
|
5
|
+
import { toFindings, toInapplicable, toPasses } from './findings.js';
|
|
6
|
+
import { capturePerf } from './perf.js';
|
|
7
|
+
import { readDocumentFacts } from './document.js';
|
|
8
|
+
import { pageRuntime } from './observe.js';
|
|
9
|
+
import { recordUnscannable } from './unscannable.js';
|
|
10
|
+
import { isDisabled } from './options.js';
|
|
11
|
+
import { runStatusMessages, runTextSpacing } from './checks/dynamic.js';
|
|
12
|
+
import { setsquareCheckVersions } from './checks/versions.js';
|
|
13
|
+
import { runReadingLevel } from './reading-level.js';
|
|
14
|
+
import { readingLevelEnabled, rulePolicy, scanTimeoutMs } from './runtime.js';
|
|
15
|
+
import { tagsFor } from './rules.js';
|
|
16
|
+
import { sanitiseLabel } from './sanitise.js';
|
|
17
|
+
import { toUrlPattern } from './sanitise.js';
|
|
18
|
+
/**
|
|
19
|
+
* Page loads, tracked so scans of one load share a `visitRef`.
|
|
20
|
+
*
|
|
21
|
+
* A `WeakMap` because the key is a Playwright `Page` and we must not keep it
|
|
22
|
+
* alive: a suite with thousands of pages would otherwise leak every one of them
|
|
23
|
+
* for the lifetime of the worker.
|
|
24
|
+
*
|
|
25
|
+
* The heuristic is "same URL means same visit", which is right for the case that
|
|
26
|
+
* matters — scanning one page repeatedly as its state changes — and wrong only if
|
|
27
|
+
* a test navigates away and back to the identical URL, where it merges two visits
|
|
28
|
+
* that produced identical perf numbers anyway.
|
|
29
|
+
*/
|
|
30
|
+
const visits = new WeakMap();
|
|
31
|
+
function visitFor(page, url) {
|
|
32
|
+
const existing = visits.get(page);
|
|
33
|
+
if (existing && existing.url === url)
|
|
34
|
+
return { ref: existing.ref, firstOfVisit: false };
|
|
35
|
+
const ref = createHash('sha256')
|
|
36
|
+
.update(`${url}#${Date.now()}#${Math.random()}`, 'utf8')
|
|
37
|
+
.digest('hex')
|
|
38
|
+
.slice(0, 16);
|
|
39
|
+
visits.set(page, { url, ref, perfCaptured: false });
|
|
40
|
+
return { ref, firstOfVisit: true };
|
|
41
|
+
}
|
|
42
|
+
/** Marks this visit's perf as taken, so later scans of the same load skip it. */
|
|
43
|
+
function markPerfCaptured(page) {
|
|
44
|
+
const existing = visits.get(page);
|
|
45
|
+
if (existing)
|
|
46
|
+
existing.perfCaptured = true;
|
|
47
|
+
}
|
|
48
|
+
function perfAlreadyCaptured(page) {
|
|
49
|
+
return visits.get(page)?.perfCaptured ?? false;
|
|
50
|
+
}
|
|
51
|
+
async function scan(page, surface, options = {}) {
|
|
52
|
+
const declared = { surface };
|
|
53
|
+
const state = sanitiseLabel(options.state);
|
|
54
|
+
if (state)
|
|
55
|
+
declared.state = state;
|
|
56
|
+
const theme = sanitiseLabel(options.theme);
|
|
57
|
+
if (theme)
|
|
58
|
+
declared.theme = theme;
|
|
59
|
+
const persona = sanitiseLabel(options.persona);
|
|
60
|
+
if (persona)
|
|
61
|
+
declared.persona = persona;
|
|
62
|
+
const policy = rulePolicy();
|
|
63
|
+
/*
|
|
64
|
+
* `SETSQUARE_DISABLED` means off, including here.
|
|
65
|
+
*
|
|
66
|
+
* An explicit `setsquare.scan()` is the customer's own call, so there is an argument
|
|
67
|
+
* for honouring it regardless — but the switch exists so somebody can answer
|
|
68
|
+
* "is it you or is it me" in one command, and a baseline with half the SDK
|
|
69
|
+
* still running answers nothing. Off means off.
|
|
70
|
+
*
|
|
71
|
+
* Returns the same shape a refused scan returns, so a spec that asserts on the
|
|
72
|
+
* result keeps working rather than throwing on `undefined` the moment the
|
|
73
|
+
* switch is set.
|
|
74
|
+
*/
|
|
75
|
+
if (isDisabled()) {
|
|
76
|
+
return {
|
|
77
|
+
surface,
|
|
78
|
+
wcagTarget: policy.target,
|
|
79
|
+
violations: 0,
|
|
80
|
+
incomplete: 0,
|
|
81
|
+
passes: 0,
|
|
82
|
+
scanned: false,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const scanStartedAt = Date.now();
|
|
87
|
+
const results = await runAxe(page, policy, scanTimeoutMs());
|
|
88
|
+
const axeMs = Date.now() - scanStartedAt;
|
|
89
|
+
if (!results) {
|
|
90
|
+
// Explicit scans get the same ceiling as automatic ones. A customer who
|
|
91
|
+
// called `setsquare.scan()` still did not agree to lend us their whole test
|
|
92
|
+
// budget, and the honest return is "no scan" rather than an empty one.
|
|
93
|
+
recordUnscannable(page, toUrlPattern(page.url()).pattern, 'scan-timed-out');
|
|
94
|
+
return {
|
|
95
|
+
surface,
|
|
96
|
+
wcagTarget: policy.target,
|
|
97
|
+
violations: 0,
|
|
98
|
+
incomplete: 0,
|
|
99
|
+
passes: 0,
|
|
100
|
+
scanned: false,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const findings = toFindings(results);
|
|
104
|
+
const passes = toPasses(results);
|
|
105
|
+
const inapplicable = toInapplicable(results);
|
|
106
|
+
// Setsquare's own checks ride alongside axe's, in the same findings array
|
|
107
|
+
// and under the same never-fail discipline. The digest names the run's
|
|
108
|
+
// configured check set, never which scans happened to execute one — see
|
|
109
|
+
// setsquareCheckVersions for why.
|
|
110
|
+
const setsquareChecks = setsquareCheckVersions(policy);
|
|
111
|
+
if (readingLevelEnabled()) {
|
|
112
|
+
const readingLevel = await runReadingLevel(page);
|
|
113
|
+
if (readingLevel)
|
|
114
|
+
findings.push(readingLevel);
|
|
115
|
+
}
|
|
116
|
+
/*
|
|
117
|
+
* Dynamic checks run on explicit scans because the moment is safe by
|
|
118
|
+
* construction: the customer's test is awaiting this call, the page is
|
|
119
|
+
* single-threaded, and the text-spacing probe injects, measures and
|
|
120
|
+
* removes its stylesheet before we return. An explicit scan of an error
|
|
121
|
+
* state (`{ state: 'validation-errors' }`) is also exactly when the
|
|
122
|
+
* status-message layer has something real to look at.
|
|
123
|
+
*/
|
|
124
|
+
for (const check of [await runTextSpacing(page), await runStatusMessages(page)]) {
|
|
125
|
+
if (check.finding)
|
|
126
|
+
findings.push(check.finding);
|
|
127
|
+
if (check.pass)
|
|
128
|
+
passes.push(check.pass);
|
|
129
|
+
if (check.inapplicable)
|
|
130
|
+
inapplicable.push(check.inapplicable);
|
|
131
|
+
}
|
|
132
|
+
const url = page.url();
|
|
133
|
+
const visit = visitFor(page, url);
|
|
134
|
+
const record = {
|
|
135
|
+
recordVersion: SCAN_RECORD_VERSION,
|
|
136
|
+
kind: 'scan',
|
|
137
|
+
page: {
|
|
138
|
+
project: test.info().project.name,
|
|
139
|
+
engine: engineOf(page),
|
|
140
|
+
url,
|
|
141
|
+
},
|
|
142
|
+
declared,
|
|
143
|
+
visitRef: visit.ref,
|
|
144
|
+
axeVersion: engineVersion(results) ?? 'unknown',
|
|
145
|
+
rulesetDigest: rulesetDigest(engineVersion(results) ?? 'unknown', results, tagsFor(policy), setsquareChecks),
|
|
146
|
+
wcagTarget: policy.target,
|
|
147
|
+
findings,
|
|
148
|
+
passes,
|
|
149
|
+
inapplicable,
|
|
150
|
+
};
|
|
151
|
+
const viewport = page.viewportSize();
|
|
152
|
+
if (viewport)
|
|
153
|
+
record.page.viewport = viewport;
|
|
154
|
+
const probesStartedAt = Date.now();
|
|
155
|
+
// One round-trip for theme and document facts, as the auto-scan path does.
|
|
156
|
+
const facts = await readDocumentFacts(page);
|
|
157
|
+
if (facts.theme)
|
|
158
|
+
record.page.observedTheme = facts.theme;
|
|
159
|
+
if (facts.document)
|
|
160
|
+
record.document = facts.document;
|
|
161
|
+
/*
|
|
162
|
+
* Performance is a property of the PAGE LOAD, not of the scan.
|
|
163
|
+
*
|
|
164
|
+
* Scanning one page repeatedly as its state changes — modal closed, then
|
|
165
|
+
* modal open — produces several scans from a single navigation, and every one
|
|
166
|
+
* of them would report identical timings. Emitting it once per visit is what
|
|
167
|
+
* stops pillar 3's averages being weighted by how many states a surface
|
|
168
|
+
* happens to have, which would skew the trend line in a way nobody could see
|
|
169
|
+
* from the chart.
|
|
170
|
+
*
|
|
171
|
+
* Emitted once rather than emitted repeatedly and de-duplicated server-side:
|
|
172
|
+
* the same outcome, minus a whole class of bug.
|
|
173
|
+
*/
|
|
174
|
+
if (!perfAlreadyCaptured(page)) {
|
|
175
|
+
const perf = await capturePerf(page);
|
|
176
|
+
if (perf)
|
|
177
|
+
record.perf = perf;
|
|
178
|
+
// Page-runtime rides the same gate for the same reason: it describes the
|
|
179
|
+
// LOAD, so scanning one page in four states must not report the same 500
|
|
180
|
+
// four times.
|
|
181
|
+
const runtime = pageRuntime(page);
|
|
182
|
+
if (runtime)
|
|
183
|
+
record.runtime = runtime;
|
|
184
|
+
// Marked regardless of whether either block materialised — the gate means
|
|
185
|
+
// "this visit's page-load facts have been taken", not "perf existed".
|
|
186
|
+
markPerfCaptured(page);
|
|
187
|
+
}
|
|
188
|
+
const probesMs = Date.now() - probesStartedAt;
|
|
189
|
+
record.cost = {
|
|
190
|
+
totalMs: Date.now() - scanStartedAt,
|
|
191
|
+
axeMs,
|
|
192
|
+
...(probesMs > 0 ? { probesMs } : {}),
|
|
193
|
+
};
|
|
194
|
+
await test.info().attach(SCAN_RECORD_ATTACHMENT, {
|
|
195
|
+
body: Buffer.from(JSON.stringify(record), 'utf8'),
|
|
196
|
+
contentType: 'application/json',
|
|
197
|
+
});
|
|
198
|
+
return {
|
|
199
|
+
surface,
|
|
200
|
+
wcagTarget: policy.target,
|
|
201
|
+
violations: findings.filter((finding) => finding.status === 'violation').length,
|
|
202
|
+
incomplete: findings.filter((finding) => finding.status === 'incomplete').length,
|
|
203
|
+
passes: passes.length,
|
|
204
|
+
scanned: true,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
/*
|
|
209
|
+
* A scan failure is never the customer's build failing.
|
|
210
|
+
*
|
|
211
|
+
* The suite's verdict belongs to the suite; our evidence-gathering has no
|
|
212
|
+
* standing to overturn it. This warns through Playwright's own annotation
|
|
213
|
+
* channel — so it appears in their report next to the test it happened in —
|
|
214
|
+
* and returns a summary that says plainly that nothing was scanned, rather
|
|
215
|
+
* than zeroes that would read as a clean page.
|
|
216
|
+
*/
|
|
217
|
+
recordUnscannable(page, toUrlPattern(page.url()).pattern, 'scan-failed');
|
|
218
|
+
test.info().annotations.push({
|
|
219
|
+
type: 'setsquare-warning',
|
|
220
|
+
description: `Setsquare could not scan "${surface}": ${error instanceof Error ? error.message : String(error)}. Your test is unaffected.`,
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
surface,
|
|
224
|
+
wcagTarget: policy.target,
|
|
225
|
+
violations: 0,
|
|
226
|
+
incomplete: 0,
|
|
227
|
+
passes: 0,
|
|
228
|
+
scanned: false,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** Playwright exposes the browser type on the page's context. */
|
|
233
|
+
function engineOf(page) {
|
|
234
|
+
const name = page.context().browser()?.browserType().name();
|
|
235
|
+
return name === 'firefox' || name === 'webkit' ? name : 'chromium';
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The explicit API surface.
|
|
239
|
+
*
|
|
240
|
+
* An object rather than a bare function so the shape has room to grow — P5 adds
|
|
241
|
+
* `setsquare.verify()` — without the import line changing under anyone.
|
|
242
|
+
*/
|
|
243
|
+
export const setsquare = { scan };
|
|
244
|
+
/** Exported for the reporter's surface normalisation; the same function scans use. */
|
|
245
|
+
export { toUrlPattern };
|
|
246
|
+
//# sourceMappingURL=scan.js.map
|
package/dist/scan.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"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"}
|