/** * Die Überschriftenstruktur jeder Ansicht. * * Wer mit einem Screenreader arbeitet, springt nach Überschriftenebenen. * Dafür muss in jeder Ansicht dasselbe gelten: genau eine h1, die benennt, * worum es hier geht, und darunter keine übersprungene Ebene. * * Vorher war es uneinheitlich – in der Lernsitzung war die Frage die h1, in * den Nebenansichten der Anwendungsname, und die eigentliche * Ansichtsüberschrift steckte eine Ebene tiefer. Mal landete man an der * Sache, mal beim Programmnamen. */ import { type ElectronApplication, type Page } from '@playwright/test'; import { amStart, appStarten, bauPruefen } from './electron-hilfe'; import { expect, test } from './konsolenwache'; let app: ElectronApplication | undefined; let fenster: Page; test.beforeAll(async () => { // Fehlender oder veralteter Bau ist ein Fehler, kein Grund zum Schweigen. bauPruefen(); ({ app, fenster } = await appStarten()); }); test.afterAll(async () => { await app?.close(); }); interface Befund { readonly ebenen: number[]; readonly texte: string[]; } async function ueberschriften(): Promise { return fenster.evaluate(() => { const alle = [...document.querySelectorAll('h1, h2, h3, h4, h5, h6')].filter((h) => { /* Zugeklappte
zählen mit: Ein Screenreader führt sie in der Überschriftenliste, und die Ebene gilt dort genauso. Wirklich verborgene Elemente nicht. */ return h.closest('[hidden]') === null; }); return { ebenen: alle.map((h) => Number(h.tagName.slice(1))), texte: alle.map((h) => h.textContent.trim().slice(0, 40)), }; }); } const ANWENDUNGSNAME = 'Waffensachkunde – Lernsoftware'; /** Genau eine h1, Beginn bei 1, kein Sprung nach unten. */ function pruefen(name: string, befund: Befund, benenntAnsicht = true): void { const einsen = befund.ebenen.filter((e) => e === 1).length; const liste = befund.ebenen .map((e, i) => `h${String(e)} „${befund.texte[i] ?? ''}“`) .join('\n '); expect(einsen, `${name}: ${String(einsen)} h1 statt genau einer.\n ${liste}`).toBe(1); expect(befund.ebenen[0], `${name}: beginnt nicht mit h1.\n ${liste}`).toBe(1); /* Die h1 muss die Ansicht benennen, nicht das Programm. Sonst ist die Struktur zwar formal in Ordnung, hilft aber niemandem: Wer per Überschrift navigiert, landete in jeder Ansicht bei derselben, nichtssagenden Antwort auf die Frage „wo bin ich?“. */ if (benenntAnsicht) { expect(befund.texte[0], `${name}: Die h1 nennt nur den Anwendungsnamen.`).not.toBe( ANWENDUNGSNAME, ); } let vorige = 1; for (const [i, ebene] of befund.ebenen.entries()) { expect( ebene, `${name}: Sprung auf h${String(ebene)} bei „${befund.texte[i] ?? ''}“.\n ${liste}`, ).toBeLessThanOrEqual(vorige + 1); vorige = ebene; } } async function zumStart(): Promise { for (let i = 0; i < 4; i++) { if (await amStart(fenster)) { return; } const zurueck = fenster.getByRole('button', { name: /^(Zum Start|Sitzung beenden)$/u }); if ((await zurueck.count()) === 0) { return; } await zurueck.first().click(); await fenster.waitForTimeout(250); } } test.describe('Überschriften je Ansicht', () => { test('Startbildschirm', async () => { await zumStart(); /* Hier ist der Anwendungsname die richtige Antwort auf „wo bin ich?“. */ pruefen('Startbildschirm', await ueberschriften(), false); }); test('Glossar', async () => { await zumStart(); await fenster.getByRole('button', { name: /Fachbegriffe nachschlagen/u }).click(); await fenster.getByRole('heading', { name: 'Glossar', exact: true }).waitFor(); pruefen('Glossar', await ueberschriften()); }); test('Gesetze lesen', async () => { await zumStart(); await fenster.getByRole('button', { name: /In den Gesetzen nachlesen/u }).click(); await fenster.getByRole('heading', { name: 'Gesetze lesen', level: 1 }).waitFor(); pruefen('Gesetze lesen', await ueberschriften()); }); test('Über diese Software', async () => { await zumStart(); await fenster.getByRole('button', { name: /Lizenzen und Herkunft anzeigen/u }).click(); await fenster.waitForTimeout(400); pruefen('Über diese Software', await ueberschriften()); }); test('Fragen durchsuchen', async () => { /* Mit Trefferliste geprüft, nicht leer: Jede Trefferkarte trägt eine h2, und genau dort könnte die Hierarchie brechen. */ await zumStart(); await fenster.getByRole('button', { name: /Fragen durchsuchen/u }).click(); await fenster.getByRole('heading', { name: 'Fragen durchsuchen', level: 1 }).waitFor(); await fenster.getByLabel('Suchbegriff').fill('Anscheinswaffe'); await fenster.waitForTimeout(300); pruefen('Fragen durchsuchen', await ueberschriften()); }); test('Kapitelwahl', async () => { await zumStart(); await fenster.getByRole('button', { name: /Kapitel wählen/u }).click(); await fenster.waitForTimeout(300); pruefen('Kapitelwahl', await ueberschriften()); }); test('Prüfungswahl', async () => { await zumStart(); await fenster.getByRole('button', { name: /Prüfungssimulation vorbereiten/u }).click(); await fenster.waitForTimeout(400); pruefen('Prüfungswahl', await ueberschriften()); }); test('Lernsitzung', async () => { await zumStart(); await fenster.getByRole('button', { name: /Weiterlernen/u }).click(); await fenster.waitForTimeout(500); pruefen('Lernsitzung', await ueberschriften()); }); test('Lernsitzung nach der Antwort', async () => { /* Gesucht ist irgendeine Auswahlfrage, nicht ausdruecklich eine mit Einfachauswahl: Die Sitzung wird zufaellig zusammengestellt, und ob unter den ersten Fragen eine Einfachauswahl ist, entscheidet der Zufall. Der Test hat deshalb sporadisch versagt, ohne dass etwas kaputt war. Fuer die Ueberschriftenstruktur macht es keinen Unterschied, ob angekreuzt oder angehakt wird. */ const auswahlfeld = fenster.getByRole('radio').or(fenster.getByRole('checkbox')); for (let i = 0; i < 20; i++) { if ((await auswahlfeld.count()) > 0) { break; } await fenster .getByRole('button', { name: /Musterantwort anzeigen|^Antwort bestätigen$/u }) .first() .click(); const b = fenster.getByRole('button', { name: /^(Gewusst|Sicher gewusst)$/u }); if ((await b.count()) > 0) { await b.first().click(); } const w = fenster.getByRole('button', { name: /^Nächste Frage$/u }); if ((await w.count()) === 0) { break; } await w.first().click(); await fenster.waitForTimeout(150); } await auswahlfeld.first().click(); await fenster.getByRole('button', { name: 'Antwort bestätigen' }).click(); await fenster.waitForTimeout(300); pruefen('Lernsitzung nach der Antwort', await ueberschriften()); }); });