import { expect, test, type Page } from '@playwright/test'; import { APP_LOCALES } from '../src/app/core/i18n/locale'; import { pagePath } from './helpers'; const VIEWPORTS = [768, 820, 1024, 1280, 1440] as const; const VIEWPORT_HEIGHT = 900; const MAX_HEADER_HEIGHT = 200; async function waitForScrollSettle(page: Page): Promise { await page.evaluate(async () => { await new Promise((resolve) => { let last = window.scrollY; let stableFrames = 0; const tick = () => { if (window.scrollY === last) { stableFrames += 1; if (stableFrames >= 8) { resolve(); return; } } else { stableFrames = 0; last = window.scrollY; } requestAnimationFrame(tick); }; requestAnimationFrame(tick); }); }); } test.describe('fragment scrolling', () => { test('case headings stay fully visible under the compact header', async ({ page }, testInfo) => { test.skip( testInfo.project.name === 'mobile', 'desktop widths are measured here to avoid a duplicate run', ); for (const width of VIEWPORTS) { await page.setViewportSize({ width, height: VIEWPORT_HEIGHT }); for (const locale of APP_LOCALES) { const path = `${pagePath('projects', locale)}#bannier`; const label = `${width}px ${locale}`; await page.goto(path, { waitUntil: 'networkidle' }); const header = page.locator('.site-header'); const heading = page.locator('#bannier h2'); await expect(heading).toBeVisible(); await waitForScrollSettle(page); const headerBox = await header.boundingBox(); expect(headerBox, label).toBeTruthy(); expect( headerBox!.height, `header height at ${label} must be at most ${MAX_HEADER_HEIGHT}px`, ).toBeLessThanOrEqual(MAX_HEADER_HEIGHT); expect( headerBox!.height, `header height at ${label} must be at most 25% of the viewport`, ).toBeLessThanOrEqual(VIEWPORT_HEIGHT * 0.25); await expect(header).not.toHaveCSS('position', 'sticky'); await expect(heading).toBeInViewport({ ratio: 1 }); const headingOwnsCentre = await heading.evaluate((element) => { const rect = element.getBoundingClientRect(); const node = document.elementFromPoint( rect.left + rect.width / 2, rect.top + rect.height / 2, ); return node !== null && (element === node || element.contains(node)); }); expect(headingOwnsCentre, `heading centre must not be covered at ${label}`).toBe(true); } } }); });