138 lines
9.4 KiB
JavaScript
138 lines
9.4 KiB
JavaScript
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(); }
|