@12ui/design 0.2.42 → 0.2.44
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/README.md +10 -5
- package/SKILL.md +1 -1
- package/dist/cli-branch-command.d.ts.map +1 -1
- package/dist/cli-branch-command.js +10 -2
- package/dist/cli-branch-command.js.map +1 -1
- package/dist/cli-capabilities.d.ts +1 -1
- package/dist/cli-capabilities.js +1 -1
- package/dist/cli-capabilities.js.map +1 -1
- package/dist/cli-prototype-command.d.ts.map +1 -1
- package/dist/cli-prototype-command.js +4 -1
- package/dist/cli-prototype-command.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/prototype-gates.d.ts +8 -0
- package/dist/prototype-gates.d.ts.map +1 -1
- package/dist/prototype-gates.js +39 -5
- package/dist/prototype-gates.js.map +1 -1
- package/dist/prototype-label-client.d.ts +6 -1
- package/dist/prototype-label-client.d.ts.map +1 -1
- package/dist/prototype-label-client.js +21 -2
- package/dist/prototype-label-client.js.map +1 -1
- package/dist/prototype-poststep.d.ts.map +1 -1
- package/dist/prototype-poststep.js +5 -0
- package/dist/prototype-poststep.js.map +1 -1
- package/dist/prototype-revision.d.ts +2 -0
- package/dist/prototype-revision.d.ts.map +1 -0
- package/dist/prototype-revision.js +2 -0
- package/dist/prototype-revision.js.map +1 -0
- package/dist/prototype-run-record.d.ts +5 -1
- package/dist/prototype-run-record.d.ts.map +1 -1
- package/dist/prototype-run-record.js +89 -7
- package/dist/prototype-run-record.js.map +1 -1
- package/dist/prototype-runner.d.ts +4 -1
- package/dist/prototype-runner.d.ts.map +1 -1
- package/dist/prototype-runner.js +15 -9
- package/dist/prototype-runner.js.map +1 -1
- package/dist/run-journal.d.ts +6 -1
- package/dist/run-journal.d.ts.map +1 -1
- package/dist/run-journal.js +10 -2
- package/dist/run-journal.js.map +1 -1
- package/open-design.json +2 -2
- package/package.json +2 -2
- package/prototype-kit/build.mjs +88 -40
- package/prototype-kit/lib/actions.mjs +52 -0
- package/prototype-kit/lib/shell-identity.mjs +139 -0
- package/prototype-kit/lib/shell-stitching.mjs +70 -29
- package/prototype-kit/tests/adversarial-runtime.mjs +16 -1
- package/prototype-kit/tests/fixtures/blocking-runtime/broken-interaction.html +7 -0
- package/prototype-kit/tests/fixtures/four-dropdown/branch.plan.json +29 -0
- package/prototype-kit/tests/fixtures/four-dropdown/pages/p2-stream-proposals.html +10 -0
- package/prototype-kit/tests/fixtures/four-dropdown/pages/p4-live-stream-results.html +7 -0
- package/prototype-kit/tests/fixtures/shell-stitching-runtime/PROVENANCE.md +22 -0
- package/prototype-kit/tests/fixtures/shell-stitching-runtime/branch.plan.json +40 -0
- package/prototype-kit/tests/fixtures/shell-stitching-runtime/pages/p0-donor.html +38 -0
- package/prototype-kit/tests/fixtures/shell-stitching-runtime/pages/p1-id-collision.html +38 -0
- package/prototype-kit/tests/fixtures/shell-stitching-runtime/pages/p2-canvas-control.html +39 -0
- package/prototype-kit/tests/offline-check.mjs +1 -1
- package/prototype-kit/tests/rules-check.mjs +71 -3
- package/prototype-kit/tests/shell-stitching-runtime.mjs +116 -0
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { declaredActiveMarkers, linkedNavForUnit } from './nav-active-state.mjs';
|
|
2
2
|
import { needsCanonicalShell, shellForBand } from './shell-consistency.mjs';
|
|
3
|
+
import {
|
|
4
|
+
cssForIsolatedShell,
|
|
5
|
+
isolateTransplantedShell,
|
|
6
|
+
rulesFor,
|
|
7
|
+
shellElementsForSourceId,
|
|
8
|
+
} from './shell-identity.mjs';
|
|
3
9
|
import { shouldTransplantShell } from './shells.mjs';
|
|
4
10
|
import { normalizeText } from './text.mjs';
|
|
5
11
|
|
|
@@ -26,6 +32,7 @@ function markerSnapshots({ document, shell, markers, rulesFor, styleText }) {
|
|
|
26
32
|
if (!element) return [];
|
|
27
33
|
return [{
|
|
28
34
|
...marker,
|
|
35
|
+
element: element.cloneNode(true),
|
|
29
36
|
html: element.outerHTML,
|
|
30
37
|
css: rulesFor(css, marker.id),
|
|
31
38
|
item: shell.items.find((item) => item.id === marker.id),
|
|
@@ -34,20 +41,28 @@ function markerSnapshots({ document, shell, markers, rulesFor, styleText }) {
|
|
|
34
41
|
});
|
|
35
42
|
}
|
|
36
43
|
|
|
37
|
-
function removeMarkers(
|
|
38
|
-
for (const marker of markers)
|
|
44
|
+
function removeMarkers(shellElement, markers) {
|
|
45
|
+
for (const marker of markers) shellElementsForSourceId(shellElement, marker.id)[0]?.remove();
|
|
39
46
|
}
|
|
40
47
|
|
|
41
|
-
function placeMarker({ shellElement, shell, marker, targetItem, extraCss }) {
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
function placeMarker({ document, shellElement, shell, marker, targetItem, extraCss, namespaceParts }) {
|
|
49
|
+
const isolated = isolateTransplantedShell({
|
|
50
|
+
document,
|
|
51
|
+
sourceElement: marker.element,
|
|
52
|
+
css: marker.css,
|
|
53
|
+
namespaceParts,
|
|
54
|
+
});
|
|
55
|
+
shellElement.insertAdjacentHTML('beforeend', isolated.html);
|
|
56
|
+
if (isolated.css) extraCss.push(isolated.css);
|
|
57
|
+
const outputMarker = { ...marker, id: isolated.identities.get(marker.id) };
|
|
44
58
|
const sourceTarget = marker.targetItem;
|
|
45
59
|
const source = marker.item;
|
|
46
|
-
if (!source) return;
|
|
60
|
+
if (!source) return outputMarker;
|
|
47
61
|
const desiredX = targetItem && sourceTarget ? targetItem.x + (source.x - sourceTarget.x) : source.x;
|
|
48
62
|
const desiredY = targetItem && sourceTarget ? targetItem.y + (source.y - sourceTarget.y) : source.y;
|
|
49
|
-
extraCss.push(`#${
|
|
63
|
+
extraCss.push(`#${outputMarker.id}{position:absolute!important;left:${desiredX - shell.box[0]}px!important;top:${desiredY - shell.box[1]}px!important;`
|
|
50
64
|
+ `width:${source.w}px!important;height:${source.h}px!important;margin:0!important;translate:none!important}`);
|
|
65
|
+
return outputMarker;
|
|
51
66
|
}
|
|
52
67
|
|
|
53
68
|
function decisionForRegion({ unit, donorPage, recipientPage, region, canonicalShell }) {
|
|
@@ -83,7 +98,6 @@ export function stitchUnitShells({
|
|
|
83
98
|
unit,
|
|
84
99
|
canonicalShell,
|
|
85
100
|
navRows,
|
|
86
|
-
rulesFor,
|
|
87
101
|
styleText,
|
|
88
102
|
}) {
|
|
89
103
|
const extraCss = [];
|
|
@@ -142,31 +156,39 @@ export function stitchUnitShells({
|
|
|
142
156
|
const donorElement = donorDocument.getElementById(pair.canonical.containerId);
|
|
143
157
|
const root = bandRoot(document, pair.key);
|
|
144
158
|
const host = shellHost(root);
|
|
145
|
-
const alreadyPlaced =
|
|
159
|
+
const alreadyPlaced = root && shellElementsForSourceId(root, pair.canonical.containerId)[0];
|
|
146
160
|
if (!donorElement || !root || !host || (!root.contains(recipientElement) && !root.contains(alreadyPlaced))) {
|
|
147
161
|
throw new Error(`Canonical shell commit lost declared ${region} DOM in band ${pair.key}.`);
|
|
148
162
|
}
|
|
163
|
+
const donorAlreadyIsolated = alreadyPlaced && alreadyPlaced.id !== pair.canonical.containerId;
|
|
164
|
+
let outputShell = donorAlreadyIsolated ? alreadyPlaced : null;
|
|
149
165
|
if (recipientElement && root.contains(recipientElement)) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
recipientElement.
|
|
158
|
-
|
|
166
|
+
if (!donorAlreadyIsolated) {
|
|
167
|
+
const isolated = isolateTransplantedShell({
|
|
168
|
+
document,
|
|
169
|
+
sourceElement: donorElement,
|
|
170
|
+
css: rulesFor(styleText(donorDocument), pair.canonical.containerId),
|
|
171
|
+
namespaceParts: [pair.key, region],
|
|
172
|
+
});
|
|
173
|
+
recipientElement.insertAdjacentHTML('beforebegin', isolated.html);
|
|
174
|
+
outputShell = document.getElementById(isolated.rootId);
|
|
175
|
+
if (isolated.css) extraCss.push(isolated.css);
|
|
159
176
|
}
|
|
177
|
+
recipientElement.replaceChildren();
|
|
160
178
|
recipientElement.setAttribute('data-proto-shell-space', region);
|
|
161
179
|
recipientElement.setAttribute('aria-hidden', 'true');
|
|
162
180
|
extraCss.push(`#${recipientElement.id}{visibility:hidden!important;pointer-events:none!important}`);
|
|
163
181
|
}
|
|
164
|
-
outputShellId =
|
|
165
|
-
const outputShell = document.getElementById(outputShellId);
|
|
182
|
+
outputShellId = outputShell?.id || null;
|
|
166
183
|
if (!outputShell || !root.contains(outputShell)) {
|
|
167
184
|
throw new Error(`Canonical shell commit did not place ${region} in band ${pair.key}.`);
|
|
168
185
|
}
|
|
169
|
-
|
|
186
|
+
if (outputShell === alreadyPlaced) {
|
|
187
|
+
extraCss.push(cssForIsolatedShell(
|
|
188
|
+
outputShell,
|
|
189
|
+
rulesFor(styleText(donorDocument), pair.canonical.containerId),
|
|
190
|
+
));
|
|
191
|
+
}
|
|
170
192
|
const donorBand = donorPage.bands.find((band) => band.key === pair.key);
|
|
171
193
|
const parentShell = donorBand?.shells.find((candidate) => {
|
|
172
194
|
if (candidate.containerId === pair.canonical.containerId) return false;
|
|
@@ -180,21 +202,36 @@ export function stitchUnitShells({
|
|
|
180
202
|
pair.canonical.box[1] - parentShell.box[1],
|
|
181
203
|
]
|
|
182
204
|
: pair.canonical.box;
|
|
183
|
-
extraCss.push(`#${
|
|
205
|
+
extraCss.push(`#${outputShellId}{position:absolute!important;left:${x}px!important;top:${y}px!important;`
|
|
184
206
|
+ 'margin:0!important;z-index:100000!important}');
|
|
185
|
-
removeMarkers(
|
|
207
|
+
removeMarkers(outputShell, canonicalMarkers);
|
|
186
208
|
const matchingSource = linkedNav
|
|
187
209
|
? sourceSnapshots.find((marker) => normalizeText(marker.targetText) === normalizeText(linkedNav.text))
|
|
188
210
|
: null;
|
|
189
211
|
const markerToPlace = matchingSource || (linkedNav ? donorSnapshots[0] : null);
|
|
190
212
|
if (markerToPlace) {
|
|
191
|
-
placeMarker({
|
|
192
|
-
|
|
213
|
+
outputMarkers = [placeMarker({
|
|
214
|
+
document,
|
|
215
|
+
shellElement: outputShell,
|
|
216
|
+
shell: pair.canonical,
|
|
217
|
+
marker: markerToPlace,
|
|
218
|
+
targetItem,
|
|
219
|
+
extraCss,
|
|
220
|
+
namespaceParts: [pair.key, region, 'marker'],
|
|
221
|
+
})];
|
|
193
222
|
} else if (!linkedNav) {
|
|
223
|
+
outputMarkers = [];
|
|
194
224
|
for (const marker of sourceSnapshots) {
|
|
195
|
-
placeMarker({
|
|
225
|
+
outputMarkers.push(placeMarker({
|
|
226
|
+
document,
|
|
227
|
+
shellElement: outputShell,
|
|
228
|
+
shell: pair.canonical,
|
|
229
|
+
marker,
|
|
230
|
+
targetItem: null,
|
|
231
|
+
extraCss,
|
|
232
|
+
namespaceParts: [pair.key, region, 'marker'],
|
|
233
|
+
}));
|
|
196
234
|
}
|
|
197
|
-
outputMarkers = sourceSnapshots;
|
|
198
235
|
} else outputMarkers = [];
|
|
199
236
|
changes++;
|
|
200
237
|
}
|
|
@@ -203,6 +240,10 @@ export function stitchUnitShells({
|
|
|
203
240
|
|| outputMarkers[0]
|
|
204
241
|
|| null
|
|
205
242
|
: null;
|
|
243
|
+
const outputShell = document.getElementById(outputShellId);
|
|
244
|
+
const outputNavId = targetItem
|
|
245
|
+
? shellElementsForSourceId(outputShell || document, targetItem.id)[0]?.id || targetItem.id
|
|
246
|
+
: null;
|
|
206
247
|
bands.get(pair.key)?.regions.push({
|
|
207
248
|
region,
|
|
208
249
|
shellId: outputShellId,
|
|
@@ -210,8 +251,8 @@ export function stitchUnitShells({
|
|
|
210
251
|
consistencyStatus: decision.commit || decision.reason === 'canonical-page' ? 'canonical' : 'silent',
|
|
211
252
|
activeMarkerIds: outputMarkers.map((marker) => marker.id),
|
|
212
253
|
active: linkedNav && targetItem && activeMarker
|
|
213
|
-
? { status: 'linked', navText: linkedNav.text, navId:
|
|
214
|
-
: linkedNav ? { status: 'unproven', navText: linkedNav.text, navId:
|
|
254
|
+
? { status: 'linked', navText: linkedNav.text, navId: outputNavId, markerId: activeMarker.id }
|
|
255
|
+
: linkedNav ? { status: 'unproven', navText: linkedNav.text, navId: outputNavId, markerId: null }
|
|
215
256
|
: { status: 'silent', navText: null, navId: null, markerId: null },
|
|
216
257
|
changed: decision.commit,
|
|
217
258
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
3
4
|
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
4
6
|
import path from 'node:path';
|
|
5
7
|
import { chromium } from '@playwright/test';
|
|
6
8
|
import { checkRuntime } from '../lib/runtime-check.mjs';
|
|
@@ -130,9 +132,19 @@ const interactionReachability = {
|
|
|
130
132
|
ordinaryControl: interactionPage('ordinary-dropdown.html')?.dropdown,
|
|
131
133
|
};
|
|
132
134
|
|
|
135
|
+
const blockingScratch = fs.mkdtempSync(path.join(os.tmpdir(), 'prototype-blocking-runtime-'));
|
|
136
|
+
const blockingAuditFile = path.join(blockingScratch, 'runtime-audit.json');
|
|
137
|
+
const blockingVerification = spawnSync(process.execPath, [
|
|
138
|
+
path.join(kitDir, 'verify-runtime.mjs'),
|
|
139
|
+
path.join(kitDir, 'tests', 'fixtures', 'blocking-runtime'),
|
|
140
|
+
blockingAuditFile,
|
|
141
|
+
], { cwd: kitDir, encoding: 'utf8', timeout: 30_000 });
|
|
142
|
+
const blockingAudit = JSON.parse(fs.readFileSync(blockingAuditFile, 'utf8'));
|
|
143
|
+
fs.rmSync(blockingScratch, { recursive: true, force: true });
|
|
144
|
+
|
|
133
145
|
await browser.close();
|
|
134
146
|
await server.close();
|
|
135
|
-
const result = { doubleBinding, nestedBinding, dropdownGuard, hoverStacking, deepLinkBack, accessibleBack, pageBack, holdingComposition, shellNavigation, interactionReachability };
|
|
147
|
+
const result = { doubleBinding, nestedBinding, dropdownGuard, hoverStacking, deepLinkBack, accessibleBack, pageBack, holdingComposition, shellNavigation, interactionReachability, blockingInteraction: { status: blockingVerification.status, failures: blockingAudit.interactionFailures } };
|
|
136
148
|
if (process.argv.includes('--assert')) {
|
|
137
149
|
assert.deepEqual(doubleBinding, { overlayRoots: 1, modals: 1 }, 'runtime must bind exactly once when injected twice');
|
|
138
150
|
assert.deepEqual(nestedBinding, { modals: 1, menus: 0 }, 'the innermost action must stop a parent binding from also firing');
|
|
@@ -182,6 +194,9 @@ if (process.argv.includes('--assert')) {
|
|
|
182
194
|
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');
|
|
183
195
|
assert.ok(interactionReachability.occludedExactFailure?.point?.y < 700, 'must fire: the exact initially occluded target is measured outside the review panel');
|
|
184
196
|
assert.equal(interactionReachability.ordinaryControl?.ok, true, 'must stay silent: an ordinary already-visible dropdown remains pointer reachable');
|
|
197
|
+
assert.notEqual(blockingVerification.status, 0, 'must fire: a claimed dropdown with broken pointer wiring still refuses publication');
|
|
198
|
+
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
|
+
assert.deepEqual(blockingAudit.interactionFailures, ['broken-interaction.html'], 'must stay silent: the blocking fixture identifies only the broken page');
|
|
185
200
|
}
|
|
186
201
|
const json = JSON.stringify(result, null, 2) + '\n';
|
|
187
202
|
if (outFile) fs.writeFileSync(outFile, json);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<!doctype html><html><head><meta charset="utf-8"><style>
|
|
2
|
+
html,body{margin:0;width:1536px;height:1024px}.control{position:absolute;left:120px;top:120px;width:220px;height:44px}.interceptor{position:fixed;inset:0;z-index:20;background:rgba(255,255,255,.01)}
|
|
3
|
+
</style></head><body>
|
|
4
|
+
<button id="broken-dropdown" class="control" data-proto-action="dropdown" data-proto-options='["One","Two","Three"]'>Menu</button>
|
|
5
|
+
<div class="interceptor" aria-hidden="true"></div>
|
|
6
|
+
<script id="proto-state" type="application/json">{"unit":"broken-interaction","tokens":{},"contentFile":null}</script>
|
|
7
|
+
</body></html>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"scope": "site",
|
|
4
|
+
"concept": "Observed four-candidate dropdown degradation shape",
|
|
5
|
+
"pages": [
|
|
6
|
+
{
|
|
7
|
+
"index": 0,
|
|
8
|
+
"id": "p2-stream-proposals",
|
|
9
|
+
"title": "Stream proposals",
|
|
10
|
+
"surfaceType": "web_app",
|
|
11
|
+
"purpose": "Review proposed streams.",
|
|
12
|
+
"startsFromWinner": true,
|
|
13
|
+
"slots": ["b"]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"index": 1,
|
|
17
|
+
"id": "p4-live-stream-results",
|
|
18
|
+
"title": "Live stream results",
|
|
19
|
+
"surfaceType": "web_app",
|
|
20
|
+
"purpose": "Review live stream performance.",
|
|
21
|
+
"startsFromWinner": false,
|
|
22
|
+
"slots": ["e"]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"screens": [
|
|
26
|
+
{ "slot": "b", "pageIndex": 0, "content": "Stream proposal filters", "dependsOnSlot": null },
|
|
27
|
+
{ "slot": "e", "pageIndex": 1, "content": "Live stream date filter", "dependsOnSlot": null }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html><head><meta charset="utf-8"><style>
|
|
3
|
+
html,body,#page-flow,#viewport-1-b{margin:0;width:1536px;height:1024px;font-family:Arial,sans-serif}
|
|
4
|
+
.filter{position:absolute;display:flex;align-items:center;justify-content:space-between;width:280px;height:44px}.text{font-size:16px}.icon{width:16px;height:16px}
|
|
5
|
+
#viewport-1-b-filters{left:80px;top:180px}#viewport-1-b-sort{left:80px;top:260px}#viewport-1-b-table-footer{left:80px;top:340px}
|
|
6
|
+
</style></head><body><main id="page-flow"><section id="viewport-1-b">
|
|
7
|
+
<button class="filter" id="viewport-1-b-filters"><span class="text" id="viewport-1-b-filters-device-button">All countries</span><span class="icon" id="viewport-1-b-filters-language-icon-2" aria-label="Chevron down">⌄</span></button>
|
|
8
|
+
<button class="filter" id="viewport-1-b-sort"><span class="text" id="viewport-1-b-sort-text">Sort by</span><span class="icon" id="viewport-1-b-sort-icon" aria-label="Chevron down">⌄</span></button>
|
|
9
|
+
<button class="filter" id="viewport-1-b-table-footer"><span class="text" id="viewport-1-b-table-footer-text-2">Compare</span><span class="icon" id="viewport-1-b-table-footer-icon" aria-label="Chevron down">⌄</span></button>
|
|
10
|
+
</section></main></body></html>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html><head><meta charset="utf-8"><style>
|
|
3
|
+
html,body,#page-flow,#viewport-1-e{margin:0;width:1536px;height:1024px;font-family:Arial,sans-serif}
|
|
4
|
+
.filter{position:absolute;left:80px;top:180px;display:flex;align-items:center;justify-content:space-between;width:460px;height:44px}.text{font-size:16px}.icon{width:16px;height:16px}
|
|
5
|
+
</style></head><body><main id="page-flow"><section id="viewport-1-e">
|
|
6
|
+
<button class="filter" id="viewport-1-e-main-date-filter"><span class="text" id="viewport-1-e-main-date-filter-text">compared to Apr 6 – May 5, 2025</span><span class="icon" id="viewport-1-e-main-date-filter-icon-3" aria-label="Chevron down">⌄</span></button>
|
|
7
|
+
</section></main></body></html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Responsive shell-stitching runtime fixture
|
|
2
|
+
|
|
3
|
+
This reduced three-page branch preserves the deterministic mechanism found by
|
|
4
|
+
the 2026-08-25 b13 header-signature refusal study. The full artifact's canonical
|
|
5
|
+
Central Station Departures and stitched Network Map pages had byte-identical
|
|
6
|
+
`#mobile-390-header` DOM and identical outer geometry `[0, 0, 390, 72.5]`, but
|
|
7
|
+
their runtime signatures differed:
|
|
8
|
+
|
|
9
|
+
- donor: `47b710c2aee9240892128ad7271eb0451792db7d8b1c1650c36bc076770ca088`;
|
|
10
|
+
- recipient: `5f1aae89d7c2083b27edc7c50e265bb844ed2760722b8dccea91a8c07ab4fbf9`.
|
|
11
|
+
|
|
12
|
+
The recipient stylesheet still addressed the reused `mobile-390-header-*` ids.
|
|
13
|
+
Its `left` and `top` declarations therefore survived the later, property-partial
|
|
14
|
+
donor rules and displaced donor descendants. The fixture keeps that exact
|
|
15
|
+
boundary: the donor omits `left`/`top`, while the recipient declares them on the
|
|
16
|
+
same responsive ids. Native shell identities stay distinct so the failure is
|
|
17
|
+
isolated to the 390px band.
|
|
18
|
+
|
|
19
|
+
The third page is the paired silent control. It renders the donor shell but has
|
|
20
|
+
different product content and a substantially taller portrait canvas. Its shell
|
|
21
|
+
signature must remain equal to the donor's because canvas and body variation are
|
|
22
|
+
outside the declared shell subtree.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"scope": "site",
|
|
4
|
+
"concept": "Canonical responsive shell CSS isolation fixture",
|
|
5
|
+
"sitePurpose": "Exercise one donor shell, one colliding recipient, and one canvas-only control.",
|
|
6
|
+
"pages": [
|
|
7
|
+
{
|
|
8
|
+
"index": 0,
|
|
9
|
+
"id": "p0-donor",
|
|
10
|
+
"title": "Departures",
|
|
11
|
+
"surfaceType": "web_app",
|
|
12
|
+
"purpose": "Provide the canonical responsive header.",
|
|
13
|
+
"startsFromWinner": true,
|
|
14
|
+
"slots": ["a"]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"index": 1,
|
|
18
|
+
"id": "p1-id-collision",
|
|
19
|
+
"title": "Network map",
|
|
20
|
+
"surfaceType": "web_app",
|
|
21
|
+
"purpose": "Reuse responsive header ids under recipient-specific offsets.",
|
|
22
|
+
"startsFromWinner": false,
|
|
23
|
+
"slots": ["b"]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"index": 2,
|
|
27
|
+
"id": "p2-canvas-control",
|
|
28
|
+
"title": "Service notes",
|
|
29
|
+
"surfaceType": "web_app",
|
|
30
|
+
"purpose": "Keep the canonical header while changing content and canvas height.",
|
|
31
|
+
"startsFromWinner": false,
|
|
32
|
+
"slots": ["c"]
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"screens": [
|
|
36
|
+
{ "slot": "a", "pageIndex": 0, "content": "Departures content", "dependsOnSlot": null },
|
|
37
|
+
{ "slot": "b", "pageIndex": 1, "content": "Network map content", "dependsOnSlot": null },
|
|
38
|
+
{ "slot": "c", "pageIndex": 2, "content": "Long service notes content", "dependsOnSlot": null }
|
|
39
|
+
]
|
|
40
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html><head><meta charset="utf-8"><style>
|
|
3
|
+
html,body,#page-flow{margin:0;width:100%;min-height:100%;font-family:Arial,sans-serif;background:#eef2f4}.ly-band{display:none}.generated-screen,.ly-anchor-canvas{position:relative}.generated-screen{width:100%}.text{margin:0}.fixture-shell{box-sizing:border-box;color:#f7fafc;background:#071923}
|
|
4
|
+
@media(max-width:959px){.ly-band[data-ly-band="mobile-390"]{display:block}}
|
|
5
|
+
@media(min-width:960px){.ly-band[data-ly-band="native"]{display:block}}
|
|
6
|
+
#donor-mobile-canvas{width:390px;height:900px;background:#dce7eb}
|
|
7
|
+
#mobile-390-header{position:relative;display:flex;align-items:flex-start;margin:0;width:390px;height:72px;background:#071923}
|
|
8
|
+
#mobile-390-header-brand{position:relative;margin:20px 0 0 20px;width:100px;height:28px}
|
|
9
|
+
#mobile-390-header-time{position:relative;margin:20px 0 0 90px;width:80px;height:32px}
|
|
10
|
+
#mobile-390-header-menu{position:relative;margin:24px 0 0 25px;width:24px;height:24px}
|
|
11
|
+
#mobile-390-header-divider{position:absolute;left:0;bottom:0;width:390px;height:2px;background:#3e5e6d}
|
|
12
|
+
#mobile-390-header-brand-text{font-size:16px;line-height:28px;font-weight:700}
|
|
13
|
+
#mobile-390-header-time-text{font-size:14px;line-height:16px;font-weight:600}
|
|
14
|
+
#mobile-390-header-time-live{font-size:11px;line-height:16px;color:#b6f437}
|
|
15
|
+
#mobile-390-header-menu-icon{display:block;width:24px;height:24px;font-size:20px;line-height:24px;text-align:center}
|
|
16
|
+
#donor-mobile-content{padding:44px 24px;color:#17252c}
|
|
17
|
+
#viewport-1-a{position:relative;width:1536px;height:1024px;background:#eef2f4}
|
|
18
|
+
#viewport-1-a-header{position:absolute;left:0;top:0;display:flex;align-items:center;width:1536px;height:72px}
|
|
19
|
+
#viewport-1-a-header-brand{margin-left:32px;font-size:16px;line-height:24px;font-weight:700}
|
|
20
|
+
#viewport-1-a-header-status{margin-left:1200px;font-size:14px;line-height:24px}
|
|
21
|
+
#viewport-1-a-content{position:absolute;left:80px;top:150px;width:800px;height:400px;color:#17252c}
|
|
22
|
+
</style></head><body><main id="page-flow">
|
|
23
|
+
<div class="ly-band" data-ly-band="mobile-390"><div class="generated-screen"><div id="donor-mobile-canvas" class="ly-anchor-canvas">
|
|
24
|
+
<header id="mobile-390-header" class="fixture-shell">
|
|
25
|
+
<div id="mobile-390-header-brand"><p id="mobile-390-header-brand-text" class="text">Kinetic</p></div>
|
|
26
|
+
<div id="mobile-390-header-time"><p id="mobile-390-header-time-text" class="text">09:42</p><p id="mobile-390-header-time-live" class="text">Live</p></div>
|
|
27
|
+
<nav id="mobile-390-header-menu"><span id="mobile-390-header-menu-icon" class="icon" role="img" aria-label="Menu">≡</span></nav>
|
|
28
|
+
<div id="mobile-390-header-divider" class="divider"></div>
|
|
29
|
+
</header>
|
|
30
|
+
<article id="donor-mobile-content"><h1>Central departures</h1><p>Two platforms are boarding.</p></article>
|
|
31
|
+
</div></div></div>
|
|
32
|
+
<div class="ly-band" data-ly-band="native"><div class="generated-screen"><div class="ly-anchor-canvas">
|
|
33
|
+
<section id="viewport-1-a">
|
|
34
|
+
<header id="viewport-1-a-header" class="fixture-shell"><p id="viewport-1-a-header-brand" class="text">Kinetic</p><p id="viewport-1-a-header-status" class="text">Live service</p></header>
|
|
35
|
+
<div id="viewport-1-a-content"><h1 id="viewport-1-a-content-title">Central departures</h1><p id="viewport-1-a-content-copy">Two platforms are boarding.</p></div>
|
|
36
|
+
</section>
|
|
37
|
+
</div></div></div>
|
|
38
|
+
</main></body></html>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html><head><meta charset="utf-8"><style>
|
|
3
|
+
html,body,#page-flow{margin:0;width:100%;min-height:100%;font-family:Arial,sans-serif;background:#eef2f4}.ly-band{display:none}.generated-screen,.ly-anchor-canvas{position:relative}.generated-screen{width:100%}.text{margin:0}.fixture-shell{box-sizing:border-box;color:#f7fafc;background:#071923}
|
|
4
|
+
@media(max-width:959px){.ly-band[data-ly-band="mobile-390"]{display:block}}
|
|
5
|
+
@media(min-width:960px){.ly-band[data-ly-band="native"]{display:block}}
|
|
6
|
+
#collision-mobile-canvas{width:390px;height:900px;background:#d9e4e8}
|
|
7
|
+
#mobile-390-header{position:relative;margin:0;width:390px;height:72px;background:#0a202b}
|
|
8
|
+
#mobile-390-header-brand{position:absolute;left:25px;top:22px;width:104px;height:30px}
|
|
9
|
+
#mobile-390-header-time{position:absolute;left:215px;top:19px;width:82px;height:34px}
|
|
10
|
+
#mobile-390-header-menu{position:absolute;left:340px;top:23px;width:26px;height:26px}
|
|
11
|
+
#mobile-390-header-divider{position:absolute;left:0;top:68px;width:390px;height:2px;background:#476b7a}
|
|
12
|
+
#mobile-390-header-brand-text{font-size:16px;line-height:28px;font-weight:700}
|
|
13
|
+
#mobile-390-header-time-text{font-size:14px;line-height:16px;font-weight:600}
|
|
14
|
+
#mobile-390-header-time-live{font-size:11px;line-height:16px;color:#b6f437}
|
|
15
|
+
#mobile-390-header-menu-icon{display:block;width:24px;height:24px;font-size:20px;line-height:24px;text-align:center}
|
|
16
|
+
#collision-mobile-content{padding:70px 36px;color:#17252c}
|
|
17
|
+
#viewport-1-b{position:relative;width:1536px;height:1024px;background:#eef2f4}
|
|
18
|
+
#viewport-1-b-header{position:absolute;left:0;top:0;display:flex;align-items:center;width:1536px;height:72px}
|
|
19
|
+
#viewport-1-b-header-brand{margin-left:32px;font-size:16px;line-height:24px;font-weight:700}
|
|
20
|
+
#viewport-1-b-header-status{margin-left:1200px;font-size:14px;line-height:24px}
|
|
21
|
+
#viewport-1-b-content{position:absolute;left:110px;top:180px;width:900px;height:420px;color:#17252c}
|
|
22
|
+
</style></head><body><main id="page-flow">
|
|
23
|
+
<div class="ly-band" data-ly-band="mobile-390"><div class="generated-screen"><div id="collision-mobile-canvas" class="ly-anchor-canvas">
|
|
24
|
+
<header id="mobile-390-header" class="fixture-shell">
|
|
25
|
+
<div id="mobile-390-header-brand"><p id="mobile-390-header-brand-text" class="text">Kinetic</p></div>
|
|
26
|
+
<div id="mobile-390-header-time"><p id="mobile-390-header-time-text" class="text">09:42</p><p id="mobile-390-header-time-live" class="text">Live</p></div>
|
|
27
|
+
<nav id="mobile-390-header-menu"><span id="mobile-390-header-menu-icon" class="icon" role="img" aria-label="Menu">≡</span></nav>
|
|
28
|
+
<div id="mobile-390-header-divider" class="divider"></div>
|
|
29
|
+
</header>
|
|
30
|
+
<article id="collision-mobile-content"><h1>Network map</h1><p>Choose a station to inspect its connections.</p></article>
|
|
31
|
+
</div></div></div>
|
|
32
|
+
<div class="ly-band" data-ly-band="native"><div class="generated-screen"><div class="ly-anchor-canvas">
|
|
33
|
+
<section id="viewport-1-b">
|
|
34
|
+
<header id="viewport-1-b-header" class="fixture-shell"><p id="viewport-1-b-header-brand" class="text">Kinetic</p><p id="viewport-1-b-header-status" class="text">Live service</p></header>
|
|
35
|
+
<div id="viewport-1-b-content"><h1 id="viewport-1-b-content-title">Network map</h1><p id="viewport-1-b-content-copy">Choose a station to inspect its connections.</p></div>
|
|
36
|
+
</section>
|
|
37
|
+
</div></div></div>
|
|
38
|
+
</main></body></html>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html><head><meta charset="utf-8"><style>
|
|
3
|
+
html,body,#page-flow{margin:0;width:100%;min-height:100%;font-family:Arial,sans-serif;background:#f3eee6}.ly-band{display:none}.generated-screen,.ly-anchor-canvas{position:relative}.generated-screen{width:100%}.text{margin:0}.fixture-shell{box-sizing:border-box;color:#f7fafc;background:#071923}
|
|
4
|
+
@media(max-width:959px){.ly-band[data-ly-band="mobile-390"]{display:block}}
|
|
5
|
+
@media(min-width:960px){.ly-band[data-ly-band="native"]{display:block}}
|
|
6
|
+
#control-mobile-canvas{width:390px;height:1400px;background:#f3eee6}
|
|
7
|
+
#mobile-390-header{position:relative;display:flex;align-items:flex-start;margin:0;width:390px;height:72px;background:#071923}
|
|
8
|
+
#mobile-390-header-brand{position:relative;margin:20px 0 0 20px;width:100px;height:28px}
|
|
9
|
+
#mobile-390-header-time{position:relative;margin:20px 0 0 90px;width:80px;height:32px}
|
|
10
|
+
#mobile-390-header-menu{position:relative;margin:24px 0 0 25px;width:24px;height:24px}
|
|
11
|
+
#mobile-390-header-divider{position:absolute;left:0;bottom:0;width:390px;height:2px;background:#3e5e6d}
|
|
12
|
+
#mobile-390-header-brand-text{font-size:16px;line-height:28px;font-weight:700}
|
|
13
|
+
#mobile-390-header-time-text{font-size:14px;line-height:16px;font-weight:600}
|
|
14
|
+
#mobile-390-header-time-live{font-size:11px;line-height:16px;color:#b6f437}
|
|
15
|
+
#mobile-390-header-menu-icon{display:block;width:24px;height:24px;font-size:20px;line-height:24px;text-align:center}
|
|
16
|
+
#control-mobile-content{padding:96px 28px;color:#30271f}
|
|
17
|
+
#control-mobile-content section{margin-top:180px;padding:28px;background:#fff;border-radius:16px}
|
|
18
|
+
#viewport-1-c{position:relative;width:1536px;height:1400px;background:#f3eee6}
|
|
19
|
+
#viewport-1-c-header{position:absolute;left:0;top:0;display:flex;align-items:center;width:1536px;height:72px}
|
|
20
|
+
#viewport-1-c-header-brand{margin-left:32px;font-size:16px;line-height:24px;font-weight:700}
|
|
21
|
+
#viewport-1-c-header-status{margin-left:1200px;font-size:14px;line-height:24px}
|
|
22
|
+
#viewport-1-c-content{position:absolute;left:140px;top:220px;width:1000px;height:900px;color:#30271f}
|
|
23
|
+
</style></head><body><main id="page-flow">
|
|
24
|
+
<div class="ly-band" data-ly-band="mobile-390"><div class="generated-screen"><div id="control-mobile-canvas" class="ly-anchor-canvas">
|
|
25
|
+
<header id="mobile-390-header" class="fixture-shell">
|
|
26
|
+
<div id="mobile-390-header-brand"><p id="mobile-390-header-brand-text" class="text">Kinetic</p></div>
|
|
27
|
+
<div id="mobile-390-header-time"><p id="mobile-390-header-time-text" class="text">09:42</p><p id="mobile-390-header-time-live" class="text">Live</p></div>
|
|
28
|
+
<nav id="mobile-390-header-menu"><span id="mobile-390-header-menu-icon" class="icon" role="img" aria-label="Menu">≡</span></nav>
|
|
29
|
+
<div id="mobile-390-header-divider" class="divider"></div>
|
|
30
|
+
</header>
|
|
31
|
+
<article id="control-mobile-content"><h1>Service notes</h1><p>This deliberately taller route has unrelated portrait content.</p><section><h2>Maintenance archive</h2><p>Older notices continue far below the shared shell.</p></section></article>
|
|
32
|
+
</div></div></div>
|
|
33
|
+
<div class="ly-band" data-ly-band="native"><div class="generated-screen"><div class="ly-anchor-canvas">
|
|
34
|
+
<section id="viewport-1-c">
|
|
35
|
+
<header id="viewport-1-c-header" class="fixture-shell"><p id="viewport-1-c-header-brand" class="text">Kinetic</p><p id="viewport-1-c-header-status" class="text">Live service</p></header>
|
|
36
|
+
<div id="viewport-1-c-content"><h1 id="viewport-1-c-content-title">Service notes</h1><p id="viewport-1-c-content-copy">This route has substantially more content and a taller canvas.</p></div>
|
|
37
|
+
</section>
|
|
38
|
+
</div></div></div>
|
|
39
|
+
</main></body></html>
|
|
@@ -44,7 +44,7 @@ const outputDigest = (directory) => {
|
|
|
44
44
|
};
|
|
45
45
|
assert.equal(
|
|
46
46
|
outputDigest(threeShell.protoDir),
|
|
47
|
-
'
|
|
47
|
+
'0c2147df5bc6552016cc278faec7abaabecbc7f5945cad7e1ac9b1df6e554403',
|
|
48
48
|
'must stay silent: review support leaves the accepted shell/navigation complete output byte-identical',
|
|
49
49
|
);
|
|
50
50
|
const threeShellModel = JSON.parse(fs.readFileSync(threeShell.modelFile, 'utf8'));
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { parseHTML } from 'linkedom';
|
|
4
|
-
import { usableDropdownOptions } from '../lib/actions.mjs';
|
|
4
|
+
import { resolveDropdownAction, suppressDeterministicAction, usableDropdownOptions } from '../lib/actions.mjs';
|
|
5
5
|
import { restoreActionPointerReachability } from '../lib/action-target.mjs';
|
|
6
6
|
import { declaredActiveMarkers, linkedNavForUnit, markerOverlapsNav } from '../lib/nav-active-state.mjs';
|
|
7
7
|
import { matchPlannedPage } from '../lib/navigation.mjs';
|
|
8
8
|
import { needsCanonicalShell, selectCanonicalShellPage } from '../lib/shell-consistency.mjs';
|
|
9
|
+
import { isolateTransplantedShell, shellElementsForSourceId } from '../lib/shell-identity.mjs';
|
|
9
10
|
import { stitchUnitShells } from '../lib/shell-stitching.mjs';
|
|
10
11
|
import { compareCanonicalShells, isWithinShell, qualifiesShellCandidate, selectOneShellPerRegion, shouldSuppressDroppedShell, shouldTransplantShell } from '../lib/shells.mjs';
|
|
11
12
|
import { slugText } from '../lib/text.mjs';
|
|
@@ -25,6 +26,39 @@ assert.equal(matchPlannedPage('التقارير', [{ stateId: 'reports-ar', titl
|
|
|
25
26
|
assert.notEqual(slugText('التقارير'), slugText('الإعدادات'), 'must stay silent: distinct non-Latin holdings do not collide');
|
|
26
27
|
assert.equal(usableDropdownOptions(['Only choice']), null, 'must fire: a one-option fallback is not bound as a fake dropdown');
|
|
27
28
|
assert.deepEqual(usableDropdownOptions(['All', 'Open', 'Closed']), ['All', 'Open', 'Closed'], 'must stay silent: a real three-option dropdown remains bindable');
|
|
29
|
+
assert.equal(suppressDeterministicAction({ action: 'none' }), true, 'must fire: a recovered explicit none suppresses an unsafe deterministic dropdown');
|
|
30
|
+
assert.equal(suppressDeterministicAction({ action: 'dropdown', options: ['All', 'Open', 'Closed'] }), false, 'must stay silent: a valid labeled dropdown remains bindable');
|
|
31
|
+
assert.deepEqual(resolveDropdownAction({
|
|
32
|
+
deterministic: { action: 'dropdown' },
|
|
33
|
+
label: { action: 'none' },
|
|
34
|
+
labelId: 'filter-label',
|
|
35
|
+
}), {
|
|
36
|
+
disposition: 'static',
|
|
37
|
+
degradation: {
|
|
38
|
+
code: 'label_none_overrode_deterministic_dropdown',
|
|
39
|
+
reason: 'explicit-label-none',
|
|
40
|
+
fromAction: 'dropdown',
|
|
41
|
+
toAction: 'none',
|
|
42
|
+
options: [],
|
|
43
|
+
},
|
|
44
|
+
}, 'must fire: normalized none becomes one recorded static dropdown policy outcome');
|
|
45
|
+
assert.deepEqual(resolveDropdownAction({
|
|
46
|
+
deterministic: { action: 'dropdown' },
|
|
47
|
+
label: { action: 'dropdown', options: ['All', 'Open', 'Closed'] },
|
|
48
|
+
labelId: 'filter-label',
|
|
49
|
+
}), {
|
|
50
|
+
disposition: 'dropdown',
|
|
51
|
+
action: { action: 'dropdown', labelId: 'filter-label', options: ['All', 'Open', 'Closed'] },
|
|
52
|
+
}, 'must stay silent: a proven three-option dropdown remains actionable');
|
|
53
|
+
assert.equal(
|
|
54
|
+
resolveDropdownAction({
|
|
55
|
+
deterministic: { action: 'dropdown' },
|
|
56
|
+
label: { action: 'modal', options: null },
|
|
57
|
+
labelId: 'filter-label',
|
|
58
|
+
}).degradation.code,
|
|
59
|
+
'dropdown_options_incomplete',
|
|
60
|
+
'must fire: a deterministic dropdown with no usable label options is recorded and kept static',
|
|
61
|
+
);
|
|
28
62
|
|
|
29
63
|
const unreachableDropdown = parseHTML('<span id="viewport-1-c-journeys-harbourfront-source-row-1-icon" style="width:29.696px;height:11.264px;pointer-events:none">...</span>').document.querySelector('span');
|
|
30
64
|
assert.equal(restoreActionPointerReachability(unreachableDropdown), true, 'must fire: the exact failed Saved Journeys dropdown target becomes pointer reachable');
|
|
@@ -104,6 +138,34 @@ assert.equal(linkedNavForUnit({ navRows: [{ region: 'header', text: 'Journal', t
|
|
|
104
138
|
|
|
105
139
|
const rulesForFixture = () => '';
|
|
106
140
|
const styleTextFixture = () => '';
|
|
141
|
+
const identityRecipient = parseHTML('<html><body><div id="donor-shell"><span id="donor-shell-label"></span></div></body></html>').document;
|
|
142
|
+
const identityDonor = parseHTML('<html><body><section id="donor-shell" aria-labelledby="donor-shell-label"><label id="donor-shell-label" for="donor-shell-field">Name</label><input id="donor-shell-field"><svg id="donor-shell-icon" aria-labelledby="donor-shell-label"><use id="donor-shell-icon-use" href="#donor-shell-icon"></use></svg></section></body></html>').document;
|
|
143
|
+
const isolatedIdentity = isolateTransplantedShell({
|
|
144
|
+
document: identityRecipient,
|
|
145
|
+
sourceElement: identityDonor.getElementById('donor-shell'),
|
|
146
|
+
css: '#donor-shell{display:block}#donor-shell-label{color:red}#donor-shell-icon-use{fill:url(#donor-shell-icon)}',
|
|
147
|
+
namespaceParts: ['mobile-390', 'header'],
|
|
148
|
+
});
|
|
149
|
+
const isolatedIdentityDocument = parseHTML(`<html><body>${isolatedIdentity.html}</body></html>`).document;
|
|
150
|
+
const isolatedIdentityRoot = isolatedIdentityDocument.getElementById(isolatedIdentity.rootId);
|
|
151
|
+
const isolatedIdentityLabel = shellElementsForSourceId(isolatedIdentityRoot, 'donor-shell-label')[0];
|
|
152
|
+
const isolatedIdentityField = shellElementsForSourceId(isolatedIdentityRoot, 'donor-shell-field')[0];
|
|
153
|
+
const isolatedIdentityIcon = shellElementsForSourceId(isolatedIdentityRoot, 'donor-shell-icon')[0];
|
|
154
|
+
assert.notEqual(isolatedIdentity.rootId, 'donor-shell', 'must fire: the physical donor root cannot reuse a recipient identity');
|
|
155
|
+
assert.equal(isolatedIdentityRoot.getAttribute('aria-labelledby'), isolatedIdentityLabel.id, 'must fire: ARIA id references follow isolated identities');
|
|
156
|
+
assert.equal(isolatedIdentityLabel.getAttribute('for'), isolatedIdentityField.id, 'must fire: form id references follow isolated identities');
|
|
157
|
+
assert.equal(isolatedIdentityRoot.querySelector('use').getAttribute('href'), `#${isolatedIdentityIcon.id}`, 'must fire: fragment references follow isolated identities');
|
|
158
|
+
assert.ok(isolatedIdentity.css.includes(`#${isolatedIdentityLabel.id}{color:red}`), 'must fire: exact donor CSS selectors follow isolated identities');
|
|
159
|
+
assert.ok(isolatedIdentity.css.includes(`url(#${isolatedIdentityIcon.id})`), 'must fire: CSS fragment references follow isolated identities');
|
|
160
|
+
assert.equal(isolatedIdentity.css.includes('#donor-shell'), false, 'must stay silent: no donor id selector remains available to recipient CSS');
|
|
161
|
+
const duplicateDonor = parseHTML('<html><body><section id="duplicate-shell"><span id="duplicate-child"></span><span id="duplicate-child"></span></section></body></html>').document;
|
|
162
|
+
assert.throws(() => isolateTransplantedShell({
|
|
163
|
+
document: identityRecipient,
|
|
164
|
+
sourceElement: duplicateDonor.getElementById('duplicate-shell'),
|
|
165
|
+
css: '',
|
|
166
|
+
namespaceParts: ['native', 'header'],
|
|
167
|
+
}), /duplicate donor id duplicate-child/, 'must fire: invalid donor identity cannot be ambiguously namespaced');
|
|
168
|
+
|
|
107
169
|
const noWriteDocument = parseHTML('<html><head></head><body><header id="detail-header"><span id="detail-home">Home</span><img id="detail-ink" alt="Home navigation underline"></header></body></html>').document;
|
|
108
170
|
const noWriteBefore = noWriteDocument.documentElement.outerHTML;
|
|
109
171
|
const noWriteResult = stitchUnitShells({
|
|
@@ -133,8 +195,14 @@ const fireResult = stitchUnitShells({
|
|
|
133
195
|
styleText: styleTextFixture,
|
|
134
196
|
});
|
|
135
197
|
assert.equal(fireResult.changes, 1, 'must fire: a measured incompatible fingerprint commits the declared donor shell');
|
|
136
|
-
|
|
137
|
-
|
|
198
|
+
const isolatedHeader = shellElementsForSourceId(fireDocument, 'journal-header')[0];
|
|
199
|
+
const isolatedMarker = shellElementsForSourceId(fireDocument, 'menu-ink')[0];
|
|
200
|
+
assert.ok(isolatedHeader, 'must fire: the canonical donor outer shell replaces the recipient shell');
|
|
201
|
+
assert.notEqual(isolatedHeader.id, 'journal-header', 'must fire: transplanted donor identity is physically isolated from recipient CSS');
|
|
202
|
+
assert.ok(isolatedMarker && isolatedHeader.contains(isolatedMarker), 'must fire: the page-declared Menu underline is retained and moved into the isolated donor shell');
|
|
203
|
+
assert.equal(fireDocument.getElementById('menu-header')?.getAttribute('data-proto-shell-space'), 'header', 'must stay silent: the recipient shell remains an invisible flow spacer under its own identity');
|
|
138
204
|
assert.equal(fireResult.bands[0].regions[0].active.navText, 'Menu');
|
|
205
|
+
assert.equal(fireResult.bands[0].regions[0].shellId, isolatedHeader.id);
|
|
206
|
+
assert.equal(fireResult.bands[0].regions[0].active.markerId, isolatedMarker.id);
|
|
139
207
|
|
|
140
208
|
process.stdout.write('rules: ok\n');
|