/** * Was ohne Scrollen erreichbar sein muss. * * Anlass ist eine Rückmeldung aus der Anwendung heraus: „Was mir nicht * gefällt, dass ich zum Lernen erstmal scrollen muss." Nachgemessen bei * 1063 Pixeln sichtbarer Höhe: * * - Der Weg ins Lernen begann bei 2659 px – zweieinhalb Bildschirme * Statusbericht vor der ersten Frage. * - Nach dem Bestätigen begann die Rückmeldung bei rund 950 px und war * damit praktisch unsichtbar; „Nächste Frage" stand bei 2115 px. * * Solche Regressionen fallen in keinem anderen Test auf: Alles ist da, * alles ist bedienbar, alles ist zugänglich – es steht nur zu weit unten. * Deshalb misst dieser Lauf Positionen statt Vorhandensein. * * Gemessen wird bei 1999 × 1125 Fenstergröße. Kleinere Fenster brauchen * zwangsläufig mehr Scrollen; die Zusage gilt der üblichen Arbeitsgröße. */ 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()); await app.evaluate(({ BrowserWindow }) => { BrowserWindow.getAllWindows()[0]?.setSize(1999, 1125); }); await fenster.waitForTimeout(300); }); test.afterAll(async () => { await app?.close(); }); async function sichtbareHoehe(): Promise { return fenster.evaluate(() => window.innerHeight); } /** * Startet eine frische Sitzung und blättert bis zu einer Auswahlfrage. * * Jeder Test holt sich seine eigene Sitzung: Sonst hängt das Ergebnis davon * ab, wo der vorige aufgehört hat – und ein Durchlauf, der zufällig auf der * letzten Frage endet, landet in der Auswertung statt in der Sitzung. */ async function sitzungMitAuswahlfrage(): Promise { /* Zurück zum Start – in Schritten: „Sitzung beenden“ führt zur Auswertung, erst von dort geht es „Zum Start“. */ for (let i = 0; i < 4; i++) { if (await amStart(fenster)) { break; } const zurueck = fenster.getByRole('button', { name: /^(Zum Start|Sitzung beenden)$/u }); if ((await zurueck.count()) === 0) { break; } await zurueck.first().click(); await fenster.waitForTimeout(250); } await fenster.getByRole('button', { name: /Weiterlernen/u }).click(); await fenster.getByRole('heading', { level: 1, name: /, Frage \d/u }).waitFor(); for (let i = 0; i < 15; i++) { if ((await fenster.getByRole('radio').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) { /* Ende der Sitzung erreicht, ohne eine Auswahlfrage zu treffen – neu beginnen statt in der Auswertung stehen zu bleiben. */ await sitzungMitAuswahlfrage(); return; } await weiter.first().click(); await fenster.waitForTimeout(150); } } test.describe('Ohne Scrollen erreichbar', () => { test('der Weg ins Lernen steht im ersten Bildschirm', async () => { const hoehe = await sichtbareHoehe(); const knopf = fenster.getByRole('button', { name: /Weiterlernen/u }); await expect(knopf).toBeVisible(); const unten = await fenster.evaluate(() => { const b = [...document.querySelectorAll('button')].find((e) => e.textContent.includes('Weiterlernen'), ); return b === undefined ? Number.POSITIVE_INFINITY : Math.round(b.getBoundingClientRect().bottom); }); expect( unten, `„Weiterlernen" endet bei ${String(unten)} px, sichtbar sind ${String(hoehe)} px.`, ).toBeLessThanOrEqual(hoehe); }); test('alle fünf Einstiege stehen im ersten Bildschirm', async () => { const hoehe = await sichtbareHoehe(); /* Der fünfte Knopf („Offene Fragen", seit 0.17.0) kostet keine Zeile: Das Raster stellt bei dieser Breite drei Spalten, vier Knöpfe standen also längst in zwei Reihen. Geprüft wird das hier und nicht gerechnet. */ for (const name of ['Kapitel wählen', 'Nur Fehler', 'Gemerkte Fragen', 'Offene Fragen']) { const unten = await fenster.evaluate((gesucht) => { const b = [...document.querySelectorAll('button')].find((e) => e.textContent.includes(gesucht), ); return b === undefined ? Number.POSITIVE_INFINITY : Math.round(b.getBoundingClientRect().bottom); }, name); expect(unten, `„${name}" endet bei ${String(unten)} px`).toBeLessThanOrEqual(hoehe); } }); test('die Rückmeldung erscheint ohne Scrollen', async () => { await sitzungMitAuswahlfrage(); await fenster.getByRole('radio').first().click(); await fenster.getByRole('button', { name: 'Antwort bestätigen' }).click(); await fenster.waitForTimeout(250); await fenster.evaluate(() => { window.scrollTo(0, 0); }); const hoehe = await sichtbareHoehe(); /* Die Zusage ist genau: Wer bestätigt hat, liest Urteil UND Lösungssatz, ohne die Hand zur Maus zu nehmen. Gemessen wird deshalb die Unterkante des Lösungssatzes, nicht die Oberkante des Kastens – eine Überschrift am unteren Rand wäre keine Antwort. */ const unten = await fenster.evaluate(() => { const satz = document.querySelector('.ergebnis p'); return satz === null ? Number.POSITIVE_INFINITY : Math.round(satz.getBoundingClientRect().bottom); }); expect( unten, `Der Lösungssatz endet bei ${String(unten)} px, sichtbar sind ${String(hoehe)} px.`, ).toBeLessThanOrEqual(hoehe); }); test('die Frage ist die oberste Überschrift der Sitzung', async () => { /* Der Name der Anwendung steht in der Titelleiste des Fensters – ihn im Inhalt zu wiederholen kostete 167 Pixel und lenkte vom Stoff ab. Die oberste Überschrift benennt jetzt, worum es in dieser Ansicht geht: die Frage. Wer per Überschrift navigiert, landet damit an der Sache. */ await sitzungMitAuswahlfrage(); await expect(fenster.locator('.kopfbereich')).toHaveCount(0); const ueberschriften = fenster.getByRole('heading', { level: 1 }); await expect(ueberschriften).toHaveCount(1); await expect(ueberschriften).toHaveClass(/frage__text/u); }); test('die Frage steht weit oben', async () => { /* Vorher begann der Fragetext bei 299 Pixeln, in der ursprünglichen Fassung sogar bei 495. Gemessen wird der Moment des Ankommens. Jede neue Frage holt den Fokus auf ihre Überschrift (`Lernsitzung.tsx`); erst dann steht die Seite so, wie ein Lernender sie vorfindet. Diese Prüfung holte sich früher weder eine eigene Sitzung noch wartete sie darauf – sie maß, wo die vorige Prüfung den Rollbalken hatte stehen lassen. Nachgemessen über vier Fenstergrößen: Lag der Fokus auf der Überschrift, stand sie bei 166 px und die Seite war ungerollt; hing er noch auf „Nächste Frage", lag sie bei 4 px, bei kleinem Fenster bei −363 px. Das war eine Aussage über den Testablauf, nicht über die Anwendung. Die untere Schranke ist deshalb 0 und nicht 1: Eine Überschrift genau an der Oberkante ist der beste Fall, kein Fehler. Unter 0 wäre sie angeschnitten. */ await sitzungMitAuswahlfrage(); await expect .poll(() => fenster.evaluate(() => document.activeElement?.className ?? '')) .toContain('frage__text'); const oben = await fenster.evaluate(() => { const ueberschrift = document.querySelector('.frage__text'); return ueberschrift === null ? null : Math.round(ueberschrift.getBoundingClientRect().top); }); if (oben === null) { throw new Error('Die Sitzung zeigt keine Frageüberschrift.'); } expect(oben, `Der Fragetext beginnt bei ${String(oben)} px.`).toBeLessThan(220); expect( oben, `Der Fragetext steht über dem Sichtfeld (${String(oben)} px).`, ).toBeGreaterThanOrEqual(0); }); });