Files
KantSites/scripts/not-found-check.mjs
T
kantarci 16da844934
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s
feat: assign custom 404 pages with the website theme (1.8.0)
2026-09-14 12:18:33 +02:00

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(); }