integration: collapse the mobile nav after hydration and keep fragment targets clear of the sticky header
SSR still ships the expanded menu, but phones collapse it after render, stop sticking the header below md, and use the header-offset token so case anchors no longer sit underneath the bar. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
47
e2e/fragments.e2e.ts
Normal file
47
e2e/fragments.e2e.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { APP_LOCALES } from '../src/app/core/i18n/locale';
|
||||
import { pagePath } from './helpers';
|
||||
|
||||
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');
|
||||
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
|
||||
for (const locale of APP_LOCALES) {
|
||||
const path = `${pagePath('projects', locale)}#myspa`;
|
||||
await page.goto(path, { waitUntil: 'networkidle' });
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -10,16 +10,48 @@ test.describe('keyboard and palette', () => {
|
||||
await expect(page.locator('#main-content')).toBeFocused();
|
||||
});
|
||||
|
||||
test('mobile nav toggle keeps aria-expanded in sync', async ({ page }) => {
|
||||
test('mobile nav toggle keeps aria-expanded in sync', async ({ page, request }) => {
|
||||
const ssr = await request.get('/');
|
||||
expect(ssr.ok()).toBe(true);
|
||||
const html = await ssr.text();
|
||||
expect(html).toContain('aria-expanded="true"');
|
||||
expect(html).toContain('id="primary-nav"');
|
||||
expect(html).toContain('class="site-nav"');
|
||||
expect(html).toMatch(/<div[^>]*class="site"/);
|
||||
expect(html).not.toMatch(/<div[^>]*class="[^"]*\bsite\b[^"]*\bnav-collapsed\b/);
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto('/');
|
||||
const toggle = page.locator('.nav-toggle');
|
||||
await expect(toggle).toBeVisible();
|
||||
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
||||
await toggle.click();
|
||||
await expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
||||
await toggle.click();
|
||||
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
||||
await toggle.click();
|
||||
await expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
||||
});
|
||||
|
||||
test('collapsed header stays under 25% of the viewport and is not sticky below md', async ({
|
||||
page,
|
||||
}) => {
|
||||
const viewport = { width: 390, height: 844 };
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto('/');
|
||||
|
||||
const header = page.locator('.site-header');
|
||||
const box = await header.boundingBox();
|
||||
expect(box).toBeTruthy();
|
||||
expect(box!.height).toBeLessThan(viewport.height * 0.25);
|
||||
|
||||
await expect
|
||||
.poll(async () => header.evaluate((element) => getComputedStyle(element).position))
|
||||
.not.toBe('sticky');
|
||||
|
||||
await page.locator('.nav-toggle').click();
|
||||
await expect(page.locator('.nav-toggle')).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(await header.evaluate((element) => getComputedStyle(element).position)).not.toBe(
|
||||
'sticky',
|
||||
);
|
||||
});
|
||||
|
||||
test('palette opens with Control+K, traps focus, locks scroll and restores on Escape', async ({
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import { SITE_CONTENT_DATA } from '../src/app/core/content/site-content';
|
||||
import { expectHead, jsonLdGraph, pagePath } from './helpers';
|
||||
|
||||
async function ensurePrimaryNavOpen(page: Page): Promise<void> {
|
||||
const toggle = page.locator('.nav-toggle');
|
||||
if ((await toggle.isVisible()) && (await toggle.getAttribute('aria-expanded')) === 'false') {
|
||||
await toggle.click();
|
||||
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('server-rendered metadata', () => {
|
||||
test('German and English Home include the full head contract', async ({ request, page }) => {
|
||||
await expectHead(request, 'home', 'de');
|
||||
@@ -40,6 +48,7 @@ test.describe('server-rendered metadata', () => {
|
||||
|
||||
test('client navigation does not duplicate head tags', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await ensurePrimaryNavOpen(page);
|
||||
await page.locator('a.contact-cta').first().waitFor();
|
||||
|
||||
const assertUnique = async (canonical: string, description: string) => {
|
||||
@@ -57,6 +66,7 @@ test.describe('server-rendered metadata', () => {
|
||||
);
|
||||
};
|
||||
|
||||
await ensurePrimaryNavOpen(page);
|
||||
await page
|
||||
.locator(`a[href="${pagePath('projects', 'de')}"]`)
|
||||
.first()
|
||||
@@ -67,6 +77,7 @@ test.describe('server-rendered metadata', () => {
|
||||
SITE_CONTENT_DATA.de.pages.projects.description,
|
||||
);
|
||||
|
||||
await ensurePrimaryNavOpen(page);
|
||||
await page
|
||||
.locator(`a[href="${pagePath('servicesAi', 'de')}"]`)
|
||||
.first()
|
||||
|
||||
Reference in New Issue
Block a user