test: assert window docking in the two pass-by journey tests

This commit is contained in:
2026-10-11 01:11:09 +03:00
parent d6045ad2c1
commit f7b5802c65

View File

@@ -51,7 +51,7 @@ test('keeps navigation, HUD, progress, chapter, and arrival on the same next-fra
const viewportHeight = window.visualViewport?.height ?? window.innerHeight; const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
const destination = id === 'contact' const destination = id === 'contact'
? document.documentElement.scrollHeight ? document.documentElement.scrollHeight
: Math.max(0, top - viewportHeight * 0.35 + 1); : Math.max(0, top - viewportHeight * 0.6 + 1);
window.scrollTo({ top: destination, behavior: 'instant' }); window.scrollTo({ top: destination, behavior: 'instant' });
}, section.id); }, section.id);
await waitForOneAnimationFrame(page); await waitForOneAnimationFrame(page);
@@ -102,7 +102,7 @@ test('draws only the independently eligible next roadside sign during passive fo
const headingTop = profile.getBoundingClientRect().top + window.scrollY; const headingTop = profile.getBoundingClientRect().top + window.scrollY;
const destination = Math.max( const destination = Math.max(
1, 1,
headingTop - viewportHeight * (0.35 + candidateDistanceVh), headingTop - viewportHeight * (0.6 + candidateDistanceVh),
); );
return { destination, viewportHeight, expectedLabel: 'Profile' }; return { destination, viewportHeight, expectedLabel: 'Profile' };
}, triggerProbeDistanceVh(viewport.width, viewport.height)); }, triggerProbeDistanceVh(viewport.width, viewport.height));
@@ -133,7 +133,7 @@ test('keeps a readable sign immutable through reversal, resize, and a multi-sect
const viewportHeight = window.visualViewport?.height ?? window.innerHeight; const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
const headingTop = profile.getBoundingClientRect().top + window.scrollY; const headingTop = profile.getBoundingClientRect().top + window.scrollY;
return { return {
destination: Math.max(1, headingTop - viewportHeight * (0.35 + 0.45)), destination: Math.max(1, headingTop - viewportHeight * (0.6 + 0.45)),
viewportHeight, viewportHeight,
}; };
}); });
@@ -176,13 +176,31 @@ test('keeps a readable sign immutable through reversal, resize, and a multi-sect
expect((await readSceneInstrumentation(page)).maximumCanvasCount).toBeLessThanOrEqual(1); expect((await readSceneInstrumentation(page)).maximumCanvasCount).toBeLessThanOrEqual(1);
}); });
test('carries a departed sign past the car before its restrained edge fade', async ({ /** The gantry window's box in CSS pixels, read from the live layout. */
async function gantryWindowBox(page: import('@playwright/test').Page) {
const box = await page.locator('.gantry-window').boundingBox();
if (box === null) throw new Error('Missing gantry window');
return { left: box.x, top: box.y, right: box.x + box.width, bottom: box.y + box.height, width: box.width };
}
/**
* Round 6 replaced the roadside pass-by with window docking: the sign arcs
* in large over the road, docks into the gantry window, holds there while it
* is read, then departs in place as the gantry reports it passing.
*/
test('docks a passing sign in the gantry window after its approach arc and lets it go in place', async ({
page, page,
distUrl, distUrl,
}) => { }) => {
await page.setViewportSize({ width: 1_440, height: 900 }); await page.setViewportSize({ width: 1_440, height: 900 });
await page.goto(distUrl, { waitUntil: 'domcontentloaded' }); await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running'); await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
await page.evaluate(() => {
const states: string[] = [];
(window as unknown as { __gantryStates: string[] }).__gantryStates = states;
new MutationObserver(() => states.push(document.documentElement.dataset.gantry ?? ''))
.observe(document.documentElement, { attributes: true, attributeFilter: ['data-gantry'] });
});
const viewport = page.viewportSize(); const viewport = page.viewportSize();
if (viewport === null) throw new Error('Missing browser viewport'); if (viewport === null) throw new Error('Missing browser viewport');
@@ -193,37 +211,49 @@ test('carries a departed sign past the car before its restrained edge fade', asy
return Math.max( return Math.max(
1, 1,
profile.getBoundingClientRect().top + window.scrollY profile.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + candidateDistanceVh), - viewportHeight * (0.6 + candidateDistanceVh),
); );
}, triggerProbeDistanceVh(viewport.width, viewport.height)); }, triggerProbeDistanceVh(viewport.width, viewport.height));
await page.mouse.wheel(0, 1); await page.mouse.wheel(0, 1);
await page.evaluate((scrollY) => window.scrollTo(0, scrollY), destination); await page.evaluate((scrollY) => window.scrollTo(0, scrollY), destination);
await expect.poll(async () => ( await expect.poll(async () => (
await readSceneInstrumentation(page) await readSceneInstrumentation(page)
).signFrames.some((frame) => frame.targetId === 'profile' && frame.phase === 'dwell')).toBe(true); ).signFrames.some((frame) => frame.targetId === 'profile' && frame.phase === 'dwell'), { timeout: 10_000 }).toBe(true);
await expect.poll(async () => page.evaluate(() => (
(window as unknown as { __gantryStates: string[] }).__gantryStates.includes('passing')
)), { timeout: 5_000 }).toBe(true);
await expect.poll(async () => ( await expect.poll(async () => (
await readSceneInstrumentation(page) await readSceneInstrumentation(page)
).signFrames.some((frame) => ( ).signFrames.some((frame) => frame.targetId === 'profile' && frame.phase === 'depart')).toBe(true);
frame.targetId === 'profile'
&& frame.phase === 'depart'
&& frame.progress >= 0.52
)), { intervals: [16], timeout: 5_000 }).toBe(true);
const frames = (await readSceneInstrumentation(page)).signFrames; const windowBox = await gantryWindowBox(page);
const dwell = frames.findLast((frame) => frame.targetId === 'profile' && frame.phase === 'dwell'); const frames = (await readSceneInstrumentation(page)).signFrames
const passing = frames.findLast((frame) => ( .filter((frame) => frame.targetId === 'profile');
frame.targetId === 'profile' const approachPeak = Math.max(...frames
&& frame.phase === 'depart' .filter((frame) => frame.phase === 'approach')
&& frame.progress >= 0.3 .map((frame) => frame.bounds.width));
&& frame.opacity >= 0.98 const dwell = frames.findLast((frame) => frame.phase === 'dwell');
)); const departs = frames.filter((frame) => frame.phase === 'depart');
if (dwell === undefined || passing === undefined) { if (dwell === undefined || departs.length === 0) {
throw new Error('Missing comparable Profile dwell and pass-by frames'); throw new Error('Missing comparable Profile dwell and depart frames');
} }
expect(passing.opacity).toBeGreaterThanOrEqual(0.98); // The arc: the sign is read large over the road before it docks.
expect(passing.bounds.width).toBeGreaterThan(dwell.bounds.width * 1.18); expect(approachPeak).toBeGreaterThan(dwell.bounds.width * 1.5);
expect(passing.bounds.left).toBeLessThan(dwell.bounds.left - 24); // Docked: wholly inside the window, never across the reading column.
expect(passing.targetId).toBe(dwell.targetId); expect(dwell.opacity).toBeGreaterThanOrEqual(0.98);
expect(dwell.bounds.left).toBeGreaterThanOrEqual(windowBox.left - 1);
expect(dwell.bounds.right).toBeLessThanOrEqual(windowBox.right + 1);
expect(dwell.bounds.top).toBeGreaterThanOrEqual(windowBox.top - 1);
expect(dwell.bounds.bottom).toBeLessThanOrEqual(windowBox.bottom + 1);
// Departing: it fades where it docked instead of sweeping past the car.
for (const depart of departs) {
expect(Math.abs(depart.bounds.left - dwell.bounds.left)).toBeLessThanOrEqual(1);
expect(Math.abs(depart.bounds.width - dwell.bounds.width)).toBeLessThanOrEqual(1);
}
const states = await page.evaluate(() => (window as unknown as { __gantryStates: string[] }).__gantryStates);
const order = ['incoming', 'docked', 'passing'].map((state) => states.indexOf(state));
expect(order.every((index) => index >= 0)).toBe(true);
expect(order).toEqual([...order].sort((a, b) => a - b));
}); });
test('shows exactly one destination sign for a navigator click and suppresses history restoration', async ({ test('shows exactly one destination sign for a navigator click and suppresses history restoration', async ({
@@ -274,7 +304,7 @@ test('keeps the non-navigator Explore work section jump sign-suppressed', async
return Math.max( return Math.max(
1, 1,
heading.getBoundingClientRect().top + window.scrollY heading.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + candidateDistanceVh), - viewportHeight * (0.6 + candidateDistanceVh),
); );
}, triggerProbeDistanceVh(viewport.width, viewport.height)); }, triggerProbeDistanceVh(viewport.width, viewport.height));
await page.mouse.wheel(0, 1); await page.mouse.wheel(0, 1);
@@ -414,7 +444,7 @@ test('moves an already-readable portrait sign into navigator-safe placement with
return Math.max( return Math.max(
1, 1,
profile.getBoundingClientRect().top + window.scrollY profile.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + 0.22), - viewportHeight * (0.6 + 0.22),
); );
}); });
await page.mouse.wheel(0, 1); await page.mouse.wheel(0, 1);
@@ -468,7 +498,7 @@ test('re-arms passive signage after a navigator round trip to Start', async ({
return Math.max( return Math.max(
1, 1,
profile.getBoundingClientRect().top + window.scrollY profile.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + candidateDistanceVh), - viewportHeight * (0.6 + candidateDistanceVh),
); );
}, triggerProbeDistanceVh(viewport.width, viewport.height)); }, triggerProbeDistanceVh(viewport.width, viewport.height));
await page.mouse.wheel(0, 1); await page.mouse.wheel(0, 1);
@@ -494,7 +524,7 @@ test('reconciles an idle rapid jump to the latest active route without flashing
if (!(target instanceof HTMLElement)) throw new Error('Missing selected builds heading'); if (!(target instanceof HTMLElement)) throw new Error('Missing selected builds heading');
const top = target.getBoundingClientRect().top + window.scrollY; const top = target.getBoundingClientRect().top + window.scrollY;
const viewportHeight = window.visualViewport?.height ?? window.innerHeight; const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
window.scrollTo(0, Math.max(1, top - viewportHeight * 0.35 + 1)); window.scrollTo(0, Math.max(1, top - viewportHeight * 0.6 + 1));
}); });
await expect.poll(async () => (await readSceneInstrumentation(page)).routeLabelsDrawn) await expect.poll(async () => (await readSceneInstrumentation(page)).routeLabelsDrawn)
@@ -515,7 +545,7 @@ test('reconciles an idle rapid jump to the latest active route without flashing
expect(dwell.bounds.right).toBeLessThanOrEqual(1_432); expect(dwell.bounds.right).toBeLessThanOrEqual(1_432);
}); });
test('keeps the readable Profile face outside its destination panel with verified texel density', async ({ test('docks the readable Profile face in the gantry window, clear of its panel, with verified texel density', async ({
page, page,
distUrl, distUrl,
}) => { }) => {
@@ -530,7 +560,7 @@ test('keeps the readable Profile face outside its destination panel with verifie
return Math.max( return Math.max(
1, 1,
profile.getBoundingClientRect().top + window.scrollY profile.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + 0.45), - viewportHeight * (0.6 + 0.45),
); );
}); });
await page.mouse.wheel(0, 1); await page.mouse.wheel(0, 1);
@@ -545,11 +575,16 @@ test('keeps the readable Profile face outside its destination panel with verifie
candidate.targetId === 'profile' && candidate.phase === 'dwell' candidate.targetId === 'profile' && candidate.phase === 'dwell'
)); ));
if (frame === undefined) throw new Error('Missing readable Profile sign frame'); if (frame === undefined) throw new Error('Missing readable Profile sign frame');
const panel = await page.locator('#profile').evaluate((element) => { // The face is read in the gantry window, which sits above the reading
const bounds = element.getBoundingClientRect(); // column, so it can never overlap the Profile panel below it.
return { left: bounds.left, top: bounds.top, right: bounds.right, bottom: bounds.bottom }; const windowBox = await gantryWindowBox(page);
}); expect(frame.bounds.left).toBeGreaterThanOrEqual(windowBox.left - 1);
expect(frame.bounds.right).toBeLessThanOrEqual(panel.left - 16); expect(frame.bounds.right).toBeLessThanOrEqual(windowBox.right + 1);
expect(frame.bounds.bottom).toBeLessThanOrEqual(windowBox.bottom + 1);
const panelTop = await page.locator('#profile .chapter-body').evaluate((element) => (
element.getBoundingClientRect().top
));
expect(frame.bounds.bottom).toBeLessThanOrEqual(panelTop);
expect(frame.texture).toMatchObject({ expect(frame.texture).toMatchObject({
width: 2_048, width: 2_048,
height: 512, height: 512,
@@ -583,7 +618,7 @@ test('keeps a fresh readable sign inside responsive world-safe corridors', async
return Math.max( return Math.max(
1, 1,
profile.getBoundingClientRect().top + window.scrollY profile.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + candidateDistanceVh), - viewportHeight * (0.6 + candidateDistanceVh),
); );
}, triggerProbeDistanceVh(viewport.width, viewport.height)); }, triggerProbeDistanceVh(viewport.width, viewport.height));
await page.mouse.wheel(0, 1); await page.mouse.wheel(0, 1);
@@ -649,7 +684,7 @@ test('keeps the upcoming Experience sign clear of both outgoing and incoming pan
return Math.max( return Math.max(
1, 1,
heading.getBoundingClientRect().top + window.scrollY heading.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + 0.45), - viewportHeight * (0.6 + 0.45),
); );
}); });
await page.mouse.wheel(0, 1); await page.mouse.wheel(0, 1);
@@ -698,7 +733,7 @@ test('keeps the short-landscape Experience sign in the right roadside gap', asyn
return Math.max( return Math.max(
1, 1,
heading.getBoundingClientRect().top + window.scrollY heading.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + 0.38), - viewportHeight * (0.6 + 0.38),
); );
}); });
await page.mouse.wheel(0, 1); await page.mouse.wheel(0, 1);
@@ -776,7 +811,7 @@ test('keeps every passive transition sign clear of visible opaque panels and fix
return Math.max( return Math.max(
1, 1,
heading.getBoundingClientRect().top + window.scrollY heading.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + candidateDistanceVh), - viewportHeight * (0.6 + candidateDistanceVh),
); );
}, { }, {
targetId: next.id, targetId: next.id,