Files
Portfolio/e2e/helpers.ts
Antonio Ledebuhr 34367181d2 integration: add Playwright, axe and Lighthouse CI gates
Lock SEO, accessibility and crawlability with headless browser checks so regressions fail before a merge instead of after publication.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 18:28:24 +02:00

93 lines
3.6 KiB
TypeScript

import { expect, type APIRequestContext, type Page } from '@playwright/test';
import { SITE_CONFIG } from '../src/app/core/content/site-config';
import { SITE_CONTENT_DATA } from '../src/app/core/content/site-content';
import { type AppLocale } from '../src/app/core/i18n/locale';
import { LOCALE_HTML_LANG } from '../src/app/core/i18n/locale';
import { type RouteId } from '../src/app/core/routing/route-ids';
import { routePath } from '../src/app/core/routing/route-paths';
import { buildRouteMetadata } from '../src/app/core/seo/route-metadata';
export const ORIGIN = SITE_CONFIG.siteOrigin;
export function pagePath(routeId: RouteId, locale: AppLocale): string {
return routePath(routeId, locale);
}
export async function readHtml(request: APIRequestContext, path: string): Promise<string> {
const response = await request.get(path);
expect(response.status(), `GET ${path} failed`).toBeLessThan(400);
return response.text();
}
export function attr(html: string, selector: string, attribute: string): string | null {
const pattern = new RegExp(
`<[^>]*${selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^>]*${attribute}="([^"]*)"`,
'i',
);
return html.match(pattern)?.[1] ?? null;
}
export function count(html: string, snippet: string): number {
return html.split(snippet).length - 1;
}
export async function expectHead(
request: APIRequestContext,
routeId: RouteId,
locale: AppLocale,
): Promise<void> {
const path =
routeId === 'notFound'
? locale === 'en'
? '/en/missing-route'
: '/missing-route'
: pagePath(routeId, locale);
const html = await readHtml(request, path);
const metadata = buildRouteMetadata(routeId, locale, SITE_CONTENT_DATA);
expect(html, `${path} title`).toContain(`<title>${metadata.title}</title>`);
expect(html).toContain(`name="description"`);
expect(html).toContain(`content="${metadata.description}"`);
expect(html).toContain(`name="robots"`);
expect(html).toContain(`content="${metadata.robots}"`);
expect(html).toContain(`<html lang="${LOCALE_HTML_LANG[locale]}"`);
expect(count(html, 'name="description"')).toBe(1);
expect(count(html, 'name="robots"')).toBe(1);
expect(count(html, 'type="application/ld+json"')).toBe(1);
if (routeId === 'notFound') {
expect(html).not.toMatch(/rel="canonical"/);
expect(html).not.toMatch(/rel="alternate"[^>]*hreflang/);
return;
}
expect(html).toContain(`rel="canonical"`);
expect(html).toContain(`href="${metadata.canonical}"`);
expect(count(html, 'rel="canonical"')).toBe(1);
expect(html).toContain('hreflang="de-DE"');
expect(html).toContain('hreflang="en"');
expect(html).toContain('hreflang="x-default"');
expect(html).toContain('property="og:type"');
expect(html).toContain('property="og:title"');
expect(html).toContain('property="og:description"');
expect(html).toContain('property="og:url"');
expect(html).toContain('property="og:site_name"');
expect(html).toContain('property="og:locale"');
expect(html).toContain('name="twitter:card"');
expect(html).toContain('name="twitter:title"');
expect(html).toContain('name="twitter:description"');
}
export async function expectNoOverflow(page: Page): Promise<void> {
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth <= window.innerWidth + 1,
);
expect(overflow, `horizontal overflow at ${page.url()}`).toBe(true);
}
export async function jsonLdGraph(page: Page): Promise<unknown> {
const raw = await page.locator('script[type="application/ld+json"]').first().textContent();
expect(raw).toBeTruthy();
return JSON.parse(raw ?? 'null');
}