Das Adressbuch bekommt in der Verwaltung eine eigene Kachel statt nur über die Verträge erreichbar zu sein, denn es speist auch den Zahlungsverkehr. Auf der Adressliste klappt zu jeder Adresse ein <details>-Element ihre letzten Buchungen auf (Buchung::letzteFuerEmpfaenger, kontenübergreifend nach dem exakten Empfängernamen – die Buchung führt kein Fremdschlüsselfeld auf die Adresse). Die Empfängerauswahl im Zahlungsverkehr-Formular ist jetzt ein Typeahead: kombobox.js (bereits für die Kategorie im Kontoblatt im Einsatz) baut das <select> in ein gefiltertes Eingabefeld um. Geprüft (PHP- und E2E-Tests), dass "Empfänger ins Adressbuch übernehmen" beim Erfassen einer Überweisung tatsächlich eine Adresse anlegt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
237 lines
10 KiB
PHP
237 lines
10 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
/**
|
||
* Die Hauptnavigation führt Termine und Verträge nicht mehr als eigene
|
||
* Menüpunkte – sie hängen jetzt als letzte Kacheln unter der Verwaltung.
|
||
*/
|
||
class NavigationTest extends TestCase
|
||
{
|
||
protected string $testName = 'Test 24: Navigation (Termine und Verträge unter Verwaltung)';
|
||
|
||
/** Die Zahlen, die die Verwaltungsseite für ihre Kacheln erwartet. */
|
||
private const ZAHLEN = [
|
||
'kategorien' => 0, 'einnahmen' => 0, 'ausgaben' => 0, 'klassen' => 0,
|
||
'konten' => 0, 'kontengruppen' => 0, 'bankinstitute' => 0, 'bargeld' => 0,
|
||
'reihen' => 3, 'erinnerungen' => 2, 'vertraege' => 7, 'fristen' => 1,
|
||
'adressen' => 4,
|
||
];
|
||
|
||
public function run(): void
|
||
{
|
||
$this->menueOhneTermineUndVertraege();
|
||
$this->verwaltungLeuchtetFuerBeide();
|
||
$this->verwaltungsseiteFuehrtZuDenTerminen();
|
||
$this->verwaltungsseiteFuehrtZuDenVertraegen();
|
||
$this->verwaltungsseiteFuehrtZurAdressverwaltung();
|
||
$this->alleKnoepfeGleichUndFussbuendig();
|
||
}
|
||
|
||
/**
|
||
* Alle Kacheln der Verwaltung tragen dieselben kleinen Knöpfe wie die von
|
||
* Termine, und die Leisten stehen unabhängig von der Textlänge auf gleicher
|
||
* Höhe am unteren Kachelrand.
|
||
*/
|
||
private function alleKnoepfeGleichUndFussbuendig(): void
|
||
{
|
||
$kacheln = $this->kacheln();
|
||
|
||
// Das abschließende \b hält die umgebende .knopfleiste heraus.
|
||
preg_match_all('#class="knopf\b([^"]*)"#', $kacheln, $treffer);
|
||
$this->assertEquals(
|
||
substr_count($kacheln, '<div class="kachel">'),
|
||
substr_count($kacheln, '<div class="knopfleiste">'),
|
||
'Jede Kachel führt eine Knopfleiste'
|
||
);
|
||
$this->assertGreaterThan(0, count($treffer[1]), 'Die Kacheln führen Knöpfe');
|
||
foreach ($treffer[1] as $zusatz) {
|
||
$this->assert(
|
||
str_contains($zusatz, ' klein'),
|
||
'Knopf "knopf' . $zusatz . '" ist klein wie die unter Termine'
|
||
);
|
||
}
|
||
|
||
$this->assert(
|
||
(bool)preg_match('#<div class="kachelraster fussbuendig">#', $kacheln),
|
||
'Das Kachelraster richtet seine Knopfleisten am Fuß aus'
|
||
);
|
||
|
||
$stil = $this->stilblatt();
|
||
$this->assert(
|
||
(bool)preg_match('#\.kachelraster\.fussbuendig > \.kachel\s*\{[^}]*flex-direction:\s*column#', $stil),
|
||
'Die Kacheln des Rasters sind Flexspalten'
|
||
);
|
||
$this->assert(
|
||
(bool)preg_match(
|
||
'#\.kachelraster\.fussbuendig > \.kachel > \.knopfleiste\s*\{[^}]*margin-top:\s*auto[^}]*padding-top:#',
|
||
$stil
|
||
),
|
||
'Die Knopfleiste sinkt nach unten und behält ihren Abstand zum Text'
|
||
);
|
||
}
|
||
|
||
private function menueOhneTermineUndVertraege(): void
|
||
{
|
||
$menue = $this->seitenleiste($this->rendere('layout/kopf.php', ['aktuellerPfad' => '/']));
|
||
|
||
$this->assert(!str_contains($menue, 'href="/termine"'), 'Kein Menüpunkt Termine mehr');
|
||
$this->assert(!str_contains($menue, '>Termine'), 'Auch die Beschriftung Termine ist verschwunden');
|
||
$this->assert(!str_contains($menue, 'href="/vertraege"'), 'Kein Menüpunkt Verträge mehr');
|
||
$this->assert(!str_contains($menue, '>Verträge'), 'Auch die Beschriftung Verträge ist verschwunden');
|
||
$this->assert(str_contains($menue, 'href="/verwaltung"'), 'Verwaltung steht weiter im Menü');
|
||
$this->assert(str_contains($menue, 'href="/planung"'), 'Die übrigen Menüpunkte bleiben unberührt');
|
||
$this->assert(str_contains($menue, 'href="/wertpapiere"'), 'Wertpapiere stehen weiter im Menü');
|
||
}
|
||
|
||
private function verwaltungLeuchtetFuerBeide(): void
|
||
{
|
||
// Ohne eigenen Menüpunkt bliebe die Seitenleiste auf diesen Seiten
|
||
// sonst ganz ohne Markierung.
|
||
$pfade = [
|
||
'/termine', '/termine/kalender', '/termine/erinnerungen',
|
||
'/vertraege', '/vertraege/adressen', '/vertraege/sparziele', '/vertraege/kredite',
|
||
];
|
||
foreach ($pfade as $pfad) {
|
||
$menue = $this->seitenleiste($this->rendere('layout/kopf.php', ['aktuellerPfad' => $pfad]));
|
||
$this->assert(
|
||
(bool)preg_match('#href="/verwaltung"\s+class="aktiv"#', $menue),
|
||
'Verwaltung ist markiert auf ' . $pfad
|
||
);
|
||
}
|
||
|
||
$cockpit = $this->seitenleiste($this->rendere('layout/kopf.php', ['aktuellerPfad' => '/']));
|
||
$this->assert(
|
||
(bool)preg_match('#href="/"\s+class="aktiv"#', $cockpit),
|
||
'Cockpit ist auf der Startseite markiert'
|
||
);
|
||
$this->assert(
|
||
!preg_match('#href="/verwaltung"\s+class="aktiv"#', $cockpit),
|
||
'Verwaltung leuchtet nicht überall mit'
|
||
);
|
||
}
|
||
|
||
private function verwaltungsseiteFuehrtZuDenTerminen(): void
|
||
{
|
||
$kacheln = $this->kacheln();
|
||
|
||
$this->assert(str_contains($kacheln, 'href="/termine"'), 'Regelmäßige Buchungen sind verlinkt');
|
||
$this->assert(str_contains($kacheln, 'href="/termine/kalender"'), 'Finanzkalender ist verlinkt');
|
||
$this->assert(str_contains($kacheln, 'href="/termine/erinnerungen"'), 'Erinnerungen sind verlinkt');
|
||
$this->assert(str_contains($kacheln, '<h2>Termine</h2>'), 'Die Kachel heißt Termine');
|
||
$this->assert(str_contains($kacheln, '>3<'), 'Die Kennzahl zeigt die Zahl der Reihen');
|
||
$this->assert(str_contains($kacheln, 'offene Erinnerungen: 2'), 'Offene Erinnerungen werden genannt');
|
||
|
||
foreach (['/termine', '/termine/kalender', '/termine/erinnerungen'] as $ziel) {
|
||
$this->assert(
|
||
(bool)preg_match('#href="' . preg_quote($ziel, '#') . '" class="knopf klein#', $kacheln),
|
||
'Der Knopf zu ' . $ziel . ' ist klein'
|
||
);
|
||
}
|
||
|
||
// Umbruch statt Überlauf am rechten Kachelrand.
|
||
$this->assert(
|
||
(bool)preg_match('#\.knopfleiste\s*\{[^}]*flex-wrap:\s*wrap#', $this->stilblatt()),
|
||
'Die Knopfleiste bricht um'
|
||
);
|
||
|
||
$this->assert(
|
||
strrpos($kacheln, '<h2>Termine</h2>') > (int)strrpos($kacheln, '<h2>Online-Banking</h2>'),
|
||
'Termine stehen hinter dem Online-Banking'
|
||
);
|
||
}
|
||
|
||
private function verwaltungsseiteFuehrtZuDenVertraegen(): void
|
||
{
|
||
$kacheln = $this->kacheln();
|
||
|
||
$this->assert(str_contains($kacheln, 'href="/vertraege"'), 'Verträge sind verlinkt');
|
||
$this->assert(str_contains($kacheln, 'href="/vertraege/adressen"'), 'Adressen sind verlinkt');
|
||
$this->assert(str_contains($kacheln, 'href="/vertraege/sparziele"'), 'Sparziele sind verlinkt');
|
||
$this->assert(str_contains($kacheln, 'href="/vertraege/kredite"'), 'Kredite sind verlinkt');
|
||
$this->assert(str_contains($kacheln, '<h2>Verträge</h2>'), 'Die Kachel heißt Verträge');
|
||
$this->assert(str_contains($kacheln, '>7<'), 'Die Kennzahl zeigt die Zahl der aktiven Verträge');
|
||
$this->assert(str_contains($kacheln, 'nächsten 90 Tagen: 1'), 'Ablaufende Fristen werden genannt');
|
||
|
||
// Vier Wege in einer Kachel: nur als kleine Knöpfe, und die Leiste bricht um.
|
||
foreach (['/vertraege', '/vertraege/adressen', '/vertraege/sparziele', '/vertraege/kredite'] as $ziel) {
|
||
$this->assert(
|
||
(bool)preg_match('#href="' . preg_quote($ziel, '#') . '" class="knopf klein#', $kacheln),
|
||
'Der Knopf zu ' . $ziel . ' ist klein'
|
||
);
|
||
}
|
||
|
||
// Letzter Punkt: keine weitere Kachel hinter der von Verträge.
|
||
$letzte = (int)strrpos($kacheln, '<h2>Verträge</h2>');
|
||
foreach (['Kategorien', 'Klassen', 'Konten', 'Import/Export', 'Online-Banking', 'Termine'] as $vorher) {
|
||
$this->assert(
|
||
$letzte > (int)strrpos($kacheln, '<h2>' . $vorher . '</h2>'),
|
||
'Verträge stehen hinter der Kachel ' . $vorher
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Fix #51: Eine eigene Kachel für die Adressverwaltung – nicht nur der
|
||
* kleine Knopf innerhalb der Verträge-Kachel, denn das Adressbuch bedient
|
||
* auch den Zahlungsverkehr, der nichts mit Verträgen zu tun hat.
|
||
*/
|
||
private function verwaltungsseiteFuehrtZurAdressverwaltung(): void
|
||
{
|
||
$kacheln = $this->kacheln();
|
||
|
||
$this->assert(str_contains($kacheln, '<h2>Adressverwaltung</h2>'), 'Die Kachel heißt Adressverwaltung');
|
||
$this->assert(str_contains($kacheln, '>4<'), 'Die Kennzahl zeigt die Zahl der Adressen');
|
||
$this->assert(
|
||
(bool)preg_match('#href="/vertraege/adressen" class="knopf klein#', $kacheln),
|
||
'Der Knopf zu den Adressen ist klein'
|
||
);
|
||
|
||
$this->assert(
|
||
strrpos($kacheln, '<h2>Adressverwaltung</h2>') > (int)strrpos($kacheln, '<h2>Online-Banking</h2>'),
|
||
'Die Adressverwaltung steht hinter dem Online-Banking'
|
||
);
|
||
$this->assert(
|
||
strrpos($kacheln, '<h2>Termine</h2>') > (int)strrpos($kacheln, '<h2>Adressverwaltung</h2>'),
|
||
'Die Termine stehen hinter der Adressverwaltung'
|
||
);
|
||
}
|
||
|
||
private function stilblatt(): string
|
||
{
|
||
return (string)file_get_contents(dirname(__DIR__) . '/public/static/app.css');
|
||
}
|
||
|
||
/** Nur der Inhalt der Verwaltungsseite – die Seitenleiste soll nicht mitzählen. */
|
||
private function kacheln(): string
|
||
{
|
||
$seite = $this->rendere('verwaltung/index.php', [
|
||
'aktuellerPfad' => '/verwaltung',
|
||
'zahlen' => self::ZAHLEN,
|
||
'bargeldAktiv' => false,
|
||
]);
|
||
|
||
return substr($seite, (int)strpos($seite, '<main'));
|
||
}
|
||
|
||
/** @param array<string,mixed> $daten */
|
||
private function rendere(string $view, array $daten): string
|
||
{
|
||
$daten += ['titel' => 'Test', 'flash' => [], 'benutzer' => null, 'csrfToken' => 'x'];
|
||
|
||
extract($daten, EXTR_SKIP);
|
||
|
||
ob_start();
|
||
include dirname(__DIR__) . '/src/Views/' . $view;
|
||
return (string)ob_get_clean();
|
||
}
|
||
|
||
/** Nur die Seitenleiste – Verlinkungen 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);
|
||
}
|
||
}
|