fix: refine responsive sign placement

This commit is contained in:
2026-09-03 18:34:39 +03:00
parent b075041918
commit 5dba071950
15 changed files with 388 additions and 17 deletions

View File

@@ -341,6 +341,53 @@ test('keeps a portrait navigator sign clear of its full-width destination panel'
}
});
test('scales a medium portrait navigator sign without changing the phone composition', async ({
page,
distUrl,
}, testInfo) => {
test.skip(testInfo.project.name !== 'desktop-chromium', 'One exact portrait breakpoint comparison is sufficient.');
const widths: number[] = [];
for (const viewport of [
{ width: 390, height: 844, minimumWidth: 85, maximumWidth: 110 },
{ width: 692, height: 793, minimumWidth: 145, maximumWidth: 210 },
] as const) {
await page.setViewportSize(viewport);
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
await clickNavigationLink(page, '#profile');
await expect(page.locator('#profile-heading')).toBeFocused();
await expect.poll(async () => (
await readSceneInstrumentation(page)
).signFrames.some((frame) => frame.targetId === 'profile' && frame.phase === 'dwell')).toBe(true);
const frame = (await readSceneInstrumentation(page)).signFrames.findLast((candidate) => (
candidate.targetId === 'profile' && candidate.phase === 'dwell'
));
if (frame === undefined) throw new Error(`Missing navigator Profile dwell at ${viewport.width}x${viewport.height}`);
expect(frame.navigationArrival).toBe(true);
expect(frame.viewportProfile).toBe('tall');
expect(frame.bounds.width).toBeGreaterThanOrEqual(viewport.minimumWidth);
expect(frame.bounds.width).toBeLessThanOrEqual(viewport.maximumWidth);
expect(frame.bounds.left).toBeGreaterThanOrEqual(8);
expect(frame.bounds.right).toBeLessThanOrEqual(viewport.width - 8);
const occupied = await page.locator('#profile, #experience').evaluateAll((elements) => (
elements.map((element) => {
const bounds = element.getBoundingClientRect();
return { id: element.id, left: bounds.left, top: bounds.top, right: bounds.right, bottom: bounds.bottom };
}).filter((bounds) => bounds.bottom > 0 && bounds.top < window.innerHeight)
));
for (const panel of occupied) {
expect(rectanglesOverlap(frame.bounds, panel, 16), JSON.stringify({ viewport, frame: frame.bounds, panel }))
.toBe(false);
}
widths.push(frame.bounds.width);
}
expect(widths[1]).toBeGreaterThanOrEqual((widths[0] ?? 0) * 1.45);
});
test('moves an already-readable portrait sign into navigator-safe placement without replaying it', async ({
page,
distUrl,
@@ -605,6 +652,8 @@ test('keeps the upcoming Experience sign clear of both outgoing and incoming pan
candidate.targetId === 'experience' && candidate.phase === 'dwell'
));
if (frame === undefined) throw new Error('Missing readable Experience sign frame');
expect(frame.corridor).toBe('right');
expect(frame.bounds.left).toBeGreaterThan(720);
const panels = await page.locator('#profile, #experience').evaluateAll((elements) => (
elements.map((element) => {
const bounds = element.getBoundingClientRect();
@@ -623,6 +672,70 @@ test('keeps the upcoming Experience sign clear of both outgoing and incoming pan
}
});
test('keeps the short-landscape Experience sign in the right roadside gap', async ({
page,
distUrl,
}, testInfo) => {
test.skip(testInfo.project.name !== 'desktop-chromium', 'One exact short-landscape composition is sufficient.');
await page.setViewportSize({ width: 844, height: 390 });
await page.goto(`${distUrl}/#profile`, { waitUntil: 'domcontentloaded' });
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
await expect(page.locator('nav a[href="#profile"]')).toHaveAttribute('aria-current', 'page');
const destination = await page.evaluate(() => {
const heading = document.getElementById('experience-heading');
if (!(heading instanceof HTMLElement)) throw new Error('Missing Experience heading');
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
return Math.max(
1,
heading.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + 0.38),
);
});
await page.mouse.wheel(0, 1);
await page.evaluate((scrollY) => window.scrollTo(0, scrollY), destination);
await expect.poll(async () => (
await readSceneInstrumentation(page)
).signFrames.findLast((frame) => frame.targetId === 'experience')?.phase, {
intervals: [16],
timeout: 5_000,
}).toBe('dwell');
const frame = (await readSceneInstrumentation(page)).signFrames.findLast((candidate) => (
candidate.targetId === 'experience' && candidate.phase === 'dwell'
));
if (frame === undefined) throw new Error('Missing readable short-landscape Experience sign frame');
expect(frame.corridor).toBe('right');
expect(frame.viewportProfile).toBe('short');
expect(frame.bounds.left).toBeGreaterThan(422);
expect(frame.bounds.right).toBeLessThanOrEqual(836);
expect(frame.bounds.top).toBeGreaterThanOrEqual(210);
expect(frame.bounds.bottom).toBeLessThanOrEqual(300);
expect(frame.bounds.width).toBeGreaterThanOrEqual(120);
const occupied = await page.locator('#profile, #experience, .site-navigation, .drive-controls, .journey-hud')
.evaluateAll((elements) => elements.map((element) => {
const bounds = element.getBoundingClientRect();
return {
id: element.id || element.className,
fixed: element.matches('.site-navigation, .drive-controls, .journey-hud'),
visible: bounds.bottom > 0 && bounds.top < window.innerHeight,
bounds: {
left: bounds.left,
top: bounds.top,
right: bounds.right,
bottom: bounds.bottom,
},
};
}).filter(({ visible }) => visible));
for (const surface of occupied) {
expect(
rectanglesOverlap(frame.bounds, surface.bounds, surface.fixed ? 8 : 16),
String(surface.id),
).toBe(false);
}
expect(frame.texture.fontReadyWhenDrawn).toBe(true);
});
test('keeps every passive transition sign clear of visible opaque panels and fixed controls', async ({
page,
distUrl,