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 destination = id === 'contact'
? 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' });
}, section.id);
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 destination = Math.max(
1,
headingTop - viewportHeight * (0.35 + candidateDistanceVh),
headingTop - viewportHeight * (0.6 + candidateDistanceVh),
);
return { destination, viewportHeight, expectedLabel: 'Profile' };
}, 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 headingTop = profile.getBoundingClientRect().top + window.scrollY;
return {
destination: Math.max(1, headingTop - viewportHeight * (0.35 + 0.45)),
destination: Math.max(1, headingTop - viewportHeight * (0.6 + 0.45)),
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);
});
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,
distUrl,
}) => {
await page.setViewportSize({ width: 1_440, height: 900 });
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
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();
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(
1,
profile.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + candidateDistanceVh),
- viewportHeight * (0.6 + candidateDistanceVh),
);
}, triggerProbeDistanceVh(viewport.width, viewport.height));
await page.mouse.wheel(0, 1);
await page.evaluate((scrollY) => window.scrollTo(0, scrollY), destination);
await expect.poll(async () => (
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 readSceneInstrumentation(page)
).signFrames.some((frame) => (
frame.targetId === 'profile'
&& frame.phase === 'depart'
&& frame.progress >= 0.52
)), { intervals: [16], timeout: 5_000 }).toBe(true);
).signFrames.some((frame) => frame.targetId === 'profile' && frame.phase === 'depart')).toBe(true);
const frames = (await readSceneInstrumentation(page)).signFrames;
const dwell = frames.findLast((frame) => frame.targetId === 'profile' && frame.phase === 'dwell');
const passing = frames.findLast((frame) => (
frame.targetId === 'profile'
&& frame.phase === 'depart'
&& frame.progress >= 0.3
&& frame.opacity >= 0.98
));
if (dwell === undefined || passing === undefined) {
throw new Error('Missing comparable Profile dwell and pass-by frames');
const windowBox = await gantryWindowBox(page);
const frames = (await readSceneInstrumentation(page)).signFrames
.filter((frame) => frame.targetId === 'profile');
const approachPeak = Math.max(...frames
.filter((frame) => frame.phase === 'approach')
.map((frame) => frame.bounds.width));
const dwell = frames.findLast((frame) => frame.phase === 'dwell');
const departs = frames.filter((frame) => frame.phase === 'depart');
if (dwell === undefined || departs.length === 0) {
throw new Error('Missing comparable Profile dwell and depart frames');
}
expect(passing.opacity).toBeGreaterThanOrEqual(0.98);
expect(passing.bounds.width).toBeGreaterThan(dwell.bounds.width * 1.18);
expect(passing.bounds.left).toBeLessThan(dwell.bounds.left - 24);
expect(passing.targetId).toBe(dwell.targetId);
// The arc: the sign is read large over the road before it docks.
expect(approachPeak).toBeGreaterThan(dwell.bounds.width * 1.5);
// Docked: wholly inside the window, never across the reading column.
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 ({
@@ -274,7 +304,7 @@ test('keeps the non-navigator Explore work section jump sign-suppressed', async
return Math.max(
1,
heading.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + candidateDistanceVh),
- viewportHeight * (0.6 + candidateDistanceVh),
);
}, triggerProbeDistanceVh(viewport.width, viewport.height));
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(
1,
profile.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + 0.22),
- viewportHeight * (0.6 + 0.22),
);
});
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(
1,
profile.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + candidateDistanceVh),
- viewportHeight * (0.6 + candidateDistanceVh),
);
}, triggerProbeDistanceVh(viewport.width, viewport.height));
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');
const top = target.getBoundingClientRect().top + window.scrollY;
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)
@@ -515,7 +545,7 @@ test('reconciles an idle rapid jump to the latest active route without flashing
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,
distUrl,
}) => {
@@ -530,7 +560,7 @@ test('keeps the readable Profile face outside its destination panel with verifie
return Math.max(
1,
profile.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + 0.45),
- viewportHeight * (0.6 + 0.45),
);
});
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'
));
if (frame === undefined) throw new Error('Missing readable Profile sign frame');
const panel = await page.locator('#profile').evaluate((element) => {
const bounds = element.getBoundingClientRect();
return { left: bounds.left, top: bounds.top, right: bounds.right, bottom: bounds.bottom };
});
expect(frame.bounds.right).toBeLessThanOrEqual(panel.left - 16);
// The face is read in the gantry window, which sits above the reading
// column, so it can never overlap the Profile panel below it.
const windowBox = await gantryWindowBox(page);
expect(frame.bounds.left).toBeGreaterThanOrEqual(windowBox.left - 1);
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({
width: 2_048,
height: 512,
@@ -583,7 +618,7 @@ test('keeps a fresh readable sign inside responsive world-safe corridors', async
return Math.max(
1,
profile.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + candidateDistanceVh),
- viewportHeight * (0.6 + candidateDistanceVh),
);
}, triggerProbeDistanceVh(viewport.width, viewport.height));
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(
1,
heading.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + 0.45),
- viewportHeight * (0.6 + 0.45),
);
});
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(
1,
heading.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + 0.38),
- viewportHeight * (0.6 + 0.38),
);
});
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(
1,
heading.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + candidateDistanceVh),
- viewportHeight * (0.6 + candidateDistanceVh),
);
}, {
targetId: next.id,