diff --git a/Changes.md b/Changes.md index 45fc381..9e07578 100644 --- a/Changes.md +++ b/Changes.md @@ -20,6 +20,14 @@ - Eigenes SVG, keine Diagrammbibliothek. Die Farben stehen als Klasse am Element und folgen damit dem hellen wie dem dunklen Thema; jedes Balkenstück trägt ein `` als Tooltip, ohne eine Zeile JavaScript. +- **Übrige Konten (n)** ist ein Link: Er führt eine Ebene tiefer (`/?ebene=N`) und zeigt + die nächstkleineren Konten einzeln – beliebig tief, bis der Rest in die Palette passt. + Unter der Legende steht der Weg zurück. Jedes Konto erscheint auf genau einer Ebene, und + eine zu hohe Ebene fällt auf die letzte zurück (`Vermoegensentwicklung::ebenen()`). +- Die Balken nutzen die Kachelbreite aus, indem sie **auseinanderrücken**, nicht indem sie + breiter werden: Die Zeichenfläche rechnet waagerecht in Prozent, die Balkenbreite bleibt + bei 16 Punkten. Achsenbeschriftung und Zeichenfläche sind dafür zwei SVGs nebeneinander – + ein einzelnes könnte von seinen Prozentangaben keine feste Achsenbreite abziehen. - Die Kachel ist so groß wie ihre Nachbarn: Der Ausgriff über zwei Rasterspalten (`.kachel.breit`) und das engere Cockpit-Raster (`.kachelraster.dicht`) entfallen wieder. - Die Legende steht unter der Zeichnung statt daneben und in kleinerer Schrift (.74rem); diff --git a/docs/cockpit.md b/docs/cockpit.md index 902bc83..20443f8 100644 --- a/docs/cockpit.md +++ b/docs/cockpit.md @@ -56,6 +56,23 @@ zusammengefasst – zwei Konten in derselben Farbe wären in der Legende nicht a halten. Jede Serie setzt `--farbe`; davon leben der Balken (`fill`) und der Legendenpunkt (`background`) gleichermaßen, und beide folgen dem hellen wie dem dunklen Thema. +### Ebenen: in „Übrige Konten“ hineinsehen + +Der Legendeneintrag **Übrige Konten (n)** ist ein Link. Er führt auf `/?ebene=1`: Dort +stehen die nächstelf Konten einzeln, und was dann noch bleibt, sammelt sich erneut. So +geht es beliebig tief, bis der Rest in die Palette passt – dann entfällt das Sammelstück, +und das ist die letzte Ebene. `Vermoegensentwicklung::ebenen()` rechnet aus, wie viele es +sind; eine zu hohe Ebene fällt auf die letzte zurück, statt eine leere Kachel zu zeigen. + +Zurück geht es über die Zeile unter der Legende: *eine Ebene zurück* und, ab der dritten, +*zu den größten Konten*. + +Jedes Konto steht auf genau einer Ebene: Was oben schon eine eigene Farbe hatte, ist im +Sammelstück nicht mehr enthalten. Sonst wüchse der Rest mit jeder Ebene, statt zu +schrumpfen. Weil auf tieferen Ebenen die großen Konten fehlen, passt sich die Achse an – +die kleinen Konten werden dadurch überhaupt erst sichtbar. Die Ebene steht in der Adresse +und nicht in den Einstellungen: Beim nächsten Aufruf beginnt man wieder oben. + ### SVG von Hand Keine Diagrammbibliothek (siehe `PLAN.md`): `src/Views/cockpit/vermoegensdiagramm.php` @@ -67,11 +84,16 @@ Die Achse rundet über `Vermoegensentwicklung::skala()` auf Schritte von 1, 2, 2 mal einer Zehnerpotenz und schließt die Null immer ein. Jedes Balkenstück trägt ein `<title>` mit Konto, Monat und Betrag – das ist der Tooltip, ohne eine Zeile JavaScript. -Die Maße stehen in echten Bildpunkten: Ein Balken ist 18 Punkte breit, zwischen zwei -Monaten bleiben 3. Damit das auch auf dem Schirm so ankommt, trägt das `<svg>` Breite und -Höhe als Attribute und wird **nicht** auf die Kachel gestreckt; `max-width: 100%` lässt es -erst mitschrumpfen, wenn die Kachel schmaler wird als die Zeichnung – in der dreispaltigen -Kachel also auf gut 90 Prozent. +Senkrecht rechnet die Zeichnung in Bildpunkten, waagerecht in Prozent: Die Balkenmitte von +Monat *i* liegt bei `(i + 0,5) / 12` der Fläche, der Balken selbst ist feste 16 Punkte breit +und wird per `transform` um seine halbe Breite nach links gerückt. In einer breiten Kachel +rücken die Balken damit **auseinander**, statt breiter zu werden. + +Deshalb sind es zwei SVGs nebeneinander (`.diagrammflaeche`): links die Achsenbeschriftung +mit fester Breite, rechts die Zeichenfläche. In einem einzigen SVG ginge das nicht – +Prozentangaben rechnen immer auf die volle Breite, und ein Attribut kann davon keine feste +Achsenbreite abziehen. Ein `viewBox` verbietet sich aus demselben Grund: Es würde alles +gleichmäßig strecken, die Balken eingeschlossen. Die Kachel ist eine gewöhnliche Kachel des Cockpit-Rasters, so breit wie ihre Nachbarn: Ein Ausgriff über zwei Spalten machte sie zum Fremdkörper zwischen den anderen dreien. @@ -84,6 +106,8 @@ Tooltip und in der Kachel *Kontostände*; in der Legende kosteten sie nur Breite * `tests/13_VermoegensentwicklungTest.php` – Zeitraum, Fortschreibung der Stände, Umbuchungen, Σ-Kennzeichen, Schalter „nur aktive“, Zusammenfassen jenseits der Palette, - Skala + Ebenen (jedes Konto genau einmal, letzte Ebene ohne Sammelstück, zu tiefe Ebene fällt + zurück), Skala * `tests/e2e/specs/smoke.spec.js` – die Kachel zeichnet zwölf Balken, keine Farbe der - Legende kommt doppelt vor, und der Schalter behält seinen Zustand über den Neuaufruf + Legende kommt doppelt vor, der Schalter behält seinen Zustand über den Neuaufruf, und + *Übrige Konten* führt eine Ebene tiefer und wieder zurück diff --git a/public/static/app.css b/public/static/app.css index a852fdd..3599ee2 100644 --- a/public/static/app.css +++ b/public/static/app.css @@ -361,23 +361,30 @@ form.inline { display: inline; margin: 0; } .serie-11 { --farbe: #e07b91; } .serie-12 { --farbe: #7b8a99; } -/* Das SVG bringt seine Maße als Attribute mit und wird nicht auf die Kachel - gestreckt: Nur so sind die 18 Punkte Balkenbreite auch 18 Bildpunkte. Erst - wenn die Kachel schmaler wird als die Zeichnung, skaliert sie mit – die - Legende darunter nimmt dann die volle Kachelbreite. */ .diagrammbereich { display: flex; flex-direction: column; - align-items: flex-start; gap: .6rem; } +/* Achsenbeschriftung und Zeichenfläche sind zwei SVGs nebeneinander. Die Achse + ist fest, die Fläche nimmt den Rest – und weil deren Balken waagerecht in + Prozent sitzen, rücken sie in einer breiten Kachel auseinander, statt breiter + zu werden. Ein einziges SVG könnte das nicht: Prozentangaben rechnen immer + auf die volle Breite, die feste Achse ließe sich nicht abziehen. */ +.diagrammflaeche { + display: flex; + align-items: flex-start; + width: 100%; +} + +.diagrammachse { flex: none; } + .diagramm { - max-width: 100%; - height: auto; + flex: 1 1 0; + min-width: 0; display: block; overflow: visible; - flex: 0 1 auto; } .diagramm rect { fill: var(--farbe); } @@ -443,6 +450,22 @@ form.inline { display: inline; margin: 0; } white-space: nowrap; } +/* „Übrige Konten“ führt eine Ebene tiefer: Der Eintrag ist ein Link und sieht + auch danach aus – sonst probierte ihn niemand aus. */ +.legendenname.tiefer { + color: var(--akzent); + text-decoration: underline dotted; + text-underline-offset: 2px; +} + +.legendenname.tiefer:hover { text-decoration: underline; } + +.diagrammpfad { + margin: 0; + font-size: .74rem; + color: var(--text-gedaempft); +} + /* ── Ein/Aus-Schalter ──────────────────────────────────────────────────────── Die Bahn mit dem laufenden Knopf, wie man ihn von Mobilgeräten kennt. Ein diff --git a/src/Controllers/CockpitController.php b/src/Controllers/CockpitController.php index e64e8f4..c5956d0 100644 --- a/src/Controllers/CockpitController.php +++ b/src/Controllers/CockpitController.php @@ -25,10 +25,20 @@ class CockpitController extends BaseController $nurAktive = Einstellung::getBool(self::SCHALTER_NUR_AKTIVE, true); + // Die Ebene steht in der Adresse, nicht in den Einstellungen: Wer in die + // „Übrigen Konten“ hineinsieht, will beim nächsten Aufruf wieder oben + // anfangen – und kann den Blick trotzdem als Lesezeichen ablegen. + $ebene = max(0, (int)$this->query('ebene')); + $this->render('cockpit/index.php', [ 'titel' => 'Cockpit', 'uebersicht' => Konto::uebersicht(), - 'vermoegen' => Vermoegensentwicklung::reihe($nurAktive), + 'vermoegen' => Vermoegensentwicklung::reihe( + $nurAktive, + Vermoegensentwicklung::MONATE, + null, + $ebene + ), 'nurAktive' => $nurAktive, ]); } diff --git a/src/Services/Vermoegensentwicklung.php b/src/Services/Vermoegensentwicklung.php index cf790e5..4a521de 100644 --- a/src/Services/Vermoegensentwicklung.php +++ b/src/Services/Vermoegensentwicklung.php @@ -24,17 +24,23 @@ class Vermoegensentwicklung public const SERIEN = 12; /** + * @param int $ebene 0 zeigt die größten Konten, jede weitere Ebene die + * jeweils nächstkleineren aus „Übrige Konten“. + * * @return array{ * monate: list<array{jahr:int, monat:int, bis:string, kuerzel:string, jahrKurz:string}>, * konten: list<array{id:int, name:string, werte:list<float>}>, * summen: list<float>, - * hatWerte: bool + * hatWerte: bool, + * ebene: int, + * ebenen: int * } */ public static function reihe( bool $nurAktive = true, int $anzahl = self::MONATE, - ?string $heute = null + ?string $heute = null, + int $ebene = 0 ): array { $monate = self::monate($anzahl, $heute); $erster = $monate[0]; @@ -75,8 +81,11 @@ class Vermoegensentwicklung } } + $ebenen = self::ebenen(count($konten)); + $ebene = max(0, min($ebene, $ebenen - 1)); + if (count($konten) > self::SERIEN) { - $konten = self::zusammenfassen($konten, count($monate)); + $konten = self::zusammenfassen($konten, count($monate), $ebene); } $hatWerte = false; @@ -90,18 +99,38 @@ class Vermoegensentwicklung 'konten' => $konten, 'summen' => array_values($summen), 'hatWerte' => $hatWerte || $konten !== [], + 'ebene' => $ebene, + 'ebenen' => $ebenen, ]; } + /** + * Wie viele Ebenen es braucht, um `$konten` Konten einzeln zu zeigen: Jede + * Ebene zeigt so viele Konten, wie Farbtöne da sind – eine weniger, wenn + * noch ein Stück „Übrige Konten“ dazukommt. + */ + public static function ebenen(int $konten): int + { + if ($konten <= self::SERIEN) { + return 1; + } + + return 1 + (int)ceil(($konten - self::SERIEN) / (self::SERIEN - 1)); + } + /** * Mehr Konten als Farbtöne: Die größten behalten ihre eigene Farbe, der Rest * wird zu einem Balkenstück zusammengefasst. Sonst bekämen zwei Konten * denselben Ton – und wer die Legende liest, ordnete falsch zu. * + * `$ebene` blättert in diesem Rest weiter: Ebene 1 überspringt die größten + * Konten und zeigt die nächstkleineren einzeln, Ebene 2 die darauf folgenden + * und so fort. Passt der Rest in die Palette, entfällt das Sammelstück. + * * @param list<array{id:int, name:string, werte:list<float>}> $konten * @return list<array{id:int, name:string, werte:list<float>}> */ - private static function zusammenfassen(array $konten, int $spalten): array + private static function zusammenfassen(array $konten, int $spalten, int $ebene = 0): array { $groesse = []; foreach ($konten as $nummer => $konto) { @@ -109,14 +138,20 @@ class Vermoegensentwicklung } arsort($groesse); + $rangfolge = array_slice(array_keys($groesse), $ebene * (self::SERIEN - 1)); + + // Passt der Rest vollständig in die Palette, braucht es kein Sammelstück + // mehr – dann ist das die letzte Ebene. + $sammelt = count($rangfolge) > self::SERIEN; + // Die größten behalten, aber in ihrer ursprünglichen Reihenfolge – der // Stapel soll nicht bei jedem Aufruf anders sortiert sein. - $eigene = array_slice(array_keys($groesse), 0, self::SERIEN - 1); + $eigene = $sammelt ? array_slice($rangfolge, 0, self::SERIEN - 1) : $rangfolge; sort($eigene); - $rest = array_fill(0, $spalten, 0.0); + $rest = array_fill(0, $spalten, 0.0); $restKonten = 0; - $ergebnis = []; + $ergebnis = []; foreach ($konten as $nummer => $konto) { if (in_array($nummer, $eigene, true)) { @@ -124,17 +159,25 @@ class Vermoegensentwicklung continue; } + // Was auf einer früheren Ebene stand, ist hier gar nicht mehr dabei – + // sonst wüchse das Sammelstück mit jeder Ebene statt zu schrumpfen. + if (!in_array($nummer, $rangfolge, true)) { + continue; + } + foreach ($konto['werte'] as $spalte => $wert) { $rest[$spalte] += $wert; } $restKonten++; } - $ergebnis[] = [ - 'id' => 0, - 'name' => 'Übrige Konten (' . $restKonten . ')', - 'werte' => array_map(static fn(float $wert): float => round($wert, 2), $rest), - ]; + if ($restKonten > 0) { + $ergebnis[] = [ + 'id' => 0, + 'name' => 'Übrige Konten (' . $restKonten . ')', + 'werte' => array_map(static fn(float $wert): float => round($wert, 2), $rest), + ]; + } return $ergebnis; } diff --git a/src/Views/cockpit/vermoegensdiagramm.php b/src/Views/cockpit/vermoegensdiagramm.php index e343d8c..1c0e3e5 100644 --- a/src/Views/cockpit/vermoegensdiagramm.php +++ b/src/Views/cockpit/vermoegensdiagramm.php @@ -16,13 +16,17 @@ use Finanzmanager\View; * monate: list<array{jahr:int, monat:int, bis:string, kuerzel:string, jahrKurz:string}>, * konten: list<array{id:int, name:string, werte:list<float>}>, * summen: list<float>, - * hatWerte: bool + * hatWerte: bool, + * ebene: int, + * ebenen: int * } $reihe */ $monate = $reihe['monate']; $konten = $reihe['konten']; $anzahl = count($monate); +$ebene = $reihe['ebene']; +$ebenen = $reihe['ebenen']; // Die Achse muss den gestapelten Positiv- und Negativteil jedes Balkens fassen. // Die Nettosumme liegt immer dazwischen und braucht keine eigene Grenze. @@ -44,29 +48,25 @@ foreach ($monate as $nummer => $monat) { $skala = Vermoegensentwicklung::skala($grenzwerte); -// Maße in echten Bildpunkten: Das SVG trägt Breite und Höhe als Attribute und -// wird deshalb nicht auf die Kachel gestreckt. Ein Balken ist 18 Punkte breit, -// zwischen zwei Monaten bleiben 3 – erst unterhalb der eigenen Breite skaliert -// die Zeichnung mit (max-width in app.css). -$balken = 18; -$luecke = 3; -$links = 52; -$rechts = 6; -$oben = 10; -$unten = 32; - -$spalte = $balken + $luecke; -$plotB = $spalte * $anzahl; -$plotH = 150; -$breite = $links + $plotB + $rechts; -$hoehe = $oben + $plotH + $unten; +// Senkrecht in echten Bildpunkten, waagerecht in Prozent: Die Zeichenfläche +// nimmt die Breite, die die Kachel hergibt, und die Balken rücken darin +// auseinander, statt breiter zu werden. Die Achsenbeschriftung sitzt in einem +// eigenen, festen SVG daneben – sonst müsste die Prozentrechnung ihre Breite +// abziehen, und das kann ein SVG-Attribut nicht. +$balken = 16; +$achse = 56; +$oben = 10; +$plotH = 150; +$unten = 32; +$hoehe = $oben + $plotH + $unten; $y = static function (float $wert) use ($skala, $oben, $plotH): float { return round($oben + $plotH * ($skala['max'] - $wert) / ($skala['max'] - $skala['min']), 2); }; -$mitte = static function (int $nummer) use ($links, $spalte): float { - return round($links + ($nummer + 0.5) * $spalte, 2); +/** Die Mitte einer Monatsspalte, als Anteil der Zeichenfläche. */ +$mitte = static function (int $nummer) use ($anzahl): string { + return round(($nummer + 0.5) / $anzahl * 100, 3) . '%'; }; $achsenzahl = static function (float $wert): string { @@ -82,60 +82,70 @@ $zeitraum = $monate[0]['kuerzel'] . ' ' . $monate[0]['jahrKurz'] ?> <div class="diagrammbereich"> -<svg class="diagramm" width="<?= $breite ?>" height="<?= $hoehe ?>" - viewBox="0 0 <?= $breite ?> <?= $hoehe ?>" role="img" - preserveAspectRatio="xMidYMid meet" - aria-label="Kontostände je Monatsende, <?= View::h($zeitraum) ?>"> - <title>Vermögensentwicklung <?= View::h($zeitraum) ?> +
+ - - - - + + + Vermögensentwicklung - + + + - $monat): ?> - - $konto): ?> + + + $monat): ?> 0) { - $von = $oberkante; - $bis = $oberkante + $wert; - $oberkante = $bis; - } else { - $von = $unterkante + $wert; - $bis = $unterkante; - $unterkante = $von; - } + $oberkante = 0.0; + $unterkante = 0.0; ?> - - - - + + + $konto): ?> + " y="" - text-anchor="middle"> - - - - - + if ($wert > 0) { + $von = $oberkante; + $bis = $oberkante + $wert; + $oberkante = $bis; + } else { + $von = $unterkante + $wert; + $bis = $unterkante; + $unterkante = $von; + } + ?> + + <?= View::h($konto['name'] . ' – ' . $monat['kuerzel'] . ' ' . $monat['jahr'] + . ': ' . View::betrag($wert)) ?> + + + + + + + + + + +
+ + 0): ?> +

