integration: compact the wide primary navigation and keep fragment targets visible

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-25 19:55:22 +02:00
parent 575ba06bb9
commit b15dfd7801
6 changed files with 163 additions and 67 deletions

View File

@@ -1,47 +1,79 @@
import { expect, test } from '@playwright/test';
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<void> {
await page.evaluate(async () => {
await new Promise<void>((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 clear the sticky header at desktop width', async ({ page }, testInfo) => {
test.skip(testInfo.project.name === 'mobile', 'sticky header applies from md up');
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',
);
await page.setViewportSize({ width: 1440, height: 900 });
for (const width of VIEWPORTS) {
await page.setViewportSize({ width, height: VIEWPORT_HEIGHT });
for (const locale of APP_LOCALES) {
const path = `${pagePath('projects', locale)}#myspa`;
await page.goto(path, { waitUntil: 'networkidle' });
for (const locale of APP_LOCALES) {
const path = `${pagePath('projects', locale)}#myspa`;
const label = `${width}px ${locale}`;
await page.goto(path, { waitUntil: 'networkidle' });
const header = page.locator('.site-header');
const heading = page.locator('#myspa h2');
await expect(heading).toBeVisible();
const header = page.locator('.site-header');
const heading = page.locator('#myspa h2');
await expect(heading).toBeVisible();
await page.evaluate(async () => {
await new Promise<void>((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);
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));
});
});
const headerBox = await header.boundingBox();
const headingBox = await heading.boundingBox();
expect(headerBox, path).toBeTruthy();
expect(headingBox, path).toBeTruthy();
expect(headingBox!.y, path).toBeGreaterThanOrEqual(headerBox!.y + headerBox!.height - 1);
expect(headingOwnsCentre, `heading centre must not be covered at ${label}`).toBe(true);
}
}
});
});