Rewrite the constraint specs and docs for the service-led information architecture.
Assertions now follow the eighteen prerenderable paths, the gist dialog, and the six-case evidence set. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,8 +9,8 @@ const AXE_PATHS = [
|
||||
pagePath('pitch', 'en'),
|
||||
pagePath('projects', 'de'),
|
||||
pagePath('projects', 'en'),
|
||||
pagePath('servicesAi', 'de'),
|
||||
pagePath('servicesAi', 'en'),
|
||||
pagePath('services', 'de'),
|
||||
pagePath('services', 'en'),
|
||||
pagePath('contact', 'de'),
|
||||
pagePath('contact', 'en'),
|
||||
pagePath('imprint', 'de'),
|
||||
|
||||
@@ -40,12 +40,12 @@ test.describe('fragment scrolling', () => {
|
||||
await page.setViewportSize({ width, height: VIEWPORT_HEIGHT });
|
||||
|
||||
for (const locale of APP_LOCALES) {
|
||||
const path = `${pagePath('projects', locale)}#myspa`;
|
||||
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('#myspa h2');
|
||||
const heading = page.locator('#bannier h2');
|
||||
await expect(heading).toBeVisible();
|
||||
|
||||
await waitForScrollSettle(page);
|
||||
|
||||
@@ -64,53 +64,44 @@ test.describe('keyboard and terminal dock', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('compact services submenu stays keyboard accessible at 1024px', async ({
|
||||
test('compact primary nav stays a single row without a services submenu', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.skip(testInfo.project.name === 'mobile', 'compact submenu applies from md up');
|
||||
test.skip(testInfo.project.name === 'mobile', 'compact nav applies from md up');
|
||||
|
||||
await page.setViewportSize({ width: 1024, height: 900 });
|
||||
await page.goto('/');
|
||||
|
||||
const servicesParent = page.locator(
|
||||
`.primary-nav > li > a[href="${pagePath('services', 'de')}"]`,
|
||||
);
|
||||
const firstChild = page.locator(
|
||||
`.primary-nav > li > .submenu a[href="${pagePath('servicesSoftware', 'de')}"]`,
|
||||
);
|
||||
const services = page.locator(`.primary-nav > li > a[href="${pagePath('services', 'de')}"]`);
|
||||
await expect(services).toBeVisible();
|
||||
await expect(page.locator('.primary-nav .submenu')).toHaveCount(0);
|
||||
await expect(page.locator('.primary-nav a')).toHaveCount(5);
|
||||
|
||||
await servicesParent.focus();
|
||||
await expect(firstChild).toBeVisible();
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(firstChild).toBeFocused();
|
||||
await services.click();
|
||||
await page.waitForURL('**/leistungen');
|
||||
await expect(services).toHaveAttribute('aria-current', 'page');
|
||||
|
||||
const headerBox = await page.locator('.site-header').boundingBox();
|
||||
expect(headerBox, 'header while submenu is open').toBeTruthy();
|
||||
expect(headerBox!.height, 'header height while submenu is open').toBeLessThanOrEqual(200);
|
||||
|
||||
const background = await firstChild.evaluate((element) => {
|
||||
const submenu = element.closest('.submenu');
|
||||
return submenu ? getComputedStyle(submenu).backgroundColor : '';
|
||||
});
|
||||
expect(background, 'submenu background must be fully opaque').toMatch(
|
||||
/^rgb\(\d+,\s*\d+,\s*\d+\)$/,
|
||||
const overflow = await page.evaluate(
|
||||
() => document.documentElement.scrollWidth <= window.innerWidth + 1,
|
||||
);
|
||||
expect(overflow, 'horizontal overflow at 1024px').toBe(true);
|
||||
});
|
||||
|
||||
test('terminal dock opens with Control+K without locking the page and restores on Escape', async ({
|
||||
test('terminal dock is open on load, does not lock the page, and restores on Escape', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/');
|
||||
const trigger = page.locator('.terminal-dock-trigger');
|
||||
await trigger.focus();
|
||||
await page.keyboard.press('Control+k');
|
||||
|
||||
const panel = page.locator('.terminal-dock-panel');
|
||||
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(trigger).toBeHidden();
|
||||
await expect(page.locator('#terminal-dock-input')).not.toBeFocused();
|
||||
await expect(page.locator('.site')).not.toHaveAttribute('inert');
|
||||
expect(await page.evaluate(() => document.body.style.overflow)).not.toBe('hidden');
|
||||
await expect(page.locator('[role="dialog"]')).toHaveCount(0);
|
||||
await expect(page.locator('.terminal-dock [role="dialog"]')).toHaveCount(0);
|
||||
|
||||
await page.locator('#terminal-dock-input').focus();
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(panel).toHaveCount(0);
|
||||
await expect(trigger).toBeFocused();
|
||||
|
||||
@@ -9,13 +9,13 @@ const CHECKED = [
|
||||
pagePath('pitch', 'en'),
|
||||
pagePath('projects', 'de'),
|
||||
pagePath('projects', 'en'),
|
||||
pagePath('servicesAi', 'de'),
|
||||
pagePath('services', 'de'),
|
||||
pagePath('contact', 'de'),
|
||||
pagePath('imprint', 'en'),
|
||||
'/missing-route',
|
||||
];
|
||||
|
||||
const VIEWPORTS = [320, 768, 1024, 1440] as const;
|
||||
const VIEWPORTS = [320, 390, 768, 1024, 1440] as const;
|
||||
|
||||
test.describe('layout and crawlability', () => {
|
||||
test('does not overflow horizontally on checked routes', async ({ page }) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import { SITE_CONTENT_DATA } from '../src/app/core/content/site-content';
|
||||
import { prerenderablePaths } from '../src/app/core/routing/route-paths';
|
||||
import { expectHead, jsonLdGraph, pagePath } from './helpers';
|
||||
|
||||
async function ensurePrimaryNavOpen(page: Page): Promise<void> {
|
||||
@@ -28,10 +29,10 @@ test.describe('server-rendered metadata', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('projects, AI service, contact, legal and 404 keep locale metadata', async ({ request }) => {
|
||||
test('projects, services, contact, legal and 404 keep locale metadata', async ({ request }) => {
|
||||
for (const locale of ['de', 'en'] as const) {
|
||||
await expectHead(request, 'projects', locale);
|
||||
await expectHead(request, 'servicesAi', locale);
|
||||
await expectHead(request, 'services', locale);
|
||||
await expectHead(request, 'contact', locale);
|
||||
await expectHead(request, 'imprint', locale);
|
||||
await expectHead(request, 'notFound', locale);
|
||||
@@ -57,6 +58,15 @@ test.describe('server-rendered metadata', () => {
|
||||
await expect(nav.locator(`a[href="${pagePath('home', 'de')}"]`)).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('every prerenderable path returns 200', async ({ request }) => {
|
||||
const paths = prerenderablePaths();
|
||||
expect(paths).toHaveLength(18);
|
||||
for (const path of paths) {
|
||||
const response = await request.get(path);
|
||||
expect(response.status(), path).toBe(200);
|
||||
}
|
||||
});
|
||||
|
||||
test('unmatched URLs return 404 and a real route stays 200', async ({ request }) => {
|
||||
const home = await request.get('/');
|
||||
expect(home.status(), 'GET /').toBe(200);
|
||||
@@ -68,7 +78,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();
|
||||
await page.locator('.language-switch').first().waitFor();
|
||||
|
||||
const assertUnique = async (canonical: string, description: string) => {
|
||||
await expect(page.locator('link[rel="canonical"]')).toHaveCount(1);
|
||||
@@ -97,23 +107,34 @@ test.describe('server-rendered metadata', () => {
|
||||
);
|
||||
|
||||
await ensurePrimaryNavOpen(page);
|
||||
const servicesParent = page.locator(
|
||||
`.primary-nav > li > a[href="${pagePath('services', 'de')}"]`,
|
||||
);
|
||||
const servicesAiLink = page
|
||||
.locator(`.primary-nav a[href="${pagePath('servicesAi', 'de')}"]`)
|
||||
.first();
|
||||
if (!(await servicesAiLink.isVisible())) {
|
||||
await servicesParent.hover();
|
||||
await expect(servicesAiLink).toBeVisible();
|
||||
}
|
||||
await servicesAiLink.click();
|
||||
await page.waitForURL('**/leistungen/ai-integration');
|
||||
await page.locator(`.primary-nav a[href="${pagePath('services', 'de')}"]`).click();
|
||||
await page.waitForURL('**/leistungen');
|
||||
await expect(page.locator('link[rel="canonical"]')).toHaveCount(1);
|
||||
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute(
|
||||
'href',
|
||||
'https://antoniolede.de/leistungen/ai-integration',
|
||||
'https://antoniolede.de/leistungen',
|
||||
);
|
||||
await expect(page.locator('script[type="application/ld+json"]')).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('legacy service URLs return HTTP 308 with the fragment Location', async ({ request }) => {
|
||||
const redirects = [
|
||||
['/leistungen/software', '/leistungen#software-data'],
|
||||
['/leistungen/software/', '/leistungen#software-data'],
|
||||
['/leistungen/hardware-netzwerk', '/leistungen#infrastructure-network'],
|
||||
['/leistungen/cluster', '/leistungen#platform-operations'],
|
||||
['/leistungen/ai-integration', '/leistungen#ai-workflows'],
|
||||
['/en/services/software', '/en/services#software-data'],
|
||||
['/en/services/hardware-network', '/en/services#infrastructure-network'],
|
||||
['/en/services/clusters', '/en/services#platform-operations'],
|
||||
['/en/services/ai-integration', '/en/services#ai-workflows'],
|
||||
['/en/services/ai-integration/', '/en/services#ai-workflows'],
|
||||
] as const;
|
||||
|
||||
for (const [from, to] of redirects) {
|
||||
const response = await request.get(from, { maxRedirects: 0 });
|
||||
expect(response.status(), from).toBe(308);
|
||||
expect(response.headers().location, from).toBe(to);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { SIGNATURE_COPY } from '../src/app/core/content/signature-copy';
|
||||
import { pagePath } from './helpers';
|
||||
|
||||
test.describe('Systems Map', () => {
|
||||
test('keeps the card list visible and shows the SVG from 1024px', async ({ page }) => {
|
||||
await page.goto(pagePath('services', 'de'));
|
||||
|
||||
for (const width of [390, 768, 1024, 1440]) {
|
||||
for (const width of [320, 390, 768, 1024, 1440]) {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
const list = page.locator('.systems-map-list');
|
||||
await expect(list).toBeVisible();
|
||||
@@ -59,8 +60,41 @@ test.describe('Systems Map', () => {
|
||||
|
||||
expect(hrefs.length).toBeGreaterThan(0);
|
||||
for (const href of hrefs) {
|
||||
const response = await request.get(href);
|
||||
const path = href.split('#')[0];
|
||||
const response = await request.get(path);
|
||||
expect(response.status(), href).toBe(200);
|
||||
}
|
||||
});
|
||||
|
||||
test('plain activation opens the gist dialog without navigating', async ({ page }) => {
|
||||
const copy = SIGNATURE_COPY.de.systemsMap;
|
||||
await page.goto(pagePath('services', 'de'));
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
|
||||
const first = page.locator('.systems-map-cards a').first();
|
||||
const href = await first.getAttribute('href');
|
||||
expect(href).toContain('#infrastructure-network');
|
||||
|
||||
await first.click();
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(page).toHaveURL(new RegExp(`${pagePath('services', 'de')}$`));
|
||||
await expect(dialog.getByRole('link')).toHaveAttribute('href', href ?? '');
|
||||
await expect(dialog.getByRole('button', { name: copy.closeLabel })).toBeVisible();
|
||||
|
||||
const dialogBox = await dialog.boundingBox();
|
||||
expect(dialogBox, 'dialog readable at 390px').toBeTruthy();
|
||||
expect(dialogBox!.width).toBeGreaterThan(200);
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(dialog).toHaveCount(0);
|
||||
await expect(first).toBeFocused();
|
||||
});
|
||||
|
||||
test('a modified click follows the node href', async ({ page }) => {
|
||||
await page.goto(pagePath('services', 'de'));
|
||||
const first = page.locator('.systems-map-cards a').first();
|
||||
await first.click({ modifiers: ['Control'] });
|
||||
await expect(page.getByRole('dialog')).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { SIGNATURE_COPY } from '../src/app/core/content/signature-copy';
|
||||
import { expectNoOverflow } from './helpers';
|
||||
|
||||
const VIEWPORTS = [320, 390, 768, 1024, 1440] as const;
|
||||
|
||||
test.describe('terminal dock', () => {
|
||||
test('walks collapsed, expanded and maximized and runs the grammar', async ({
|
||||
test('defaults open, keeps one session scrollport and runs the grammar', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
const copy = SIGNATURE_COPY.de.terminal;
|
||||
@@ -11,14 +14,17 @@ test.describe('terminal dock', () => {
|
||||
const trigger = page.locator('.terminal-dock-trigger');
|
||||
const panel = page.locator('.terminal-dock-panel');
|
||||
const input = page.locator('#terminal-dock-input');
|
||||
const log = page.locator('.terminal-dock-log');
|
||||
const session = page.locator('.terminal-dock-session');
|
||||
const maximize = page.locator('.terminal-dock-control[aria-pressed]');
|
||||
const collapse = page.getByRole('button', { name: copy.collapseLabel });
|
||||
|
||||
await expect(trigger).toBeVisible();
|
||||
await expect(panel).toHaveCount(0);
|
||||
|
||||
await trigger.click();
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(trigger).toBeHidden();
|
||||
await expect(input).not.toBeFocused();
|
||||
await expect(session).toBeVisible();
|
||||
await expect(session.locator('form')).toHaveCount(1);
|
||||
await expect(collapse).toHaveAttribute('title', copy.collapseTooltip);
|
||||
await expect(maximize).toHaveAttribute('aria-label', copy.maximizeLabel);
|
||||
await expect(page.locator('.terminal-dock-prompt')).toContainText(copy.prompt);
|
||||
|
||||
if (testInfo.project.name === 'mobile') {
|
||||
@@ -28,6 +34,7 @@ test.describe('terminal dock', () => {
|
||||
} else {
|
||||
await maximize.click();
|
||||
await expect(maximize).toHaveAttribute('aria-pressed', 'true');
|
||||
await expect(maximize).toHaveAttribute('aria-label', copy.restoreLabel);
|
||||
await maximize.click();
|
||||
await expect(maximize).toHaveAttribute('aria-pressed', 'false');
|
||||
}
|
||||
@@ -36,90 +43,88 @@ test.describe('terminal dock', () => {
|
||||
await input.press('Enter');
|
||||
await page.waitForURL('**/pitch');
|
||||
await expect(page).toHaveURL(/\/pitch$/);
|
||||
await expect(log).toContainText(`${copy.prompt} navigate pitch`);
|
||||
await expect(session).toContainText(`${copy.prompt} navigate pitch`);
|
||||
|
||||
await input.press('ArrowUp');
|
||||
await expect(input).toHaveValue('navigate pitch');
|
||||
|
||||
await input.fill('history');
|
||||
await input.press('Enter');
|
||||
await expect(log).toContainText(copy.historyIntro);
|
||||
await expect(log).toContainText('navigate pitch');
|
||||
await expect(session).toContainText(copy.historyIntro);
|
||||
await expect(session).toContainText('navigate pitch');
|
||||
|
||||
await input.fill('clear');
|
||||
await input.press('Enter');
|
||||
await expect(log).toHaveText(copy.clearedMessage);
|
||||
await expect(session.locator('p')).toHaveText(copy.clearedMessage);
|
||||
|
||||
await input.fill('navigate p');
|
||||
await input.press('Tab');
|
||||
await expect(log).toContainText('pitch');
|
||||
await expect(log).toContainText('projects');
|
||||
await expect(session).toContainText('pitch');
|
||||
await expect(session).toContainText('projects');
|
||||
|
||||
await input.fill('navigate nowhere');
|
||||
await input.press('Enter');
|
||||
await expect(log).toContainText(copy.validTargetsLabel);
|
||||
await expect(log).toContainText('pitch');
|
||||
await expect(session).toContainText(copy.validTargetsLabel);
|
||||
await expect(session).toContainText('pitch');
|
||||
|
||||
await input.fill('nav');
|
||||
await input.press('Tab');
|
||||
await expect(input).toHaveValue('navigate');
|
||||
|
||||
await input.fill('navigate services sof');
|
||||
await input.press('Tab');
|
||||
await expect(input).toHaveValue('navigate services software');
|
||||
});
|
||||
|
||||
test('clicking the visible desktop trigger while open collapses the panel', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
testInfo.project.name === 'mobile',
|
||||
'Below md the trigger is hidden while the panel is open.',
|
||||
);
|
||||
|
||||
test('collapse control hides the panel and returns focus to the trigger', async ({ page }) => {
|
||||
const copy = SIGNATURE_COPY.de.terminal;
|
||||
await page.goto('/');
|
||||
const trigger = page.locator('.terminal-dock-trigger');
|
||||
const panel = page.locator('.terminal-dock-panel');
|
||||
|
||||
await trigger.click();
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(trigger).toBeVisible();
|
||||
await expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
||||
await expect(trigger).toBeHidden();
|
||||
|
||||
await trigger.click();
|
||||
const collapse = page.getByRole('button', { name: copy.collapseLabel });
|
||||
await collapse.click();
|
||||
await expect(panel).toHaveCount(0);
|
||||
await expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
||||
await expect(trigger).toBeVisible();
|
||||
await expect(trigger).toBeFocused();
|
||||
await expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
||||
});
|
||||
|
||||
test('keeps the newest echoed line in the log scrollport after overflow', async ({ page }) => {
|
||||
test('keeps the prompt in the session scrollport after overflow', async ({ page }) => {
|
||||
const copy = SIGNATURE_COPY.de.terminal;
|
||||
await page.goto('/');
|
||||
|
||||
const trigger = page.locator('.terminal-dock-trigger');
|
||||
const input = page.locator('#terminal-dock-input');
|
||||
const log = page.locator('.terminal-dock-log');
|
||||
const session = page.locator('.terminal-dock-session');
|
||||
|
||||
await trigger.click();
|
||||
await expect(log).toBeVisible();
|
||||
await expect(session).toBeVisible();
|
||||
|
||||
for (let index = 0; index < 8; index += 1) {
|
||||
await input.fill('help');
|
||||
await input.press('Enter');
|
||||
}
|
||||
|
||||
await expect(log.locator('p.echo').last()).toHaveText(`${copy.prompt} help`);
|
||||
await expect(session.locator('p.echo').last()).toHaveText(`${copy.prompt} help`);
|
||||
|
||||
await expect
|
||||
.poll(async () =>
|
||||
log.evaluate((element) => {
|
||||
session.evaluate((element) => {
|
||||
return element.scrollTop + element.clientHeight >= element.scrollHeight - 2;
|
||||
}),
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
const last = log.locator('p').last();
|
||||
const logBox = await log.boundingBox();
|
||||
const lastBox = await last.boundingBox();
|
||||
expect(logBox && lastBox).toBeTruthy();
|
||||
expect(lastBox!.y).toBeGreaterThanOrEqual(logBox!.y - 1);
|
||||
expect(lastBox!.y + lastBox!.height).toBeLessThanOrEqual(logBox!.y + logBox!.height + 1);
|
||||
const form = session.locator('form');
|
||||
const sessionBox = await session.boundingBox();
|
||||
const formBox = await form.boundingBox();
|
||||
expect(sessionBox && formBox).toBeTruthy();
|
||||
expect(formBox!.y).toBeGreaterThanOrEqual(sessionBox!.y - 1);
|
||||
expect(formBox!.y + formBox!.height).toBeLessThanOrEqual(
|
||||
sessionBox!.y + sessionBox!.height + 1,
|
||||
);
|
||||
});
|
||||
|
||||
test('Escape from a panel control collapses the dock and returns focus to the trigger', async ({
|
||||
@@ -130,8 +135,6 @@ test.describe('terminal dock', () => {
|
||||
|
||||
const trigger = page.locator('.terminal-dock-trigger');
|
||||
const panel = page.locator('.terminal-dock-panel');
|
||||
|
||||
await trigger.click();
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const collapse = page.getByRole('button', { name: copy.collapseLabel });
|
||||
@@ -142,4 +145,13 @@ test.describe('terminal dock', () => {
|
||||
await expect(panel).toHaveCount(0);
|
||||
await expect(trigger).toBeFocused();
|
||||
});
|
||||
|
||||
test('stays usable and overflow-free at the required viewports', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
for (const width of VIEWPORTS) {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await expect(page.locator('.terminal-dock-panel')).toBeVisible();
|
||||
await expectNoOverflow(page, '/', width);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user