diff --git a/web/a11y-audit/keyboard-flow.js b/web/a11y-audit/keyboard-flow.js new file mode 100644 index 00000000..5ab4502e --- /dev/null +++ b/web/a11y-audit/keyboard-flow.js @@ -0,0 +1,95 @@ +// QA-11: vollstaendige Ablaeufe nur mit der Tastatur (keine Mausereignisse) im +// echten Browser gegen die echten Handler (a11y-fixture). Jeder Ablauf prueft +// das Ergebnis ueber die API, nicht nur die Oberflaeche. +// +// A11Y_CHROME=... A11Y_API=http://127.0.0.1:8299 A11Y_BROWSER=http://127.0.0.1:4110 A11Y_IDS=ids.json node keyboard-flow.js +const fs = require("fs"); +const path = require("path"); +const nm = process.env.A11Y_NODE_MODULES || "/usr/local/lib/node_modules/pa11y/node_modules"; +const puppeteer = require(path.join(nm, "puppeteer-core")); +const ids = JSON.parse(fs.readFileSync(process.env.A11Y_IDS, "utf8")); +const API = process.env.A11Y_API; +const BROWSER = process.env.A11Y_BROWSER; + +async function tabTo(page, describe, match, max = 40) { + for (let i = 0; i < max; i++) { + await page.keyboard.press("Tab"); + const cur = await page.evaluate(() => { + const el = document.activeElement; + if (!el || el === document.body) return null; + return { tag: el.tagName, role: el.getAttribute("role"), type: el.getAttribute("type"), + text: ((el.getAttribute("aria-label") || "") + " " + (el.textContent || "")).trim().slice(0, 80) }; + }); + if (cur && match(cur)) return cur; + } + throw new Error(`Element nicht per Tab erreichbar: ${describe}`); +} +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const getJSON = async (p) => (await fetch(API + p)).json(); + +const flows = { + async "Papierkorb: Dokument nur mit Tastatur wiederherstellen"(page) { + await page.goto(`${BROWSER}/trash`, { waitUntil: "networkidle0" }); + const before = await page.evaluate(() => document.body.innerText.includes("alt.txt")); + if (!before) throw new Error("alt.txt nicht im Papierkorb sichtbar"); + await tabTo(page, "Auswahl-Kontrollkaestchen der Zeile", (c) => c.tag === "INPUT" && c.type === "checkbox"); + await page.keyboard.press("Space"); + await tabTo(page, "Button Auswahl wiederherstellen", (c) => c.tag === "BUTTON" && /wiederherstell/i.test(c.text)); + await page.keyboard.press("Enter"); + await sleep(1500); + const gone = await page.evaluate(() => !document.body.innerText.includes("alt.txt")); + if (!gone) throw new Error("alt.txt nach Wiederherstellen noch im Papierkorb"); + const docs = await getJSON(`/folders/${ids.folder_inbox}/documents`); + if (!docs.some((d) => d.id === ids.trashed_doc)) throw new Error("Dokument ist per API nicht wiederhergestellt"); + }, + + async "Ordnerbaum: Ordner nur mit Tastatur auswaehlen, Dokumente erscheinen"(page) { + await page.goto(`${BROWSER}/browse`, { waitUntil: "networkidle0" }); + await tabTo(page, "Ordnerbaum", (c) => c.role === "tree"); + let opened = false; + for (let i = 0; i < 6 && !opened; i++) { + await page.keyboard.press("ArrowDown"); + await page.keyboard.press("Enter"); + await sleep(600); + opened = (await page.evaluate(() => document.querySelectorAll('[role="option"]').length)) > 0; + } + if (!opened) throw new Error("nach Tastaturauswahl erscheinen keine Dokumente in der Liste"); + }, + + async "Metadaten: Wert nur mit Tastatur erfassen und speichern"(page) { + await page.goto(`${BROWSER}/documents/${ids.pdf_doc}/metadata`, { waitUntil: "networkidle0" }); + await tabTo(page, "Betrag-Feld", (c) => c.tag === "INPUT" && c.type !== "checkbox" && !/tag/i.test(c.text)); + await page.keyboard.type("12.5"); + await tabTo(page, "Speichern", (c) => c.tag === "BUTTON" && /speicher|save/i.test(c.text)); + await page.keyboard.press("Enter"); + await sleep(1500); + const values = await getJSON(`/documents/${ids.pdf_doc}/metadata`); + if (!JSON.stringify(values).includes("12.5")) throw new Error(`Wert per API nicht gespeichert: ${JSON.stringify(values).slice(0, 200)}`); + }, +}; + +(async () => { + const browser = await puppeteer.launch({ executablePath: process.env.A11Y_CHROME, args: ["--no-sandbox", "--disable-setuid-sandbox"] }); + let failed = false; + const mouse = { events: 0 }; + for (const [name, fn] of Object.entries(flows)) { + const page = await browser.newPage(); + await page.setViewport({ width: 1280, height: 900 }); + await page.evaluateOnNewDocument(() => { + window.__mouse = 0; + for (const t of ["mousedown", "mouseup", "click"]) window.addEventListener(t, (e) => { if (e.isTrusted && e.detail > 0) window.__mouse++; }, true); + }); + try { + await fn(page); + const clicks = await page.evaluate(() => window.__mouse); + mouse.events += clicks; + console.log(`ok\t${name}\t(Mausereignisse: ${clicks})`); + } catch (e) { + failed = true; + console.log(`FEHLER\t${name}\t${e.message}`); + } + await page.close(); + } + await browser.close(); + process.exit(failed ? 1 : 0); +})();