fix: refine responsive sign placement
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('clock-driven sign lifecycle', () => {
|
||||
expect(SIGN_ROUTE_ORDER.map((id) => signRouteFor(id))).toEqual([
|
||||
{ id: 'start', label: 'Start', marker: '01', eligible: false, corridor: null, triggerMaximumVh: 0 },
|
||||
{ id: 'profile', label: 'Profile', marker: '02', eligible: true, corridor: 'left', triggerMaximumVh: 0.55 },
|
||||
{ id: 'experience', label: 'Experience', marker: '03', eligible: true, corridor: 'left', triggerMaximumVh: 0.55 },
|
||||
{ id: 'experience', label: 'Experience', marker: '03', eligible: true, corridor: 'right', triggerMaximumVh: 0.55 },
|
||||
{ id: 'infrastructure', label: 'Infrastructure', marker: '04', eligible: true, corridor: 'outer-right', triggerMaximumVh: 0.55, dwellOffsetX: -1.6 },
|
||||
{ id: 'selected-builds', label: 'Selected builds', marker: '05', eligible: true, corridor: 'right', triggerMaximumVh: 0.55 },
|
||||
{ id: 'additional-work', label: 'Additional work', marker: '06', eligible: true, corridor: 'outer-left', triggerMaximumVh: 0.55 },
|
||||
|
||||
@@ -724,7 +724,7 @@ describe("procedural world models", () => {
|
||||
});
|
||||
|
||||
expect(face.userData.textureUpdateCount).toBe(2);
|
||||
expect(sign.inspect(createChaseCamera(16 / 9), 1_440, 900)?.corridor).toBe("left");
|
||||
expect(sign.inspect(createChaseCamera(16 / 9), 1_440, 900)?.corridor).toBe("right");
|
||||
resources.dispose();
|
||||
});
|
||||
|
||||
@@ -925,6 +925,48 @@ describe("procedural world models", () => {
|
||||
resources.dispose();
|
||||
});
|
||||
|
||||
it("grows a medium portrait navigator sign without enlarging the phone pose", () => {
|
||||
const phoneResources = new SceneResourceBag();
|
||||
const phoneSign = createSign(phoneResources);
|
||||
const phoneCamera = createChaseCamera(390 / 844);
|
||||
phoneSign.setViewportSize(390, 844);
|
||||
phoneSign.update({
|
||||
targetId: "profile",
|
||||
phase: "dwell",
|
||||
progress: 1,
|
||||
opacity: 1,
|
||||
navigationArrival: true,
|
||||
});
|
||||
const phone = phoneSign.inspect(phoneCamera, 390, 844);
|
||||
if (phone === null) throw new Error("Missing phone navigator sign");
|
||||
|
||||
const mediumResources = new SceneResourceBag();
|
||||
const mediumSign = createSign(mediumResources);
|
||||
const mediumCamera = createChaseCamera(692 / 793);
|
||||
mediumSign.setViewportSize(692, 793);
|
||||
mediumSign.update({
|
||||
targetId: "profile",
|
||||
phase: "dwell",
|
||||
progress: 1,
|
||||
opacity: 1,
|
||||
navigationArrival: true,
|
||||
});
|
||||
const medium = mediumSign.inspect(mediumCamera, 692, 793);
|
||||
if (medium === null) throw new Error("Missing medium navigator sign");
|
||||
|
||||
expect(phone.bounds.width).toBeGreaterThanOrEqual(85);
|
||||
expect(phone.bounds.width).toBeLessThanOrEqual(110);
|
||||
expect(medium.viewportProfile).toBe("tall");
|
||||
expect(medium.bounds.width).toBeGreaterThanOrEqual(145);
|
||||
expect(medium.bounds.width).toBeLessThanOrEqual(210);
|
||||
expect(medium.bounds.width).toBeGreaterThanOrEqual(phone.bounds.width * 1.45);
|
||||
expect(medium.bounds.top).toBeLessThanOrEqual(555);
|
||||
expect(medium.bounds.bottom).toBeLessThanOrEqual(590);
|
||||
|
||||
phoneResources.dispose();
|
||||
mediumResources.dispose();
|
||||
});
|
||||
|
||||
it("lands a resized Profile lifecycle in the short-landscape safe corridor", () => {
|
||||
const resources = new SceneResourceBag();
|
||||
const sign = createSign(resources);
|
||||
@@ -961,10 +1003,26 @@ describe("procedural world models", () => {
|
||||
expect(profileBounds.maxX).toBeLessThan(0.08);
|
||||
|
||||
sign.update(null);
|
||||
sign.update({ targetId: "contact", phase: "dwell", progress: 1, opacity: 1 });
|
||||
sign.update({ targetId: "experience", phase: "dwell", progress: 1, opacity: 1 });
|
||||
sign.group.updateMatrixWorld(true);
|
||||
const experienceBounds = projectedBounds(profileFace, camera);
|
||||
const experiencePresentation = sign.inspect(camera, 1_440, 900);
|
||||
expect(experienceBounds.minX).toBeGreaterThan(0);
|
||||
expect(experiencePresentation?.corridor).toBe("right");
|
||||
expect(experiencePresentation?.bounds.top).toBeGreaterThanOrEqual(540);
|
||||
expect(experiencePresentation?.bounds.bottom).toBeLessThanOrEqual(630);
|
||||
|
||||
sign.update(null);
|
||||
sign.setViewportSize(844, 390);
|
||||
sign.update({ targetId: "experience", phase: "dwell", progress: 1, opacity: 1 });
|
||||
const shortCamera = createChaseCamera(844 / 390);
|
||||
shortCamera.position.set(-1.35, 8.6, 22);
|
||||
shortCamera.lookAt(1.6, 4.8, -128);
|
||||
const shortExperience = sign.inspect(shortCamera, 844, 390);
|
||||
expect(shortExperience?.corridor).toBe("right");
|
||||
expect(shortExperience?.bounds.left).toBeGreaterThan(422);
|
||||
expect(shortExperience?.bounds.top).toBeGreaterThanOrEqual(210);
|
||||
expect(shortExperience?.bounds.bottom).toBeLessThanOrEqual(300);
|
||||
resources.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user