Beitraege optional, Unterstriche nur im Text, eigenes Symbol
Eine Website ohne Beitraege redet nirgends von Beitraegen: Archiv und Feed antworten mit 404, der Feedverweis faellt aus dem Kopf, und der Hinweiskasten der Beitragsliste bleibt im Editor statt oeffentlich zu erscheinen. Das gilt auch je Kategorie, sobald deren letzter Beitrag weg ist. Dabei gefunden: die Liste fragte je Beitrag nach Bild und Begriffen - jetzt je eine Abfrage fuer die ganze Liste (22 -> 16 Abfragen auf der Startseite des Nachbaus). Unterstrichene Links gibt es nur noch im Fliesstext, dort mit einem Schalter im Designbereich; Karten, Menue, Kontaktzeilen und der Ansehen-Streifen haben keinen mehr. Die aktuelle Seite im Menue wird ueber das Schriftgewicht kenntlich - die Akzentfarbe fiel im dunklen Fussmenue durch die Kontrastpruefung. Neu: das Symbol der Website laesst sich festlegen. Aus einem Bild der Mediathek entstehen beim Speichern quadratische PNG-Dateien in 32 und 180 px; schlaegt das fehl, wird die Einstellung nicht gespeichert. Die Browserpruefung laeuft jetzt gegen jede Installation statt nur gegen die Demo: geprueft wird, was in der Sitemap steht, und das Ladebudget trennt Bilder vom Beiwerk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f0b634f0c1
commit
f2ef2a4901
28 files changed
+482
-64
No files matched your search
+59
-19
@@ -14,12 +14,23 @@ try {
|
||||
// itself. Without bin/dev-router.php they never reach the front controller - exactly the gap
|
||||
// that let them stay broken locally while every kernel test passed.
|
||||
const context = await browser.newContext();
|
||||
for (const [path, type] of [['/sitemap.xml', 'application/xml'], ['/robots.txt', 'text/plain'], ['/feed.xml', 'application/xml']]) {
|
||||
const sitemap = await context.request.get(origin + '/sitemap.xml');
|
||||
assert.equal(sitemap.status(), 200, '/sitemap.xml');
|
||||
assert.match(sitemap.headers()['content-type'] ?? '', /application\/xml/, '/sitemap.xml');
|
||||
// Eine Website ohne Beiträge kennt weder Archiv noch Feed: dann ist 404 die richtige Antwort,
|
||||
// und die Sitemap führt die Adresse gar nicht erst auf.
|
||||
const sitemapText = await sitemap.text();
|
||||
const hasPosts = sitemapText.includes('/beitraege');
|
||||
for (const [path, type] of [['/robots.txt', 'text/plain'], ['/feed.xml', 'application/xml']]) {
|
||||
const response = await context.request.get(origin + path);
|
||||
assert.equal(response.status(), 200, path);
|
||||
assert.match(response.headers()['content-type'] ?? '', new RegExp(type), path);
|
||||
const expected = '/feed.xml' === path && !hasPosts ? 404 : 200;
|
||||
assert.equal(response.status(), expected, path);
|
||||
// Auch die 404 muss aus der Anwendung kommen - der eingebaute PHP-Server beantwortet Dateien
|
||||
// mit Endung sonst selbst, und genau das soll dieser Test aufdecken.
|
||||
assert.ok('x-kant-queries' in response.headers(), `${path}: nicht von der Anwendung beantwortet`);
|
||||
if (200 === expected) assert.match(response.headers()['content-type'] ?? '', new RegExp(type), path);
|
||||
}
|
||||
results.push({ name: 'machine readable routes reachable', paths: 3 });
|
||||
results.push({ name: 'machine readable routes reachable', paths: 3, hasPosts });
|
||||
await context.close();
|
||||
|
||||
// A load budget, measured rather than assumed: what a page actually transfers, and how many
|
||||
@@ -34,7 +45,7 @@ try {
|
||||
const length = Number(response.headers()['content-length'] ?? 0);
|
||||
transferred.set(response.url(), { type, length });
|
||||
});
|
||||
for (const path of ['/', '/beitraege']) {
|
||||
for (const path of hasPosts ? ['/', '/beitraege'] : ['/']) {
|
||||
transferred.clear();
|
||||
const response = await page.goto(origin + path, { waitUntil: 'networkidle' });
|
||||
const queries = Number(response.headers()['x-kant-queries'] ?? -1);
|
||||
@@ -42,29 +53,49 @@ try {
|
||||
const styles = [...transferred.values()].filter(entry => entry.type === 'text/css').reduce((sum, entry) => sum + entry.length, 0);
|
||||
const scripts = [...transferred.values()].filter(entry => entry.type.includes('javascript')).length;
|
||||
const requests = transferred.size;
|
||||
// Bilder gehören zum Inhalt: eine bebilderte Startseite lädt zu Recht mehr Dateien als eine
|
||||
// Textseite. Gedeckelt wird deshalb das Beiwerk (Stile, Schriften, Skripte) und das
|
||||
// übertragene Gewicht - nicht die Zahl der Bilder.
|
||||
const beiwerk = [...transferred.values()].filter(entry => !entry.type.startsWith('image/')).length;
|
||||
const gewicht = [...transferred.values()].reduce((sum, entry) => sum + entry.length, 0);
|
||||
|
||||
assert.ok(html < 150000, `${path}: HTML ${html} Bytes, Obergrenze 150000`);
|
||||
assert.ok(styles < 120000, `${path}: CSS ${styles} Bytes, Obergrenze 120000`);
|
||||
// A public page runs no script unless it carries tabs or its own; the budget keeps that honest.
|
||||
assert.equal(scripts, 0, `${path}: ${scripts} Skript(e) geladen`);
|
||||
assert.ok(requests <= 12, `${path}: ${requests} Anfragen, Obergrenze 12`);
|
||||
// Eine öffentliche Seite lädt kein Skript, solange sie keines braucht - und das steht in
|
||||
// ihrem eigenen Kopf: nur wenn die Seite Skripte erlaubt (Reiter, Slider mit Autolauf,
|
||||
// eigener Code), darf überhaupt eines kommen, dann höchstens eines.
|
||||
const erlaubt = !/script-src 'none'/.test(response.headers()['content-security-policy'] ?? '');
|
||||
assert.ok(erlaubt ? scripts <= 1 : 0 === scripts, `${path}: ${scripts} Skript(e) geladen, erlaubt ${erlaubt ? 1 : 0}`);
|
||||
assert.ok(beiwerk <= 12, `${path}: ${beiwerk} Anfragen ohne Bilder, Obergrenze 12`);
|
||||
assert.ok(gewicht < 1500000, `${path}: ${gewicht} Bytes übertragen, Obergrenze 1500000`);
|
||||
if (queries >= 0) assert.ok(queries <= 20, `${path}: ${queries} Datenbankabfragen, Obergrenze 20`);
|
||||
results.push({ name: `load budget ${path}`, html, styles, requests, queries });
|
||||
console.log(`Budget ${path}: HTML ${html} B, CSS ${styles} B, ${requests} Anfragen, ${queries} Abfragen, ${scripts} Skripte`);
|
||||
results.push({ name: `load budget ${path}`, html, styles, requests, beiwerk, gewicht, queries });
|
||||
console.log(`Budget ${path}: HTML ${html} B, CSS ${styles} B, ${requests} Anfragen (${beiwerk} ohne Bilder), ${gewicht} B gesamt, ${queries} Abfragen, ${scripts} Skripte`);
|
||||
}
|
||||
await budget.close();
|
||||
}
|
||||
|
||||
// Die Seiten, die geprüft werden, stehen in der Sitemap - vorher waren es drei fest
|
||||
// geschriebene Adressen der Demo, und gegen eine andere Installation lief der Check ins Leere.
|
||||
const seiten = ['/', ...[...sitemapText.matchAll(/<loc>([^<]+)<\/loc>/g)]
|
||||
.map(treffer => new URL(treffer[1]).pathname)
|
||||
.filter(pfad => '/' !== pfad && !pfad.startsWith('/beitraege') && !pfad.startsWith('/kategorie/') && !pfad.startsWith('/schlagwort/'))
|
||||
.slice(0, 2)];
|
||||
|
||||
for (const width of [1440, 390]) {
|
||||
const context = await browser.newContext({ viewport: { width, height: 1000 } });
|
||||
const page = await context.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
for (const path of ['/', '/grundlagen', '/projektstand']) {
|
||||
for (const path of seiten) {
|
||||
const response = await page.goto(origin + path);
|
||||
assert.equal(response.status(), 200);
|
||||
assert.equal(await page.locator('h1').count(), 1);
|
||||
assert.equal(await page.locator('script').count(), 0);
|
||||
// Strukturierte Daten stehen zwar in einem script-Element, führen aber nichts aus. Gezählt
|
||||
// wird, was laufen kann - und das nur, wenn die Seite es in ihrer eigenen Richtlinie erlaubt.
|
||||
const laufend = await page.locator('script:not([type="application/ld+json"])').count();
|
||||
const darfSkripte = !/script-src 'none'/.test(response.headers()['content-security-policy'] ?? '');
|
||||
assert.ok(darfSkripte ? laufend <= 1 : 0 === laufend, `${path}: ${laufend} Skript(e), erlaubt ${darfSkripte ? 1 : 0}`);
|
||||
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), 'Horizontal overflow');
|
||||
const axe = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'best-practice']).analyze();
|
||||
|
||||
@@ -104,18 +135,27 @@ try {
|
||||
assert.equal(await page.locator(':focus').innerText(), 'Zum Inhalt springen');
|
||||
await page.keyboard.press('Enter');
|
||||
assert.equal(await page.locator(':focus').getAttribute('id'), 'main');
|
||||
await page.getByRole('link', { name: 'Die Grundlage entdecken' }).click();
|
||||
assert.equal(new URL(page.url()).pathname, '/grundlagen');
|
||||
await page.getByRole('link', { name: 'Projektstand' }).click();
|
||||
assert.equal(new URL(page.url()).pathname, '/projektstand');
|
||||
// Weiter mit der Tastatur: der erste Verweis im Inhalt muss anklickbar sein und tatsächlich
|
||||
// woandershin führen. Vorher standen hier die Linktexte der Demo - gegen jede andere
|
||||
// Installation lief die Prüfung damit ins Leere statt etwas zu prüfen.
|
||||
const ersterLink = page.locator('main a[href^="/"]').first();
|
||||
const ziel = await ersterLink.getAttribute('href');
|
||||
await ersterLink.click();
|
||||
assert.equal(new URL(page.url()).pathname, ziel.split('#')[0] || '/');
|
||||
assert.equal(await page.locator('h1').count(), 1, ziel + ': keine Überschrift');
|
||||
assert.deepEqual(errors, []);
|
||||
await context.close();
|
||||
}
|
||||
// Ohne JavaScript muss dieselbe Website benutzbar sein - eine öffentliche Seite lädt ohnehin
|
||||
// keines, aber das soll gemessen und nicht geglaubt werden.
|
||||
const noJs = await browser.newPage({ javaScriptEnabled: false });
|
||||
await noJs.goto(origin);
|
||||
assert.match(await noJs.locator('h1').innerText(), /Deine Inhalte/);
|
||||
await noJs.getByRole('link', { name: 'Die Grundlage entdecken' }).click();
|
||||
assert.equal(new URL(noJs.url()).pathname, '/grundlagen');
|
||||
assert.equal(await noJs.locator('h1').count(), 1);
|
||||
const ohneJs = noJs.locator('main a[href^="/"]').first();
|
||||
const zielOhneJs = await ohneJs.getAttribute('href');
|
||||
await ohneJs.click();
|
||||
assert.equal(new URL(noJs.url()).pathname, zielOhneJs.split('#')[0] || '/');
|
||||
assert.equal(await noJs.locator('h1').count(), 1);
|
||||
await writeFile('reports/browser.json', JSON.stringify({ origin, results, editorialFindings: findings, keyboard: 'passed', noJavaScript: 'passed' }, null, 2));
|
||||
console.log('Passed: 6 page/viewport accessibility checks, keyboard navigation, no-JS navigation.');
|
||||
if (findings.length > 0) {
|
||||
|
||||
Reference in new issue
Block a user