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>
This commit is contained in:
92
e2e/helpers.ts
Normal file
92
e2e/helpers.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
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');
|
||||
}
|
||||
Reference in New Issue
Block a user