@12ui/design 0.2.47 → 0.2.49
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-execution.js +2 -2
- package/dist/branch-execution.js.map +1 -1
- package/dist/branch-page-conversion.d.ts +4 -1
- package/dist/branch-page-conversion.d.ts.map +1 -1
- package/dist/branch-page-conversion.js +27 -17
- package/dist/branch-page-conversion.js.map +1 -1
- package/dist/branch-run-record.d.ts +2 -0
- package/dist/branch-run-record.d.ts.map +1 -1
- package/dist/branch-run-record.js.map +1 -1
- package/dist/cli-branch-command.d.ts +2 -0
- package/dist/cli-branch-command.d.ts.map +1 -1
- package/dist/cli-branch-command.js +3 -0
- package/dist/cli-branch-command.js.map +1 -1
- package/dist/cli-convert-command.js +2 -2
- package/dist/cli-convert-command.js.map +1 -1
- package/dist/cli-draft-command.d.ts +11 -5
- package/dist/cli-draft-command.d.ts.map +1 -1
- package/dist/cli-draft-command.js +37 -35
- package/dist/cli-draft-command.js.map +1 -1
- package/dist/cli-help/design.d.ts +10 -0
- package/dist/cli-help/design.d.ts.map +1 -1
- package/dist/cli-help/design.js +29 -0
- package/dist/cli-help/design.js.map +1 -1
- package/dist/cli-help/index.d.ts +21 -0
- package/dist/cli-help/index.d.ts.map +1 -1
- package/dist/cli-help/index.js +18 -2
- package/dist/cli-help/index.js.map +1 -1
- package/dist/cli-help/meta.d.ts +10 -0
- package/dist/cli-help/meta.d.ts.map +1 -1
- package/dist/cli-help/meta.js +20 -0
- package/dist/cli-help/meta.js.map +1 -1
- package/dist/cli-next-command.d.ts.map +1 -1
- package/dist/cli-next-command.js +26 -14
- package/dist/cli-next-command.js.map +1 -1
- package/dist/cli-progress.d.ts +2 -0
- package/dist/cli-progress.d.ts.map +1 -1
- package/dist/cli-progress.js +2 -0
- package/dist/cli-progress.js.map +1 -1
- package/dist/cli-redesign-command.d.ts +42 -0
- package/dist/cli-redesign-command.d.ts.map +1 -0
- package/dist/cli-redesign-command.js +520 -0
- package/dist/cli-redesign-command.js.map +1 -0
- package/dist/cli-run.d.ts.map +1 -1
- package/dist/cli-run.js +16 -0
- package/dist/cli-run.js.map +1 -1
- package/dist/cli-spend-command.d.ts +20 -0
- package/dist/cli-spend-command.d.ts.map +1 -0
- package/dist/cli-spend-command.js +46 -0
- package/dist/cli-spend-command.js.map +1 -0
- package/dist/cli-usage.d.ts.map +1 -1
- package/dist/cli-usage.js +9 -1
- package/dist/cli-usage.js.map +1 -1
- package/dist/conversion-receipt-store.d.ts +2 -2
- package/dist/conversion-receipt-store.d.ts.map +1 -1
- package/dist/conversion-receipt-store.js +11 -5
- package/dist/conversion-receipt-store.js.map +1 -1
- package/dist/conversion-receipt.d.ts +2 -3
- package/dist/conversion-receipt.d.ts.map +1 -1
- package/dist/conversion-receipt.js +17 -12
- package/dist/conversion-receipt.js.map +1 -1
- package/dist/draft-dispatch-attempt.d.ts +0 -7
- package/dist/draft-dispatch-attempt.d.ts.map +1 -1
- package/dist/draft-dispatch-attempt.js +1 -25
- package/dist/draft-dispatch-attempt.js.map +1 -1
- package/dist/draft-identity.d.ts +2 -0
- package/dist/draft-identity.d.ts.map +1 -1
- package/dist/draft-identity.js +1 -0
- package/dist/draft-identity.js.map +1 -1
- package/dist/draft-run.d.ts +2 -0
- package/dist/draft-run.d.ts.map +1 -1
- package/dist/draft-run.js +4 -3
- package/dist/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 +12 -0
- package/dist/package-submission.d.ts.map +1 -1
- package/dist/package-submission.js +5 -0
- package/dist/package-submission.js.map +1 -1
- package/dist/prototype-label-artifact.d.ts +29 -0
- package/dist/prototype-label-artifact.d.ts.map +1 -0
- package/dist/prototype-label-artifact.js +104 -0
- package/dist/prototype-label-artifact.js.map +1 -0
- package/dist/prototype-runner.d.ts +1 -0
- package/dist/prototype-runner.d.ts.map +1 -1
- package/dist/prototype-runner.js +18 -2
- package/dist/prototype-runner.js.map +1 -1
- package/dist/redesign-brand-asset-inventory.d.ts +71 -0
- package/dist/redesign-brand-asset-inventory.d.ts.map +1 -0
- package/dist/redesign-brand-asset-inventory.js +318 -0
- package/dist/redesign-brand-asset-inventory.js.map +1 -0
- package/dist/redesign-capture.d.ts +57 -0
- package/dist/redesign-capture.d.ts.map +1 -0
- package/dist/redesign-capture.js +181 -0
- package/dist/redesign-capture.js.map +1 -0
- package/dist/redesign-pinned-asset.d.ts +20 -0
- package/dist/redesign-pinned-asset.d.ts.map +1 -0
- package/dist/redesign-pinned-asset.js +35 -0
- package/dist/redesign-pinned-asset.js.map +1 -0
- package/dist/redesign-plan-client.d.ts +49 -0
- package/dist/redesign-plan-client.d.ts.map +1 -0
- package/dist/redesign-plan-client.js +90 -0
- package/dist/redesign-plan-client.js.map +1 -0
- package/dist/redesign-plan-identity.d.ts +8 -0
- package/dist/redesign-plan-identity.d.ts.map +1 -0
- package/dist/redesign-plan-identity.js +16 -0
- package/dist/redesign-plan-identity.js.map +1 -0
- package/dist/redesign-plan.d.ts +30 -0
- package/dist/redesign-plan.d.ts.map +1 -0
- package/dist/redesign-plan.js +99 -0
- package/dist/redesign-plan.js.map +1 -0
- package/dist/redesign-workspace.d.ts +142 -0
- package/dist/redesign-workspace.d.ts.map +1 -0
- package/dist/redesign-workspace.js +82 -0
- package/dist/redesign-workspace.js.map +1 -0
- package/dist/run-journal-commands.d.ts.map +1 -1
- package/dist/run-journal-commands.js +16 -0
- package/dist/run-journal-commands.js.map +1 -1
- package/dist/spend-client.d.ts +97 -0
- package/dist/spend-client.d.ts.map +1 -0
- package/dist/spend-client.js +31 -0
- package/dist/spend-client.js.map +1 -0
- package/dist/spend-report-lines.d.ts +4 -0
- package/dist/spend-report-lines.d.ts.map +1 -0
- package/dist/spend-report-lines.js +131 -0
- package/dist/spend-report-lines.js.map +1 -0
- package/open-design.json +2 -2
- package/package.json +2 -2
- package/prototype-kit/build.mjs +9 -3
- package/prototype-kit/lib/action-target.mjs +26 -0
- package/prototype-kit/lib/local-document.mjs +16 -0
- package/prototype-kit/lib/runtime-check.mjs +83 -19
- package/prototype-kit/lib/shell-runtime-equivalence.mjs +41 -0
- package/prototype-kit/polish/harm.mjs +27 -5
- package/prototype-kit/polish/measure.mjs +101 -5
- package/prototype-kit/polish/overrides.mjs +32 -3
- package/prototype-kit/polish/polish.mjs +23 -11
- package/prototype-kit/polish/rules/align.mjs +73 -3
- package/prototype-kit/polish/rules/left-rail.mjs +2 -3
- package/prototype-kit/polish/rules/section-breathing.mjs +0 -1
- package/prototype-kit/polish/tests/fixtures/align/expect.json +1 -1
- package/prototype-kit/polish/tests/fixtures/align/fire.html +4 -1
- package/prototype-kit/polish/tests/fixtures/align/silent.html +8 -0
- package/prototype-kit/polish/tests/fixtures/align/zoom-constrained.html +17 -0
- package/prototype-kit/polish/tests/fixtures/harm/left-rail-contained.html +16 -0
- package/prototype-kit/polish/tests/fixtures/harm/left-rail-overflow.html +16 -0
- package/prototype-kit/polish/tests/fixtures/harm/nested-actions.html +6 -0
- package/prototype-kit/polish/tests/fixtures/harm/unidentified-parent.html +3 -0
- package/prototype-kit/polish/tests/fixtures/shell-seam/fire.html +1 -1
- package/prototype-kit/polish/tests/harm-check.mjs +131 -2
- package/prototype-kit/polish/tests/rules-check.mjs +7 -2
- package/prototype-kit/runtime/proto.js +4 -1
- package/prototype-kit/tests/adversarial-runtime.mjs +3 -0
- package/prototype-kit/tests/align-rule-check.mjs +105 -0
- package/prototype-kit/tests/fixtures/interaction-runtime/review-bottom-action.html +8 -0
- package/prototype-kit/tests/offline-check.mjs +17 -1
- package/prototype-kit/tests/rules-check.mjs +67 -1
- package/prototype-kit/tests/shell-stitching-runtime.mjs +15 -8
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
// Paired cases for the structure harm guard (harm.mjs): a synthetic override that breaks an icon column or collapses
|
|
3
3
|
// the rail/content gutter MUST be flagged; a rhythm-sized nudge (≤ 6 px) on a gap MUST NOT.
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
5
7
|
import { chromium } from '@playwright/test';
|
|
6
8
|
import { measurePage } from '../measure.mjs';
|
|
7
|
-
import { applyOverridesScript } from '../overrides.mjs';
|
|
9
|
+
import { applyOverridesScript, mergeOverrides, overridesToCss, partitionProtectedShellOverridesScript } from '../overrides.mjs';
|
|
8
10
|
import { structureHarm } from '../harm.mjs';
|
|
11
|
+
import * as align from '../rules/align.mjs';
|
|
12
|
+
import * as leftRail from '../rules/left-rail.mjs';
|
|
13
|
+
import { polishPrototype } from '../polish.mjs';
|
|
9
14
|
const here = path.dirname(new URL(import.meta.url).pathname);
|
|
10
15
|
const browser = await chromium.launch();
|
|
11
16
|
const page = await browser.newPage({ viewport: { width: 1536, height: 1024 } });
|
|
@@ -29,7 +34,7 @@ const gapBefore = await measurePage(page, {});
|
|
|
29
34
|
await page.evaluate(applyOverridesScript(), '#viewport-1-a-footer{transform:translate(0,32px)!important}');
|
|
30
35
|
const gapAfter = await measurePage(page, {});
|
|
31
36
|
const undeclaredGap = structureHarm(gapBefore, gapAfter);
|
|
32
|
-
const declaredGap = structureHarm(gapBefore, gapAfter, new Set(
|
|
37
|
+
const declaredGap = structureHarm(gapBefore, gapAfter, new Set(['viewport-1-a-content|viewport-1-a-footer']));
|
|
33
38
|
const okDeclaredGap = undeclaredGap.spacing.length > 0 && declaredGap.spacing.length === 0;
|
|
34
39
|
if (!okDeclaredGap) failures++;
|
|
35
40
|
console.log(`${okDeclaredGap ? 'ok ' : 'FAIL'} harm/declared-gap: undeclared=${undeclaredGap.spacing.length} declared=${declaredGap.spacing.length}`);
|
|
@@ -47,6 +52,130 @@ const pairs = mo.overlap.map((o) => [o.a, o.b].sort().join('|'));
|
|
|
47
52
|
const okBtn = pairs.length === 1 && pairs[0] === 'viewport-1-a-hero-eyebrow|viewport-1-a-hero-stray';
|
|
48
53
|
if (!okBtn) failures++;
|
|
49
54
|
console.log(`${okBtn ? 'ok ' : 'FAIL'} overlap/button-over-text: ${JSON.stringify(pairs)} (expected eyebrow|stray only)`);
|
|
55
|
+
// The page-gallery failure class: a positioned card is not a stacking context until align adds transform. Its high-z
|
|
56
|
+
// action can initially paint over a canvas sibling, then becomes trapped underneath it. The hit-test guard must catch
|
|
57
|
+
// that legacy emission; align's current inset emission must preserve reachability and contain no transform.
|
|
58
|
+
await page.goto('file://' + path.join(here, 'fixtures/align/fire.html'), { waitUntil: 'load' });
|
|
59
|
+
const alignBefore = await measurePage(page, {});
|
|
60
|
+
const alignOverrides = await page.evaluate(align.browserFn(), { metrics: alignBefore, params: align.params });
|
|
61
|
+
const protectedAlign = await page.evaluate(partitionProtectedShellOverridesScript(), {
|
|
62
|
+
overrides: alignOverrides,
|
|
63
|
+
shellIds: ['viewport-1-a-cards'],
|
|
64
|
+
});
|
|
65
|
+
const legacyAlignCss = alignOverrides.map((o) => `#${o.id}{transform:translate(${o.dx || 0}px,${o.dy || 0}px)!important}`).join('\n');
|
|
66
|
+
await page.evaluate(applyOverridesScript(), legacyAlignCss);
|
|
67
|
+
const alignLegacyAfter = await measurePage(page, {});
|
|
68
|
+
const legacyAlignHarm = structureHarm(alignBefore, alignLegacyAfter);
|
|
69
|
+
const fixedAlignCss = overridesToCss(mergeOverrides(alignOverrides));
|
|
70
|
+
await page.evaluate(applyOverridesScript(), fixedAlignCss);
|
|
71
|
+
const alignFixedAfter = await measurePage(page, {});
|
|
72
|
+
const fixedAlignHarm = structureHarm(alignBefore, alignFixedAfter);
|
|
73
|
+
const okAlignHit = alignBefore.counts.reachableActions === 1
|
|
74
|
+
&& alignLegacyAfter.counts.reachableActions === 0
|
|
75
|
+
&& legacyAlignHarm.unreachableActions.length === 1
|
|
76
|
+
&& alignFixedAfter.counts.reachableActions === 1
|
|
77
|
+
&& fixedAlignHarm.unreachableActions.length === 0
|
|
78
|
+
&& !fixedAlignCss.includes('transform:')
|
|
79
|
+
&& !fixedAlignCss.includes('height:')
|
|
80
|
+
&& protectedAlign.allowed.length === 0
|
|
81
|
+
&& protectedAlign.protected.length === 2;
|
|
82
|
+
if (!okAlignHit) failures++;
|
|
83
|
+
console.log(`${okAlignHit ? 'ok ' : 'FAIL'} harm/align-hit-test: legacy reachable ${alignBefore.counts.reachableActions}→${alignLegacyAfter.counts.reachableActions}, fixed ${alignBefore.counts.reachableActions}→${alignFixedAfter.counts.reachableActions}, fixed transform=${fixedAlignCss.includes('transform:')}, canonical-shell blocked=${protectedAlign.protected.length}`);
|
|
84
|
+
// Insets use layout coordinates, while alignment findings use visual coordinates. Under CSS zoom the emitted inset
|
|
85
|
+
// must divide by the visual scale, and a left+right constrained target must retain its used width.
|
|
86
|
+
await page.goto('file://' + path.join(here, 'fixtures/align/zoom-constrained.html'), { waitUntil: 'load' });
|
|
87
|
+
const scaledBefore = await measurePage(page, {});
|
|
88
|
+
const scaledWidthsBefore = await page.evaluate(() => ['viewport-2-a-box-2', 'viewport-2-a-box-3'].map((id) => document.getElementById(id).getBoundingClientRect().width));
|
|
89
|
+
const verticalHeightsBefore = await page.evaluate(() => ['viewport-3-a-box-1', 'viewport-3-a-box-2'].map((id) => document.getElementById(id).getBoundingClientRect().height));
|
|
90
|
+
const scaledOverrides = await page.evaluate(align.browserFn(), { metrics: scaledBefore, params: align.params });
|
|
91
|
+
const scaledCss = overridesToCss(mergeOverrides(scaledOverrides));
|
|
92
|
+
await page.evaluate(applyOverridesScript(), scaledCss);
|
|
93
|
+
const scaledAfter = await measurePage(page, {});
|
|
94
|
+
const scaledWidthsAfter = await page.evaluate(() => ['viewport-2-a-box-2', 'viewport-2-a-box-3'].map((id) => document.getElementById(id).getBoundingClientRect().width));
|
|
95
|
+
const verticalHeightsAfter = await page.evaluate(() => ['viewport-3-a-box-1', 'viewport-3-a-box-2'].map((id) => document.getElementById(id).getBoundingClientRect().height));
|
|
96
|
+
const verticalTop = await page.evaluate(() => ['viewport-3-a-box-1', 'viewport-3-a-box-2'].map((id) => document.getElementById(id).getBoundingClientRect().top));
|
|
97
|
+
const zoomTop = await page.evaluate(() => ['viewport-1-a-card-1', 'viewport-1-a-card-2', 'viewport-1-a-card-3'].map((id) => document.getElementById(id).getBoundingClientRect().top));
|
|
98
|
+
const constrained = scaledOverrides.filter((override) => override.id?.startsWith('viewport-2-a-box-') && override.id !== 'viewport-2-a-box-1');
|
|
99
|
+
const verticalConstrained = scaledOverrides.find((override) => override.id === 'viewport-3-a-box-2');
|
|
100
|
+
const okScaledAlign = Math.max(...zoomTop) - Math.min(...zoomTop) < 0.2
|
|
101
|
+
&& scaledWidthsAfter.every((width, index) => Math.abs(width - scaledWidthsBefore[index]) < 0.2)
|
|
102
|
+
&& constrained.length === 2
|
|
103
|
+
&& constrained.every((override) => override.css.width)
|
|
104
|
+
&& verticalConstrained?.css.top === '20px'
|
|
105
|
+
&& verticalConstrained?.css.bottom === '20px'
|
|
106
|
+
&& verticalConstrained?.css.height == null
|
|
107
|
+
&& !scaledCss.includes('height:')
|
|
108
|
+
&& Math.max(...verticalTop) - Math.min(...verticalTop) < 0.2
|
|
109
|
+
&& verticalHeightsAfter.every((height, index) => Math.abs(height - verticalHeightsBefore[index]) < 0.2)
|
|
110
|
+
&& structureHarm(scaledBefore, scaledAfter).unreachableActions.length === 0;
|
|
111
|
+
if (!okScaledAlign) failures++;
|
|
112
|
+
console.log(`${okScaledAlign ? 'ok ' : 'FAIL'} align/zoom-constrained: zoom top spread=${(Math.max(...zoomTop) - Math.min(...zoomTop)).toFixed(2)} width deltas=${scaledWidthsAfter.map((width, index) => (width - scaledWidthsBefore[index]).toFixed(2)).join(',')} vertical top spread=${(Math.max(...verticalTop) - Math.min(...verticalTop)).toFixed(2)} height lock=${scaledCss.includes('height:')}`);
|
|
113
|
+
// Owner-found centering pair: measurement must expose the tempting 2 px top-edge near miss, while align stays silent
|
|
114
|
+
// because each icon is vertically centered in its containing circle button.
|
|
115
|
+
await page.goto('file://' + path.join(here, 'fixtures/align/silent.html'), { waitUntil: 'load' });
|
|
116
|
+
const centeredBefore = await measurePage(page, {});
|
|
117
|
+
const centeredIds = new Set(['viewport-1-a-centered-controls-icon-1', 'viewport-1-a-centered-controls-icon-2']);
|
|
118
|
+
const centeredNearMiss = centeredBefore.alignment.some((pair) => pair.axis === 'top' && centeredIds.has(pair.a) && centeredIds.has(pair.b));
|
|
119
|
+
const centeredOverrides = await page.evaluate(align.browserFn(), { metrics: centeredBefore, params: align.params });
|
|
120
|
+
const centeredTouched = centeredOverrides.filter((override) => centeredIds.has(override.id));
|
|
121
|
+
const okCenteredAlign = centeredNearMiss && centeredTouched.length === 0;
|
|
122
|
+
if (!okCenteredAlign) failures++;
|
|
123
|
+
console.log(`${okCenteredAlign ? 'ok ' : 'FAIL'} align/centered-control-silent: near-miss=${centeredNearMiss} overrides=${centeredTouched.length}`);
|
|
124
|
+
// The autopilot-inbox failure shape: the actual left-rail rule moves seven full-width rows beyond their direct parent.
|
|
125
|
+
// A wider parent is the paired healthy case: the same seven proposals remain contained and the guard stays silent.
|
|
126
|
+
for (const [fixture, expectedHarm] of [['left-rail-overflow.html', 7], ['left-rail-contained.html', 0]]) {
|
|
127
|
+
await page.goto('file://' + path.join(here, 'fixtures/harm', fixture), { waitUntil: 'load' });
|
|
128
|
+
const railBefore = await measurePage(page, {});
|
|
129
|
+
const railOverrides = await page.evaluate(leftRail.browserFn(), { metrics: railBefore, params: leftRail.params });
|
|
130
|
+
await page.evaluate(applyOverridesScript(), overridesToCss(mergeOverrides(railOverrides)));
|
|
131
|
+
const railAfter = await measurePage(page, {});
|
|
132
|
+
const railHarm = structureHarm(railBefore, railAfter);
|
|
133
|
+
const okRail = railOverrides.length === 7 && railHarm.containment.length === expectedHarm;
|
|
134
|
+
if (!okRail) failures++;
|
|
135
|
+
console.log(`${okRail ? 'ok ' : 'FAIL'} harm/${fixture.replace('.html', '')}: proposals=${railOverrides.length} containment=${railHarm.containment.length}`);
|
|
136
|
+
}
|
|
137
|
+
// Exercise the production acceptance loop as well as the pure comparator: the overflow case must publish unchanged
|
|
138
|
+
// with left-rail reverted, while the contained twin accepts all seven moves.
|
|
139
|
+
{
|
|
140
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), '12ui-polish-harm-'));
|
|
141
|
+
try {
|
|
142
|
+
for (const [fixture, status, accepted] of [['left-rail-overflow.html', 'reverted', 0], ['left-rail-contained.html', 'fired', 7]]) {
|
|
143
|
+
const stem = fixture.replace('.html', '');
|
|
144
|
+
const source = path.join(root, `${stem}-source`); const out = path.join(root, `${stem}-out`);
|
|
145
|
+
fs.mkdirSync(source);
|
|
146
|
+
fs.copyFileSync(path.join(here, 'fixtures/harm', fixture), path.join(source, `${stem}.html`));
|
|
147
|
+
fs.writeFileSync(path.join(source, 'proto-manifest.json'), JSON.stringify({ units: [{ id: stem, states: [] }], holdings: [] }));
|
|
148
|
+
const report = await polishPrototype({ protoDir: source, outDir: out, ruleNames: ['left-rail'], concurrency: 1 });
|
|
149
|
+
const record = report.pages[0].ruleRecords.find((entry) => entry.rule === 'left-rail');
|
|
150
|
+
const okAcceptance = record?.status === status && record.accepted === accepted
|
|
151
|
+
&& (status !== 'reverted' || record.reasons?.includes('containment'));
|
|
152
|
+
if (!okAcceptance) failures++;
|
|
153
|
+
console.log(`${okAcceptance ? 'ok ' : 'FAIL'} harm/${stem}-acceptance: status=${record?.status} accepted=${record?.accepted} reasons=${record?.reasons?.join(',') || 'none'}`);
|
|
154
|
+
}
|
|
155
|
+
} finally {
|
|
156
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Interaction identity is the nearest interactive ancestor, not merely DOM containment. An outer action completely
|
|
160
|
+
// covered by a nested action is unreachable even though element.contains(elementFromPoint(...)) would be true.
|
|
161
|
+
await page.goto('file://' + path.join(here, 'fixtures/harm/nested-actions.html'), { waitUntil: 'load' });
|
|
162
|
+
const nestedActions = await measurePage(page, {});
|
|
163
|
+
const nestedById = new Map(nestedActions.actions.map((action) => [action.id, action.reachable]));
|
|
164
|
+
const okNested = nestedById.get('viewport-1-a-outer') === false
|
|
165
|
+
&& nestedById.get('viewport-1-a-inner') === true
|
|
166
|
+
&& nestedById.get('viewport-1-a-stacked') === true
|
|
167
|
+
&& !nestedById.has('viewport-1-a-stacked-button');
|
|
168
|
+
if (!okNested) failures++;
|
|
169
|
+
console.log(`${okNested ? 'ok ' : 'FAIL'} harm/nested-action-identity: outer=${nestedById.get('viewport-1-a-outer')} inner=${nestedById.get('viewport-1-a-inner')} wrapper-over-native=${nestedById.get('viewport-1-a-stacked')}`);
|
|
170
|
+
// An ordinary un-IDed layout wrapper still owns containment and cannot be a blind spot.
|
|
171
|
+
await page.goto('file://' + path.join(here, 'fixtures/harm/unidentified-parent.html'), { waitUntil: 'load' });
|
|
172
|
+
const anonymousBefore = await measurePage(page, {});
|
|
173
|
+
await page.evaluate(applyOverridesScript(), '#viewport-1-a-row{transform:translate(20px,0)!important}');
|
|
174
|
+
const anonymousAfter = await measurePage(page, {});
|
|
175
|
+
const anonymousHarm = structureHarm(anonymousBefore, anonymousAfter);
|
|
176
|
+
const okAnonymous = anonymousHarm.containment.length === 1 && anonymousHarm.containment[0].id === 'viewport-1-a-row';
|
|
177
|
+
if (!okAnonymous) failures++;
|
|
178
|
+
console.log(`${okAnonymous ? 'ok ' : 'FAIL'} harm/unidentified-parent: containment=${anonymousHarm.containment.length}`);
|
|
50
179
|
// contrast refinement: declared brand colour keeps its hue (orange → readable orange, not black); readable declared
|
|
51
180
|
// colour untouched; a hue that cannot reach the threshold falls back and records the fact
|
|
52
181
|
{
|
|
@@ -5,6 +5,7 @@ import fs from 'node:fs';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { chromium } from '@playwright/test';
|
|
7
7
|
import { measurePage } from '../measure.mjs';
|
|
8
|
+
import { structureHarm } from '../harm.mjs';
|
|
8
9
|
import { loadRules, ALL } from '../rules/index.mjs';
|
|
9
10
|
import { applyOverridesScript, overridesToCss, mergeOverrides } from '../overrides.mjs';
|
|
10
11
|
|
|
@@ -31,8 +32,12 @@ for (const rule of rules) {
|
|
|
31
32
|
const merged = mergeOverrides(found);
|
|
32
33
|
await page.evaluate(applyOverridesScript(), overridesToCss(merged));
|
|
33
34
|
const after = await measurePage(page, {});
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
const structural = structureHarm(before, after);
|
|
36
|
+
harm = after.counts.overlap > before.counts.overlap
|
|
37
|
+
|| after.overflow.filter((o) => o.kind !== 'spill').length > before.overflow.filter((o) => o.kind !== 'spill').length
|
|
38
|
+
|| structural.containment.length > 0
|
|
39
|
+
|| structural.unreachableActions.length > 0;
|
|
40
|
+
detail += `; overlap ${before.counts.overlap}→${after.counts.overlap}, overflow ${before.counts.overflow}→${after.counts.overflow}, containment ${before.counts.containment}→${after.counts.containment} (new ${structural.containment.length}), reachable ${before.counts.reachableActions}→${after.counts.reachableActions} (lost ${structural.unreachableActions.length}), align ${before.counts.alignment}→${after.counts.alignment}, ws ${before.counts.whitespace}→${after.counts.whitespace}`;
|
|
36
41
|
const expectFile = path.join(dir, 'expect.json');
|
|
37
42
|
if (fs.existsSync(expectFile)) {
|
|
38
43
|
const expect = JSON.parse(fs.readFileSync(expectFile, 'utf8'));
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
// ---- content layer: replace text nodes from content/<state>.json (or content-<set>/)
|
|
12
12
|
async function hydrate() {
|
|
13
|
-
|
|
13
|
+
// Direct-file review already carries the same baked-in copy. Browsers do
|
|
14
|
+
// not permit fetch(file://...), and attempting it emits a console error
|
|
15
|
+
// even when the rejected promise is caught.
|
|
16
|
+
if (!cfg.contentFile || location.protocol === 'file:') return;
|
|
14
17
|
const dir = contentSet ? `content-${contentSet}` : 'content';
|
|
15
18
|
try {
|
|
16
19
|
const res = await fetch(`${dir}/${cfg.contentFile}`, { cache: 'no-store' });
|
|
@@ -130,6 +130,7 @@ const interactionPage = (file) => interactionRuntime.pages.find((page) => page.f
|
|
|
130
130
|
const interactionReachability = {
|
|
131
131
|
occludedExactFailure: interactionPage('occluded-saved-journeys.html')?.dropdown,
|
|
132
132
|
ordinaryControl: interactionPage('ordinary-dropdown.html')?.dropdown,
|
|
133
|
+
reviewBottomControl: interactionPage('review-bottom-action.html')?.modal,
|
|
133
134
|
};
|
|
134
135
|
|
|
135
136
|
const blockingScratch = fs.mkdtempSync(path.join(os.tmpdir(), 'prototype-blocking-runtime-'));
|
|
@@ -194,6 +195,8 @@ if (process.argv.includes('--assert')) {
|
|
|
194
195
|
assert.equal(interactionReachability.occludedExactFailure?.ok, true, 'must fire: the exact Saved Journeys dropdown is scrolled clear of the fixed review framing before pointer hit-testing');
|
|
195
196
|
assert.ok(interactionReachability.occludedExactFailure?.point?.y < 700, 'must fire: the exact initially occluded target is measured outside the review panel');
|
|
196
197
|
assert.equal(interactionReachability.ordinaryControl?.ok, true, 'must stay silent: an ordinary already-visible dropdown remains pointer reachable');
|
|
198
|
+
assert.equal(interactionReachability.reviewBottomControl?.ok, true, 'must fire: a bottom-edge action receives temporary scroll clearance from fixed review chrome before hit-testing');
|
|
199
|
+
assert.ok(interactionReachability.reviewBottomControl?.point?.y < 800, 'must fire: the bottom-edge action is measured above the review panel');
|
|
197
200
|
assert.notEqual(blockingVerification.status, 0, 'must fire: a claimed dropdown with broken pointer wiring still refuses publication');
|
|
198
201
|
assert.match(blockingVerification.stderr, /Prototype runtime gate failed: 1 page\(s\) with broken dropdown\/modal interaction/, 'must fire: the refusal names the genuinely blocking interaction gate');
|
|
199
202
|
assert.deepEqual(blockingAudit.interactionFailures, ['broken-interaction.html'], 'must stay silent: the blocking fixture identifies only the broken page');
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import * as align from '../polish/rules/align.mjs';
|
|
3
|
+
|
|
4
|
+
const rect = (left, top, width, height) => ({ left, top, width, height, right: left + width, bottom: top + height });
|
|
5
|
+
|
|
6
|
+
export function checkAlignRule() {
|
|
7
|
+
const previousDocument = globalThis.document;
|
|
8
|
+
const previousGetComputedStyle = globalThis.getComputedStyle;
|
|
9
|
+
const elements = new Map();
|
|
10
|
+
const section = { getBoundingClientRect: () => rect(0, 0, 1536, 1024) };
|
|
11
|
+
const defaultStyle = {
|
|
12
|
+
position: 'static', left: 'auto', right: 'auto', top: 'auto', bottom: 'auto', width: 'auto', height: 'auto',
|
|
13
|
+
backgroundColor: 'rgba(0, 0, 0, 0)', backgroundImage: 'none',
|
|
14
|
+
borderTopWidth: '0px', borderRightWidth: '0px', borderBottomWidth: '0px', borderLeftWidth: '0px',
|
|
15
|
+
};
|
|
16
|
+
const element = (id, box, style = {}, { tagName = 'DIV', classes = [] } = {}) => {
|
|
17
|
+
const classSet = new Set(classes);
|
|
18
|
+
const el = {
|
|
19
|
+
id, tagName, parentElement: null, children: [], box, style: { ...defaultStyle, ...style },
|
|
20
|
+
classList: { contains: (name) => classSet.has(name) },
|
|
21
|
+
getBoundingClientRect: () => el.box,
|
|
22
|
+
closest: (selector) => selector.startsWith('section') ? section : null,
|
|
23
|
+
querySelectorAll: () => [],
|
|
24
|
+
contains: (candidate) => candidate === el,
|
|
25
|
+
matches: (selector) => (tagName === 'BUTTON' && selector.includes('button'))
|
|
26
|
+
|| (classSet.has('button') && selector.includes('.button'))
|
|
27
|
+
|| (classSet.has('badge') && selector.includes('.badge'))
|
|
28
|
+
|| (classSet.has('chip') && selector.includes('.chip'))
|
|
29
|
+
|| (classSet.has('circle') && selector.includes('.circle')),
|
|
30
|
+
};
|
|
31
|
+
elements.set(id, el);
|
|
32
|
+
return el;
|
|
33
|
+
};
|
|
34
|
+
const parent = (id, box, children) => {
|
|
35
|
+
const el = element(id, box);
|
|
36
|
+
el.children = children;
|
|
37
|
+
for (const child of children) child.parentElement = el;
|
|
38
|
+
return el;
|
|
39
|
+
};
|
|
40
|
+
const run = (alignment) => align.browserFn()({ metrics: { alignment, columns: [] } });
|
|
41
|
+
|
|
42
|
+
globalThis.document = { getElementById: (id) => elements.get(id) || null, body: section };
|
|
43
|
+
globalThis.getComputedStyle = (el) => el.style;
|
|
44
|
+
try {
|
|
45
|
+
const card1 = element('card-1', rect(0, 0, 100, 80), { position: 'relative', top: '0px', height: '80px' });
|
|
46
|
+
const card2 = element('card-2', rect(120, 1.02, 100, 80), { position: 'relative', top: '1.02px', height: '80px' });
|
|
47
|
+
const card3 = element('card-3', rect(240, 2.05, 100, 80), { position: 'relative', top: '2.05px', height: '80px' });
|
|
48
|
+
parent('stray-row', rect(0, 0, 400, 180), [card1, card2, card3]);
|
|
49
|
+
const fire = run([
|
|
50
|
+
{ axis: 'top', a: 'card-1', b: 'card-2', delta: 1.02 },
|
|
51
|
+
{ axis: 'top', a: 'card-1', b: 'card-3', delta: 2.05 },
|
|
52
|
+
]);
|
|
53
|
+
assert.deepEqual(fire.map((override) => override.id), ['card-2', 'card-3'], 'must fire: genuine strays in an edge row still snap');
|
|
54
|
+
|
|
55
|
+
elements.clear();
|
|
56
|
+
const button1 = element('centered-button-1', rect(0, 0, 40, 40), { backgroundColor: 'rgb(220, 230, 210)' }, { tagName: 'BUTTON', classes: ['button'] });
|
|
57
|
+
const icon1 = element('centered-icon-1', rect(12, 12, 16, 16), { position: 'absolute', top: '12px', height: '16px' }, { classes: ['icon'] });
|
|
58
|
+
const button2 = element('centered-circle-2', rect(72, 0, 44, 44), { backgroundColor: 'rgb(220, 230, 210)' }, { classes: ['circle'] });
|
|
59
|
+
const icon2 = element('centered-icon-2', rect(86, 14, 16, 16), { position: 'absolute', top: '14px', height: '16px' }, { classes: ['icon'] });
|
|
60
|
+
parent('centered-controls', rect(0, 0, 116, 44), [button1, icon1, button2, icon2]);
|
|
61
|
+
const silent = run([{ axis: 'top', a: 'centered-icon-1', b: 'centered-icon-2', delta: 2 }]);
|
|
62
|
+
assert.deepEqual(silent, [], 'must stay silent: a centered icon keeps its button/circle relationship');
|
|
63
|
+
|
|
64
|
+
elements.clear();
|
|
65
|
+
const anchorButton = element('anchor-button', rect(0, 0, 40, 40), {}, { tagName: 'BUTTON', classes: ['button'] });
|
|
66
|
+
const centeredAnchor = element('centered-anchor-icon', rect(12, 12, 16, 16), { position: 'absolute', top: '12px', height: '16px' }, { classes: ['icon'] });
|
|
67
|
+
const ordinaryStray = element('ordinary-stray-icon', rect(80, 14, 16, 16), { position: 'absolute', top: '14px', height: '16px' }, { classes: ['icon'] });
|
|
68
|
+
parent('mixed-controls', rect(0, 0, 116, 44), [anchorButton, centeredAnchor, ordinaryStray]);
|
|
69
|
+
assert.deepEqual(
|
|
70
|
+
run([{ axis: 'top', a: 'centered-anchor-icon', b: 'ordinary-stray-icon', delta: 2 }]).map((override) => override.id),
|
|
71
|
+
['ordinary-stray-icon'],
|
|
72
|
+
'must fire: an uncontained stray still snaps to a centered anchor',
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
elements.clear();
|
|
76
|
+
const boundaryAnchor = element('boundary-anchor-icon', rect(12, 12, 16, 16), { position: 'absolute', top: '12px', height: '16px' }, { classes: ['icon'] });
|
|
77
|
+
const boundaryTarget = element('boundary-target-icon', rect(86, 14, 16, 16), { position: 'absolute', top: '14px', height: '16px' }, { classes: ['icon'] });
|
|
78
|
+
const boundaryButton = element('boundary-button', rect(72, 0, 44, 45.024), {}, { tagName: 'BUTTON', classes: ['button'] });
|
|
79
|
+
parent('boundary-controls', rect(0, 0, 116, 46), [boundaryAnchor, boundaryButton, boundaryTarget]);
|
|
80
|
+
assert.deepEqual(
|
|
81
|
+
run([{ axis: 'top', a: 'boundary-anchor-icon', b: 'boundary-target-icon', delta: 2 }]),
|
|
82
|
+
[],
|
|
83
|
+
'must stay silent: 1.024px inset imbalance is inside the measured centered cluster',
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
boundaryButton.box = rect(72, 0, 44, 46.048);
|
|
87
|
+
const outsideBoundary = run([{ axis: 'top', a: 'boundary-anchor-icon', b: 'boundary-target-icon', delta: 2 }]);
|
|
88
|
+
assert.deepEqual(
|
|
89
|
+
outsideBoundary.map((override) => override.id),
|
|
90
|
+
['boundary-target-icon'],
|
|
91
|
+
'must fire: 2.048px inset imbalance is outside the measured centered cluster',
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
elements.clear();
|
|
95
|
+
const box1 = element('constrained-box-1', rect(80, 20, 100, 80), { position: 'absolute', top: '20px', bottom: '20px', width: '100px', height: '80px' });
|
|
96
|
+
const box2 = element('constrained-box-2', rect(220, 22, 100, 80), { position: 'absolute', top: '22px', bottom: '18px', width: '100px', height: '80px' });
|
|
97
|
+
parent('constrained-row', rect(0, 0, 600, 120), [box1, box2]);
|
|
98
|
+
const constrained = run([{ axis: 'top', a: 'constrained-box-1', b: 'constrained-box-2', delta: 2 }]);
|
|
99
|
+
assert.equal(constrained.length, 1, 'must fire: a genuine constrained vertical stray still snaps');
|
|
100
|
+
assert.deepEqual(constrained[0].css, { top: '20px', bottom: '20px' }, 'must stay silent: align shifts paired insets and does not freeze height');
|
|
101
|
+
} finally {
|
|
102
|
+
globalThis.document = previousDocument;
|
|
103
|
+
globalThis.getComputedStyle = previousGetComputedStyle;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<!doctype html><html><head><meta charset="utf-8"><link rel="stylesheet" href="../../../runtime/proto.css"><style>
|
|
2
|
+
html,body{margin:0;width:1536px;height:1024px}.page{position:relative;width:1536px;height:1024px}.proto-review-framing{position:fixed;right:18px;bottom:18px;z-index:2147483646;width:430px;height:170px;background:#14161a}.control{position:absolute;left:1246px;top:921px;width:215px;height:59px}
|
|
3
|
+
</style></head><body>
|
|
4
|
+
<aside id="proto-review-framing" class="proto-review-framing">Prototype review</aside>
|
|
5
|
+
<main class="page"><button id="review-bottom-modal" class="control" data-proto-action="modal" data-proto-modal='{"title":"Analyze my site?","body":"Confirm analysis.","confirm":"Analyze my site"}'>Analyze my site</button></main>
|
|
6
|
+
<script id="proto-state" type="application/json">{"unit":"review-bottom-action","tokens":{},"contentFile":null}</script>
|
|
7
|
+
<script src="../../../runtime/proto.js"></script>
|
|
8
|
+
</body></html>
|
|
@@ -4,13 +4,29 @@ import { createHash } from 'node:crypto';
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { parseHTML } from 'linkedom';
|
|
7
8
|
import { reviewFramingMarkup } from '../lib/review-framing.mjs';
|
|
9
|
+
import { removeRemoteHeadLinks } from '../lib/local-document.mjs';
|
|
8
10
|
import { buildPrototype } from './lib/run-process.mjs';
|
|
9
11
|
|
|
10
12
|
const testsDir = path.dirname(new URL(import.meta.url).pathname);
|
|
11
13
|
const kitDir = path.resolve(testsDir, '..');
|
|
12
14
|
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'proto-kit-offline-'));
|
|
13
15
|
|
|
16
|
+
const localDocument = parseHTML([
|
|
17
|
+
'<html><head>',
|
|
18
|
+
'<link rel="preconnect" href="https://fonts.example">',
|
|
19
|
+
'<link rel="stylesheet" href="//cdn.example/font.css">',
|
|
20
|
+
'<link rel="stylesheet" href="proto.css">',
|
|
21
|
+
'</head></html>',
|
|
22
|
+
].join('')).document;
|
|
23
|
+
assert.equal(removeRemoteHeadLinks(localDocument), 2, 'must fire: direct-file prototypes remove remote head links');
|
|
24
|
+
assert.deepEqual(
|
|
25
|
+
[...localDocument.querySelectorAll('head link')].map((link) => link.getAttribute('href')),
|
|
26
|
+
['proto.css'],
|
|
27
|
+
'must stay silent: prototype-local stylesheets survive offline sanitization',
|
|
28
|
+
);
|
|
29
|
+
|
|
14
30
|
function buildFixture(name) {
|
|
15
31
|
const runDir = path.join(testsDir, 'fixtures', name);
|
|
16
32
|
return buildPrototype({ kitDir, run: { runDir }, runWorkDir: path.join(scratch, name) });
|
|
@@ -44,7 +60,7 @@ const outputDigest = (directory) => {
|
|
|
44
60
|
};
|
|
45
61
|
assert.equal(
|
|
46
62
|
outputDigest(threeShell.protoDir),
|
|
47
|
-
'
|
|
63
|
+
'0a24a0c220106a1592598f1ad11685081337cb60d3044953e56e11e239dbc8bd',
|
|
48
64
|
'must stay silent: review support leaves the accepted shell/navigation complete output byte-identical',
|
|
49
65
|
);
|
|
50
66
|
const threeShellModel = JSON.parse(fs.readFileSync(threeShell.modelFile, 'utf8'));
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { parseHTML } from 'linkedom';
|
|
4
4
|
import { resolveDropdownAction, suppressDeterministicAction, usableDropdownOptions } from '../lib/actions.mjs';
|
|
5
|
-
import { restoreActionPointerReachability } from '../lib/action-target.mjs';
|
|
5
|
+
import { restoreActionPointerReachability, selectUniqueActionOwners } from '../lib/action-target.mjs';
|
|
6
|
+
import { checkAlignRule } from './align-rule-check.mjs';
|
|
6
7
|
import { declaredActiveMarkers, linkedNavForUnit, markerOverlapsNav } from '../lib/nav-active-state.mjs';
|
|
7
8
|
import { matchPlannedPage } from '../lib/navigation.mjs';
|
|
8
9
|
import { needsCanonicalShell, selectCanonicalShellPage } from '../lib/shell-consistency.mjs';
|
|
9
10
|
import { isolateTransplantedShell, shellElementsForSourceId } from '../lib/shell-identity.mjs';
|
|
11
|
+
import { compareCanonicalShellSamples, SHELL_RUNTIME_GEOMETRY_EPSILON_PX } from '../lib/shell-runtime-equivalence.mjs';
|
|
10
12
|
import { stitchUnitShells } from '../lib/shell-stitching.mjs';
|
|
11
13
|
import { compareCanonicalShells, isWithinShell, qualifiesShellCandidate, selectOneShellPerRegion, shouldSuppressDroppedShell, shouldTransplantShell } from '../lib/shells.mjs';
|
|
12
14
|
import { slugText } from '../lib/text.mjs';
|
|
@@ -69,6 +71,68 @@ const reachableBefore = reachableDropdown.outerHTML;
|
|
|
69
71
|
assert.equal(restoreActionPointerReachability(reachableDropdown), false, 'must stay silent: an already reachable dropdown target is unchanged');
|
|
70
72
|
assert.equal(reachableDropdown.outerHTML, reachableBefore);
|
|
71
73
|
|
|
74
|
+
const repeatedDropdown = { action: 'dropdown', options: ['All opportunities', 'High intent', 'Low intent'] };
|
|
75
|
+
const distinctDropdown = { action: 'dropdown', options: ['All countries', 'Australia', 'Canada'] };
|
|
76
|
+
const landsFilterDocument = parseHTML(`<div id="filters"><button id="filter-1"></button><button id="filter-2"></button><button id="filter-3"></button><button id="filter-4"></button><button id="filter-5"></button><button id="filter-6"></button></div>`).document;
|
|
77
|
+
const landsFilterActions = new Map([
|
|
78
|
+
['filters', repeatedDropdown],
|
|
79
|
+
['filter-1', repeatedDropdown],
|
|
80
|
+
['filter-2', distinctDropdown],
|
|
81
|
+
['filter-3', distinctDropdown],
|
|
82
|
+
['filter-4', distinctDropdown],
|
|
83
|
+
['filter-5', distinctDropdown],
|
|
84
|
+
['filter-6', distinctDropdown],
|
|
85
|
+
]);
|
|
86
|
+
const uniqueLandsFilters = selectUniqueActionOwners(landsFilterDocument, landsFilterActions);
|
|
87
|
+
assert.deepEqual(uniqueLandsFilters.removed, [{ wrapperId: 'filters', ownerIds: ['filter-1', 'filter-2', 'filter-3', 'filter-4', 'filter-5', 'filter-6'], action: 'dropdown' }], 'must fire: the six-child Ghost Sitemap filter shape gives the wrapper action to its terminal controls');
|
|
88
|
+
assert.deepEqual([...uniqueLandsFilters.actions.keys()], ['filter-1', 'filter-2', 'filter-3', 'filter-4', 'filter-5', 'filter-6'], 'must stay silent: every native filter control remains independently actionable');
|
|
89
|
+
const landsModalDocument = parseHTML('<div id="submit"><button id="submit-button"></button></div>').document;
|
|
90
|
+
const landsModalActions = new Map([
|
|
91
|
+
['submit', { action: 'modal', modal: { title: 'Analyze my site?', confirm: 'Analyze my site' } }],
|
|
92
|
+
['submit-button', { action: 'modal', modal: { title: 'Analyze my site?', confirm: 'Analyze my site' } }],
|
|
93
|
+
]);
|
|
94
|
+
const uniqueLandsModal = selectUniqueActionOwners(landsModalDocument, landsModalActions);
|
|
95
|
+
assert.deepEqual(uniqueLandsModal.removed, [{ wrapperId: 'submit', ownerIds: ['submit-button'], action: 'modal' }], 'must fire: the One Question modal wrapper yields to its terminal button');
|
|
96
|
+
assert.deepEqual([...uniqueLandsModal.actions.keys()], ['submit-button']);
|
|
97
|
+
const territoryFilterDocument = parseHTML('<aside id="filter-bar"><button id="filter-a"></button><button id="filter-b"></button><button id="filter-c"></button><button id="filter-d"></button></aside>').document;
|
|
98
|
+
const territoryFilterActions = new Map([
|
|
99
|
+
['filter-bar', { action: 'dropdown', options: ['All', 'Open', 'Closed'] }],
|
|
100
|
+
['filter-a', { action: 'dropdown', options: ['All territories', 'East', 'West'] }],
|
|
101
|
+
['filter-b', { action: 'dropdown', options: ['All visits', 'High', 'Low'] }],
|
|
102
|
+
['filter-c', { action: 'dropdown', options: ['All claims', 'Claimed', 'Open'] }],
|
|
103
|
+
['filter-d', { action: 'dropdown', options: ['Sort', 'Visits', 'Fit'] }],
|
|
104
|
+
]);
|
|
105
|
+
const uniqueTerritoryFilters = selectUniqueActionOwners(territoryFilterDocument, territoryFilterActions);
|
|
106
|
+
assert.deepEqual(uniqueTerritoryFilters.removed, [{ wrapperId: 'filter-bar', ownerIds: ['filter-a', 'filter-b', 'filter-c', 'filter-d'], action: 'dropdown' }], 'must fire: the Territory Map four-button aside yields to every terminal dropdown even when their option payloads differ');
|
|
107
|
+
assert.deepEqual([...uniqueTerritoryFilters.actions.keys()], ['filter-a', 'filter-b', 'filter-c', 'filter-d']);
|
|
108
|
+
const distinctNestedDocument = parseHTML('<div id="parent"><button id="child"></button></div>').document;
|
|
109
|
+
const distinctNestedActions = new Map([
|
|
110
|
+
['parent', { action: 'dropdown', options: ['One', 'Two', 'Three'] }],
|
|
111
|
+
['child', { action: 'modal', modal: { title: 'Child' } }],
|
|
112
|
+
]);
|
|
113
|
+
const uniqueDistinctNested = selectUniqueActionOwners(distinctNestedDocument, distinctNestedActions);
|
|
114
|
+
assert.deepEqual([...uniqueDistinctNested.actions.keys()], ['parent', 'child'], 'must stay silent: distinct nested actions retain the runtime child-wins contract');
|
|
115
|
+
|
|
116
|
+
const shellSample = ({ visualOffset = 0, shellOffset = 0, appearanceSignature = 'same' } = {}) => ({
|
|
117
|
+
appearanceSignature,
|
|
118
|
+
visualGeometry: [[0, 0, 390, 72], [20 + visualOffset, 20, 100, 28]],
|
|
119
|
+
shellGeometry: [shellOffset, 0, 390, 72],
|
|
120
|
+
});
|
|
121
|
+
assert.equal(SHELL_RUNTIME_GEOMETRY_EPSILON_PX, 0.5, 'fixture contract: canonical shell geometry tolerance is half a CSS pixel');
|
|
122
|
+
const subpixelShellComparison = compareCanonicalShellSamples(shellSample(), shellSample({ visualOffset: 0.4 }));
|
|
123
|
+
assert.equal(subpixelShellComparison.equivalent, true, 'must stay silent: a sub-half-pixel glyph measurement drift is visually equivalent');
|
|
124
|
+
assert.ok(Math.abs(subpixelShellComparison.maxGeometryDelta - 0.4) < 1e-9, 'fixture contract: the comparator reports the measured subpixel drift');
|
|
125
|
+
assert.deepEqual(compareCanonicalShellSamples(shellSample(), shellSample({ visualOffset: 24 })), {
|
|
126
|
+
equivalent: false,
|
|
127
|
+
reason: 'geometry',
|
|
128
|
+
maxGeometryDelta: 24,
|
|
129
|
+
}, 'must fire: the b13-class displaced header descendant remains a canonical-shell mismatch');
|
|
130
|
+
assert.deepEqual(compareCanonicalShellSamples(shellSample(), shellSample({ appearanceSignature: 'changed' })), {
|
|
131
|
+
equivalent: false,
|
|
132
|
+
reason: 'appearance',
|
|
133
|
+
maxGeometryDelta: null,
|
|
134
|
+
}, 'must fire: geometry tolerance never conceals structural or style differences');
|
|
135
|
+
|
|
72
136
|
const headerNav = { containerId: 'nav', region: 'header', semantic: true, items: 8, share: 0.8, box: [0, 40, 1536, 76] };
|
|
73
137
|
const announcement = { containerId: 'announcement', region: 'header', semantic: false, items: 3, share: 1, box: [0, 0, 1536, 40] };
|
|
74
138
|
const footer = { containerId: 'footer', region: 'footer', semantic: true, items: 5, share: 0.8, box: [0, 900, 1536, 124] };
|
|
@@ -205,4 +269,6 @@ assert.equal(fireResult.bands[0].regions[0].active.navText, 'Menu');
|
|
|
205
269
|
assert.equal(fireResult.bands[0].regions[0].shellId, isolatedHeader.id);
|
|
206
270
|
assert.equal(fireResult.bands[0].regions[0].active.markerId, isolatedMarker.id);
|
|
207
271
|
|
|
272
|
+
checkAlignRule();
|
|
273
|
+
|
|
208
274
|
process.stdout.write('rules: ok\n');
|
|
@@ -8,9 +8,9 @@ import { checkRuntime } from '../lib/runtime-check.mjs';
|
|
|
8
8
|
import { startStaticServer } from '../lib/static-server.mjs';
|
|
9
9
|
import { buildPrototype } from './lib/run-process.mjs';
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
const unexpectedArgs = process.argv.slice(2).filter((arg) => arg !== '--
|
|
13
|
-
if (unexpectedArgs.length > 0) throw new Error(`usage: shell-stitching-runtime.mjs [--
|
|
11
|
+
const reintroduceDefect = process.argv.includes('--reintroduce-defect');
|
|
12
|
+
const unexpectedArgs = process.argv.slice(2).filter((arg) => arg !== '--reintroduce-defect');
|
|
13
|
+
if (unexpectedArgs.length > 0) throw new Error(`usage: shell-stitching-runtime.mjs [--reintroduce-defect]`);
|
|
14
14
|
|
|
15
15
|
const testsDir = path.dirname(new URL(import.meta.url).pathname);
|
|
16
16
|
const kitDir = path.resolve(testsDir, '..');
|
|
@@ -49,6 +49,13 @@ try {
|
|
|
49
49
|
bandHeight(inventory, 'p2-canvas-control', 'mobile-390'),
|
|
50
50
|
], [900, 1400], 'fixture contract: the portrait control has a substantially taller canvas');
|
|
51
51
|
|
|
52
|
+
if (reintroduceDefect) {
|
|
53
|
+
const collisionFile = path.join(built.protoDir, 'id-collision.html');
|
|
54
|
+
const collisionHtml = fs.readFileSync(collisionFile, 'utf8');
|
|
55
|
+
const displaced = '<style id="reintroduced-b13-defect">@media(max-width:600px){[data-proto-shell-source-id="mobile-390-header-brand"]{transform:translateX(24px)!important}}</style>';
|
|
56
|
+
fs.writeFileSync(collisionFile, collisionHtml.replace('</head>', `${displaced}</head>`));
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
server = await startStaticServer(runWorkDir);
|
|
53
60
|
browser = await chromium.launch();
|
|
54
61
|
const audit = await checkRuntime({
|
|
@@ -77,11 +84,11 @@ try {
|
|
|
77
84
|
assert.equal(collision1536.actualSignature, donor1536.actualSignature, 'must stay silent: distinct native identities render the same canonical header');
|
|
78
85
|
|
|
79
86
|
const groups = audit.shellConsistencyFailures.map((failure) => failure.group);
|
|
80
|
-
if (
|
|
81
|
-
assert.deepEqual(groups, ['390:mobile-390:header'], '
|
|
82
|
-
assert.notEqual(collision390.actualSignature, donor390.actualSignature, 'must fire
|
|
87
|
+
if (reintroduceDefect) {
|
|
88
|
+
assert.deepEqual(groups, ['390:mobile-390:header'], 'reintroduced b13 evidence must isolate one canonical collision to the mobile header');
|
|
89
|
+
assert.notEqual(collision390.actualSignature, donor390.actualSignature, 'must fire with the defect present: a 24px recipient displacement alters transplanted donor descendants');
|
|
83
90
|
const failure = audit.shellConsistencyFailures[0];
|
|
84
|
-
assert.equal(failure.reason, 'prototype-shell-signature-mismatch', '
|
|
91
|
+
assert.equal(failure.reason, 'prototype-shell-signature-mismatch', 'reintroduced evidence is the rendered canonical-shell mismatch');
|
|
85
92
|
assert.deepEqual(failure.pages, [
|
|
86
93
|
{ file: 'donor.html', signature: donor390.actualSignature, geometry: donor390.actualGeometry },
|
|
87
94
|
{ file: 'id-collision.html', signature: collision390.actualSignature, geometry: collision390.actualGeometry },
|
|
@@ -92,7 +99,7 @@ try {
|
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
process.stdout.write(JSON.stringify({
|
|
95
|
-
expectation:
|
|
102
|
+
expectation: reintroduceDefect ? 'reintroduced-defect-failure' : 'post-fix-pass',
|
|
96
103
|
groups,
|
|
97
104
|
mobile: {
|
|
98
105
|
donor: donor390.actualSignature,
|