import { chromium } from 'playwright'; import AxeBuilder from '@axe-core/playwright'; import assert from 'node:assert/strict'; import { execFileSync, spawn } from 'node:child_process'; import { mkdir, writeFile } from 'node:fs/promises'; import { randomBytes } from 'node:crypto'; import net from 'node:net'; const php = process.env.PHP_BINARY || 'php'; const fixture = JSON.parse(execFileSync(php, ['tests/pilot-fixture.php'], { encoding: 'utf8', windowsHide: true })); const env = { ...process.env, APP_ENV: 'prod', APP_DEBUG: '0', APP_SECRET: randomBytes(32).toString('hex'), DATABASE_URL: fixture.databaseUrl, MEDIA_DIRECTORY: fixture.mediaDirectory }; execFileSync(php, ['bin/console', 'cache:clear', '--env=prod', '--no-debug'], { stdio: 'ignore', windowsHide: true, env }); const probe = net.createServer(); await new Promise(resolve => probe.listen(0, '127.0.0.1', resolve)); const port = probe.address().port; await new Promise(resolve => probe.close(resolve)); const origin = `http://127.0.0.1:${port}`; const server = spawn(php, ['-d', 'memory_limit=256M', '-S', `127.0.0.1:${port}`, '-t', 'public', 'bin/dev-router.php'], { windowsHide: true, stdio: 'ignore', env: { ...env, SITE_ORIGIN: origin } }); const report = 'reports/not-found'; await mkdir(report, { recursive: true }); let browser, page; const checks = [], errors = []; try { for (let attempt = 0; attempt < 60; attempt++) { try { if ((await fetch(origin)).ok) break; } catch {} if (attempt === 59) throw new Error('Pilot test server did not start'); await new Promise(resolve => setTimeout(resolve, 200)); } browser = await chromium.launch(); const context = await browser.newContext({ viewport: { width: 1600, height: 1050 } }); page = await context.newPage(); page.setDefaultTimeout(15000); page.on('pageerror', error => errors.push(error.message)); await page.goto(origin + '/admin/login'); await page.getByLabel('Benutzername').fill(fixture.username); await page.getByLabel('Passwort', { exact: true }).fill(fixture.password); await page.getByRole('button', { name: 'Anmelden', exact: true }).click(); await page.waitForURL(origin + '/admin'); const visitor = await context.newPage(); visitor.on('pageerror', error => errors.push(error.message)); await visitor.goto(origin); const shell = await visitor.evaluate(() => ({ header: document.querySelector('.site-header').textContent.replace(/\s+/g, ' ').trim(), footer: document.querySelector('.site-footer').textContent.replace(/\s+/g, ' ').trim(), font: getComputedStyle(document.body).fontFamily, })); const checkError = async (target, heading = 'Seite nicht gefunden', targetPage = visitor) => { const response = await targetPage.goto(origin + target); assert.equal(response.status(), 404); assert.ok(response.headers()['x-robots-tag'].includes('noindex')); assert.ok(response.headers()['content-security-policy'].includes("'sha256-")); assert.equal(await targetPage.locator('h1').innerText(), heading); assert.equal((await targetPage.locator('.site-header').textContent()).replace(/\s+/g, ' ').trim(), shell.header); assert.equal((await targetPage.locator('.site-footer').textContent()).replace(/\s+/g, ' ').trim(), shell.footer); assert.equal(await targetPage.evaluate(() => getComputedStyle(document.body).fontFamily), shell.font); assert.equal(await targetPage.locator('link[rel="canonical"]').count(), 0); assert.equal(await targetPage.locator('meta[name="robots"]').getAttribute('content'), 'noindex, follow'); }; const audit = async targetPage => { const result = await new AxeBuilder({ page: targetPage }).analyze(); assert.deepEqual(result.violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => n.target) })), []); assert.equal(await targetPage.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true); }; await checkError('/eine-unbekannte-seite'); await audit(visitor); await visitor.screenshot({ path: `${report}/default-desktop.png`, fullPage: true }); await checkError('/Alte/Seite.html'); await checkError('/eins/zwei/drei/vier/fuenf'); await visitor.setViewportSize({ width: 390, height: 900 }); await checkError('/__404-vorschau__'); await audit(visitor); await visitor.screenshot({ path: `${report}/default-mobile.png`, fullPage: true }); checks.push('default 404, unusual/deep URLs, correct status/noindex, standard header/footer/fonts, desktop/mobile axe and layout'); await page.goto(origin + '/admin/settings#not-found'); await page.getByRole('button', { name: '404-Vorlage anlegen', exact: true }).click(); await page.waitForURL(/\/admin\/pages\/[a-f0-9]{32}$/); const id = new URL(page.url()).pathname.split('/').pop(); const selectHeading = async () => { await page.locator('#structure [data-block-id="error-heading"]').click(); await page.locator('[data-panel="content"]').click(); }; await selectHeading(); await page.getByLabel('Text', { exact: true }).fill('Hier bist du falsch abgebogen'); await page.locator('#save').click(); await page.locator('#save-status').filter({ hasText: 'Entwurf gespeichert' }).waitFor(); await checkError('/ein-fehlender-pfad'); await page.locator('#publish').click(); await page.locator('#confirm-publish').click(); await page.locator('#save-status').filter({ hasText: 'Veröffentlicht' }).waitFor(); await checkError('/ein-fehlender-pfad', 'Hier bist du falsch abgebogen'); await checkError('/404', 'Hier bist du falsch abgebogen'); await audit(visitor); await visitor.screenshot({ path: `${report}/custom-mobile.png`, fullPage: true }); await visitor.setViewportSize({ width: 1600, height: 1050 }); await checkError('/ein-fehlender-pfad', 'Hier bist du falsch abgebogen'); await audit(visitor); await visitor.screenshot({ path: `${report}/custom-desktop.png`, fullPage: true }); const sitemap = await (await context.request.get(origin + '/sitemap.xml')).text(); assert.ok(!sitemap.includes(origin + '/404')); checks.push('create an assigned draft, edit in normal editor, keep draft private, publish immediately, assigned own URL remains 404 and out of sitemap'); await page.goto(origin + '/admin/settings#not-found'); await page.getByRole('link', { name: '404-Seite bearbeiten', exact: true }).waitFor(); const clear = () => page.getByRole('button', { name: '404-Seite suchen: Auswahl entfernen', exact: true }); const saveAssignment = async () => { await page.getByRole('button', { name: '404-Zuordnung speichern', exact: true }).click(); await page.waitForURL(origin + '/admin/settings#not-found'); }; await clear().click(); await saveAssignment(); await page.getByText('Aktiv: Standard-404 im Website-Design.', { exact: true }).waitFor(); await checkError('/ein-fehlender-pfad'); const assignExisting = async () => { await page.getByLabel('404-Seite suchen', { exact: true }).fill('Seite nicht gefunden'); await page.locator('#not-found-picker-results [role="option"]').first().waitFor(); await page.getByLabel('404-Seite suchen', { exact: true }).press('ArrowDown'); await page.getByLabel('404-Seite suchen', { exact: true }).press('Enter'); assert.equal(await page.locator('#not-found-form .picker-value').inputValue(), id); await saveAssignment(); await page.getByRole('link', { name: '404-Seite bearbeiten', exact: true }).waitFor(); }; await assignExisting(); await page.reload(); assert.equal(await page.locator('#not-found-form .picker-value').inputValue(), id); await checkError('/ein-fehlender-pfad', 'Hier bist du falsch abgebogen'); const stale = await context.newPage(); await stale.goto(origin + '/admin/settings#not-found'); await clear().click(); await saveAssignment(); await stale.getByRole('button', { name: '404-Zuordnung speichern', exact: true }).click(); await stale.getByText('Die 404-Zuordnung wurde inzwischen geändert. Lade die Einstellungen neu.', { exact: true }).waitFor(); await checkError('/ein-fehlender-pfad'); await stale.close(); await assignExisting(); for (const width of [1600, 390]) { await page.setViewportSize({ width, height: 1100 }); await page.locator('#not-found').scrollIntoViewIfNeeded(); await audit(page); await page.locator('#not-found').screenshot({ path: `${report}/settings-${width}.png` }); } checks.push('live search without submitting, keyboard assignment, reload persistence, reset to default and protection against stale settings forms'); const withoutScripts = await browser.newContext({ javaScriptEnabled: false, viewport: { width: 390, height: 900 } }); const plain = await withoutScripts.newPage(); await checkError('/Alte/Seite.html', 'Hier bist du falsch abgebogen', plain); await plain.getByRole('link', { name: 'Zur Startseite', exact: true }).click(); await plain.waitForURL(origin + '/'); await withoutScripts.close(); const normal = await visitor.goto(origin); assert.equal(normal.status(), 200); assert.equal(await visitor.locator('link[rel="canonical"]').getAttribute('href'), origin + '/'); await audit(visitor); checks.push('public 404 and working home link without JavaScript; normal home remains 200 with canonical and passes axe'); assert.deepEqual(errors, []); await writeFile(`${report}/result.json`, JSON.stringify({ checks, passed: true }, null, 2)); console.log('Custom 404 checks passed:', checks.length, 'groups.'); } catch (error) { if (page) await page.screenshot({ path: `${report}/failure.png`, fullPage: true }).catch(() => {}); throw error; } finally { await browser?.close(); server.kill(); }