From 5695e2e3df13b1fb91f131162a0be3672314db87 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Tue, 29 Sep 2026 11:37:52 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20n.n.:=20E2E-L=C3=A4ufe=20sichern=20Einste?= =?UTF-8?q?llungen,=20Kippschalter=20statt=20Ankreuzfeldern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .gitignore | 1 + Changes.md | 18 ++++++ public/static/app.css | 63 ++++++++++++++++++ scripts/e2e-einstellungen.php | 52 +++++++++++++++ src/Services/E2EEinstellungen.php | 95 ++++++++++++++++++++++++++++ tests/48_E2EEinstellungenTest.php | 52 +++++++++++++++ tests/e2e/README.md | 6 ++ tests/e2e/run.sh | 16 ++++- tests/e2e/specs/kippschalter.spec.js | 49 ++++++++++++++ 9 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 scripts/e2e-einstellungen.php create mode 100644 src/Services/E2EEinstellungen.php create mode 100644 tests/48_E2EEinstellungenTest.php create mode 100644 tests/e2e/specs/kippschalter.spec.js diff --git a/.gitignore b/.gitignore index 361df22..e522bd5 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ tests/e2e/node_modules/ tests/e2e/playwright-report/ tests/e2e/test-results/ tests/e2e/.auth/ +/data/e2e/ diff --git a/Changes.md b/Changes.md index cb408b8..caf890d 100644 --- a/Changes.md +++ b/Changes.md @@ -1,5 +1,23 @@ # Änderungen +## E2E-Läufe lassen die Einstellungen unverändert (29.09.2026) + +- Die Browser-Tests laufen gegen die echte Instanz; der Bargeld-Test hatte dabei die Adresse + des Bargeld-Tools geleert. `tests/e2e/run.sh` sichert jetzt die ganze Tabelle + `einstellungen` vor dem Lauf und stellt sie danach wieder her (auch bei Abbruch mit + Strg+C). Eine nach hartem Abbruch liegengebliebene Sicherung wird vor dem nächsten Lauf + eingespielt. +- Neu: `Services/E2EEinstellungen`, `scripts/e2e-einstellungen.php sichern|wiederherstellen`, + Test 48. Die Sicherung liegt unter `data/e2e/` (nicht im Repository). + +## Kippschalter statt Ankreuzfeldern (29.09.2026) + +- Alle Ankreuzfelder erscheinen als Kippschalter im blauen Design des Projekts: aus = + graue Fläche mit ✕, an = Akzentfarbe mit Häkchen (helles und dunkles Thema). Reine CSS- + Änderung in `public/static/app.css`; die Elemente bleiben echte Ankreuzfelder. +- Ausgenommen ist die Zeilenauswahl in den Listen (Kontoblatt, Depotbuchungen, + Umsätze inkl. „alle wählen“). + ## Regeln: Summe der Anteile in den Splittabellen (29.09.2026) - Die Spalte „Anteil %“ heißt in den Splittabellen der Regeln nur noch „Anteil“, da die diff --git a/public/static/app.css b/public/static/app.css index 67f78e1..88770c0 100644 --- a/public/static/app.css +++ b/public/static/app.css @@ -2314,3 +2314,66 @@ table.liste tr.luecke td { .protokolleintrag p { margin: .4rem 0; } .protokolleintrag summary { cursor: pointer; font-size: .88rem; color: var(--text-gedaempft); } .protokolleintrag .protokolltext { max-height: 22rem; overflow: auto; word-break: break-all; } + +/* Kippschalter statt Ankreuzfeld. Gilt für jedes Ankreuzfeld; ausgenommen ist nur die + Zeilenauswahl in den Listen (Kopf-Ankreuzfeld „alle wählen“ und die Auswahl je Buchung + bzw. Umsatz), wo ein Schalter je Zeile die Tabelle aufblähen würde. Das Element bleibt + ein echtes Ankreuzfeld: Tastatur, Formularversand und Skripte ändern sich nicht. */ +input[type="checkbox"]:not(.alle-waehlen):not([name="buchungen[]"]):not([name="umsaetze[]"]) { + -webkit-appearance: none; + appearance: none; + position: relative; + flex: none; + width: 2.6rem; + height: 1.4rem; + margin: 0; + border-radius: 999px; + background-color: var(--rand-kraeftig); + background-repeat: no-repeat; + background-position: right .5rem center; + background-size: .6rem; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath d='M1.5 1.5l7 7m0-7l-7 7' stroke='%23fff' stroke-width='1.8' stroke-linecap='round' fill='none'/%3E%3C/svg%3E"); + box-shadow: inset 0 1px 2px rgba(16, 24, 40, .18); + cursor: pointer; + vertical-align: middle; + transition: background-color .15s ease; +} + +input[type="checkbox"]:not(.alle-waehlen):not([name="buchungen[]"]):not([name="umsaetze[]"])::before { + content: ""; + position: absolute; + top: .15rem; + left: .15rem; + width: 1.1rem; + height: 1.1rem; + border-radius: 50%; + background: linear-gradient(#ffffff, #eceff3); + box-shadow: 0 1px 3px rgba(16, 24, 40, .35); + transition: transform .15s ease; +} + +input[type="checkbox"]:not(.alle-waehlen):not([name="buchungen[]"]):not([name="umsaetze[]"]):checked { + background-color: var(--akzent); + background-position: left .5rem center; + background-size: .7rem; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath d='M1.3 5.4l2.5 2.5 5-5.6' stroke='%23fff' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E"); +} + +input[type="checkbox"]:not(.alle-waehlen):not([name="buchungen[]"]):not([name="umsaetze[]"]):checked::before { + transform: translateX(1.2rem); +} + +input[type="checkbox"]:not(.alle-waehlen):not([name="buchungen[]"]):not([name="umsaetze[]"]):focus-visible { + outline: 2px solid var(--akzent); + outline-offset: 2px; +} + +input[type="checkbox"]:not(.alle-waehlen):not([name="buchungen[]"]):not([name="umsaetze[]"]):disabled { + opacity: .5; + cursor: not-allowed; +} + +@media (prefers-reduced-motion: reduce) { + input[type="checkbox"], + input[type="checkbox"]::before { transition: none; } +} diff --git a/scripts/e2e-einstellungen.php b/scripts/e2e-einstellungen.php new file mode 100644 index 0000000..922bec4 --- /dev/null +++ b/scripts/e2e-einstellungen.php @@ -0,0 +1,52 @@ +#!/usr/bin/env php +tempPfad('.json'); + Einstellung::set('e2e_probe_url', 'https://kasse.example.net', 'bargeld'); + Einstellung::set('e2e_probe_tage', '10', 'bargeld'); + + $vorher = Database::fetchAll('SELECT * FROM einstellungen ORDER BY schluessel'); + + $anzahl = E2EEinstellungen::sichern($pfad); + $this->assertEquals(count($vorher), $anzahl, 'Alle Zeilen werden gesichert'); + + // Der Lauf ändert, leert, löscht und legt an. + Einstellung::set('e2e_probe_url', '', 'bargeld'); + Einstellung::set('e2e_probe_tage', '99', 'bargeld'); + Einstellung::set('e2e_probe_neu', 'x', 'allgemein'); + Database::execute("DELETE FROM einstellungen WHERE bereich = 'darstellung'"); + + $this->assertNull(E2EEinstellungen::sichern($pfad), 'Eine vorhandene Sicherung wird nicht überschrieben'); + + $wieder = E2EEinstellungen::wiederherstellen($pfad); + $this->assertEquals(count($vorher), $wieder, 'Alle Zeilen kommen zurück'); + Einstellung::cacheLeeren(); + + $this->assertEquals('https://kasse.example.net', Einstellung::get('e2e_probe_url'), 'Die geleerte Adresse ist wieder da'); + $this->assertEquals('10', Einstellung::get('e2e_probe_tage'), 'Der geänderte Wert ist zurückgesetzt'); + $this->assertNull(Einstellung::get('e2e_probe_neu'), 'Vom Lauf angelegte Einstellungen verschwinden'); + $this->assertEquals( + $vorher, + Database::fetchAll('SELECT * FROM einstellungen ORDER BY schluessel'), + 'Die Tabelle ist Zeile für Zeile wie vorher, einschließlich Bereich und Änderungszeit' + ); + $this->assert(!is_file($pfad), 'Die Sicherung ist danach gelöscht'); + $this->assertNull(E2EEinstellungen::wiederherstellen($pfad), 'Ohne Sicherung geschieht nichts'); + } +} diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 155ca36..2fca077 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -51,6 +51,12 @@ Hierher gehört nur, was einen Browser braucht: Formularabläufe, Redirects, gerenderte Views, JavaScript. Rechen- und Model-Logik gehört in die PHP-Suite (`tests/NN_*.php`) – schneller und isoliert. +## Einstellungen + +`run.sh` sichert die Tabelle `einstellungen` vor dem Lauf und stellt sie danach wieder +her (`scripts/e2e-einstellungen.php`). Specs dürfen Einstellungen also frei ändern, müssen +sie aber nicht selbst zurücksetzen. + ## Aufräumen Jeder Lauf legt Konten, Kategorien, Klassen und Buchungen neu an – mit einer diff --git a/tests/e2e/run.sh b/tests/e2e/run.sh index 784855d..9d50b27 100755 --- a/tests/e2e/run.sh +++ b/tests/e2e/run.sh @@ -42,9 +42,23 @@ if [ "${E2E_KEIN_AUFRAEUMEN:-0}" != "1" ]; then fi fi +# Die Tests laufen gegen die echte Instanz und speichern dabei Formulare des +# Einstellungsdialogs. Deshalb: Einstellungen sichern, nach dem Lauf – auch nach +# einem Abbruch – wiederherstellen. Bleibt nach einem harten Abbruch (kill -9) eine +# Sicherung liegen, spielt der nächste Lauf sie zuerst ein; sie hält den echten Stand. +PROJEKT=$(CDPATH= cd -- "$TEST_DIR/../.." && pwd) +einstellungen() { (cd "$PROJEKT" && docker compose exec -T -u www-data php php scripts/e2e-einstellungen.php "$1" --still); } + +einstellungen wiederherstellen \ + || echo "WARNUNG: Alte Einstellungssicherung nicht einspielbar." >&2 +einstellungen sichern \ + || echo "WARNUNG: Einstellungen nicht gesichert — der Lauf kann sie verändern." >&2 +trap 'einstellungen wiederherstellen || echo "WARNUNG: Einstellungen nicht wiederhergestellt (scripts/e2e-einstellungen.php wiederherstellen)." >&2' EXIT +trap 'exit 130' INT TERM + # Zugangsdaten des Testnutzers durchreichen; der Runner kennt nur BASE_URL. cd "$RUNNER_DIR" -TEST_DIR="$TEST_DIR" BASE_URL="$BASE_URL" exec docker compose run --rm \ +TEST_DIR="$TEST_DIR" BASE_URL="$BASE_URL" docker compose run --rm \ ${E2E_USER:+-e E2E_USER="$E2E_USER"} \ ${E2E_PASSWORD:+-e E2E_PASSWORD="$E2E_PASSWORD"} \ playwright diff --git a/tests/e2e/specs/kippschalter.spec.js b/tests/e2e/specs/kippschalter.spec.js new file mode 100644 index 0000000..ec8fb3f --- /dev/null +++ b/tests/e2e/specs/kippschalter.spec.js @@ -0,0 +1,49 @@ +// @ts-check +const { test, expect } = require('@playwright/test') + +const BENUTZER = process.env.E2E_USER || 'e2e' +const PASSWORT = process.env.E2E_PASSWORD || 'e2e-testpasswort' + +async function anmelden(page) { + await page.goto('/login') + await page.fill('#benutzername', BENUTZER) + await page.fill('#passwort', PASSWORT) + await page.click('button[type="submit"]') + await expect(page).toHaveURL(/\/$/) +} + +test.describe('Kippschalter', () => { + test.beforeEach(async ({ page }) => { + await anmelden(page) + }) + + test('Ankreuzfelder erscheinen als Schalter und lassen sich umschalten', async ({ page }) => { + await page.goto('/konten/neu') + const schalter = page.locator('input[name="in_summe"]') + + const aussehen = () => schalter.evaluate((el) => { + const stil = getComputedStyle(el) + return { breite: parseFloat(stil.width), hintergrund: stil.backgroundColor, appearance: stil.appearance } + }) + + const vorher = await aussehen() + expect(vorher.appearance).toBe('none') + expect(vorher.breite).toBeGreaterThan(30) + + const war = await schalter.isChecked() + await schalter.click() + expect(await schalter.isChecked()).toBe(!war) + // Die Farbe läuft über eine kurze Übergangsanimation. + await expect.poll(async () => (await aussehen()).hintergrund).not.toBe(vorher.hintergrund) + }) + + test('Zeilenauswahl in den Listen bleibt ein gewöhnliches Ankreuzfeld', async ({ page }) => { + await page.goto('/banking/umsaetze') + const auswahl = page.locator('input.alle-waehlen').first() + if (await auswahl.count() === 0) { + test.skip(true, 'keine Umsätze vorhanden') + } + const appearance = await auswahl.evaluate((el) => getComputedStyle(el).appearance) + expect(appearance).not.toBe('none') + }) +})