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 { 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'); await expectHead(request, 'home', 'en'); await page.goto('/'); const graph = (await jsonLdGraph(page)) as { '@graph': Array<{ '@type': string }> }; const types = graph['@graph'].map((node) => node['@type']); expect(types).toContain('Person'); expect(types).toContain('ProfessionalService'); await page.goto('/en'); const english = (await jsonLdGraph(page)) as { '@graph': Array<{ '@type': string }> }; expect(english['@graph'].map((node) => node['@type'])).toEqual( expect.arrayContaining(['Person', 'ProfessionalService']), ); }); 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, 'services', locale); await expectHead(request, 'contact', locale); await expectHead(request, 'imprint', locale); await expectHead(request, 'notFound', locale); } }); test('pitch has full head metadata and stays out of the primary nav', async ({ request, page, }) => { await expectHead(request, 'pitch', 'de'); await expectHead(request, 'pitch', 'en'); const sitemap = await request.get('/sitemap.xml'); const xml = await sitemap.text(); expect(xml).toContain('https://antoniolede.de/pitch'); expect(xml).toContain('https://antoniolede.de/en/pitch'); await page.goto('/'); await ensurePrimaryNavOpen(page); const nav = page.locator('.primary-nav'); await expect(nav.locator(`a[href="${pagePath('pitch', 'de')}"]`)).toHaveCount(0); 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); await expectHead(request, 'notFound', 'de'); await expectHead(request, 'notFound', 'en'); }); test('client navigation does not duplicate head tags', async ({ page }) => { await page.goto('/'); await ensurePrimaryNavOpen(page); await page.locator('.language-switch').first().waitFor(); const assertUnique = async (canonical: string, description: string) => { await expect(page.locator('link[rel="canonical"]')).toHaveCount(1); await expect(page.locator('link[rel="alternate"][hreflang="de-DE"]')).toHaveCount(1); await expect(page.locator('link[rel="alternate"][hreflang="en"]')).toHaveCount(1); await expect(page.locator('link[rel="alternate"][hreflang="x-default"]')).toHaveCount(1); await expect(page.locator('meta[name="description"]')).toHaveCount(1); await expect(page.locator('meta[name="robots"]')).toHaveCount(1); await expect(page.locator('script[type="application/ld+json"]')).toHaveCount(1); await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', canonical); await expect(page.locator('meta[name="description"]')).toHaveAttribute( 'content', description, ); }; await ensurePrimaryNavOpen(page); await page .locator(`a[href="${pagePath('projects', 'de')}"]`) .first() .click(); await page.waitForURL('**/projekte'); await assertUnique( 'https://antoniolede.de/projekte', SITE_CONTENT_DATA.de.pages.projects.description, ); await ensurePrimaryNavOpen(page); 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', ); 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); } }); });