@12ui/design 0.2.51 → 0.2.53
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/SKILL.md +1 -1
- package/dist/branch-client.d.ts +5 -0
- package/dist/branch-client.d.ts.map +1 -1
- package/dist/branch-client.js.map +1 -1
- package/dist/branch-completion.d.ts.map +1 -1
- package/dist/branch-completion.js +7 -0
- package/dist/branch-completion.js.map +1 -1
- package/dist/branch-identity.d.ts +25 -0
- package/dist/branch-identity.d.ts.map +1 -1
- package/dist/branch-identity.js +28 -15
- package/dist/branch-identity.js.map +1 -1
- package/dist/branch-page-conversion.d.ts.map +1 -1
- package/dist/branch-page-conversion.js +49 -4
- package/dist/branch-page-conversion.js.map +1 -1
- package/dist/branch-plan.d.ts +4 -1
- package/dist/branch-plan.d.ts.map +1 -1
- package/dist/branch-plan.js +7 -0
- package/dist/branch-plan.js.map +1 -1
- package/dist/branch-run-record.d.ts +4 -0
- package/dist/branch-run-record.d.ts.map +1 -1
- package/dist/branch-run-record.js.map +1 -1
- package/dist/cli-draft-command.d.ts.map +1 -1
- package/dist/cli-draft-command.js +17 -1
- package/dist/cli-draft-command.js.map +1 -1
- package/dist/hosted-draft-run.d.ts +25 -1
- package/dist/hosted-draft-run.d.ts.map +1 -1
- package/dist/hosted-draft-run.js +82 -2
- package/dist/hosted-draft-run.js.map +1 -1
- package/dist/package-client.d.ts +11 -0
- package/dist/package-client.d.ts.map +1 -1
- package/dist/package-client.js.map +1 -1
- package/dist/package-submission.d.ts +13 -2
- package/dist/package-submission.d.ts.map +1 -1
- package/dist/package-submission.js +34 -1
- package/dist/package-submission.js.map +1 -1
- package/dist/prototype-retention.d.ts +10 -0
- package/dist/prototype-retention.d.ts.map +1 -0
- package/dist/prototype-retention.js +14 -0
- package/dist/prototype-retention.js.map +1 -0
- package/dist/prototype-revision.d.ts +1 -1
- package/dist/prototype-revision.js +1 -1
- package/dist/prototype-runner.d.ts.map +1 -1
- package/dist/prototype-runner.js +39 -15
- package/dist/prototype-runner.js.map +1 -1
- package/dist/review-server.d.ts.map +1 -1
- package/dist/review-server.js +10 -1
- package/dist/review-server.js.map +1 -1
- package/open-design.json +2 -2
- package/package.json +1 -1
- package/prototype-kit/build.mjs +54 -9
- package/prototype-kit/lib/action-target.mjs +34 -8
- package/prototype-kit/lib/runtime-check.mjs +31 -12
- package/prototype-kit/polish/ground-probe.mjs +4 -1
- package/prototype-kit/polish/measure.mjs +6 -6
- package/prototype-kit/polish/rules/section-breathing.mjs +461 -237
- package/prototype-kit/polish/tests/breathing-check.mjs +75 -27
- package/prototype-kit/polish/tests/fixtures/section-breathing/diagram-silent.html +20 -0
- package/prototype-kit/polish/tests/fixtures/section-breathing/fire.html +2 -2
- package/prototype-kit/polish/tests/fixtures/section-breathing/floor.html +3 -3
- package/prototype-kit/polish/tests/fixtures/section-breathing/no-scale-silent.html +18 -0
- package/prototype-kit/polish/tests/fixtures/section-breathing/rhythm-nested.html +21 -0
- package/prototype-kit/polish/tests/fixtures/section-breathing/semantic-fire.html +10 -4
- package/prototype-kit/polish/tests/fixtures/section-breathing/semantic-silent.html +2 -2
- package/prototype-kit/polish/tests/fixtures/section-breathing/silent.html +2 -2
- package/prototype-kit/polish/tests/fixtures/section-breathing/tight.html +4 -4
- package/prototype-kit/runtime/proto.js +52 -2
- package/prototype-kit/tests/adversarial-runtime.mjs +179 -4
- package/prototype-kit/tests/fixtures/blocking-runtime/broken-interaction.html +1 -0
- package/prototype-kit/tests/fixtures/interaction-runtime/PROVENANCE.md +5 -0
- package/prototype-kit/tests/fixtures/interaction-runtime/inert-field.html +23 -0
- package/prototype-kit/tests/fixtures/interaction-runtime/pointer-cascade.html +19 -0
- package/prototype-kit/tests/fixtures/multiple-trigger-runtime/probe-all.html +14 -0
- package/prototype-kit/tests/fixtures/nested-inert/branch.plan.json +5 -0
- package/prototype-kit/tests/fixtures/nested-inert/pages/p0.html +17 -0
- package/prototype-kit/tests/offline-check.mjs +6 -1
- package/prototype-kit/tests/rules-check.mjs +28 -9
- package/prototype-kit/verify-runtime.mjs +10 -5
- package/prototype-kit/polish/tests/fixtures/section-breathing/attachment-fire.html +0 -5
- package/prototype-kit/polish/tests/fixtures/section-breathing/attachment-silent.html +0 -5
- package/prototype-kit/polish/tests/fixtures/section-breathing/shadow-fire.html +0 -5
- package/prototype-kit/polish/tests/fixtures/section-breathing/shadow-silent.html +0 -5
package/prototype-kit/build.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { parseHTML } from 'linkedom';
|
|
6
6
|
import { resolveDropdownAction } from './lib/actions.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import { selectUniqueActionOwners, sourceInertActionReason } from './lib/action-target.mjs';
|
|
8
8
|
import {
|
|
9
9
|
HOLDING_SHELL_ATTRIBUTE,
|
|
10
10
|
holdingBlockGeometry,
|
|
@@ -157,6 +157,7 @@ const report = {
|
|
|
157
157
|
unbound: [],
|
|
158
158
|
invalidDropdowns: [],
|
|
159
159
|
actionDemotions: [],
|
|
160
|
+
inertActionCandidates: [],
|
|
160
161
|
duplicateActionOwners: [],
|
|
161
162
|
shellSizeDelta: [],
|
|
162
163
|
shellAmbiguities: model.shellAmbiguities || [],
|
|
@@ -242,12 +243,20 @@ function actionsFor(s) {
|
|
|
242
243
|
}
|
|
243
244
|
|
|
244
245
|
function bindAction(el, action) {
|
|
246
|
+
const inertReason = sourceInertActionReason(el);
|
|
247
|
+
if (inertReason) {
|
|
248
|
+
for (const attribute of ['data-proto-action', 'data-proto-target', 'data-proto-options', 'data-proto-label', 'data-proto-modal']) {
|
|
249
|
+
el.removeAttribute(attribute);
|
|
250
|
+
}
|
|
251
|
+
el.setAttribute('data-proto-inert', inertReason);
|
|
252
|
+
return { bound: false, reason: inertReason };
|
|
253
|
+
}
|
|
245
254
|
el.setAttribute('data-proto-action', action.action);
|
|
246
255
|
if (action.target) el.setAttribute('data-proto-target', action.target);
|
|
247
256
|
if (action.options) el.setAttribute('data-proto-options', JSON.stringify(action.options));
|
|
248
257
|
if (action.labelId) el.setAttribute('data-proto-label', action.labelId);
|
|
249
258
|
if (action.modal) el.setAttribute('data-proto-modal', JSON.stringify(action.modal));
|
|
250
|
-
|
|
259
|
+
return { bound: true, reason: null };
|
|
251
260
|
}
|
|
252
261
|
|
|
253
262
|
const stateActions = new Map(states.map((state) => [state.stateId, actionsFor(state)]));
|
|
@@ -321,7 +330,12 @@ for (const u of units) {
|
|
|
321
330
|
else report.unbound.push({ state: s.stateId, id, action: a.action });
|
|
322
331
|
continue;
|
|
323
332
|
}
|
|
324
|
-
bindAction(el, a);
|
|
333
|
+
const binding = bindAction(el, a);
|
|
334
|
+
if (!binding.bound) {
|
|
335
|
+
report.inertActionCandidates.push({ state: s.stateId, id, action: a.action, reason: binding.reason });
|
|
336
|
+
report.counts.none++;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
325
339
|
report.counts[a.action]++;
|
|
326
340
|
allActions[id] = a;
|
|
327
341
|
}
|
|
@@ -342,13 +356,33 @@ for (const u of units) {
|
|
|
342
356
|
: [...el.querySelectorAll('.text')].find((t) => !t.closest('button'))
|
|
343
357
|
|| (el.textContent.trim() && [...el.children].every((c) => !c.children.length) ? el : null);
|
|
344
358
|
if (!textEl || textEl.closest('button')) continue;
|
|
359
|
+
const fieldInertReason = sourceInertActionReason(el);
|
|
360
|
+
if (fieldInertReason) {
|
|
361
|
+
el.setAttribute('data-proto-inert', fieldInertReason);
|
|
362
|
+
report.inertActionCandidates.push({ state: s.stateId, id: el.id, action: 'field', reason: fieldInertReason });
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
345
365
|
el.setAttribute('data-proto-field', textEl.id);
|
|
346
366
|
el.setAttribute('data-proto-submit', /search/i.test(f.id + f.placeholder) ? 'Search' : 'Send');
|
|
347
367
|
// a sibling send/submit button under the same parent submits the field
|
|
348
368
|
const parent = el.parentElement;
|
|
349
369
|
const send = parent && [...parent.querySelectorAll('button')].find((b) => /send|submit|go$|search|arrow/.test(b.id) && !b.textContent.trim());
|
|
350
370
|
// bind the button's container too: the icon often sits beside (not inside) the button and swallows the click
|
|
351
|
-
if (send)
|
|
371
|
+
if (send) {
|
|
372
|
+
const submitOwner = send.parentElement && send.parentElement !== parent ? send.parentElement : send;
|
|
373
|
+
// Check the native button itself, not just the (possibly promoted)
|
|
374
|
+
// owner: closest() only walks UPWARD, so a submitOwner check alone
|
|
375
|
+
// misses a disabled attribute sitting directly on `send` when the
|
|
376
|
+
// owner is its parent wrapper. Checking `send` covers `send` AND
|
|
377
|
+
// everything above it, submitOwner included.
|
|
378
|
+
const submitInertReason = sourceInertActionReason(send);
|
|
379
|
+
if (submitInertReason) {
|
|
380
|
+
submitOwner.setAttribute('data-proto-inert', submitInertReason);
|
|
381
|
+
report.inertActionCandidates.push({ state: s.stateId, id: submitOwner.id || send.id, action: 'field-submit', reason: submitInertReason });
|
|
382
|
+
} else {
|
|
383
|
+
submitOwner.setAttribute('data-proto-submit-for', textEl.id);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
352
386
|
fieldCount++; report.counts.field++;
|
|
353
387
|
}
|
|
354
388
|
// content layer
|
|
@@ -359,14 +393,17 @@ for (const u of units) {
|
|
|
359
393
|
const unitRegions = new Set(u.states.flatMap((state) => state.shell.map((item) => item.region)));
|
|
360
394
|
for (const region of unitRegions) for (const [id, action] of canonicalShellActions[region] || []) {
|
|
361
395
|
const elements = shellElementsForSourceId(document, id);
|
|
396
|
+
let bound = false;
|
|
362
397
|
for (const element of elements) {
|
|
363
398
|
const scope = element.closest('.ly-band[data-ly-band]') || document;
|
|
364
399
|
const labelId = action.labelId
|
|
365
400
|
? shellElementsForSourceId(scope, action.labelId)[0]?.id || action.labelId
|
|
366
401
|
: null;
|
|
367
|
-
bindAction(element, labelId ? { ...action, labelId } : action);
|
|
402
|
+
const binding = bindAction(element, labelId ? { ...action, labelId } : action);
|
|
403
|
+
if (binding.bound) bound = true;
|
|
404
|
+
else report.inertActionCandidates.push({ unit: u.id, region, id: element.id || id, action: action.action, reason: binding.reason });
|
|
368
405
|
}
|
|
369
|
-
if (
|
|
406
|
+
if (bound && !allActions[id]) {
|
|
370
407
|
allActions[id] = action;
|
|
371
408
|
report.shellActionRemaps++;
|
|
372
409
|
}
|
|
@@ -378,7 +415,11 @@ for (const u of units) {
|
|
|
378
415
|
for (const el of targets) {
|
|
379
416
|
const tgt = n.target ? (unitOf(n.target)?.id === u.id ? null : fileOf(n.target)) : holdingFile(n.text);
|
|
380
417
|
if (!tgt) { el.removeAttribute('data-proto-action'); continue; }
|
|
381
|
-
el
|
|
418
|
+
const binding = bindAction(el, { action: 'navigate', target: tgt });
|
|
419
|
+
if (!binding.bound) {
|
|
420
|
+
report.inertActionCandidates.push({ unit: u.id, id: el.id || n.rowId, action: 'navigate', reason: binding.reason });
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
382
423
|
for (const d of el.querySelectorAll('[data-proto-action]')) { d.removeAttribute('data-proto-action'); d.removeAttribute('data-proto-target'); delete allActions[d.id]; }
|
|
383
424
|
allActions[n.rowId] = { action: 'navigate', target: tgt, nav: true };
|
|
384
425
|
}
|
|
@@ -609,7 +650,11 @@ for (const u of units) {
|
|
|
609
650
|
const targets = elements.length > 0 ? elements : shellElementsForSourceId(document, n.id);
|
|
610
651
|
for (const el of targets) {
|
|
611
652
|
const tgt = n.holding === hslug ? null : n.target ? fileOf(n.target) : holdingFile(n.text);
|
|
612
|
-
if (tgt) {
|
|
653
|
+
if (tgt) {
|
|
654
|
+
const binding = bindAction(el, { action: 'navigate', target: tgt });
|
|
655
|
+
if (binding.bound) for (const d of el.querySelectorAll('[data-proto-action]')) d.removeAttribute('data-proto-action');
|
|
656
|
+
else report.inertActionCandidates.push({ holding: hslug, id: el.id || n.rowId, action: 'navigate', reason: binding.reason });
|
|
657
|
+
}
|
|
613
658
|
}
|
|
614
659
|
}
|
|
615
660
|
document.title = htitle;
|
|
@@ -629,5 +674,5 @@ fs.writeFileSync(path.join(outDir, 'index.html'), `<!doctype html><meta http-equ
|
|
|
629
674
|
const manifestHoldings = review
|
|
630
675
|
? [...usedHoldings.entries()].map(([holding, title]) => [holding.replace(/\.html$/, ''), title])
|
|
631
676
|
: [...usedHoldings.entries()];
|
|
632
|
-
fs.writeFileSync(path.join(outDir, 'proto-manifest.json'), JSON.stringify({ units: report.units, nav: navRows.map((n) => ({ text: n.text, target: n.target, holding: n.holding, rowId: n.rowId })), holdings: manifestHoldings, counts: report.counts, unbound: report.unbound, invalidDropdowns: report.invalidDropdowns, ...(report.actionDemotions.length > 0 ? { actionDemotions: report.actionDemotions } : {}), ...(report.duplicateActionOwners.length > 0 ? { duplicateActionOwners: report.duplicateActionOwners } : {}), shellSizeDelta: report.shellSizeDelta, shellAmbiguities: report.shellAmbiguities, suppressedShellOverlaps: report.suppressedShellOverlaps, skippedShellTransplants: report.skippedShellTransplants, replacedShellActions: report.replacedShellActions, shellActionRemaps: report.shellActionRemaps, scriptDirections: states.map(({ stateId, scriptDirection, writingMode }) => ({ stateId, scriptDirection, writingMode })), canonicalShell: model.canonicalShell, canonicalShellPage: report.canonicalShellPage, shellConsistency: report.shellConsistency, ...(review ? { review } : {}) }, null, 1));
|
|
677
|
+
fs.writeFileSync(path.join(outDir, 'proto-manifest.json'), JSON.stringify({ units: report.units, nav: navRows.map((n) => ({ text: n.text, target: n.target, holding: n.holding, rowId: n.rowId })), holdings: manifestHoldings, counts: report.counts, unbound: report.unbound, invalidDropdowns: report.invalidDropdowns, ...(report.actionDemotions.length > 0 ? { actionDemotions: report.actionDemotions } : {}), ...(report.inertActionCandidates.length > 0 ? { inertActionCandidates: report.inertActionCandidates } : {}), ...(report.duplicateActionOwners.length > 0 ? { duplicateActionOwners: report.duplicateActionOwners } : {}), shellSizeDelta: report.shellSizeDelta, shellAmbiguities: report.shellAmbiguities, suppressedShellOverlaps: report.suppressedShellOverlaps, skippedShellTransplants: report.skippedShellTransplants, replacedShellActions: report.replacedShellActions, shellActionRemaps: report.shellActionRemaps, scriptDirections: states.map(({ stateId, scriptDirection, writingMode }) => ({ stateId, scriptDirection, writingMode })), canonicalShell: model.canonicalShell, canonicalShellPage: report.canonicalShellPage, shellConsistency: report.shellConsistency, ...(review ? { review } : {}) }, null, 1));
|
|
633
678
|
console.error('counts', JSON.stringify(report.counts), 'unbound', report.unbound.length);
|
|
@@ -1,12 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
const SOURCE_INERT_ATTRIBUTES = [
|
|
2
|
+
['disabled', 'disabled'],
|
|
3
|
+
['inert', 'inert'],
|
|
4
|
+
['hidden', 'hidden'],
|
|
5
|
+
['data-proto-demoted', 'demoted'],
|
|
6
|
+
// The converter's own canonical marker for a non-native control it
|
|
7
|
+
// deliberately left presentational (unresolved anchor/role control) —
|
|
8
|
+
// see workers/api/src/conversionPackage/siteAuthoredControls.ts. This is
|
|
9
|
+
// a real source-declared inert state, not a model artifact, and must be
|
|
10
|
+
// checked here alongside the native disabled/inert/hidden cases.
|
|
11
|
+
['data-12ui-presentational-control', 'presentational-control'],
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
// WAI-ARIA 1.2: aria-disabled="true" on an ancestor disables the element and
|
|
15
|
+
// ALL focusable descendants — a nearer descendant's own aria-disabled="false"
|
|
16
|
+
// does not cancel it. closest('[aria-disabled]') stops at the first match
|
|
17
|
+
// regardless of its value, so it can find a descendant's "false" and miss a
|
|
18
|
+
// "true" further up. Walk the whole ancestor chain instead.
|
|
19
|
+
function ariaDisabledAncestor(element) {
|
|
20
|
+
for (let node = element; node; node = node.parentElement) {
|
|
21
|
+
const value = node.getAttribute && node.getAttribute('aria-disabled');
|
|
22
|
+
if (value != null && value.trim().toLowerCase() === 'true') return node;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A source-declared inert state outranks a model-selected action. The builder
|
|
28
|
+
* records the rejected selection, while the browser runtime repairs only the
|
|
29
|
+
* targets that survive this boundary and its own action validation. */
|
|
30
|
+
export function sourceInertActionReason(element) {
|
|
31
|
+
for (const [attribute, reason] of SOURCE_INERT_ATTRIBUTES) {
|
|
32
|
+
if (element.closest(`[${attribute}]`)) return reason;
|
|
6
33
|
}
|
|
7
|
-
element
|
|
8
|
-
|
|
9
|
-
return true;
|
|
34
|
+
if (ariaDisabledAncestor(element)) return 'aria-disabled';
|
|
35
|
+
return null;
|
|
10
36
|
}
|
|
11
37
|
|
|
12
38
|
/**
|
|
@@ -23,9 +23,8 @@ function configuredWidths(protoDir, file) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
async function smokeInteraction(page,
|
|
27
|
-
const
|
|
28
|
-
if (!await locator.count()) return { present: false, ok: true };
|
|
26
|
+
async function smokeInteraction(page, locator, assertion, index) {
|
|
27
|
+
const id = await locator.getAttribute('id');
|
|
29
28
|
let reviewClearanceAdded = false;
|
|
30
29
|
try {
|
|
31
30
|
reviewClearanceAdded = await locator.evaluate((element) => {
|
|
@@ -49,7 +48,7 @@ async function smokeInteraction(page, selector, assertion) {
|
|
|
49
48
|
// the gate measures whether the action is reachable anywhere in the
|
|
50
49
|
// scrollable page, rather than only at its initial occluded position.
|
|
51
50
|
await locator.evaluate((element) => element.scrollIntoView({ block: 'center', inline: 'nearest' }));
|
|
52
|
-
const
|
|
51
|
+
const hitTest = await locator.evaluate((element) => {
|
|
53
52
|
const rect = element.getBoundingClientRect();
|
|
54
53
|
const insetX = Math.min(8, rect.width / 4);
|
|
55
54
|
const insetY = Math.min(8, rect.height / 4);
|
|
@@ -60,22 +59,26 @@ async function smokeInteraction(page, selector, assertion) {
|
|
|
60
59
|
[rect.left + insetX, rect.bottom - insetY],
|
|
61
60
|
[rect.right - insetX, rect.bottom - insetY],
|
|
62
61
|
];
|
|
62
|
+
const receivers = [];
|
|
63
63
|
for (const [x, y] of points) {
|
|
64
64
|
const receiver = document.elementFromPoint(x, y);
|
|
65
|
-
|
|
65
|
+
receivers.push(receiver?.id || receiver?.tagName?.toLowerCase() || null);
|
|
66
|
+
if (receiver?.closest('[data-proto-runtime-bound]') === element) {
|
|
67
|
+
return { point: { x, y }, receivers, pointerEvents: getComputedStyle(element).pointerEvents };
|
|
68
|
+
}
|
|
66
69
|
}
|
|
67
|
-
return null;
|
|
70
|
+
return { point: null, receivers, pointerEvents: getComputedStyle(element).pointerEvents };
|
|
68
71
|
});
|
|
69
|
-
if (!point) return { present: true, ok: false, error: 'no pointer-reachable point' };
|
|
72
|
+
if (!hitTest.point) return { id, index, present: true, ok: false, ...hitTest, error: 'no pointer-reachable point' };
|
|
70
73
|
// A real pointer lands on the rendered child and bubbles to its action
|
|
71
74
|
// wrapper. locator.click() rejects that valid shape as "intercepted".
|
|
72
|
-
await page.mouse.click(point.x, point.y);
|
|
75
|
+
await page.mouse.click(hitTest.point.x, hitTest.point.y);
|
|
73
76
|
const opened = await page.locator(assertion).count();
|
|
74
77
|
const ok = opened === 1;
|
|
75
78
|
await page.keyboard.press('Escape');
|
|
76
|
-
return { present: true, ok,
|
|
79
|
+
return { id, index, present: true, ok, ...hitTest, opened };
|
|
77
80
|
} catch (error) {
|
|
78
|
-
return { present: true, ok: false, error: errorText(error) };
|
|
81
|
+
return { id, index, present: true, ok: false, error: errorText(error) };
|
|
79
82
|
} finally {
|
|
80
83
|
if (reviewClearanceAdded) {
|
|
81
84
|
await page.locator('[data-proto-runtime-clearance="true"]').evaluateAll((nodes) => nodes.forEach((node) => node.remove())).catch(() => null);
|
|
@@ -83,6 +86,22 @@ async function smokeInteraction(page, selector, assertion) {
|
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
async function smokeInteractions(page, action, assertion) {
|
|
90
|
+
const locators = page.locator(`[data-proto-runtime-bound="${action}"]`);
|
|
91
|
+
const count = await locators.count();
|
|
92
|
+
if (count === 0) return { present: false, ok: true, probed: 0, probes: [] };
|
|
93
|
+
const probes = [];
|
|
94
|
+
for (let index = 0; index < count; index++) {
|
|
95
|
+
probes.push(await smokeInteraction(page, locators.nth(index), assertion, index));
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
present: true,
|
|
99
|
+
ok: probes.every((probe) => probe.ok),
|
|
100
|
+
probed: probes.length,
|
|
101
|
+
probes,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
86
105
|
async function checkDeepLinkBack(browser, url, pageFile) {
|
|
87
106
|
const page = await browser.newPage({ viewport: { width: 1536, height: 1024 } });
|
|
88
107
|
try {
|
|
@@ -426,8 +445,8 @@ export async function checkRuntime({ browser, baseUrl, relativeProtoPath, protoD
|
|
|
426
445
|
if (!fs.existsSync(targetPath)) brokenTargets.push({ page: file, target, reason: 'missing file' });
|
|
427
446
|
else if (hash && !fs.readFileSync(targetPath, 'utf8').includes(`id="${hash}"`)) brokenTargets.push({ page: file, target, reason: 'missing anchor' });
|
|
428
447
|
}
|
|
429
|
-
dropdown = await
|
|
430
|
-
modal = await
|
|
448
|
+
dropdown = await smokeInteractions(page, 'dropdown', '.proto-menu');
|
|
449
|
+
modal = await smokeInteractions(page, 'modal', '.proto-modal');
|
|
431
450
|
}
|
|
432
451
|
viewports.push({ width, ok: pageErrors.length === 0 && composition.ok && shellState.ok, pageErrors, consoleErrors, composition, shellState });
|
|
433
452
|
} catch (error) {
|
|
@@ -44,7 +44,10 @@ export function shadowGradientProfile(data, info, x, want) {
|
|
|
44
44
|
export async function probeSeams(page, seams, baselineDocumentHeight) {
|
|
45
45
|
const failures = []; let probes = 0; let toleratedShadowGradients = 0;
|
|
46
46
|
const height = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
47
|
-
const
|
|
47
|
+
const declaredExpectedHeights = [...new Set(seams.map((seam) => seam.expectedDocumentHeight).filter(Number.isFinite))];
|
|
48
|
+
if (declaredExpectedHeights.length > 1) failures.push({ kind: 'document-height-declaration', declared: declaredExpectedHeights });
|
|
49
|
+
const expected = declaredExpectedHeights[0]
|
|
50
|
+
?? (baselineDocumentHeight || 0) + seams.reduce((n, s) => n + s.addition, 0);
|
|
48
51
|
if (baselineDocumentHeight && Math.abs(height - expected) > 1) failures.push({ kind: 'document-height', expected, actual: height });
|
|
49
52
|
const floor = await page.evaluate(() => ({
|
|
50
53
|
body: getComputedStyle(document.body).backgroundColor,
|
|
@@ -25,11 +25,11 @@ export function pageMetricsScript() {
|
|
|
25
25
|
return c;
|
|
26
26
|
};
|
|
27
27
|
const kindOf = (el) => isText(el) ? 'text' : (el.tagName === 'BUTTON' || el.classList.contains('button')) ? 'button' : el.classList.contains('icon') ? 'icon' : (el.tagName === 'IMG' || el.classList.contains('image')) ? 'image' : el.classList.contains('badge') ? 'badge' : el.classList.contains('divider') ? 'divider' : 'box';
|
|
28
|
-
const sameKind = (A, B) => { const ka = kindOf(A.el), kb = kindOf(B.el); if (ka !== kb) return false; if (ka === 'text') return Math.abs(parseFloat(getComputedStyle(A.el).fontSize) - parseFloat(getComputedStyle(B.el).fontSize)) < 1 && Math.abs(A.r.h - B.r.h) < 3; if (ka === 'box') return Math.abs(A.r.h - B.r.h) <= 4 || Math.abs(A.r.w - B.r.w) <= 4; return Math.abs(A.r.h - B.r.h) < 3; };
|
|
28
|
+
const sameKind = (A, B) => { const ka = kindOf(A.el), kb = kindOf(B.el); if (ka !== kb) return false; if (ka === 'text') return Math.abs(Number.parseFloat(getComputedStyle(A.el).fontSize) - Number.parseFloat(getComputedStyle(B.el).fontSize)) < 1 && Math.abs(A.r.h - B.r.h) < 3; if (ka === 'box') return Math.abs(A.r.h - B.r.h) <= 4 || Math.abs(A.r.w - B.r.w) <= 4; return Math.abs(A.r.h - B.r.h) < 3; };
|
|
29
29
|
const imageBacked = (el, r) => { let a = el.parentElement; while (a) { if (getComputedStyle(a).backgroundImage !== 'none') return true; a = a.parentElement; } const s = secOf(el); for (const im of s.querySelectorAll('img, .image, .decoration, .chart, svg, canvas, video')) { const ir = im.getBoundingClientRect(); if (ir.width && ir.height && Math.min(ir.right, r.right) - Math.max(ir.left, r.left) > 0 && Math.min(ir.bottom, r.bottom) - Math.max(ir.top, r.top) > 0) return true; } return false; };
|
|
30
30
|
const visible = (el) => {
|
|
31
31
|
const cs = getComputedStyle(el);
|
|
32
|
-
if (cs.display === 'none' || cs.visibility === 'hidden' || parseFloat(cs.opacity) === 0) return false;
|
|
32
|
+
if (cs.display === 'none' || cs.visibility === 'hidden' || Number.parseFloat(cs.opacity) === 0) return false;
|
|
33
33
|
const r = el.getBoundingClientRect();
|
|
34
34
|
return r.width > 0 && r.height > 0;
|
|
35
35
|
};
|
|
@@ -54,7 +54,7 @@ export function pageMetricsScript() {
|
|
|
54
54
|
.filter((el) => visible(el) && !el.closest('#proto-overlay-root') && !el.querySelector('.text, .paragraph') && !(el.matches('button, .button') && el.closest('.proto-notice, .proto-holding-block')));
|
|
55
55
|
const containers = qsa('div[id], button[id], aside[id], header[id], footer[id], nav[id], article[id], form[id], main[id]')
|
|
56
56
|
.filter((el) => visible(el) && !el.closest('#proto-overlay-root'));
|
|
57
|
-
const parseRgb = (s) => { const m = s && s.match(/rgba?\(([^)]+)\)/); if (!m) return null; const p = m[1].split(',').map(parseFloat); return { r: p[0], g: p[1], b: p[2], a: p[3] === undefined ? 1 : p[3] }; };
|
|
57
|
+
const parseRgb = (s) => { const m = s && s.match(/rgba?\(([^)]+)\)/); if (!m) return null; const p = m[1].split(',').map(Number.parseFloat); return { r: p[0], g: p[1], b: p[2], a: p[3] === undefined ? 1 : p[3] }; };
|
|
58
58
|
const lum = (c) => { const f = (v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; }; return 0.2126 * f(c.r) + 0.7152 * f(c.g) + 0.0722 * f(c.b); };
|
|
59
59
|
// what is actually painted under the text: walk the hit-test stack at the text centre
|
|
60
60
|
const effectiveBg = (el) => {
|
|
@@ -88,7 +88,7 @@ export function pageMetricsScript() {
|
|
|
88
88
|
let a = el.parentElement, esc = null;
|
|
89
89
|
while (a && a !== sec) {
|
|
90
90
|
const cs2 = getComputedStyle(a); const bg = parseRgb(cs2.backgroundColor);
|
|
91
|
-
if ((bg && bg.a > 0.5) || parseFloat(cs2.borderTopWidth) > 0) {
|
|
91
|
+
if ((bg && bg.a > 0.5) || Number.parseFloat(cs2.borderTopWidth) > 0) {
|
|
92
92
|
const ar = rel(a.getBoundingClientRect(), el);
|
|
93
93
|
const over = Math.max(t.r - ar.r, ar.x - t.x, t.b - ar.b, ar.y - t.y);
|
|
94
94
|
if (over > 1.5) esc = { kind: 'escape', px: +over.toFixed(1), container: a.id, ...box };
|
|
@@ -248,7 +248,7 @@ export function pageMetricsScript() {
|
|
|
248
248
|
let la = hr.left, ra = hr.right;
|
|
249
249
|
for (const b of boxes) { if (b === host || g.some((x) => b === x.el || b.contains(x.el))) continue; const br = b.getBoundingClientRect(); if (!br.width || br.top >= bottom || br.bottom <= top) continue; if (br.right <= left + 1) la = Math.max(la, br.right); if (br.left >= right - 1) ra = Math.min(ra, br.left); }
|
|
250
250
|
const li = left - la, ri = ra - right;
|
|
251
|
-
const expL = parseFloat(host.getAttribute('data-proto-fit-left')), expR = parseFloat(host.getAttribute('data-proto-fit-right'));
|
|
251
|
+
const expL = Number.parseFloat(host.getAttribute('data-proto-fit-left')), expR = Number.parseFloat(host.getAttribute('data-proto-fit-right'));
|
|
252
252
|
let bad = false;
|
|
253
253
|
const partial = host.hasAttribute('data-proto-fit-partial');
|
|
254
254
|
// a partial re-centre (ink stopped at the box edge) is judged as 'no worse than the donor', not as centred
|
|
@@ -264,7 +264,7 @@ export function pageMetricsScript() {
|
|
|
264
264
|
out.rail = [];
|
|
265
265
|
{
|
|
266
266
|
const INK = '.text, .paragraph, h1, h2, h3, h4, h5, h6, .icon, img, .image, .badge, button, .button';
|
|
267
|
-
const paintedBox = (el) => { const cs = getComputedStyle(el); const bg = parseRgb(cs.backgroundColor); return (bg && bg.a > 0.5) || parseFloat(cs.borderLeftWidth) > 0; };
|
|
267
|
+
const paintedBox = (el) => { const cs = getComputedStyle(el); const bg = parseRgb(cs.backgroundColor); return (bg && bg.a > 0.5) || Number.parseFloat(cs.borderLeftWidth) > 0; };
|
|
268
268
|
for (const sec of scope) {
|
|
269
269
|
const sb = sec.getBoundingClientRect();
|
|
270
270
|
const edgeOf = (el) => { const r = el.getBoundingClientRect(); let left = paintedBox(el) ? r.left : Infinity, right = paintedBox(el) ? r.right : -Infinity; for (const k of [...(el.matches(INK) ? [el] : []), ...el.querySelectorAll(INK)]) { if (!visible(k)) continue; const kr = k.getBoundingClientRect(); if (kr.left - sb.left <= 2) continue; left = Math.min(left, kr.left); right = Math.max(right, kr.right); } for (const k of el.querySelectorAll('[id]:not(.text):not(.icon):not(img):not(button)')) { if (!visible(k) || !paintedBox(k)) continue; const kr = k.getBoundingClientRect(); if (kr.left - sb.left <= 2 || kr.width < 40) continue; left = Math.min(left, kr.left); right = Math.max(right, kr.right); } return Number.isFinite(left) ? { left: left - sb.left, right: right - sb.left } : null; };
|