import { clickNavigationLink, expect, test } from '../helpers/serve-dist'; import { installSceneInstrumentation, readSceneInstrumentation, waitForOneAnimationFrame, } from '../helpers/scene-instrumentation'; const sections = [ { id: 'start', label: 'Start' }, { id: 'profile', label: 'Profile' }, { id: 'experience', label: 'Experience' }, { id: 'infrastructure', label: 'Infrastructure' }, { id: 'selected-builds', label: 'Selected builds' }, { id: 'additional-work', label: 'Additional work' }, { id: 'toolkit', label: 'Toolkit' }, { id: 'contact', label: 'Contact' }, ] as const; test.beforeEach(async ({ page }) => installSceneInstrumentation(page)); test('keeps navigation, HUD, progress, chapter, and arrival on the same next-frame section snapshot', async ({ page, distUrl, }) => { const graphicsErrors: string[] = []; page.on('console', (message) => { if ( message.type() === 'error' && /THREE\.WebGLProgram|Shader Error|VALIDATE_STATUS/iu.test(message.text()) ) graphicsErrors.push(message.text()); }); await page.goto(distUrl, { waitUntil: 'domcontentloaded' }); await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', /running|failed|static/u); const chapterColors: string[] = []; for (const section of sections) { await page.evaluate((id) => { const heading = document.getElementById(`${id}-heading`); if (!(heading instanceof HTMLElement)) throw new Error(`Missing heading: ${id}`); const top = heading.getBoundingClientRect().top + window.scrollY; const viewportHeight = window.visualViewport?.height ?? window.innerHeight; const destination = id === 'contact' ? document.documentElement.scrollHeight : Math.max(0, top - viewportHeight * 0.35 + 1); window.scrollTo({ top: destination, behavior: 'instant' }); }, section.id); await waitForOneAnimationFrame(page); await expect(page.locator(`nav a[href="#${section.id}"]`)).toHaveAttribute('aria-current', 'page'); await expect(page.locator('.journey-hud__section')).toHaveText(section.label); const routeText = await page.locator('.journey-hud__route').textContent(); expect(routeText).toMatch(/^ROUTE (?:100|[0-9]{1,2})%$/u); if (section.id === 'contact') { await expect(page.locator('.journey-hud__arrival')).toHaveText('ARRIVED'); await expect(page.locator('.journey-hud__route')).toHaveText('ROUTE 100%'); } else { await expect(page.locator('.journey-hud__arrival')).toHaveCount(0); } // The section snapshot is emitted in the scroll RAF above; the scene's // already-running loop must consume it no later than its next RAF. await waitForOneAnimationFrame(page); const clearColors = (await readSceneInstrumentation(page)).clearColors.filter((color) => color !== '#000000'); const latestChapterColor = clearColors.at(-1); if (latestChapterColor === undefined) throw new Error(`Missing rendered chapter color for ${section.id}`); chapterColors.push(latestChapterColor); } const instrumentation = await readSceneInstrumentation(page); expect(instrumentation.maximumCanvasCount).toBeLessThanOrEqual(1); const chapterEnergy = chapterColors.map(colorEnergy); for (let index = 1; index < chapterEnergy.length; index += 1) { expect(chapterEnergy[index]).toBeGreaterThanOrEqual(chapterEnergy[index - 1] ?? 0); } expect(chapterEnergy.at(-1)).toBeGreaterThan(chapterEnergy[0] ?? 0); expect(new Set(chapterColors).size).toBeGreaterThanOrEqual(4); expect(graphicsErrors).toEqual([]); }); test('draws only the independently eligible next roadside sign during passive forward travel', async ({ page, distUrl, }) => { await page.goto(distUrl, { waitUntil: 'domcontentloaded' }); await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running'); const oracle = await page.evaluate(() => { const profile = document.getElementById('profile-heading'); if (!(profile instanceof HTMLElement)) throw new Error('Missing profile heading'); const viewportHeight = window.visualViewport?.height ?? window.innerHeight; const headingTop = profile.getBoundingClientRect().top + window.scrollY; const destination = Math.max(1, headingTop - viewportHeight * (0.35 + 0.45)); return { destination, viewportHeight, expectedLabel: 'Profile' }; }); await page.evaluate(() => window.dispatchEvent(new WheelEvent('wheel', { deltaY: 1 }))); for (let y = 1; y < oracle.destination; y += oracle.viewportHeight * 0.5) { await page.evaluate((nextY) => window.scrollTo(0, nextY), y); await waitForOneAnimationFrame(page); } await page.evaluate((destination) => window.scrollTo(0, destination), oracle.destination); await waitForOneAnimationFrame(page); await expect.poll(async () => (await readSceneInstrumentation(page)).routeLabelsDrawn).toContain(oracle.expectedLabel); const labels = (await readSceneInstrumentation(page)).routeLabelsDrawn; expect([...new Set(labels)]).toEqual([oracle.expectedLabel]); }); test('keeps a readable sign immutable through reversal, resize, and a multi-section jump', async ({ page, distUrl, }) => { await page.setViewportSize({ width: 1440, height: 900 }); await page.goto(distUrl, { waitUntil: 'domcontentloaded' }); await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running'); const oracle = await page.evaluate(() => { const profile = document.getElementById('profile-heading'); if (!(profile instanceof HTMLElement)) throw new Error('Missing profile heading'); 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)), viewportHeight, }; }); await page.mouse.wheel(0, 1); for (let y = 1; y < oracle.destination; y += oracle.viewportHeight * 0.5) { await page.evaluate((nextY) => window.scrollTo(0, nextY), y); await waitForOneAnimationFrame(page); } await page.evaluate((destination) => window.scrollTo(0, destination), oracle.destination); await expect.poll(async () => (await readSceneInstrumentation(page)).routeLabelsDrawn) .toContain('Profile'); await page.waitForTimeout(1_250); await page.mouse.wheel(0, -120); await page.setViewportSize({ width: 844, height: 390 }); await page.evaluate(() => { const target = document.getElementById('selected-builds-heading'); if (!(target instanceof HTMLElement)) throw new Error('Missing selected builds heading'); window.scrollTo(0, target.getBoundingClientRect().top + window.scrollY); }); await page.waitForTimeout(450); const labels = (await readSceneInstrumentation(page)).routeLabelsDrawn; expect([...new Set(labels)]).toEqual(['Profile']); expect((await readSceneInstrumentation(page)).maximumCanvasCount).toBeLessThanOrEqual(1); }); test('suppresses roadside signs for explicit navigation and history restoration', async ({ page, distUrl, }) => { await page.goto(distUrl, { waitUntil: 'domcontentloaded' }); await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running'); await clickNavigationLink(page, '#selected-builds'); await expect(page.locator('#selected-builds-heading')).toBeFocused(); await page.waitForTimeout(400); expect((await readSceneInstrumentation(page)).routeLabelsDrawn).toEqual([]); await page.goBack({ waitUntil: 'domcontentloaded' }); await page.waitForTimeout(400); expect((await readSceneInstrumentation(page)).routeLabelsDrawn).toEqual([]); }); test('restores the next sign after a navigator round trip', async ({ page, distUrl, }) => { 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 clickNavigationLink(page, '#start'); await expect(page.locator('#start-heading')).toBeFocused(); const destination = await page.evaluate(() => { const profile = document.getElementById('profile-heading'); if (!(profile instanceof HTMLElement)) throw new Error('Missing profile heading'); const viewportHeight = window.visualViewport?.height ?? window.innerHeight; return Math.max( 1, profile.getBoundingClientRect().top + window.scrollY - viewportHeight * (0.35 + 0.45), ); }); 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')).toBe(true); }); test('reconciles an idle rapid jump to the latest active route without flashing intermediates', async ({ page, distUrl, }) => { await page.goto(distUrl, { waitUntil: 'domcontentloaded' }); await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running'); await page.mouse.wheel(0, 1); await page.evaluate(() => { const target = document.getElementById('selected-builds-heading'); 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)); }); await expect.poll(async () => (await readSceneInstrumentation(page)).routeLabelsDrawn) .toContain('Selected builds'); expect([...new Set((await readSceneInstrumentation(page)).routeLabelsDrawn)]) .toEqual(['Selected builds']); }); test('keeps the readable Profile face outside its destination panel with verified texel density', async ({ page, distUrl, }) => { await page.setViewportSize({ width: 1440, height: 900 }); await page.goto(distUrl, { waitUntil: 'domcontentloaded' }); await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running'); const destination = await page.evaluate(() => { const profile = document.getElementById('profile-heading'); if (!(profile instanceof HTMLElement)) throw new Error('Missing profile heading'); const viewportHeight = window.visualViewport?.height ?? window.innerHeight; return Math.max( 1, profile.getBoundingClientRect().top + window.scrollY - viewportHeight * (0.35 + 0.45), ); }); 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); const instrumentation = await readSceneInstrumentation(page); const frame = instrumentation.signFrames.findLast((candidate) => ( 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); expect(frame.texture).toMatchObject({ width: 2_048, height: 512, fontReadyWhenDrawn: true, }); expect(frame.texture.width / (frame.bounds.width * frame.rendererPixelRatio)) .toBeGreaterThanOrEqual(4); }); test('keeps a fresh readable sign inside responsive world-safe corridors', async ({ page, distUrl, }, testInfo) => { test.skip(testInfo.project.name !== 'desktop-chromium', 'One exact responsive matrix is sufficient.'); for (const viewport of [ { width: 390, height: 844, profile: 'tall', minimumFaceWidth: 120 }, { width: 844, height: 390, profile: 'short', minimumFaceWidth: 130 }, { width: 768, height: 1_024, profile: 'tall', minimumFaceWidth: 150 }, { width: 1_440, height: 900, profile: 'wide', minimumFaceWidth: 180 }, { width: 2_560, height: 1_440, profile: 'wide', minimumFaceWidth: 240 }, ] as const) { await page.setViewportSize(viewport); await page.goto(distUrl, { waitUntil: 'domcontentloaded' }); await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running'); const destination = await page.evaluate(() => { const profile = document.getElementById('profile-heading'); if (!(profile instanceof HTMLElement)) throw new Error('Missing profile heading'); const viewportHeight = window.visualViewport?.height ?? window.innerHeight; return Math.max( 1, profile.getBoundingClientRect().top + window.scrollY - viewportHeight * (0.35 + 0.45), ); }); await page.mouse.wheel(0, 1); await page.evaluate((scrollY) => window.scrollTo(0, scrollY), destination); await expect.poll(async () => ( await readSceneInstrumentation(page) ).signFrames.at(-1)?.phase, { intervals: [16], timeout: 5_000 }).toBe('dwell'); const frame = (await readSceneInstrumentation(page)).signFrames.findLast((candidate) => ( candidate.targetId === 'profile' && candidate.phase === 'dwell' )); if (frame === undefined) throw new Error(`Missing Profile dwell at ${viewport.width}x${viewport.height}`); const geometry = await page.evaluate(() => { const boundsFor = (selector: string) => { const element = document.querySelector(selector); if (!(element instanceof HTMLElement)) return null; const bounds = element.getBoundingClientRect(); return { left: bounds.left, top: bounds.top, right: bounds.right, bottom: bounds.bottom }; }; return { panel: boundsFor('#profile'), adjacentPanels: [boundsFor('.hero-journey__content')].filter((bounds) => bounds !== null), fixedZones: [ boundsFor('.site-navigation'), boundsFor('.drive-controls'), boundsFor('.journey-hud'), ].filter((bounds) => bounds !== null), }; }); if (geometry.panel === null) throw new Error('Missing Profile panel bounds'); expect(frame.viewportProfile).toBe(viewport.profile); expect(frame.bounds.left).toBeGreaterThanOrEqual(8); expect(frame.bounds.right).toBeLessThanOrEqual(viewport.width - 8); expect(frame.bounds.top).toBeGreaterThanOrEqual(8); expect(frame.bounds.bottom).toBeLessThanOrEqual(viewport.height - 8); expect(frame.bounds.width).toBeGreaterThanOrEqual(viewport.minimumFaceWidth); expect(rectanglesOverlap(frame.bounds, geometry.panel, 16)).toBe(false); for (const adjacentPanel of geometry.adjacentPanels) { expect(rectanglesOverlap(frame.bounds, adjacentPanel, 16)).toBe(false); } for (const fixedZone of geometry.fixedZones) { expect(rectanglesOverlap(frame.bounds, fixedZone, 8)).toBe(false); } expect(frame.texture.fontReadyWhenDrawn).toBe(true); expect(frame.texture.width / (frame.bounds.width * frame.rendererPixelRatio)) .toBeGreaterThanOrEqual(4); } }); function colorEnergy(color: string): number { const value = Number.parseInt(color.slice(1), 16); return ((value >> 16) & 0xff) + ((value >> 8) & 0xff) + (value & 0xff); } function rectanglesOverlap( first: { left: number; top: number; right: number; bottom: number }, second: { left: number; top: number; right: number; bottom: number }, gap: number, ): boolean { return !( first.right <= second.left - gap || first.left >= second.right + gap || first.bottom <= second.top - gap || first.top >= second.bottom + gap ); }