Seitenleiste: Marke mit Münzstapel, ein Klick schiebt sie auf Symbolbreite
Die Marke oben in der Seitenleiste trägt jetzt das Coinstack-Symbol aus den Altdaten und den zweifarbigen Schriftzug Finanz|manager – wie die sidebar-brand im Steuerkram. Sie ist zugleich der Schalter: Ein Klick schiebt die Leiste von 232 px auf 64 px zusammen, ein Klick auf das Symbol klappt sie wieder auf. Die Menüpunkte behalten dabei Ziel und Symbol. Der Zustand steht in einem Cookie und nicht im localStorage: So rendert der Server die Leiste schon in der richtigen Breite, statt sie bei jedem Seitenwechsel kurz breit aufblitzen zu lassen. Die Beschriftungen wandern eingeklappt per clip-path aus dem Bild statt per display:none – sonst verlören die Verweise ihren Namen für Vorleseprogramme. Nebenbei: Die Marke hieß bisher .marke, wie die kleine Statusmarke weiter unten im Stilblatt, deren Regeln sie überschrieben. Sie heißt jetzt .seitenleisten-marke. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0b4a216954
commit
280664b8a3
7 changed files with 440 additions and 9 deletions
23
Changes.md
23
Changes.md
|
|
@ -1,5 +1,28 @@
|
|||
# Änderungen
|
||||
|
||||
## 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
|
||||
Altdaten als Symbol (`public/static/coinstack.png`, auf 128 px gerechnet) und daneben der
|
||||
zweifarbige Schriftzug **Finanz**manager – dieselbe Machart wie die `sidebar-brand` im
|
||||
Schwesterprojekt Steuerkram.
|
||||
|
||||
Die Marke ist zugleich der **Schalter**: Ein Klick schiebt die Seitenleiste von 232 px auf
|
||||
64 px zusammen, dann stehen dort nur noch das Markensymbol und die Symbole der Menüpunkte –
|
||||
mit unveränderten Zielen; der gewonnene Platz geht an den Hauptbereich. Ein Klick auf das
|
||||
Symbol ganz oben klappt sie wieder auf ihre alte Breite auf.
|
||||
|
||||
Zwei Feinheiten: Der Zustand steht in einem **Cookie**, nicht im `localStorage` – so rendert
|
||||
der Server die Leiste schon in der richtigen Breite, statt sie bei jedem Seitenwechsel kurz
|
||||
breit aufblitzen zu lassen. Und die Beschriftungen werden eingeklappt nur **aus dem Bild
|
||||
geschoben** (`clip-path`), nicht mit `display:none` entfernt: Sonst verlören die Verweise
|
||||
ihren Namen für Vorleseprogramme. In schmalen Fenstern, wo die Navigation ohnehin waagerecht
|
||||
liegt, bleibt alles wie gehabt.
|
||||
|
||||
Nebenbei behoben: Die Marke der Seitenleiste hieß bisher schlicht `.marke` – dieselbe Klasse
|
||||
wie die kleine Statusmarke weiter unten im Stilblatt, deren Regeln sie überschrieben. Sie
|
||||
heißt jetzt `.seitenleisten-marke`.
|
||||
|
||||
## Bankleitzahlen-Verzeichnis: die FinTS-Adresse nachschlagen statt suchen (18.08.2026)
|
||||
|
||||
Beim Anlegen einer FinTS-Verbindung fehlt in aller Regel genau eine Angabe: der
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
--navi-text: #cdd7e2;
|
||||
--navi-text-aktiv: #ffffff;
|
||||
--navi-aktiv: #1f6feb;
|
||||
--navi-marke: #6ea8fe;
|
||||
--positiv: #1a7f4b;
|
||||
--negativ: #c0392b;
|
||||
--warnung: #9a6700;
|
||||
|
|
@ -117,14 +118,41 @@ code {
|
|||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.marke {
|
||||
/* Die Marke der Seitenleiste hat eine eigene Klasse: ".marke" allein ist
|
||||
weiter unten die kleine Statusmarke, deren Regeln hier sonst gewönnen. */
|
||||
.seitenleisten-marke {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .6rem;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, .08);
|
||||
font-family: inherit;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
padding: 1.25rem 1.25rem 1rem;
|
||||
padding: 1.1rem 1.25rem;
|
||||
letter-spacing: .01em;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.seitenleisten-marke:hover { background: rgba(255, 255, 255, .05); }
|
||||
|
||||
|
||||
.marken-symbol {
|
||||
width: 1.6em;
|
||||
height: 1.6em;
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.marken-text { color: #fff; white-space: nowrap; }
|
||||
|
||||
/* Wie im Steuerkram: die zweite Worthälfte trägt die Akzentfarbe. */
|
||||
.marken-text span { color: var(--navi-marke); }
|
||||
|
||||
.seitenleiste ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
|
|
@ -166,6 +194,32 @@ code {
|
|||
min-width: 0;
|
||||
}
|
||||
|
||||
/* ── Eingeklappte Seitenleiste ───────────────────────────────────────────── */
|
||||
|
||||
/* Ein Klick auf die Marke schiebt die Leiste auf Symbolbreite zusammen. Die
|
||||
Beschriftungen verschwinden nur aus dem Bild, nicht aus dem Baum – sonst
|
||||
verlören die Verweise ihren Namen für Vorleseprogramme. */
|
||||
.rahmen.eingeklappt { grid-template-columns: 64px 1fr; }
|
||||
|
||||
.rahmen.eingeklappt .seitenleisten-marke,
|
||||
.rahmen.eingeklappt .seitenleiste a {
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
|
||||
.rahmen.eingeklappt .marken-text,
|
||||
.rahmen.eingeklappt .beschriftung {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Werkzeugleiste ──────────────────────────────────────────────────────── */
|
||||
|
||||
.werkzeugleiste {
|
||||
|
|
@ -2070,7 +2124,25 @@ table.liste tr.luecke td {
|
|||
/* ── Schmale Fenster ─────────────────────────────────────────────────────── */
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.rahmen { grid-template-columns: 1fr; }
|
||||
.rahmen,
|
||||
.rahmen.eingeklappt { grid-template-columns: 1fr; }
|
||||
|
||||
/* Waagerecht gibt es nichts zu sparen: Beschriftungen bleiben sichtbar. */
|
||||
.rahmen.eingeklappt .marken-text,
|
||||
.rahmen.eingeklappt .beschriftung {
|
||||
position: static;
|
||||
width: auto;
|
||||
height: auto;
|
||||
clip-path: none;
|
||||
}
|
||||
|
||||
.rahmen.eingeklappt .seitenleisten-marke,
|
||||
.rahmen.eingeklappt .seitenleiste a {
|
||||
justify-content: flex-start;
|
||||
gap: .6rem;
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 1.25rem;
|
||||
}
|
||||
|
||||
.seitenleiste {
|
||||
position: static;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,24 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
// Die Marke klappt die Seitenleiste auf Symbolbreite zusammen und wieder
|
||||
// auf. Der Zustand steht in einem Cookie und nicht im localStorage: So
|
||||
// rendert der Server die Leiste schon in der richtigen Breite, statt sie
|
||||
// bei jedem Seitenwechsel kurz breit aufblitzen zu lassen.
|
||||
const schalter = document.querySelector('[data-seitenleiste-schalter]');
|
||||
const rahmen = document.querySelector('.rahmen');
|
||||
if (schalter && rahmen) {
|
||||
schalter.addEventListener('click', function () {
|
||||
const eingeklappt = rahmen.classList.toggle('eingeklappt');
|
||||
|
||||
schalter.setAttribute('aria-expanded', eingeklappt ? 'false' : 'true');
|
||||
schalter.title = eingeklappt ? 'Seitenleiste ausklappen' : 'Seitenleiste einklappen';
|
||||
|
||||
document.cookie = 'seitenleiste=' + (eingeklappt ? 'eingeklappt' : 'offen')
|
||||
+ '; path=/; max-age=31536000; samesite=lax';
|
||||
});
|
||||
}
|
||||
|
||||
// Formulare mit data-bestaetigen fragen vor dem Absenden nach.
|
||||
// In der Capture-Phase, damit die Rückfrage auch dann greift, wenn ein
|
||||
// anderer Handler das Ereignis unterwegs stoppt.
|
||||
|
|
|
|||
BIN
public/static/coinstack.png
Normal file
BIN
public/static/coinstack.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
|
|
@ -20,6 +20,11 @@ $navigation = [
|
|||
['pfad' => '/verwaltung', 'name' => 'Verwaltung', 'icon' => '⚙', 'auch' => ['/termine', '/vertraege']],
|
||||
];
|
||||
|
||||
// Ob die Seitenleiste eingeklappt ist, merkt sich ein Cookie. Das Skript
|
||||
// setzt es beim Umschalten; hier entscheidet es schon beim Rendern, damit die
|
||||
// breite Leiste nicht bei jedem Seitenwechsel kurz aufblitzt.
|
||||
$eingeklappt = ($_COOKIE['seitenleiste'] ?? '') === 'eingeklappt';
|
||||
|
||||
/** @param list<string> $auch */
|
||||
$istAktiv = static function (string $pfad, array $auch = []) use ($aktuellerPfad): bool {
|
||||
if (!is_string($aktuellerPfad)) {
|
||||
|
|
@ -50,16 +55,26 @@ $istAktiv = static function (string $pfad, array $auch = []) use ($aktuellerPfad
|
|||
</head>
|
||||
<body>
|
||||
<?php include __DIR__ . '/meldungen.php'; ?>
|
||||
<div class="rahmen">
|
||||
<nav class="seitenleiste" aria-label="Hauptnavigation">
|
||||
<div class="marke">Finanzmanager</div>
|
||||
<div class="rahmen<?= $eingeklappt ? ' eingeklappt' : '' ?>">
|
||||
<nav class="seitenleiste" id="seitenleiste" aria-label="Hauptnavigation">
|
||||
<!-- Die Marke ist zugleich der Schalter: Ein Klick schiebt die
|
||||
Seitenleiste auf Symbolbreite zusammen und wieder auf. -->
|
||||
<button type="button" class="seitenleisten-marke" data-seitenleiste-schalter
|
||||
aria-controls="seitenleiste" aria-expanded="<?= $eingeklappt ? 'false' : 'true' ?>"
|
||||
title="<?= $eingeklappt ? 'Seitenleiste ausklappen' : 'Seitenleiste einklappen' ?>">
|
||||
<img src="/static/coinstack.png" alt="" class="marken-symbol">
|
||||
<span class="marken-text">Finanz<span>manager</span></span>
|
||||
</button>
|
||||
<ul>
|
||||
<?php foreach ($navigation as $eintrag): ?>
|
||||
<li>
|
||||
<?php /* title bleibt hinten: Der Navigationstest prüft href und
|
||||
class in dieser Reihenfolge unmittelbar hintereinander. */ ?>
|
||||
<a href="<?= View::h($eintrag['pfad']) ?>"
|
||||
class="<?= $istAktiv($eintrag['pfad'], $eintrag['auch'] ?? []) ? 'aktiv' : '' ?>">
|
||||
class="<?= $istAktiv($eintrag['pfad'], $eintrag['auch'] ?? []) ? 'aktiv' : '' ?>"
|
||||
title="<?= View::h($eintrag['name']) ?>">
|
||||
<span class="icon" aria-hidden="true"><?= $eintrag['icon'] ?></span>
|
||||
<?= View::h($eintrag['name']) ?>
|
||||
<span class="beschriftung"><?= View::h($eintrag['name']) ?></span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
|
|
|
|||
231
tests/33_SeitenleisteEinklappenTest.php
Normal file
231
tests/33_SeitenleisteEinklappenTest.php
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Die Marke der Seitenleiste trägt das Coinstack-Symbol und den zweifarbigen
|
||||
* Schriftzug – und dient zugleich als Schalter: Ein Klick schiebt die Leiste
|
||||
* auf Symbolbreite zusammen, ein zweiter klappt sie wieder auf.
|
||||
*/
|
||||
class SeitenleisteEinklappenTest extends TestCase
|
||||
{
|
||||
protected string $testName = 'Test 33: Seitenleiste ein- und ausklappen';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->markeZeigtSymbolUndSchriftzug();
|
||||
$this->markeIstDerSchalter();
|
||||
$this->cookieEntscheidetUeberDieBreite();
|
||||
$this->stilblattSchiebtNurDieBeschriftungenWeg();
|
||||
$this->skriptMerktSichDenZustand();
|
||||
}
|
||||
|
||||
private function markeZeigtSymbolUndSchriftzug(): void
|
||||
{
|
||||
$symbol = dirname(__DIR__) . '/public/static/coinstack.png';
|
||||
$this->assert(is_file($symbol), 'Das Markensymbol liegt unter public/static/coinstack.png');
|
||||
$this->assertEquals('image/png', (string)(getimagesize($symbol)['mime'] ?? ''), 'Das Symbol ist ein PNG');
|
||||
|
||||
$leiste = $this->seitenleiste($this->rendere('/'));
|
||||
|
||||
$this->assert(
|
||||
str_contains($leiste, '<img src="/static/coinstack.png" alt="" class="marken-symbol">'),
|
||||
'Die Marke führt das Coinstack-Symbol'
|
||||
);
|
||||
$this->assert(
|
||||
str_contains($leiste, '<span class="marken-text">Finanz<span>manager</span></span>'),
|
||||
'Der Schriftzug ist wie im Steuerkram zweigeteilt'
|
||||
);
|
||||
$this->assert(
|
||||
!str_contains($leiste, '<div class="marke">'),
|
||||
'Die alte Marke als Statusmarken-Div ist verschwunden'
|
||||
);
|
||||
}
|
||||
|
||||
private function markeIstDerSchalter(): void
|
||||
{
|
||||
$leiste = $this->seitenleiste($this->rendere('/'));
|
||||
|
||||
$this->assert(
|
||||
(bool)preg_match('#<button type="button" class="seitenleisten-marke" data-seitenleiste-schalter#', $leiste),
|
||||
'Die Marke ist ein Schalter'
|
||||
);
|
||||
$this->assert(
|
||||
str_contains($leiste, 'aria-controls="seitenleiste"'),
|
||||
'Der Schalter nennt die Seitenleiste, die er bedient'
|
||||
);
|
||||
$this->assert(
|
||||
str_contains($leiste, 'id="seitenleiste"'),
|
||||
'Die Seitenleiste trägt die angesprochene Kennung'
|
||||
);
|
||||
|
||||
// Die Symbole behalten ihr Ziel; nur die Beschriftung bekommt eine
|
||||
// eigene Hülle, damit sie sich eingeklappt wegschieben lässt.
|
||||
foreach (['/', '/konten', '/banking', '/zahlungsverkehr', '/auswertungen', '/planung', '/wertpapiere', '/verwaltung'] as $ziel) {
|
||||
$this->assert(
|
||||
(bool)preg_match('#href="' . preg_quote($ziel, '#') . '"#', $leiste),
|
||||
'Der Menüpunkt ' . $ziel . ' bleibt erreichbar'
|
||||
);
|
||||
}
|
||||
$this->assertEquals(
|
||||
8,
|
||||
substr_count($leiste, '<span class="beschriftung">'),
|
||||
'Jeder Menüpunkt trägt seine Beschriftung in einer eigenen Hülle'
|
||||
);
|
||||
$this->assertEquals(
|
||||
8,
|
||||
substr_count($leiste, '<span class="icon" aria-hidden="true">'),
|
||||
'Und jeder behält sein Symbol'
|
||||
);
|
||||
$this->assert(
|
||||
str_contains($leiste, 'title="Kontenübersicht"'),
|
||||
'Eingeklappt verrät der Titel, wohin ein Symbol führt'
|
||||
);
|
||||
}
|
||||
|
||||
private function cookieEntscheidetUeberDieBreite(): void
|
||||
{
|
||||
$offen = $this->rendere('/');
|
||||
$this->assert(str_contains($offen, '<div class="rahmen">'), 'Ohne Cookie steht die Leiste offen');
|
||||
$this->assert(str_contains($offen, 'aria-expanded="true"'), 'Der Schalter meldet die offene Leiste');
|
||||
$this->assert(str_contains($offen, 'title="Seitenleiste einklappen"'), 'Und lädt zum Einklappen ein');
|
||||
|
||||
$eingeklappt = $this->rendere('/', 'eingeklappt');
|
||||
$this->assert(
|
||||
str_contains($eingeklappt, '<div class="rahmen eingeklappt">'),
|
||||
'Mit Cookie rendert der Server die schmale Leiste sofort'
|
||||
);
|
||||
$this->assert(str_contains($eingeklappt, 'aria-expanded="false"'), 'Der Schalter meldet die schmale Leiste');
|
||||
$this->assert(
|
||||
str_contains($eingeklappt, 'title="Seitenleiste ausklappen"'),
|
||||
'Und lädt zum Ausklappen ein'
|
||||
);
|
||||
|
||||
$unsinn = $this->rendere('/', 'irgendwas');
|
||||
$this->assert(
|
||||
str_contains($unsinn, '<div class="rahmen">'),
|
||||
'Ein fremder Cookie-Wert lässt die Leiste offen'
|
||||
);
|
||||
}
|
||||
|
||||
private function stilblattSchiebtNurDieBeschriftungenWeg(): void
|
||||
{
|
||||
$stil = $this->stilblatt();
|
||||
|
||||
$this->assert(
|
||||
(bool)preg_match('#\.rahmen\.eingeklappt\s*\{[^}]*grid-template-columns:\s*64px 1fr#', $stil),
|
||||
'Eingeklappt bleibt nur eine Symbolspalte stehen'
|
||||
);
|
||||
$this->assert(
|
||||
(bool)preg_match('#\.rahmen\s*\{[^}]*grid-template-columns:\s*232px 1fr#', $stil),
|
||||
'Offen behält die Leiste ihre ursprüngliche Breite'
|
||||
);
|
||||
|
||||
// display:none nähme den Verweisen ihren Namen – deshalb wird die
|
||||
// Beschriftung nur aus dem Bild geschoben.
|
||||
$this->assert(
|
||||
(bool)preg_match(
|
||||
'#\.rahmen\.eingeklappt \.marken-text,\s*\.rahmen\.eingeklappt \.beschriftung\s*\{[^}]*clip-path:\s*inset\(50%\)#',
|
||||
$stil
|
||||
),
|
||||
'Die Beschriftungen verschwinden aus dem Bild, nicht aus dem Baum'
|
||||
);
|
||||
$this->assert(
|
||||
!preg_match('#\.rahmen\.eingeklappt \.marken-symbol[^{]*\{[^}]*display:\s*none#', $stil),
|
||||
'Das Markensymbol bleibt eingeklappt sichtbar'
|
||||
);
|
||||
|
||||
$this->assert(
|
||||
(bool)preg_match('#\.seitenleisten-marke\s*\{[^}]*cursor:\s*pointer#', $stil),
|
||||
'Die Marke zeigt sich als anklickbar'
|
||||
);
|
||||
$this->assert(
|
||||
(bool)preg_match('#\.marken-text span\s*\{\s*color:\s*var\(--navi-marke\)#', $stil),
|
||||
'Die zweite Worthälfte trägt die Akzentfarbe'
|
||||
);
|
||||
$this->assert(
|
||||
(bool)preg_match('#--navi-marke:\s*#', $stil),
|
||||
'Die Akzentfarbe steht als Custom Property'
|
||||
);
|
||||
|
||||
// In schmalen Fenstern liegt die Navigation waagerecht - dort gibt es
|
||||
// nichts einzusparen.
|
||||
$schmal = substr($stil, (int)strpos($stil, '@media (max-width: 820px)'));
|
||||
$this->assert(
|
||||
(bool)preg_match('#\.rahmen,\s*\.rahmen\.eingeklappt\s*\{\s*grid-template-columns:\s*1fr#', $schmal),
|
||||
'Im schmalen Fenster spielt das Einklappen keine Rolle'
|
||||
);
|
||||
$this->assert(
|
||||
(bool)preg_match('#\.rahmen\.eingeklappt \.beschriftung\s*\{[^}]*clip-path:\s*none#', $schmal),
|
||||
'Und die Beschriftungen bleiben dort lesbar'
|
||||
);
|
||||
}
|
||||
|
||||
private function skriptMerktSichDenZustand(): void
|
||||
{
|
||||
$skript = (string)file_get_contents(dirname(__DIR__) . '/public/static/app.js');
|
||||
|
||||
$this->assert(
|
||||
str_contains($skript, "querySelector('[data-seitenleiste-schalter]')"),
|
||||
'Das Skript findet den Schalter'
|
||||
);
|
||||
$this->assert(
|
||||
str_contains($skript, "rahmen.classList.toggle('eingeklappt')"),
|
||||
'Ein Klick klappt die Leiste um'
|
||||
);
|
||||
$this->assert(
|
||||
(bool)preg_match("#document\.cookie = 'seitenleiste='#", $skript),
|
||||
'Der Zustand landet im Cookie, damit der Server ihn kennt'
|
||||
);
|
||||
$this->assert(
|
||||
str_contains($skript, 'samesite=lax'),
|
||||
'Das Cookie reist nicht zu fremden Seiten mit'
|
||||
);
|
||||
$this->assert(
|
||||
str_contains($skript, "schalter.setAttribute('aria-expanded'"),
|
||||
'Der Schalter meldet den neuen Zustand'
|
||||
);
|
||||
}
|
||||
|
||||
private function stilblatt(): string
|
||||
{
|
||||
return (string)file_get_contents(dirname(__DIR__) . '/public/static/app.css');
|
||||
}
|
||||
|
||||
private function rendere(string $pfad, ?string $cookie = null): string
|
||||
{
|
||||
$vorher = $_COOKIE['seitenleiste'] ?? null;
|
||||
if ($cookie === null) {
|
||||
unset($_COOKIE['seitenleiste']);
|
||||
} else {
|
||||
$_COOKIE['seitenleiste'] = $cookie;
|
||||
}
|
||||
|
||||
$titel = 'Test';
|
||||
$flash = [];
|
||||
$benutzer = null;
|
||||
$csrfToken = 'x';
|
||||
$aktuellerPfad = $pfad;
|
||||
|
||||
ob_start();
|
||||
include dirname(__DIR__) . '/src/Views/layout/kopf.php';
|
||||
$html = (string)ob_get_clean();
|
||||
|
||||
if ($vorher === null) {
|
||||
unset($_COOKIE['seitenleiste']);
|
||||
} else {
|
||||
$_COOKIE['seitenleiste'] = $vorher;
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/** Nur die Seitenleiste – Verweise im Inhalt sollen nicht mitzählen. */
|
||||
private function seitenleiste(string $html): string
|
||||
{
|
||||
$anfang = (int)strpos($html, '<nav');
|
||||
$ende = (int)strpos($html, '</nav>');
|
||||
|
||||
return substr($html, $anfang, $ende - $anfang);
|
||||
}
|
||||
}
|
||||
72
tests/e2e/specs/seitenleiste.spec.js
Normal file
72
tests/e2e/specs/seitenleiste.spec.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// @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')
|
||||
test.skip(page.url().includes('/einrichtung'), 'Instanz noch nicht eingerichtet')
|
||||
|
||||
await page.fill('#benutzername', BENUTZER)
|
||||
await page.fill('#passwort', PASSWORT)
|
||||
await page.click('button[type="submit"]')
|
||||
await expect(page).toHaveURL(/\/$/)
|
||||
}
|
||||
|
||||
test.describe('Seitenleiste ein- und ausklappen', () => {
|
||||
test('Klick auf die Marke schiebt die Leiste auf Symbolbreite und zurück', async ({ page }) => {
|
||||
await anmelden(page)
|
||||
|
||||
const rahmen = page.locator('.rahmen')
|
||||
const marke = page.locator('[data-seitenleiste-schalter]')
|
||||
const symbol = page.locator('.marken-symbol')
|
||||
const beschriftung = page.locator('.seitenleiste .beschriftung').first()
|
||||
|
||||
await expect(symbol).toBeVisible()
|
||||
await expect(beschriftung).toBeVisible()
|
||||
const breitOffen = (await page.locator('.seitenleiste').boundingBox()).width
|
||||
|
||||
await marke.click()
|
||||
|
||||
await expect(rahmen).toHaveClass(/eingeklappt/)
|
||||
await expect(marke).toHaveAttribute('aria-expanded', 'false')
|
||||
// Das Markensymbol und die Symbole der Menüpunkte bleiben stehen,
|
||||
// nur die Beschriftungen verschwinden.
|
||||
await expect(symbol).toBeVisible()
|
||||
await expect(beschriftung).toBeHidden()
|
||||
await expect(page.locator('.seitenleiste .icon').first()).toBeVisible()
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
test('Die Symbole führen eingeklappt zu denselben Seiten', async ({ page }) => {
|
||||
await anmelden(page)
|
||||
|
||||
await page.locator('[data-seitenleiste-schalter]').click()
|
||||
await expect(page.locator('.rahmen')).toHaveClass(/eingeklappt/)
|
||||
|
||||
await page.locator('.seitenleiste a[href="/konten"]').click()
|
||||
await expect(page).toHaveURL(/\/konten/)
|
||||
await expect(page.locator('h1')).toContainText('Kontenübersicht')
|
||||
|
||||
// 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 page.locator('[data-seitenleiste-schalter]').click()
|
||||
await expect(page.locator('.rahmen')).not.toHaveClass(/eingeklappt/)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue