/** * Erzeugt Bildschirmfotos der Anwendung zur visuellen Abnahme. * * Kein Prüf-, sondern ein Dokumentationslauf: Er hält fest, wie die Oberfläche * in den drei Farbschemata und in den wichtigsten Ansichten tatsächlich * aussieht. Ausgabe nach `docs/bildschirmfotos/`. * * Aufruf: npx playwright test e2e/bildschirmfotos.spec.ts */ import { mkdirSync } from 'node:fs'; import { join } from 'node:path'; import { type ElectronApplication, type Page } from '@playwright/test'; import { animationenAbwarten, appStarten, bauPruefen, projektWurzel } from './electron-hilfe'; import { test } from './konsolenwache'; const ZIEL = join(projektWurzel, '..', 'docs', 'bildschirmfotos'); let app: ElectronApplication; let fenster: Page; test.beforeAll(async () => { // Fehlender oder veralteter Bau ist ein Fehler, kein Grund zum Schweigen. bauPruefen(); mkdirSync(ZIEL, { recursive: true }); ({ app, fenster } = await appStarten()); await fenster.setViewportSize({ width: 1280, height: 900 }); }); test.afterAll(async () => { await app.close(); }); async function ablegen(name: string): Promise { await animationenAbwarten(fenster); await fenster.screenshot({ path: join(ZIEL, `${name}.png`), fullPage: true }); } /** * Bildausschnitt statt ganzer Seite. * * Nötig für alles, was in der obersten Ebene liegt: Ein modaler Dialog steht * im sichtbaren Ausschnitt, während `fullPage` die ganze, viel längere Seite * zusammensetzt. Er säße dann irgendwo im unteren Drittel eines Bildes, das * ihn gar nicht zeigen soll. */ async function ausschnittAblegen(name: string): Promise { await animationenAbwarten(fenster); await fenster.screenshot({ path: join(ZIEL, `${name}.png`) }); } async function themaWaehlen(beschriftung: string): Promise { await fenster.getByRole('radio', { name: beschriftung, exact: true }).check(); await animationenAbwarten(fenster); } /** * Zurück zum Startbildschirm, egal aus welcher Ansicht. * * Die Ansichten bieten unterschiedliche Rückwege; probiert wird deshalb der * Reihe nach, bis die Überschrift „Lernen“ wieder da ist. */ async function zumStart(): Promise { const start = fenster.getByRole('heading', { name: 'Heute lernen', exact: true }); /* Die Reihenfolge ist der Weg heraus, nicht bloß eine Liste: Aus einem laufenden Bogen führen zwei Schritte – „Simulation abbrechen“ öffnet die Rückfrage, „Abbrechen und verwerfen“ beantwortet sie. Bis Fassung 0.20.0 stand hier `/prüfung abbrechen/i`. Mit der Umbenennung von „Prüfung“ auf „Simulation“ traf das keinen Knopf mehr: Der Lauf blieb im Bogen stehen, den der vorige Test gestartet hatte, und „Erklärung zur Antwort“ lief in die Zeitüberschreitung. Aufgefallen ist es erst hier, weil diese Datei zuvor nie gegen den geänderten Stand lief. */ const wege = [ /sitzung beenden/i, /zum start/i, /simulation abbrechen/i, /abbrechen und verwerfen/i, ]; for (const weg of wege) { if (await start.isVisible().catch(() => false)) { return; } const knopf = fenster.getByRole('button', { name: weg }).first(); if (await knopf.isVisible().catch(() => false)) { await knopf.click(); await fenster.waitForTimeout(400); } } await start.waitFor({ timeout: 5000 }); } test('Startbildschirm in allen drei Farbschemata', async () => { await ablegen('01-startbildschirm-hell'); await themaWaehlen('Dunkel'); await ablegen('02-startbildschirm-dunkel'); await themaWaehlen('Hoher Kontrast'); await ablegen('03-startbildschirm-hochkontrast'); await themaWaehlen('Hell'); }); /** * Blättert in der laufenden Sitzung vor bis zu einer Auswahlfrage. * * „Weiterlernen“ zieht die Frage nach der Wiedervorlage, und die kann eine * offene sein. Dann gibt es weder Radioknopf noch Kästchen, und „Antwort * bestätigen“ heißt „Musterantwort anzeigen“. */ async function bisZurAuswahlfrage(): Promise { const auswahlfeld = fenster.getByRole('radio').or(fenster.getByRole('checkbox')); for (let i = 0; i < 15; i++) { if ((await auswahlfeld.count()) > 0) { return; } await fenster .getByRole('button', { name: /Musterantwort anzeigen|^Antwort bestätigen$/u }) .first() .click(); const bewertung = fenster.getByRole('button', { name: /^(Gewusst|Sicher gewusst)$/u }); if ((await bewertung.count()) > 0) { await bewertung.first().click(); } const weiter = fenster.getByRole('button', { name: /^Nächste Frage$/u }); if ((await weiter.count()) === 0) { throw new Error('Die Sitzung endete, ohne eine Auswahlfrage zu zeigen.'); } await weiter.first().click(); await fenster.waitForTimeout(150); } throw new Error('Nach fünfzehn Fragen stand noch keine Auswahlfrage an.'); } test('Lernsitzung mit Rückmeldung', async () => { await fenster .getByRole('button', { name: /Weiterlernen/i }) .first() .click(); await fenster.waitForTimeout(700); /* Erst vorblättern, dann fotografieren – beide Bilder zeigen dieselbe Frage. Vorher wurde geknipst, was gerade dastand, und Bild 05 nur dann, wenn zufällig eine Auswahlfrage gezogen war: `/bestätigen|prüfen/i` trifft „Musterantwort anzeigen“ nicht. Bei einer offenen Frage entstand still kein neues Bild, und die alte Aufnahme blieb als vermeintlich aktuelle liegen – genau der Fall, den `gepackt.spec.ts` für das Paket beschreibt. Nachgemessen: Im Lauf vom 27.08. blieb 05 unverändert stehen, während 04 neu geschrieben wurde. */ await bisZurAuswahlfrage(); await ablegen('04-lernsitzung'); await fenster.getByRole('radio').or(fenster.getByRole('checkbox')).first().check(); await fenster.getByRole('button', { name: 'Antwort bestätigen' }).click(); await fenster.waitForTimeout(600); await ablegen('05-lernsitzung-feedback'); await zumStart(); }); test('Prüfungssimulation: Auswahl und Lauf', async () => { await fenster .getByRole('button', { name: /Prüfungssimulation vorbereiten/i }) .first() .click(); await fenster.waitForTimeout(700); await ablegen('06-pruefungswahl'); // Ein kleines, schnelles Profil wählen, damit der Lauf überschaubar bleibt. const frei = fenster.getByRole('radio', { name: /Selbst einstellen/i }).first(); if (await frei.isVisible().catch(() => false)) { await frei.check().catch(() => undefined); await animationenAbwarten(fenster); await ablegen('07-pruefungswahl-frei'); } const starten = fenster.getByRole('button', { name: /simulation starten|starten/i }).last(); if (await starten.isVisible().catch(() => false)) { await starten.click(); await fenster.waitForTimeout(900); await ablegen('08-pruefungslauf'); } }); test('Erklärung zur Antwort', async () => { /* Abschnitt I.1 gezielt ansteuern: Dort liegen Erklärungen vor. Bei „Weiterlernen" bestimmt die Wiedervorlage die Frage, und die kann eine ohne Erklärung sein. */ await zumStart(); await fenster .getByRole('button', { name: /Kapitel wählen/i }) .first() .click(); await fenster.getByRole('button', { name: /^I\.1 – / }).click(); await fenster.waitForTimeout(700); const option = fenster.getByRole('radio').first(); const kaestchen = fenster.getByRole('checkbox').first(); if (await option.isVisible().catch(() => false)) { await option.check().catch(() => undefined); } else if (await kaestchen.isVisible().catch(() => false)) { await kaestchen.check().catch(() => undefined); } const bestaetigen = fenster.getByRole('button', { name: /bestätigen|anzeigen/i }).first(); if (await bestaetigen.isVisible().catch(() => false)) { await bestaetigen.click(); await fenster.waitForTimeout(600); } // Bei offenen Fragen kommt zuerst die Selbstbewertung. const gewusst = fenster.getByRole('button', { name: /^Gewusst/i }).first(); if (await gewusst.isVisible().catch(() => false)) { await gewusst.click(); await fenster.waitForTimeout(500); } await ablegen('10-erklaerung'); await zumStart(); }); test('Glossar', async () => { await zumStart(); await fenster .getByRole('button', { name: /Fachbegriffe nachschlagen/i }) .first() .click(); await fenster.getByRole('heading', { name: 'Glossar', level: 1 }).waitFor(); await ablegen('11-glossar'); await fenster.getByRole('navigation', { name: 'Bereichswechsel' }).getByRole('button').click(); }); test('Über diese Software', async () => { // Bewusst ohne Absicherung: Kommt der Lauf hier nicht an, ist das ein // Befund und kein Grund, still einen fehlenden Screenshot hinzunehmen. await zumStart(); await fenster .getByRole('button', { name: /Lizenzen und Herkunft anzeigen/i }) .first() .click(); await fenster.getByRole('heading', { name: /Über diese Software/i, level: 1 }).waitFor(); await ablegen('09-ueber-software'); }); test('Hilfe', async () => { /* Der Dialog liegt in der obersten Ebene über der Ansicht. Das Bild soll genau das zeigen: Die Hilfe ersetzt den Startbildschirm nicht, sie legt sich darüber. */ await zumStart(); await fenster.getByRole('contentinfo').getByRole('button', { name: 'Hilfe' }).click(); await fenster.getByRole('dialog', { name: 'Hilfe' }).waitFor(); await ausschnittAblegen('12-hilfe'); await fenster.keyboard.press('Escape'); }); test('Fragen durchsuchen', async () => { await zumStart(); await fenster .getByRole('button', { name: /Fragen durchsuchen/i }) .first() .click(); await fenster.getByRole('heading', { name: 'Fragen durchsuchen', level: 1 }).waitFor(); await fenster.getByLabel('Suchbegriff').fill('Anscheinswaffe'); await fenster.waitForTimeout(600); await ablegen('13-suche'); await zumStart(); });