+ Ebene von · + ← eine Ebene zurück + 1): ?> + · zu den größten Konten + +

+ diff --git a/tests/13_VermoegensentwicklungTest.php b/tests/13_VermoegensentwicklungTest.php index 4b0a130..1880b81 100644 --- a/tests/13_VermoegensentwicklungTest.php +++ b/tests/13_VermoegensentwicklungTest.php @@ -31,6 +31,7 @@ class VermoegensentwicklungTest extends TestCase $this->summenkennzeichenFiltertNicht(); $this->schalterFiltertStillgelegteKonten(); $this->mehrKontenAlsFarbenWerdenZusammengefasst(); + $this->ebenenBlaetternDurchDieUebrigenKonten(); $this->skalaRundetAufLesbareSchritte(); } @@ -54,9 +55,9 @@ class VermoegensentwicklungTest extends TestCase $this->kategorie = Kategorie::findenOderAnlegenNachPfad('600 - Vermögenstest', 'ausgabe'); } - private function reihe(bool $nurAktive = true): array + private function reihe(bool $nurAktive = true, int $ebene = 0): array { - return Vermoegensentwicklung::reihe($nurAktive, 12, self::HEUTE); + return Vermoegensentwicklung::reihe($nurAktive, 12, self::HEUTE, $ebene); } /** @return array|null */ @@ -238,6 +239,63 @@ class VermoegensentwicklungTest extends TestCase ); } + /** + * „Übrige Konten“ ist keine Sackgasse: Jede Ebene zeigt die nächstkleineren + * Konten einzeln, bis der Rest in die Palette passt. + */ + private function ebenenBlaetternDurchDieUebrigenKonten(): void + { + $this->assertEquals(1, Vermoegensentwicklung::ebenen(12), 'Was in die Palette passt, hat eine Ebene'); + $this->assertEquals(2, Vermoegensentwicklung::ebenen(13), 'Ein Konto zu viel macht die zweite Ebene auf'); + $this->assertEquals(2, Vermoegensentwicklung::ebenen(23), 'Elf eigene und zwölf übrige: noch zwei Ebenen'); + $this->assertEquals(3, Vermoegensentwicklung::ebenen(24), 'Eines mehr, und es braucht eine dritte'); + + // Aus dem vorigen Test stehen mehr Konten da, als die Palette Töne hat. + $erste = $this->reihe(true, 0); + $zweite = $this->reihe(true, 1); + + $this->assertGreaterThan(1, $erste['ebenen'], 'Der Testbestand füllt mehr als eine Ebene'); + $this->assertEquals(0, $erste['ebene'], 'Ohne Angabe steht man auf der ersten Ebene'); + $this->assertEquals(1, $zweite['ebene'], 'Die zweite Ebene ist erreichbar'); + + $ids = static fn(array $reihe): array => array_map( + static fn(array $konto): int => $konto['id'], + $reihe['konten'] + ); + + $this->assert(in_array(0, $ids($erste), true), 'Die erste Ebene sammelt den Rest ein'); + $this->assertEquals( + [], + array_intersect(array_diff($ids($erste), [0]), $ids($zweite)), + 'Kein Konto steht auf zwei Ebenen' + ); + + // Die letzte Ebene braucht kein Sammelstück mehr – sonst käme man nie an. + $letzte = $this->reihe(true, $erste['ebenen'] - 1); + $this->assert(!in_array(0, $ids($letzte), true), 'Auf der letzten Ebene stehen nur noch echte Konten'); + + // Zusammen ergeben alle Ebenen wieder den ganzen Bestand. + $gesehen = []; + for ($ebene = 0; $ebene < $erste['ebenen']; $ebene++) { + $gesehen = array_merge($gesehen, array_diff($ids($this->reihe(true, $ebene)), [0])); + } + $this->assertEquals( + count($gesehen), + count(array_unique($gesehen)), + 'Über alle Ebenen hinweg taucht kein Konto doppelt auf' + ); + $this->assertGreaterThan( + Vermoegensentwicklung::SERIEN, + count($gesehen), + 'Und in Summe sind es mehr Konten, als eine Ebene fasst' + ); + + // Eine Ebene jenseits der letzten gibt es nicht: Sie fällt darauf zurück. + $zuTief = $this->reihe(true, 99); + $this->assertEquals($erste['ebenen'] - 1, $zuTief['ebene'], 'Zu tief geraten landet man auf der letzten'); + $this->assertEquals($ids($letzte), $ids($zuTief), 'Mit denselben Konten'); + } + private function skalaRundetAufLesbareSchritte(): void { $skala = Vermoegensentwicklung::skala([0.0, 1234.0, 4870.0]); diff --git a/tests/e2e/specs/smoke.spec.js b/tests/e2e/specs/smoke.spec.js index 6fbd491..fa187c6 100644 --- a/tests/e2e/specs/smoke.spec.js +++ b/tests/e2e/specs/smoke.spec.js @@ -72,6 +72,30 @@ test.describe('Grundgerüst', () => { 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)