diff --git a/Changes.md b/Changes.md index 79a0849..75bfd15 100644 --- a/Changes.md +++ b/Changes.md @@ -1,5 +1,32 @@ # Änderungen +## Berichte: Tabellen so breit wie nötig, Zahlen rechtsbündig (18.08.2026) + +Ein Bericht mit zwei oder vier Spalten wurde bisher über die gesamte Fensterbreite gezogen – +zwischen Bezeichnung und Betrag klaffte ein leeres Feld, und das Auge musste die Zeile +zweimal überqueren. Die **Berichtstabellen nehmen jetzt nur den Platz, den ihre Spalten +brauchen** (`width: auto`) und stehen links; erst wenn der Inhalt mehr verlangt, wachsen sie +bis zur vollen Breite. Im Scrollrahmen `.breittabelle` (zwölf Monatsspalten) bleibt es beim +bisherigen Verhalten: Die Tabelle darf hinausragen und waagerecht scrollen, statt umzubrechen. + +Dabei fiel auf, dass die Zahlenspalten **gar nicht rechtsbündig** standen: `.rechts` und +`.betrag` sind nur einfach spezifisch, `table.liste th, table.liste td { text-align: left }` +weiter oben im Stilblatt ist spezifischer und gewann. Solange die Spalten über den Bildschirm +gezogen waren, fiel es kaum auf – in der schmalen Tabelle sofort. Für Berichte steht die +Ausrichtung jetzt ausdrücklich (`table.liste.bericht td.rechts`), sodass die letzte Spalte – +die Betragsspalte – ihre Zahlen rechtsbündig an ihrer eigenen Kante ausrichtet. Die übrigen +Listen (Kontenübersicht, Wertpapiere) sind unverändert geblieben. + +Der neue E2E-Test misst beides: dass der Summenbericht deutlich schmaler bleibt als sein +Rahmen, dass die letzte Spalte `text-align: right` hat und dass die Monatsansicht weiterhin +breiter wird. + +Nachgezogen: Der E2E-Test der eingeklappten Seitenleiste erwartete `toBeHidden()` für die +Beschriftungen. Die sind aber mit Absicht nur aus dem Bild geschoben (`clip-path`, 1×1 Pixel) +statt entfernt, damit Vorleseprogramme die Namen behalten – für Playwright bleibt so ein +Element „sichtbar“. Geprüft wird jetzt, dass die Beschriftung keinen Platz mehr einnimmt und +der Verweis trotzdem seinen zugänglichen Namen behält. + ## Seitenleiste: Marke mit Münzstapel, ein Klick schiebt sie auf Symbolbreite (18.08.2026) Oben in der Seitenleiste steht jetzt eine richtige **Marke**: das Bild `Coinstack.png` aus den diff --git a/public/static/app.css b/public/static/app.css index 63ad251..5a6c30e 100644 --- a/public/static/app.css +++ b/public/static/app.css @@ -1737,6 +1737,25 @@ body.schlicht { table.bericht { font-size: .9rem; } table.bericht td { padding: .28rem .7rem; } +/* Ein Bericht mit wenigen Spalten soll nicht über die ganze Breite gezogen + werden: Die Tabelle nimmt nur den Platz, den ihre Spalten brauchen, und + steht links. Die Zahlenspalten bleiben in sich rechtsbündig (.rechts), die + letzte schließt damit bündig mit dem Tabellenrand ab. */ +table.liste.bericht { + width: auto; + max-width: 100%; +} + +/* Im Scrollrahmen darf die Tabelle über die sichtbare Breite hinausgehen – + sonst quetschte max-width die zwölf Monatsspalten in Umbrüche. */ +.breittabelle table.liste.bericht { max-width: none; } + +/* `.rechts` allein reicht hier nicht: `table.liste th, td` setzt weiter oben + text-align auf links und ist spezifischer. Die Zahlenspalten – allen voran + die letzte – stehen deshalb ausdrücklich rechtsbündig. */ +table.liste.bericht th.rechts, +table.liste.bericht td.rechts { text-align: right; } + /* Abschnittsüberschrift „Einnahmen“ / „Ausgaben“. */ table.bericht tr.abschnittszeile th { text-align: left; diff --git a/tests/e2e/specs/auswertungen.spec.js b/tests/e2e/specs/auswertungen.spec.js index 2a8c7a6..4c6577a 100644 --- a/tests/e2e/specs/auswertungen.spec.js +++ b/tests/e2e/specs/auswertungen.spec.js @@ -145,6 +145,32 @@ test.describe('Auswertungen', () => { await expect(page.locator('.meldung-erfolg')).toContainText('gelöscht') }) + test('Schmale Berichte bleiben schmal, die Zahlenspalte bleibt rechtsbündig', async ({ page }) => { + // Zwei Spalten brauchen keine Bildschirmbreite: Die Tabelle steht links + // und ist deutlich schmaler als der Platz, den sie hätte. + await vorlage(page, 'summenbericht') + + const tabelle = page.locator('table.bericht') + const rahmen = page.locator('.breittabelle') + + const tabelleBreit = (await tabelle.boundingBox()).width + const rahmenBreit = (await rahmen.boundingBox()).width + expect(tabelleBreit).toBeLessThan(rahmenBreit * 0.9) + + // Letzte Spalte: Zahlen rechtsbündig in ihrer eigenen Spalte. + // Die erste Zeile eines Abschnitts ist seine Überschrift (th) – deshalb + // die erste echte Wertzeile. + const letzte = tabelle.locator('tbody tr > td:last-child').first() + await expect(letzte).toHaveCSS('text-align', 'right') + + // Zwölf Monatsspalten dürfen dagegen weiterhin über die Breite gehen + // und im Rahmen scrollen, statt umzubrechen. + await page.selectOption('select[name="aufteilung"]', 'monat') + await page.click('button:has-text("Anzeigen")') + expect((await tabelle.boundingBox()).width) + .toBeGreaterThan(tabelleBreit) + }) + test('CSV-Ausgabe kommt als Datei mit deutschem Zahlenformat', async ({ page }) => { await vorlage(page, 'summenbericht') diff --git a/tests/e2e/specs/seitenleiste.spec.js b/tests/e2e/specs/seitenleiste.spec.js index 89bfe34..72b8a66 100644 --- a/tests/e2e/specs/seitenleiste.spec.js +++ b/tests/e2e/specs/seitenleiste.spec.js @@ -14,6 +14,19 @@ async function anmelden(page) { await expect(page).toHaveURL(/\/$/) } +/** + * Eingeklappt werden die Beschriftungen nur aus dem Bild geschoben (`clip-path` + * auf 1×1 Pixel), nicht mit `display:none` entfernt – sonst verlören die + * Verweise ihren Namen für Vorleseprogramme. Für Playwright bleibt ein solches + * Element „sichtbar“; geprüft wird deshalb, dass es keinen Platz mehr einnimmt. + */ +async function istWeggeschoben(beschriftung) { + await expect.poll(async () => { + const kasten = await beschriftung.boundingBox() + return kasten === null ? 0 : Math.max(kasten.width, kasten.height) + }, { message: 'Beschriftung nimmt keinen Platz mehr ein' }).toBeLessThanOrEqual(1) +} + test.describe('Seitenleiste ein- und ausklappen', () => { test('Klick auf die Marke schiebt die Leiste auf Symbolbreite und zurück', async ({ page }) => { await anmelden(page) @@ -26,6 +39,7 @@ test.describe('Seitenleiste ein- und ausklappen', () => { await expect(symbol).toBeVisible() await expect(beschriftung).toBeVisible() const breitOffen = (await page.locator('.seitenleiste').boundingBox()).width + const hauptbereichOffen = (await page.locator('.hauptbereich').boundingBox()).width await marke.click() @@ -34,21 +48,25 @@ test.describe('Seitenleiste ein- und ausklappen', () => { // Das Markensymbol und die Symbole der Menüpunkte bleiben stehen, // nur die Beschriftungen verschwinden. await expect(symbol).toBeVisible() - await expect(beschriftung).toBeHidden() + await istWeggeschoben(beschriftung) await expect(page.locator('.seitenleiste .icon').first()).toBeVisible() + // Der Verweis behält dabei seinen Namen – nur das Auge verliert ihn. + await expect(page.locator('.seitenleiste a[href="/konten"]')) + .toHaveAccessibleName(/Kontenübersicht/) + const breitEingeklappt = (await page.locator('.seitenleiste').boundingBox()).width expect(breitEingeklappt).toBeLessThan(breitOffen) // Der Hauptbereich bekommt den gewonnenen Platz. - const hauptOffen = breitOffen - expect((await page.locator('.hauptbereich').boundingBox()).width).toBeGreaterThan(0) + expect((await page.locator('.hauptbereich').boundingBox()).width) + .toBeGreaterThan(hauptbereichOffen) await marke.click() await expect(rahmen).not.toHaveClass(/eingeklappt/) await expect(marke).toHaveAttribute('aria-expanded', 'true') await expect(beschriftung).toBeVisible() - expect((await page.locator('.seitenleiste').boundingBox()).width).toBe(hauptOffen) + expect((await page.locator('.seitenleiste').boundingBox()).width).toBe(breitOffen) }) test('Die Symbole führen eingeklappt zu denselben Seiten', async ({ page }) => { @@ -64,7 +82,7 @@ test.describe('Seitenleiste ein- und ausklappen', () => { // Der Zustand hält über den Seitenwechsel hinweg – ohne Aufblitzen, // weil der Server ihn schon aus dem Cookie kennt. await expect(page.locator('.rahmen')).toHaveClass(/eingeklappt/) - await expect(page.locator('.seitenleiste .beschriftung').first()).toBeHidden() + await istWeggeschoben(page.locator('.seitenleiste .beschriftung').first()) await page.locator('[data-seitenleiste-schalter]').click() await expect(page.locator('.rahmen')).not.toHaveClass(/eingeklappt/)