import { 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, }) => { 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 expectedChapterColors = (page.viewportSize()?.width ?? 0) < 768 ? [ '#040713', '#040713', '#060918', '#080b1d', '#0d1025', '#0d1025', '#18152c', '#2b2035', ] : [ '#000102', '#000102', '#000102', '#010103', '#010105', '#010105', '#020206', '#060409', ]; expect(chapterColors).toEqual(expectedChapterColors); }); 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.8)); return { destination, viewportHeight, expectedLabel: '01 / 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]); });