test: judge marker reuse by node identity, not by a transition

The marker half of the space-switch witness asked whether a box-shadow
transition was running on the shell. That worked only because such a
transition existed; #524 removed it — the shadow is sized in container
units and animating it cost a real user 9.4 frames per second — and the
check became trivially true. The mutant that removes the keys from the
marker list has been surviving ever since, and nobody noticed until the
next gate ran it.

The witness now keeps references to the marker nodes and requires that
none of them stays in the DOM under a different data-id after the
switch. Node identity is what the keys are for, and it does not depend
on any stylesheet.

The door half is untouched: there the transition is part of the product
contract, not a side effect.

Issue: #528
User-Visible: no
This commit is contained in:
Codex
2026-09-11 10:01:46 +03:00
parent 78bc76073e
commit f37ccdcebc
+16 -1
View File
@@ -97,6 +97,14 @@ const res = await page.evaluate(async () => {
await settle();
const leafBefore = root().querySelector('.op-leaf');
out.doorIsDrawnInTheFirstSpace = !!leafBefore;
// #528: маркеры судятся по идентичности узлов, а не по бегущему переходу.
// Прежняя проверка смотрела на переход `box-shadow` у оболочки — он исчез
// вместе с правкой #524, и мутант «маркеры без ключей» начал выживать:
// признак стал тривиально истинным. Идентичность узла не зависит ни от
// одного перехода и переживёт любую правку стилей.
const markersBefore = [...root().querySelectorAll('[data-hp="device"]')]
.map((node) => ({ node, id: node.dataset.id }));
out.markersDrawnInTheFirstSpace = markersBefore.length >= 3;
out.quietBeforeTheSwitch = runningAnimations().filter((entry) => !EXPECTED(entry)).length === 0;
// поворот створки живёт инлайновым стилем, не атрибутом
const leafTransform = (leaf) => leaf?.style?.transform || leaf?.getAttribute('transform') || null;
@@ -113,6 +121,13 @@ const res = await page.evaluate(async () => {
.filter((entry) => /op-leaf|op-arc/.test(entry.cls)).length === 0;
out.noMarkerTransitionOnSwitch = afterSwitch
.filter((entry) => /device-shell-frame/.test(entry.cls)).length === 0;
// Узел, живший до переключения, не имеет права остаться в DOM под чужим
// `data-id`: это и есть позиционное переиспользование, ради которого в
// `repeat` стоят ключи (#525 AC2, #528).
const reused = markersBefore.filter((entry) => entry.node.isConnected
&& entry.node.dataset.id !== entry.id);
out.noMarkerNodeReusedForAnotherDevice = reused.length === 0;
out.reusedMarkerNodes = reused.map((entry) => `${entry.id} → ${entry.node.dataset.id}`);
const unexpected = afterSwitch.filter((entry) => !EXPECTED(entry));
out.onlyTheSwitchItselfAnimates = unexpected.length === 0;
out.unexpectedAnimations = unexpected.map((entry) => `${entry.cls}:${entry.property}`);
@@ -135,5 +150,5 @@ const res = await page.evaluate(async () => {
.filter((entry) => /op-leaf/.test(entry.cls)).length >= 1;
return out;
});
checkAll(res, { unexpectedAnimations: [] });
checkAll(res, { unexpectedAnimations: [], reusedMarkerNodes: [] });
await finish(browser, res);