Aus der Kachel QIF-Import wird Import/Export mit je einem Knopf für beide Richtungen; davor steht die Wahl des Dateiformats (QIF oder CSV), die als Dialog und ohne Skript als eigene Seite funktioniert. - CsvBuchungsImport/CsvTabelle: Buchungen samt Splitzeilen, Spalten über die Kopfzeile, dieselbe Dublettenerkennung und der Trockenlauf wie beim QIF. - BuchungsExport/ExportController: Filter nach Zeitraum, Konto, Kategorie und Klasse; die Datei entsteht im Speicher, ein Exportverzeichnis gibt es nicht. - Housekeeping: Hochgeladene Importdateien und data/tmp werden nach der eingestellten Frist gelöscht (Vorgabe 14 Tage, 0 schaltet ab). Migration 013, Test 29 sowie E2E-Spec importexport.spec.js. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
208 lines
8.9 KiB
PHP
208 lines
8.9 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, 'reihen' => 3, 'erinnerungen' => 2,
|
||
'vertraege' => 7, 'fristen' => 1,
|
||
];
|
||
|
||
public function run(): void
|
||
{
|
||
$this->menueOhneTermineUndVertraege();
|
||
$this->verwaltungLeuchtetFuerBeide();
|
||
$this->verwaltungsseiteFuehrtZuDenTerminen();
|
||
$this->verwaltungsseiteFuehrtZuDenVertraegen();
|
||
$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
|
||
);
|
||
}
|
||
}
|
||
|
||
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,
|
||
]);
|
||
|
||
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);
|
||
}
|
||
}
|