// @ts-check const { test, expect } = require('@playwright/test') const BENUTZER = process.env.E2E_USER || 'e2e' const PASSWORT = process.env.E2E_PASSWORD || 'e2e-testpasswort' /** * Meldet sich an. Die Anwendung leitet nicht angemeldete Aufrufe auf /login um; * bei einer frischen Installation stattdessen auf /einrichtung. */ async function anmelden(page) { await page.goto('/login') if (page.url().includes('/einrichtung')) { throw new Error( 'Instanz ist noch nicht eingerichtet. Zuerst einen Benutzer anlegen: ' + 'docker compose exec -u www-data php php scripts/e2e-benutzer.php' ) } await page.fill('#benutzername', BENUTZER) await page.fill('#passwort', PASSWORT) await page.click('button[type="submit"]') await expect(page).toHaveURL(/\/$/) } test.describe('Grundgerüst', () => { test('Anmeldeseite ist erreichbar', async ({ page }) => { await page.goto('/login') await expect(page.locator('h1, .anmelde-marke')).toContainText(/Finanzmanager|Anmeldung/) await expect(page.locator('#benutzername')).toBeVisible() }) test('Anmeldeseite zeigt dieselbe Marke wie die Seitenleiste', async ({ page }) => { const stil = loc => loc.evaluate(k => { const s = getComputedStyle(k) return { familie: s.fontFamily, gewicht: s.fontWeight } }) await page.goto('/login') const marke = page.locator('.anmelde-marke') await expect(marke.locator('img.marken-symbol')).toHaveAttribute('src', '/static/coinstack.png') await expect(marke.locator('img.marken-symbol')).toBeVisible() await expect(marke.locator('.marken-text')).toHaveText('Finanzmanager') const anmeldung = await stil(marke.locator('.marken-text')) // Die zweite Worthälfte hebt sich farblich ab, wie in der Seitenleiste. const farben = await marke.locator('.marken-text').evaluate(k => [ getComputedStyle(k).color, getComputedStyle(k.querySelector('span')).color, ]) expect(farben[0]).not.toBe(farben[1]) await anmelden(page) const leiste = await stil(page.locator('.seitenleisten-marke .marken-text')) expect(anmeldung).toEqual(leiste) }) test('Ohne Anmeldung führt jeder Weg zur Anmeldung', async ({ page }) => { await page.goto('/einstellungen') await expect(page).toHaveURL(/\/(login|einrichtung)/) }) test('Falsches Passwort wird abgewiesen', async ({ page }) => { await page.goto('/login') test.skip(page.url().includes('/einrichtung'), 'Instanz noch nicht eingerichtet') await page.fill('#benutzername', BENUTZER) await page.fill('#passwort', 'ganz sicher falsch') await page.click('button[type="submit"]') await expect(page.locator('.meldung-fehler')).toBeVisible() await expect(page).toHaveURL(/\/login/) }) test('Anmeldung führt ins Cockpit', async ({ page }) => { await anmelden(page) await expect(page.locator('h1')).toContainText('Cockpit') await expect(page.locator('.seitenleiste')).toContainText('Kontenübersicht') }) test('Vermögensentwicklung zeigt zwölf gestapelte Monatsbalken', async ({ page }) => { await anmelden(page) const kachel = page.locator('.kachel', { hasText: 'Vermögensentwicklung' }) await expect(kachel.locator('svg.diagramm')).toBeVisible() // Zwölf Monate, jeder mit eigener Beschriftung unter dem Balken. await expect(kachel.locator('.diagramm-beschriftung:not(.schwach)')).toHaveCount(12) await expect(kachel.locator('.diagramm rect').first()).toBeVisible() // Jedes Balkenstück gehört zu einem Eintrag der Legende, keiner doppelt. const farben = await kachel.locator('.legendenfarbe:not(.leer)').evaluateAll( knoten => knoten.map(k => getComputedStyle(k).backgroundColor) ) expect(farben.length).toBeGreaterThan(0) expect(new Set(farben).size).toBe(farben.length) }) test('„Übrige Konten“ führt eine Ebene tiefer und wieder zurück', async ({ page }) => { await anmelden(page) const kachel = page.locator('.kachel', { hasText: 'Vermögensentwicklung' }) const sammel = kachel.locator('.legendenname.tiefer') // Der Testbestand hat mehr Konten als Farbtöne; sonst gäbe es nichts // aufzuklappen und der Test hätte hier nichts zu prüfen. await expect(sammel).toHaveText(/Übrige Konten \(\d+\)/) const oben = await kachel.locator('.legendenname').allTextContents() await sammel.click() await expect(page).toHaveURL(/ebene=1/) await expect(kachel.locator('.diagrammpfad')).toContainText('Ebene 2 von') // Eine Ebene tiefer stehen andere Konten in der Legende. const unten = await kachel.locator('.legendenname').allTextContents() expect(unten.filter(name => oben.includes(name) && !name.startsWith('Übrige'))).toHaveLength(0) await kachel.locator('.diagrammpfad a').first().click() await expect(page).toHaveURL(/ebene=0/) expect(await kachel.locator('.legendenname').allTextContents()).toEqual(oben) }) test('Schalter „nur aktive“ behält seinen Zustand', async ({ page }) => { await anmelden(page) const schalter = page.locator('.kachel', { hasText: 'Vermögensentwicklung' }) .locator('.schalter') await expect(schalter).toContainText('nur aktive') // Ein früherer Lauf kann den Schalter umgelegt haben. if (await schalter.getAttribute('aria-checked') === 'false') { await schalter.click() } await expect(schalter).toHaveAttribute('aria-checked', 'true') await schalter.click() await expect(schalter).toHaveAttribute('aria-checked', 'false') // Gespeichert, nicht nur angezeigt: Der Zustand überlebt den Neuaufruf. await page.goto('/') await expect(schalter).toHaveAttribute('aria-checked', 'false') await schalter.click() await expect(schalter).toHaveAttribute('aria-checked', 'true') }) test('Einstellungsdialog zeigt alle Reiter', async ({ page }) => { await anmelden(page) await page.click('a[href="/einstellungen"]') await expect(page.locator('h1')).toContainText('Einstellungen') for (const reiter of ['Allgemein', 'Darstellung', 'Buchhaltung', 'Diagnose']) { await expect(page.locator('.tableiste')).toContainText(reiter) } }) test('Einstellung lässt sich speichern', async ({ page }) => { await anmelden(page) await page.goto('/einstellungen?bereich=darstellung') await page.fill('#zeilen_pro_seite', '250') // Eindeutig das Einstellungsformular treffen – im Kopfbereich steht // ebenfalls ein Submit-Knopf (Abmelden), und der kommt im DOM zuerst. await page.click('.formular.breit button[type="submit"]') await expect(page.locator('.meldung-erfolg')).toBeVisible() await expect(page.locator('#zeilen_pro_seite')).toHaveValue('250') }) test('Abmelden beendet die Sitzung', async ({ page }) => { await anmelden(page) await page.click('form[action="/logout"] button') await expect(page).toHaveURL(/\/login/) await page.goto('/') await expect(page).toHaveURL(/\/login/) }) })