test(daycycle): judge pinch frames, not the first frames of the recording (#734)

presentedFramesHaveNoWhiteTile in smoke_daycycle_layer_budget turned red now
and then on frames that are not pinch frames: the first one or two frames of
the screencast sometimes show the room before its fill (white paper, 0.997
near-white), before any pinch move, and the next frames are light grey. The
check judged every recorded frame, so a stale opening frame failed a gesture
that painted correctly.

The check now judges pinch frames only. Not judged: a frame whose swap
time (screencast metadata) is earlier than the first pinch move, and a frame
before the first one that shows the room filled. The guard keeps its power:
a white tile during the pinch comes after a filled frame and stays red; a
room white from the recording start through the whole gesture leaves no
judged frame inside the gesture, which fails both the frame count and the
white-tile check (an empty set no longer passes `every`). The frame count
counts judged frames inside the gesture, not every recorded frame.

Why the fill appears later is out of scope.

Issue: #734
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
Claude
2026-10-01 05:33:49 +03:00
parent 52dc08a0e5
commit b3dd9444b5
+33 -6
View File
@@ -13,7 +13,11 @@ cdp.on('LayerTree.layerTreeDidChange', ({ layers }) => { latestLayers = layers;
await cdp.send('LayerTree.enable');
const presentedFrames = [];
cdp.on('Page.screencastFrame', (event) => {
if (presentedFrames.length < 90) presentedFrames.push(event.data);
// #734: the swap time (seconds since epoch) tells a frame presented before
// the pinch from a pinch frame; without it the frame is judged.
if (presentedFrames.length < 90) {
presentedFrames.push({ data: event.data, swappedAt: Number(event.metadata?.timestamp) * 1000 });
}
void cdp.send('Page.screencastFrameAck', { sessionId: event.sessionId });
});
@@ -132,7 +136,11 @@ await page.evaluate(() => {
window.__hp582Touch('pointerdown', 5822, cx + 70);
});
// #734: taken before the first move is dispatched, on the same wall clock as
// the frame swap time — a frame swapped earlier cannot show the pinch.
let pinchStartedAt = null;
for (const distance of [78, 92, 66, 104, 72, 98]) {
pinchStartedAt ??= Date.now();
await page.evaluate((distance) => {
const { cx } = window.__hp582Pinch;
window.__hp582Touch('pointermove', 5821, cx - distance);
@@ -150,6 +158,7 @@ await page.evaluate(() => {
delete window.__hp582Touch;
delete window.__hp582Pinch;
});
const pinchEndedAt = Date.now();
await settle();
await page.waitForTimeout(100);
await cdp.send('Page.stopScreencast');
@@ -163,7 +172,7 @@ const frameMetrics = await page.evaluate(async ({ encoded, stage }) => {
image.src = `data:image/png;base64,${data}`;
});
const out = [];
for (const data of encoded) {
for (const { data, swappedAt } of encoded) {
const image = await decode(data);
const scaleX = image.width / innerWidth;
const scaleY = image.height / innerHeight;
@@ -188,7 +197,7 @@ const frameMetrics = await page.evaluate(async ({ encoded, stage }) => {
}
}
}
out.push({ nearWhiteRatio: nearWhite / Math.max(1, sampled) });
out.push({ nearWhiteRatio: nearWhite / Math.max(1, sampled), swappedAt });
}
return out;
}, {
@@ -328,10 +337,24 @@ checks.settledPlanLayerIsExplicitAndBounded = !!settledPlanLayer
&& settledPlanLayer.reasons.some((reason) => reason.includes('will-change: opacity'))
&& settledPlanLayer.reasons.every((reason) => !reason.includes('will-change: transform'))
&& settledPlanLayer.reasons.every((reason) => !reason.includes('Overlaps other composited content'));
checks.capturedPresentedPinchFrames = frameMetrics.length >= 3;
checks.presentedFramesHaveNoWhiteTile = frameMetrics.every(
(frame) => frame.nearWhiteRatio < 0.01,
// #734: the check judges pinch frames. The first one or two frames of the
// recording sometimes show the room before its fill (white paper, 0.997
// near-white) — before any pinch move, then light grey. Not judged: a frame
// swapped before the first move, and a frame before the first one that shows
// the room filled. A tile lost during the pinch stays red: it comes after a
// filled frame. A room white from the recording start through the whole
// pinch leaves no judged frame inside the gesture, and that is red too.
const hasFill = (frame) => frame.nearWhiteRatio < 0.01;
const firstFilledFrame = frameMetrics.findIndex(hasFill);
const swappedBefore = (frame, at) => Number.isFinite(frame.swappedAt) && frame.swappedAt < at;
const pinchFrameMetrics = frameMetrics.filter((frame, index) => firstFilledFrame >= 0
&& index >= firstFilledFrame && !swappedBefore(frame, pinchStartedAt));
const framesInsideGesture = pinchFrameMetrics.filter(
(frame) => !Number.isFinite(frame.swappedAt) || frame.swappedAt <= pinchEndedAt,
);
checks.capturedPresentedPinchFrames = framesInsideGesture.length >= 3;
checks.presentedFramesHaveNoWhiteTile = framesInsideGesture.length > 0
&& pinchFrameMetrics.every(hasFill);
checks.staticCardUsesSeparateFilteredOutline = staticStage.outlinePresent
&& staticStage.outlineVisibility === 'visible'
&& /drop-shadow/.test(staticStage.outlineFilter)
@@ -357,6 +380,10 @@ console.log(JSON.stringify({
activeLayers: active.layers,
settledLayers: settled.layers,
presentedFrameMetrics: frameMetrics,
pinchStartedAt,
pinchEndedAt,
judgedPinchFrames: pinchFrameMetrics.length,
judgedFramesInsideGesture: framesInsideGesture.length,
staticStage,
staticLayers,
staticOversized,