@motion-proto/live-tokens 0.76.0 → 0.78.0
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/.claude/skills/live-tokens-check-compliance/SKILL.md +2 -0
- package/.claude/skills/live-tokens-create-component/SKILL.md +3 -3
- package/.claude/skills/live-tokens-create-component/references/contract-tests.md +99 -3
- package/.claude/skills/live-tokens-create-page/SKILL.md +5 -4
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +7 -0
- package/CHANGELOG.md +90 -0
- package/bin/check-component.mjs +2 -0
- package/bin/check-page.mjs +131 -9
- package/bin/cli.mjs +25 -3
- package/bin/contractRunner.mjs +349 -30
- package/bin/lib/pageRoutes.mjs +309 -0
- package/package.json +5 -3
- package/src/editor/overlay/ColumnsOverlay.svelte +1 -1
- package/src/editor/overlay/LiveEditorOverlay.svelte +1 -0
- package/src/editor/overlay/LiveTokensRouter.svelte +1 -0
- package/src/editor/skill-atlas/skillSources.generated.ts +5 -5
- package/src/editor/skill-atlas/trees/check-compliance.ts +18 -18
- package/src/editor/skill-atlas/trees/create-component.ts +8 -3
- package/src/editor/skill-atlas/trees/create-page.ts +26 -26
- package/src/editor/skill-atlas/trees/fix-findings.ts +46 -11
- package/src/testing-js/{chunk-LXR3MN6N.js → chunk-4JQX6WWL.js} +1 -1
- package/src/testing-js/chunk-4JQX6WWL.js.map +1 -0
- package/src/testing-js/{chunk-AO7EZHYV.js → chunk-GNIUPIU2.js} +3 -3
- package/src/testing-js/{chunk-ZMSX6CXR.js → chunk-NB3NZRBM.js} +13 -2
- package/src/testing-js/chunk-NB3NZRBM.js.map +1 -0
- package/src/testing-js/{chunk-L73N4NSO.js → chunk-WIZ6W7UT.js} +2 -2
- package/src/testing-js/component-alias.contract.js +2 -2
- package/src/testing-js/component-editor.contract.js +21 -5
- package/src/testing-js/component-editor.contract.js.map +1 -1
- package/src/testing-js/component-render.contract.js +4 -4
- package/src/testing-js/index.d.ts +9 -3
- package/src/testing-js/index.js +20 -12
- package/src/testing-js/index.js.map +1 -1
- package/src/testing-js/page-compliance.contract.js +829 -0
- package/src/testing-js/page-compliance.contract.js.map +1 -0
- package/src/testing-js/{vitest-BE6uGF31.d.ts → vitest-C-wNWcoA.d.ts} +21 -1
- package/src/testing-js/vitest.d.ts +1 -1
- package/src/testing-js/vitest.js +1 -1
- package/template/README.md +17 -5
- package/template/live-tokens.testing.ts +6 -0
- package/template/package.json +5 -2
- package/template/src/main.ts +1 -0
- package/template/src/pages/Home.svelte +13 -0
- package/template/src/registerComponents.ts +4 -0
- package/template/tests/contracts.ts +3 -0
- package/template/tsconfig.json +1 -1
- package/src/testing-js/chunk-LXR3MN6N.js.map +0 -1
- package/src/testing-js/chunk-ZMSX6CXR.js.map +0 -1
- /package/src/testing-js/{chunk-AO7EZHYV.js.map → chunk-GNIUPIU2.js.map} +0 -0
- /package/src/testing-js/{chunk-L73N4NSO.js.map → chunk-WIZ6W7UT.js.map} +0 -0
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
import {
|
|
2
|
+
allContracts,
|
|
3
|
+
isInapplicable
|
|
4
|
+
} from "./chunk-4JQX6WWL.js";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_PAGE_VIEWPORTS,
|
|
7
|
+
PAGES_ENV,
|
|
8
|
+
PAGE_VIEWPORTS_ENV
|
|
9
|
+
} from "./chunk-NB3NZRBM.js";
|
|
10
|
+
|
|
11
|
+
// src/testing/page-compliance.contract.ts
|
|
12
|
+
import { test } from "@playwright/test";
|
|
13
|
+
|
|
14
|
+
// src/editor/ui/sections/textStyles.ts
|
|
15
|
+
var TEXT_STYLES = [
|
|
16
|
+
{
|
|
17
|
+
name: "heading-xl",
|
|
18
|
+
label: "Heading XL",
|
|
19
|
+
prefix: "--heading-xl",
|
|
20
|
+
defaultElement: "h1",
|
|
21
|
+
preview: "Page title"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "heading-lg",
|
|
25
|
+
label: "Heading LG",
|
|
26
|
+
prefix: "--heading-lg",
|
|
27
|
+
defaultElement: "h2",
|
|
28
|
+
preview: "Section heading"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "heading-md",
|
|
32
|
+
label: "Heading MD",
|
|
33
|
+
prefix: "--heading-md",
|
|
34
|
+
defaultElement: "h3",
|
|
35
|
+
preview: "Subsection heading"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "heading-sm",
|
|
39
|
+
label: "Heading SM",
|
|
40
|
+
prefix: "--heading-sm",
|
|
41
|
+
defaultElement: "h4",
|
|
42
|
+
preview: "Minor heading"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "body-md",
|
|
46
|
+
label: "Body MD",
|
|
47
|
+
prefix: "--body-md",
|
|
48
|
+
defaultElement: "p",
|
|
49
|
+
preview: "Body copy for comfortable reading."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "body-sm",
|
|
53
|
+
label: "Body SM",
|
|
54
|
+
prefix: "--body-sm",
|
|
55
|
+
defaultElement: "small",
|
|
56
|
+
preview: "Captions and secondary text."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "editorial-xl",
|
|
60
|
+
label: "Editorial XL",
|
|
61
|
+
prefix: "--editorial-xl",
|
|
62
|
+
defaultElement: "editorial-xl",
|
|
63
|
+
preview: "Pull quotes and opening statements."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "editorial-lg",
|
|
67
|
+
label: "Editorial LG",
|
|
68
|
+
prefix: "--editorial-lg",
|
|
69
|
+
defaultElement: "editorial-lg",
|
|
70
|
+
preview: "Standfirsts and section ledes."
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "editorial-md",
|
|
74
|
+
label: "Editorial MD",
|
|
75
|
+
prefix: "--editorial-md",
|
|
76
|
+
defaultElement: "editorial-md",
|
|
77
|
+
preview: "Long-form reading, set in the editorial face."
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "editorial-sm",
|
|
81
|
+
label: "Editorial SM",
|
|
82
|
+
prefix: "--editorial-sm",
|
|
83
|
+
defaultElement: "editorial-sm",
|
|
84
|
+
preview: "Captions, credits, and asides."
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "code",
|
|
88
|
+
label: "Code",
|
|
89
|
+
prefix: "--code",
|
|
90
|
+
defaultElement: "code",
|
|
91
|
+
preview: "const total = sum(items);"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "eyebrow",
|
|
95
|
+
label: "Eyebrow",
|
|
96
|
+
prefix: "--eyebrow",
|
|
97
|
+
defaultElement: "eyebrow",
|
|
98
|
+
preview: "Overline label",
|
|
99
|
+
hasTextTransform: true
|
|
100
|
+
}
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
// src/editor/core/palettes/contrast.ts
|
|
104
|
+
var AA_BODY = 4.5;
|
|
105
|
+
var AA_LARGE = 3;
|
|
106
|
+
function linearizeChannel(v8) {
|
|
107
|
+
const s = v8 / 255;
|
|
108
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
109
|
+
}
|
|
110
|
+
function relativeLuminance(hex2) {
|
|
111
|
+
const r = linearizeChannel(parseInt(hex2.slice(1, 3), 16));
|
|
112
|
+
const g = linearizeChannel(parseInt(hex2.slice(3, 5), 16));
|
|
113
|
+
const b = linearizeChannel(parseInt(hex2.slice(5, 7), 16));
|
|
114
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
115
|
+
}
|
|
116
|
+
function contrastRatio(a, b) {
|
|
117
|
+
const la = relativeLuminance(a);
|
|
118
|
+
const lb = relativeLuminance(b);
|
|
119
|
+
const hi = Math.max(la, lb);
|
|
120
|
+
const lo = Math.min(la, lb);
|
|
121
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/testing/support/pageHarness.ts
|
|
125
|
+
var PageViolation = class extends Error {
|
|
126
|
+
constructor(rule, source, line, message) {
|
|
127
|
+
super(`[${rule}] ${source}:${line}: ${message}`);
|
|
128
|
+
this.rule = rule;
|
|
129
|
+
this.source = source;
|
|
130
|
+
this.line = line;
|
|
131
|
+
this.name = "PageViolation";
|
|
132
|
+
}
|
|
133
|
+
rule;
|
|
134
|
+
source;
|
|
135
|
+
line;
|
|
136
|
+
};
|
|
137
|
+
var PAGE_CONTAINER = "[data-live-tokens-page]";
|
|
138
|
+
var CHROME = "[data-live-tokens-chrome]";
|
|
139
|
+
function pageTargets() {
|
|
140
|
+
const raw = process.env[PAGES_ENV];
|
|
141
|
+
if (!raw) return [];
|
|
142
|
+
return JSON.parse(raw);
|
|
143
|
+
}
|
|
144
|
+
function pageViewports() {
|
|
145
|
+
const raw = process.env[PAGE_VIEWPORTS_ENV];
|
|
146
|
+
if (!raw) return DEFAULT_PAGE_VIEWPORTS;
|
|
147
|
+
return JSON.parse(raw);
|
|
148
|
+
}
|
|
149
|
+
var TRANSIENT_STATE = /\b(?:hover|active|disabled|focused|error|selected|open|menu|option|on)\b/;
|
|
150
|
+
function restingState(label) {
|
|
151
|
+
return label === void 0 || !TRANSIENT_STATE.test(label.toLowerCase());
|
|
152
|
+
}
|
|
153
|
+
function variantPattern(label) {
|
|
154
|
+
if (!label) return null;
|
|
155
|
+
const words = label.toLowerCase().split(/\s+/);
|
|
156
|
+
const keys = [.../* @__PURE__ */ new Set([words.join("-"), ...words])].map((key) => key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
157
|
+
return `(?:^|-)(?:${keys.join("|")})$`;
|
|
158
|
+
}
|
|
159
|
+
function paintChecks(contract, map) {
|
|
160
|
+
const checks = [];
|
|
161
|
+
for (const [part, properties] of Object.entries(map)) {
|
|
162
|
+
const declared = contract.parts[part];
|
|
163
|
+
if (declared === void 0) continue;
|
|
164
|
+
const locator = typeof declared === "string" ? { selector: declared } : declared;
|
|
165
|
+
if (typeof declared !== "string" && declared.portal) continue;
|
|
166
|
+
for (const [css, variable] of Object.entries(properties)) {
|
|
167
|
+
checks.push({
|
|
168
|
+
part,
|
|
169
|
+
selector: part === contract.root ? null : locator.selector,
|
|
170
|
+
pseudo: (typeof declared === "string" ? void 0 : declared.pseudo) ?? null,
|
|
171
|
+
css,
|
|
172
|
+
variable
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return checks;
|
|
177
|
+
}
|
|
178
|
+
function restingPaintSpec(contract) {
|
|
179
|
+
const rootSelector = contract.parts[contract.root];
|
|
180
|
+
if (typeof rootSelector !== "string") return null;
|
|
181
|
+
const entries = [];
|
|
182
|
+
const inherited = contract.view ?? {};
|
|
183
|
+
for (const expectation of contract.properties) {
|
|
184
|
+
if ((expectation.setup ?? []).length > 0) continue;
|
|
185
|
+
if (!restingState(expectation.state)) continue;
|
|
186
|
+
const checks = paintChecks(contract, expectation.paints);
|
|
187
|
+
if (checks.length === 0) continue;
|
|
188
|
+
const variant = (expectation.variant ?? inherited.variant)?.toLowerCase() ?? null;
|
|
189
|
+
entries.push({ variant, pattern: variantPattern(variant), checks });
|
|
190
|
+
}
|
|
191
|
+
if (!isInapplicable(contract.states)) {
|
|
192
|
+
for (const state of contract.states) {
|
|
193
|
+
if (!state.paints || !restingState(state.state)) continue;
|
|
194
|
+
if ((state.setup ?? []).length > 0) continue;
|
|
195
|
+
const checks = paintChecks(contract, state.paints);
|
|
196
|
+
if (checks.length === 0) continue;
|
|
197
|
+
const variant = (state.variant ?? inherited.variant)?.toLowerCase() ?? null;
|
|
198
|
+
entries.push({ variant, pattern: variantPattern(variant), checks });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (entries.length === 0) return null;
|
|
202
|
+
return { id: contract.id, root: rootSelector, entries };
|
|
203
|
+
}
|
|
204
|
+
var TEXT_AXES = ["fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing"];
|
|
205
|
+
var BLOCK_DISPLAYS = ["block", "flow-root", "list-item", "flex", "grid", "table-cell", "table-caption"];
|
|
206
|
+
var GRID_TOLERANCE = 1;
|
|
207
|
+
var GRID_VIEWPORT = 768;
|
|
208
|
+
var LARGE_TEXT_PX = 24;
|
|
209
|
+
var LARGE_BOLD_PX = 18.66;
|
|
210
|
+
var BOLD = 700;
|
|
211
|
+
var cssName = (camel) => camel.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
212
|
+
function report(failures, scope, describe) {
|
|
213
|
+
const lines = failures.map(describe);
|
|
214
|
+
return lines.length === 1 ? lines[0] : `${lines.length} of ${scope}
|
|
215
|
+
${lines.join("\n")}`;
|
|
216
|
+
}
|
|
217
|
+
function hex(rgb) {
|
|
218
|
+
const [r, g, b] = rgb.match(/\d+/g).map(Number);
|
|
219
|
+
return `#${[r, g, b].map((c) => c.toString(16).padStart(2, "0")).join("")}`;
|
|
220
|
+
}
|
|
221
|
+
function aaFloor(pair) {
|
|
222
|
+
const large = pair.fontSize >= LARGE_TEXT_PX || pair.fontSize >= LARGE_BOLD_PX && pair.fontWeight >= BOLD;
|
|
223
|
+
return large ? AA_LARGE : AA_BODY;
|
|
224
|
+
}
|
|
225
|
+
function reasons(skipped) {
|
|
226
|
+
return Object.entries(skipped).map(([reason, count]) => `${reason} (${count})`).join(", ");
|
|
227
|
+
}
|
|
228
|
+
var PageHarness = class _PageHarness {
|
|
229
|
+
constructor(page, target, viewport) {
|
|
230
|
+
this.page = page;
|
|
231
|
+
this.target = target;
|
|
232
|
+
this.viewport = viewport;
|
|
233
|
+
}
|
|
234
|
+
page;
|
|
235
|
+
target;
|
|
236
|
+
viewport;
|
|
237
|
+
static async open(page, target, viewport) {
|
|
238
|
+
await installPageProbe(page);
|
|
239
|
+
await page.setViewportSize(viewport);
|
|
240
|
+
await page.goto(target.route);
|
|
241
|
+
await page.locator(PAGE_CONTAINER).waitFor();
|
|
242
|
+
await page.waitForFunction((selectors) => {
|
|
243
|
+
const container = document.querySelector(selectors.container);
|
|
244
|
+
if (!container) return false;
|
|
245
|
+
const content = [...container.children].some((child) => !child.matches(selectors.chrome));
|
|
246
|
+
const themed = getComputedStyle(document.documentElement).getPropertyValue("--body-md-font-size").trim().length > 0;
|
|
247
|
+
return content && themed;
|
|
248
|
+
}, { container: PAGE_CONTAINER, chrome: CHROME });
|
|
249
|
+
await page.waitForLoadState("networkidle");
|
|
250
|
+
await page.evaluate(async () => {
|
|
251
|
+
await document.fonts.ready;
|
|
252
|
+
const frame = () => new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
253
|
+
await frame();
|
|
254
|
+
await frame();
|
|
255
|
+
});
|
|
256
|
+
return new _PageHarness(page, target, viewport);
|
|
257
|
+
}
|
|
258
|
+
fail(rule, line, message) {
|
|
259
|
+
throw new PageViolation(rule, this.target.source, line, message);
|
|
260
|
+
}
|
|
261
|
+
/** Every shipped instance on the page, as the contract root selectors find
|
|
262
|
+
* them, with the variant its own root classes name. */
|
|
263
|
+
async instances(specs) {
|
|
264
|
+
return this.page.evaluate(({ specs: list, container, chrome, source }) => {
|
|
265
|
+
const found = [];
|
|
266
|
+
const root = document.querySelector(container);
|
|
267
|
+
if (!root) return found;
|
|
268
|
+
for (const spec of list) {
|
|
269
|
+
const keyed = [];
|
|
270
|
+
for (const entry of spec.entries) {
|
|
271
|
+
if (entry.pattern !== null) keyed.push({ variant: entry.variant, pattern: new RegExp(entry.pattern) });
|
|
272
|
+
}
|
|
273
|
+
for (const element of root.querySelectorAll(spec.root)) {
|
|
274
|
+
if (element.closest(chrome)) continue;
|
|
275
|
+
const classes = [...element.classList];
|
|
276
|
+
const matched = keyed.find(({ pattern }) => classes.some((name) => pattern.test(name)));
|
|
277
|
+
found.push({
|
|
278
|
+
id: spec.id,
|
|
279
|
+
variant: matched?.variant ?? null,
|
|
280
|
+
line: window.__liveTokensPageLine(element, source)
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return found;
|
|
285
|
+
}, { specs, container: PAGE_CONTAINER, chrome: CHROME, source: this.target.source });
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Every element that renders text of its own: block-level, inside the page
|
|
289
|
+
* container, outside the router's chrome, and outside every shipped
|
|
290
|
+
* component root, whose slot typography the component owns.
|
|
291
|
+
*/
|
|
292
|
+
async textElements(componentRoots2) {
|
|
293
|
+
return this.page.evaluate(({ roots, container, chrome, source, axes, displays }) => {
|
|
294
|
+
const found = [];
|
|
295
|
+
const root = document.querySelector(container);
|
|
296
|
+
if (!root) return found;
|
|
297
|
+
const inComponent = roots.join(", ");
|
|
298
|
+
for (const element of root.querySelectorAll("*")) {
|
|
299
|
+
if (element.closest(chrome)) continue;
|
|
300
|
+
if (inComponent && element.closest(inComponent)) continue;
|
|
301
|
+
const ownText = [...element.childNodes].some((node) => node.nodeType === Node.TEXT_NODE && (node.textContent ?? "").trim().length > 0);
|
|
302
|
+
if (!ownText) continue;
|
|
303
|
+
const style = getComputedStyle(element);
|
|
304
|
+
if (!displays.includes(style.display)) continue;
|
|
305
|
+
found.push({
|
|
306
|
+
line: window.__liveTokensPageLine(element, source),
|
|
307
|
+
tag: element.tagName.toLowerCase(),
|
|
308
|
+
axes: Object.fromEntries(axes.map((axis) => [axis, style[axis] ?? ""]))
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return found;
|
|
312
|
+
}, {
|
|
313
|
+
roots: componentRoots2,
|
|
314
|
+
container: PAGE_CONTAINER,
|
|
315
|
+
chrome: CHROME,
|
|
316
|
+
source: this.target.source,
|
|
317
|
+
axes: [...TEXT_AXES],
|
|
318
|
+
displays: BLOCK_DISPLAYS
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
/** The page file line the element was written on, from Svelte's dev-build
|
|
322
|
+
* element metadata. */
|
|
323
|
+
async lineOf(element) {
|
|
324
|
+
return element.evaluate((node, source) => window.__liveTokensPageLine(node, source), this.target.source);
|
|
325
|
+
}
|
|
326
|
+
/** Every resting paint obligation, measured on the instances themselves. */
|
|
327
|
+
async observePaints(specs) {
|
|
328
|
+
return this.page.evaluate(({ specs: list, container, chrome, source }) => {
|
|
329
|
+
const failures = [];
|
|
330
|
+
const uncovered = [];
|
|
331
|
+
let instances = 0;
|
|
332
|
+
let asserted = 0;
|
|
333
|
+
const root = document.querySelector(container);
|
|
334
|
+
if (!root) return { instances, asserted, failures, uncovered };
|
|
335
|
+
const normalize = (node, css, raw) => {
|
|
336
|
+
const probe = document.createElement("div");
|
|
337
|
+
probe.style.display = "none";
|
|
338
|
+
probe.style.borderStyle = "solid";
|
|
339
|
+
probe.style.outlineStyle = "solid";
|
|
340
|
+
probe.style.fontSize = getComputedStyle(node).fontSize;
|
|
341
|
+
(node.parentElement ?? document.body).appendChild(probe);
|
|
342
|
+
probe.style.setProperty(css.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`), raw);
|
|
343
|
+
const computed = getComputedStyle(probe)[css];
|
|
344
|
+
probe.remove();
|
|
345
|
+
return (computed ?? "").trim();
|
|
346
|
+
};
|
|
347
|
+
for (const spec of list) {
|
|
348
|
+
const patterns = spec.entries.map((entry) => entry.pattern === null ? null : new RegExp(entry.pattern));
|
|
349
|
+
const variants = [...new Set(spec.entries.map((entry) => entry.variant).filter((v) => v !== null))];
|
|
350
|
+
for (const element of root.querySelectorAll(spec.root)) {
|
|
351
|
+
if (element.closest(chrome)) continue;
|
|
352
|
+
instances++;
|
|
353
|
+
const classes = [...element.classList];
|
|
354
|
+
const line = window.__liveTokensPageLine(element, source);
|
|
355
|
+
let covered = false;
|
|
356
|
+
for (const [index, entry] of spec.entries.entries()) {
|
|
357
|
+
const pattern = patterns[index];
|
|
358
|
+
if (pattern && !classes.some((name) => pattern.test(name))) continue;
|
|
359
|
+
covered = true;
|
|
360
|
+
for (const check of entry.checks) {
|
|
361
|
+
const part = check.selector === null ? element : element.querySelector(check.selector);
|
|
362
|
+
if (!part) continue;
|
|
363
|
+
const style = getComputedStyle(part, check.pseudo ?? void 0);
|
|
364
|
+
const token = style.getPropertyValue(check.variable).trim();
|
|
365
|
+
if (!token) continue;
|
|
366
|
+
const expected = normalize(part, check.css, token);
|
|
367
|
+
const actual = (style[check.css] ?? "").trim();
|
|
368
|
+
asserted++;
|
|
369
|
+
if (actual !== expected) {
|
|
370
|
+
failures.push({
|
|
371
|
+
id: spec.id,
|
|
372
|
+
variant: entry.variant,
|
|
373
|
+
line,
|
|
374
|
+
part: check.part,
|
|
375
|
+
css: check.css,
|
|
376
|
+
variable: check.variable,
|
|
377
|
+
actual,
|
|
378
|
+
expected
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (!covered) uncovered.push({ id: spec.id, line, variants });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return { instances, asserted, failures, uncovered };
|
|
387
|
+
}, { specs, container: PAGE_CONTAINER, chrome: CHROME, source: this.target.source });
|
|
388
|
+
}
|
|
389
|
+
/** Every run of text on the page, against the theme's own style bundles as
|
|
390
|
+
* they resolve at this viewport. */
|
|
391
|
+
async observeTextStyles(bundles2, componentRoots2) {
|
|
392
|
+
const elements = await this.textElements(componentRoots2);
|
|
393
|
+
const expected = await this.page.evaluate(({ list, axes }) => {
|
|
394
|
+
const style = getComputedStyle(document.documentElement);
|
|
395
|
+
const probe = document.createElement("div");
|
|
396
|
+
probe.style.position = "absolute";
|
|
397
|
+
probe.style.visibility = "hidden";
|
|
398
|
+
document.body.appendChild(probe);
|
|
399
|
+
const resolved = {};
|
|
400
|
+
for (const bundle of list) {
|
|
401
|
+
probe.style.cssText = "position:absolute;visibility:hidden";
|
|
402
|
+
for (const axis of axes) {
|
|
403
|
+
probe.style.setProperty(
|
|
404
|
+
axis.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`),
|
|
405
|
+
style.getPropertyValue(`${bundle.prefix}-${axis.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`).trim()
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
const computed = getComputedStyle(probe);
|
|
409
|
+
resolved[bundle.name] = Object.fromEntries(axes.map((axis) => [axis, (computed[axis] ?? "").trim()]));
|
|
410
|
+
}
|
|
411
|
+
probe.remove();
|
|
412
|
+
return resolved;
|
|
413
|
+
}, { list: bundles2, axes: [...TEXT_AXES] });
|
|
414
|
+
const failures = [];
|
|
415
|
+
for (const element of elements) {
|
|
416
|
+
let best = { name: "", misses: [], distance: Number.POSITIVE_INFINITY };
|
|
417
|
+
for (const bundle of bundles2) {
|
|
418
|
+
const misses = TEXT_AXES.filter((axis2) => element.axes[axis2] !== expected[bundle.name][axis2]);
|
|
419
|
+
if (misses.length === 0) {
|
|
420
|
+
best = { name: bundle.name, misses: [], distance: 0 };
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
const distance = misses.length + (misses.includes("fontFamily") ? 0.4 : 0) + (misses.includes("fontSize") ? 0.2 : 0);
|
|
424
|
+
if (distance < best.distance) best = { name: bundle.name, misses, distance };
|
|
425
|
+
}
|
|
426
|
+
if (best.misses.length === 0) continue;
|
|
427
|
+
const axis = best.misses[0];
|
|
428
|
+
failures.push({
|
|
429
|
+
line: element.line,
|
|
430
|
+
tag: element.tag,
|
|
431
|
+
nearest: best.name,
|
|
432
|
+
axis,
|
|
433
|
+
actual: element.axes[axis],
|
|
434
|
+
expected: expected[best.name][axis]
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
return { elements: elements.length, failures };
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Every text and surface pair the page composes, with the tokens whose
|
|
441
|
+
* resolved values match them. The ratio itself is computed by the caller
|
|
442
|
+
* from the editor's own WCAG helper, so one implementation answers for the
|
|
443
|
+
* palette editor and for a page.
|
|
444
|
+
*/
|
|
445
|
+
async observeContrast(componentRoots2) {
|
|
446
|
+
return this.page.evaluate(({ roots, container, chrome, source, displays }) => {
|
|
447
|
+
const pairs = [];
|
|
448
|
+
const skipped = {};
|
|
449
|
+
const skip = (reason) => {
|
|
450
|
+
skipped[reason] = (skipped[reason] ?? 0) + 1;
|
|
451
|
+
};
|
|
452
|
+
const root = document.querySelector(container);
|
|
453
|
+
if (!root) return { pairs, skipped };
|
|
454
|
+
const canvas = document.createElement("canvas");
|
|
455
|
+
canvas.width = 1;
|
|
456
|
+
canvas.height = 1;
|
|
457
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
458
|
+
ctx.globalCompositeOperation = "copy";
|
|
459
|
+
const rgba = (value) => {
|
|
460
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0)";
|
|
461
|
+
ctx.fillStyle = value;
|
|
462
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
463
|
+
const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
|
|
464
|
+
return [r, g, b, a / 255];
|
|
465
|
+
};
|
|
466
|
+
const key = ([r, g, b]) => `${r},${g},${b}`;
|
|
467
|
+
const css = (c) => c[3] === 1 ? `rgb(${c[0]}, ${c[1]}, ${c[2]})` : `rgba(${c[0]}, ${c[1]}, ${c[2]}, ${c[3]})`;
|
|
468
|
+
const probe = document.createElement("div");
|
|
469
|
+
probe.style.display = "none";
|
|
470
|
+
document.body.appendChild(probe);
|
|
471
|
+
const named = /* @__PURE__ */ new Map();
|
|
472
|
+
const rootStyle = getComputedStyle(document.documentElement);
|
|
473
|
+
for (const name of rootStyle) {
|
|
474
|
+
if (!name.startsWith("--")) continue;
|
|
475
|
+
probe.style.color = "";
|
|
476
|
+
probe.style.color = `var(${name})`;
|
|
477
|
+
const resolved = getComputedStyle(probe).color;
|
|
478
|
+
if (!resolved) continue;
|
|
479
|
+
const colour = rgba(resolved);
|
|
480
|
+
if (colour[3] === 0) continue;
|
|
481
|
+
if (!named.has(key(colour))) named.set(key(colour), name);
|
|
482
|
+
}
|
|
483
|
+
probe.remove();
|
|
484
|
+
const over = (front, back) => [
|
|
485
|
+
Math.round(front[0] * front[3] + back[0] * (1 - front[3])),
|
|
486
|
+
Math.round(front[1] * front[3] + back[1] * (1 - front[3])),
|
|
487
|
+
Math.round(front[2] * front[3] + back[2] * (1 - front[3])),
|
|
488
|
+
1
|
|
489
|
+
];
|
|
490
|
+
const inComponent = roots.join(", ");
|
|
491
|
+
for (const element of root.querySelectorAll("*")) {
|
|
492
|
+
if (element.closest(chrome)) continue;
|
|
493
|
+
if (inComponent && element.closest(inComponent)) continue;
|
|
494
|
+
const ownText = [...element.childNodes].some((node) => node.nodeType === Node.TEXT_NODE && (node.textContent ?? "").trim().length > 0);
|
|
495
|
+
if (!ownText) continue;
|
|
496
|
+
const style = getComputedStyle(element);
|
|
497
|
+
if (!displays.includes(style.display)) continue;
|
|
498
|
+
if (style.visibility !== "visible") continue;
|
|
499
|
+
const colour = rgba(style.color);
|
|
500
|
+
if (colour[3] === 0) continue;
|
|
501
|
+
let surface = null;
|
|
502
|
+
let reason = null;
|
|
503
|
+
for (let node = element; node; node = node.parentElement) {
|
|
504
|
+
const behind = getComputedStyle(node);
|
|
505
|
+
if (behind.mixBlendMode !== "normal") {
|
|
506
|
+
reason = "a blend mode";
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
if (behind.backgroundImage !== "none") {
|
|
510
|
+
reason = "a gradient or an image behind the text";
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
const paint = rgba(behind.backgroundColor);
|
|
514
|
+
if (paint[3] === 1) {
|
|
515
|
+
surface = paint;
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
if (paint[3] > 0) {
|
|
519
|
+
reason = "a translucent surface";
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (!surface) {
|
|
524
|
+
skip(reason ?? "no opaque surface behind the text");
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
pairs.push({
|
|
528
|
+
line: window.__liveTokensPageLine(element, source),
|
|
529
|
+
tag: element.tagName.toLowerCase(),
|
|
530
|
+
color: css(colour[3] === 1 ? colour : over(colour, surface)),
|
|
531
|
+
background: css(surface),
|
|
532
|
+
colorToken: named.get(key(colour[3] === 1 ? colour : over(colour, surface))) ?? null,
|
|
533
|
+
backgroundToken: named.get(key(surface)) ?? null,
|
|
534
|
+
fontSize: parseFloat(style.fontSize),
|
|
535
|
+
fontWeight: parseInt(style.fontWeight, 10) || 400
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
return { pairs, skipped };
|
|
539
|
+
}, {
|
|
540
|
+
roots: componentRoots2,
|
|
541
|
+
container: PAGE_CONTAINER,
|
|
542
|
+
chrome: CHROME,
|
|
543
|
+
source: this.target.source,
|
|
544
|
+
displays: BLOCK_DISPLAYS
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
/** Where each section of the page sits against the column grid it draws. */
|
|
548
|
+
async observeGrid() {
|
|
549
|
+
return this.page.evaluate(({ container, chrome, source, tolerance }) => {
|
|
550
|
+
const failures = [];
|
|
551
|
+
let grids = 0;
|
|
552
|
+
let children = 0;
|
|
553
|
+
const root = document.querySelector(container);
|
|
554
|
+
if (!root) return { grids, children, failures, pageLine: 1 };
|
|
555
|
+
const own = [...root.children].find((child) => !child.matches(chrome));
|
|
556
|
+
const pageLine = own ? window.__liveTokensPageLine(own, source) : 1;
|
|
557
|
+
const columns = parseInt(
|
|
558
|
+
getComputedStyle(document.documentElement).getPropertyValue("--columns-count").trim(),
|
|
559
|
+
10
|
|
560
|
+
);
|
|
561
|
+
if (!columns) return { grids, children, failures, pageLine };
|
|
562
|
+
for (const grid of root.querySelectorAll("*")) {
|
|
563
|
+
if (grid.closest(chrome)) continue;
|
|
564
|
+
const style = getComputedStyle(grid);
|
|
565
|
+
const tracks = style.gridTemplateColumns.split(/\s+/).map(parseFloat);
|
|
566
|
+
if (tracks.length !== columns || tracks.some((track) => !Number.isFinite(track))) continue;
|
|
567
|
+
grids++;
|
|
568
|
+
const box = grid.getBoundingClientRect();
|
|
569
|
+
const gap = parseFloat(style.columnGap) || 0;
|
|
570
|
+
let offset = box.left + parseFloat(style.borderLeftWidth) + parseFloat(style.paddingLeft);
|
|
571
|
+
const lines = [];
|
|
572
|
+
for (const track of tracks) {
|
|
573
|
+
lines.push(offset);
|
|
574
|
+
offset += track;
|
|
575
|
+
lines.push(offset);
|
|
576
|
+
offset += gap;
|
|
577
|
+
}
|
|
578
|
+
const nearest = (edge) => lines.reduce((best, line) => Math.abs(line - edge) < Math.abs(best - edge) ? line : best, lines[0]);
|
|
579
|
+
const items = getComputedStyle(grid).justifyItems;
|
|
580
|
+
for (const child of grid.children) {
|
|
581
|
+
if (child.matches(chrome)) continue;
|
|
582
|
+
const kid = getComputedStyle(child);
|
|
583
|
+
if (kid.display === "none" || kid.display === "contents") continue;
|
|
584
|
+
if (kid.position === "absolute" || kid.position === "fixed") continue;
|
|
585
|
+
const { left, right } = child.getBoundingClientRect();
|
|
586
|
+
children++;
|
|
587
|
+
const align = kid.justifySelf === "auto" ? items : kid.justifySelf;
|
|
588
|
+
const centred = align === "center";
|
|
589
|
+
const pinned = centred ? [] : align === "start" || align === "flex-start" || align === "left" ? ["left"] : align === "end" || align === "flex-end" || align === "right" ? ["right"] : ["left", "right"];
|
|
590
|
+
if (centred) {
|
|
591
|
+
const before = left - Math.max(...lines.filter((line) => line <= left + tolerance), lines[0]);
|
|
592
|
+
const after = Math.min(...lines.filter((line) => line >= right - tolerance), lines[lines.length - 1]) - right;
|
|
593
|
+
if (Math.abs(before - after) > tolerance) {
|
|
594
|
+
failures.push({
|
|
595
|
+
line: window.__liveTokensPageLine(child, source),
|
|
596
|
+
tag: child.tagName.toLowerCase(),
|
|
597
|
+
edge: before > after ? "left" : "right",
|
|
598
|
+
off: Math.round((before - after) * 10) / 10
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
for (const edge of pinned) {
|
|
604
|
+
const at = edge === "left" ? left : right;
|
|
605
|
+
const off = at - nearest(at);
|
|
606
|
+
if (Math.abs(off) <= tolerance) continue;
|
|
607
|
+
failures.push({
|
|
608
|
+
line: window.__liveTokensPageLine(child, source),
|
|
609
|
+
tag: child.tagName.toLowerCase(),
|
|
610
|
+
edge,
|
|
611
|
+
off: Math.round(off * 10) / 10
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return { grids, children, failures, pageLine };
|
|
617
|
+
}, { container: PAGE_CONTAINER, chrome: CHROME, source: this.target.source, tolerance: GRID_TOLERANCE });
|
|
618
|
+
}
|
|
619
|
+
/** What the page pushes past the viewport, past its own container, or past
|
|
620
|
+
* the box that clips it. */
|
|
621
|
+
async observeOverflow(specs) {
|
|
622
|
+
return this.page.evaluate(({ specs: list, container, chrome, source, tolerance }) => {
|
|
623
|
+
const failures = [];
|
|
624
|
+
let elements = 0;
|
|
625
|
+
const root = document.querySelector(container);
|
|
626
|
+
if (!root) return { elements, failures };
|
|
627
|
+
const own = [...root.children].find((child) => !child.matches(chrome));
|
|
628
|
+
const pageLine = own ? window.__liveTokensPageLine(own, source) : 1;
|
|
629
|
+
const page = document.documentElement;
|
|
630
|
+
if (page.scrollWidth > window.innerWidth + tolerance) {
|
|
631
|
+
failures.push({
|
|
632
|
+
line: pageLine,
|
|
633
|
+
tag: "html",
|
|
634
|
+
kind: "document",
|
|
635
|
+
detail: `the document scrolls to ${page.scrollWidth}px at a ${window.innerWidth}px viewport`
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
for (const element of root.querySelectorAll("*")) {
|
|
639
|
+
if (element.closest(chrome)) continue;
|
|
640
|
+
const style = getComputedStyle(element);
|
|
641
|
+
if (style.display === "none" || style.display === "contents") continue;
|
|
642
|
+
if (element.clientWidth === 0) continue;
|
|
643
|
+
elements++;
|
|
644
|
+
if (style.overflowX === "auto" || style.overflowX === "scroll") continue;
|
|
645
|
+
if (element.scrollWidth > element.clientWidth + tolerance) {
|
|
646
|
+
failures.push({
|
|
647
|
+
line: window.__liveTokensPageLine(element, source),
|
|
648
|
+
tag: element.tagName.toLowerCase(),
|
|
649
|
+
kind: "element",
|
|
650
|
+
detail: `<${element.tagName.toLowerCase()}> holds ${element.scrollWidth}px of content in ${element.clientWidth}px, and its overflow-x is ${style.overflowX}`
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const clips = (style) => style.overflowX !== "visible" || style.overflowY !== "visible";
|
|
655
|
+
for (const spec of list) {
|
|
656
|
+
for (const instance of root.querySelectorAll(spec.root)) {
|
|
657
|
+
if (instance.closest(chrome)) continue;
|
|
658
|
+
let clipper = null;
|
|
659
|
+
for (let node = instance.parentElement; node; node = node.parentElement) {
|
|
660
|
+
if (clips(getComputedStyle(node))) {
|
|
661
|
+
clipper = node;
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (!clipper) continue;
|
|
666
|
+
const box = instance.getBoundingClientRect();
|
|
667
|
+
const bounds = clipper.getBoundingClientRect();
|
|
668
|
+
const past = [
|
|
669
|
+
box.left < bounds.left - tolerance ? `${Math.round(bounds.left - box.left)}px past the left edge` : null,
|
|
670
|
+
box.right > bounds.right + tolerance ? `${Math.round(box.right - bounds.right)}px past the right edge` : null
|
|
671
|
+
].filter((side) => side !== null);
|
|
672
|
+
if (past.length === 0) continue;
|
|
673
|
+
failures.push({
|
|
674
|
+
line: window.__liveTokensPageLine(instance, source),
|
|
675
|
+
tag: spec.id,
|
|
676
|
+
kind: "instance",
|
|
677
|
+
detail: `${spec.id} sits ${past.join(" and ")} of the <${clipper.tagName.toLowerCase()}> that clips it`
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return { elements, failures };
|
|
682
|
+
}, { specs, container: PAGE_CONTAINER, chrome: CHROME, source: this.target.source, tolerance: 1 });
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Every rule reports the same three ways: it throws a `PageViolation` the
|
|
686
|
+
* runner turns into a finding, returns a reason the run records as
|
|
687
|
+
* `inapplicable`, or returns null, which is the pass. The suite files hold
|
|
688
|
+
* no rule of their own so the defect fixtures measure what a consumer runs.
|
|
689
|
+
*/
|
|
690
|
+
async assertComponentPaint(specs) {
|
|
691
|
+
const observed = await this.observePaints(specs);
|
|
692
|
+
if (observed.instances === 0) return `${this.target.source} renders no shipped component`;
|
|
693
|
+
if (observed.failures.length > 0) {
|
|
694
|
+
this.fail(
|
|
695
|
+
"page-component-paint",
|
|
696
|
+
observed.failures[0].line,
|
|
697
|
+
report(observed.failures, `${observed.asserted} contracted paints`, (failure) => `line ${failure.line}: ${failure.id}${failure.variant ? ` (${failure.variant})` : ""} paints ${failure.part} ${cssName(failure.css)}: ${failure.actual}, but ${failure.variable} resolves to ${failure.expected}`)
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
if (observed.uncovered.length > 0) {
|
|
701
|
+
return `${this.target.source}: no entry names the variant of ` + observed.uncovered.map((instance) => `${instance.id} at line ${instance.line}, which offers ${instance.variants.join(", ")}`).join("; ");
|
|
702
|
+
}
|
|
703
|
+
if (observed.asserted === 0) {
|
|
704
|
+
return `no contracted part of a shipped instance is rendered on ${this.target.source}`;
|
|
705
|
+
}
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
async assertTextStyle(bundles2, componentRoots2) {
|
|
709
|
+
const observed = await this.observeTextStyles(bundles2, componentRoots2);
|
|
710
|
+
if (observed.elements === 0) return `${this.target.source} renders no text outside a shipped component`;
|
|
711
|
+
if (observed.failures.length > 0) {
|
|
712
|
+
this.fail(
|
|
713
|
+
"page-text-style",
|
|
714
|
+
observed.failures[0].line,
|
|
715
|
+
report(observed.failures, `${observed.elements} runs of text`, (failure) => `line ${failure.line}: <${failure.tag}> is in no shipped text style. Nearest is ${failure.nearest}: its ${cssName(failure.axis)} is ${failure.expected}, the element's is ${failure.actual}`)
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
async assertContrast(componentRoots2) {
|
|
721
|
+
const observed = await this.observeContrast(componentRoots2);
|
|
722
|
+
const failures = observed.pairs.map((pair) => ({ pair, ratio: contrastRatio(hex(pair.color), hex(pair.background)), floor: aaFloor(pair) })).filter(({ ratio, floor }) => ratio < floor);
|
|
723
|
+
if (failures.length > 0) {
|
|
724
|
+
const names = (colour, token) => token ? `${token} (${colour})` : colour;
|
|
725
|
+
this.fail(
|
|
726
|
+
"page-contrast",
|
|
727
|
+
failures[0].pair.line,
|
|
728
|
+
report(failures, `${observed.pairs.length} text and surface pairs`, ({ pair, ratio, floor }) => `line ${pair.line}: <${pair.tag}> at ${pair.fontSize}px/${pair.fontWeight} sets ${names(pair.color, pair.colorToken)} on ${names(pair.background, pair.backgroundToken)}, a ratio of ${ratio.toFixed(2)} against the ${floor} AA floor`)
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
if (observed.pairs.length === 0) {
|
|
732
|
+
const why = reasons(observed.skipped);
|
|
733
|
+
return why ? `no text on ${this.target.source} sits on a surface the rule can read: ${why}` : `${this.target.source} renders no text outside a shipped component`;
|
|
734
|
+
}
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
async assertGrid() {
|
|
738
|
+
if (this.viewport.width < GRID_VIEWPORT) {
|
|
739
|
+
return `the page is one column at ${this.viewport.width}px, below the ${GRID_VIEWPORT}px the grid needs`;
|
|
740
|
+
}
|
|
741
|
+
const observed = await this.observeGrid();
|
|
742
|
+
if (observed.grids === 0) {
|
|
743
|
+
this.fail(
|
|
744
|
+
"page-grid",
|
|
745
|
+
observed.pageLine,
|
|
746
|
+
`${this.target.source} draws no column grid: no element on it has as many tracks as --columns-count. The page is the column grid, so its sections sit on one`
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
if (observed.failures.length > 0) {
|
|
750
|
+
this.fail(
|
|
751
|
+
"page-grid",
|
|
752
|
+
observed.failures[0].line,
|
|
753
|
+
report(observed.failures, `${observed.children} sections on ${observed.grids} column grids`, (failure) => `line ${failure.line}: <${failure.tag}> sits ${Math.abs(failure.off)}px to the ${failure.off < 0 ? "left" : "right"} of the nearest column line on its ${failure.edge}`)
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
async assertOverflow(specs) {
|
|
759
|
+
const observed = await this.observeOverflow(specs);
|
|
760
|
+
if (observed.failures.length > 0) {
|
|
761
|
+
this.fail(
|
|
762
|
+
"page-overflow",
|
|
763
|
+
observed.failures[0].line,
|
|
764
|
+
report(observed.failures, `${observed.elements} boxes`, (failure) => `line ${failure.line}: ${failure.detail}`)
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
async function installPageProbe(page) {
|
|
771
|
+
await page.addInitScript(() => {
|
|
772
|
+
window.__liveTokensPageLine = (node, source) => {
|
|
773
|
+
for (let element = node; element; element = element.parentElement) {
|
|
774
|
+
const loc = element.__svelte_meta?.loc;
|
|
775
|
+
if (!loc || typeof loc.file !== "string" || typeof loc.line !== "number") continue;
|
|
776
|
+
if (loc.file.split("\\").join("/").endsWith(source)) return loc.line;
|
|
777
|
+
}
|
|
778
|
+
return 1;
|
|
779
|
+
};
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/testing/page-compliance.contract.ts
|
|
784
|
+
var targets = pageTargets();
|
|
785
|
+
var viewports = pageViewports();
|
|
786
|
+
var bundles = TEXT_STYLES.map((style) => ({ name: style.name, prefix: style.prefix }));
|
|
787
|
+
async function paintSpecs() {
|
|
788
|
+
const contracts = await allContracts();
|
|
789
|
+
return contracts.map((contract) => restingPaintSpec(contract)).filter((spec) => spec !== null);
|
|
790
|
+
}
|
|
791
|
+
async function componentRoots() {
|
|
792
|
+
const contracts = await allContracts();
|
|
793
|
+
return contracts.map((contract) => contract.parts[contract.root]).filter((declared) => typeof declared === "string");
|
|
794
|
+
}
|
|
795
|
+
for (const target of targets) {
|
|
796
|
+
for (const viewport of viewports) {
|
|
797
|
+
const at = `${viewport.width}x${viewport.height}`;
|
|
798
|
+
test(`page-component-paint | ${target.source} | ${at}`, async ({ page }) => {
|
|
799
|
+
const specs = await paintSpecs();
|
|
800
|
+
const harness = await PageHarness.open(page, target, viewport);
|
|
801
|
+
const inapplicable = await harness.assertComponentPaint(specs);
|
|
802
|
+
test.skip(inapplicable !== null, inapplicable ?? "");
|
|
803
|
+
});
|
|
804
|
+
test(`page-text-style | ${target.source} | ${at}`, async ({ page }) => {
|
|
805
|
+
const roots = await componentRoots();
|
|
806
|
+
const harness = await PageHarness.open(page, target, viewport);
|
|
807
|
+
const inapplicable = await harness.assertTextStyle(bundles, roots);
|
|
808
|
+
test.skip(inapplicable !== null, inapplicable ?? "");
|
|
809
|
+
});
|
|
810
|
+
test(`page-contrast | ${target.source} | ${at}`, async ({ page }) => {
|
|
811
|
+
const roots = await componentRoots();
|
|
812
|
+
const harness = await PageHarness.open(page, target, viewport);
|
|
813
|
+
const inapplicable = await harness.assertContrast(roots);
|
|
814
|
+
test.skip(inapplicable !== null, inapplicable ?? "");
|
|
815
|
+
});
|
|
816
|
+
test(`page-grid | ${target.source} | ${at}`, async ({ page }) => {
|
|
817
|
+
const harness = await PageHarness.open(page, target, viewport);
|
|
818
|
+
const inapplicable = await harness.assertGrid();
|
|
819
|
+
test.skip(inapplicable !== null, inapplicable ?? "");
|
|
820
|
+
});
|
|
821
|
+
test(`page-overflow | ${target.source} | ${at}`, async ({ page }) => {
|
|
822
|
+
const specs = await paintSpecs();
|
|
823
|
+
const harness = await PageHarness.open(page, target, viewport);
|
|
824
|
+
const inapplicable = await harness.assertOverflow(specs);
|
|
825
|
+
test.skip(inapplicable !== null, inapplicable ?? "");
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
//# sourceMappingURL=page-compliance.contract.js.map
|