Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
/**
* Deckt die Bedienung der Tresor-Dialoge und die Ordnerzuordnung ab.
*
* Drei Zusagen tragen sie: Eine aufgeklappte Auswahlliste bleibt in einem
* Dialog bedienbar (sie hängt am `<body>`, gehört aber zum Dialog), der
* Verschieben-Dialog spricht die Schnittstelle des Servers, und ein Eintrag
* behält seinen Ordner – gesetzt wird nur, was zum Tresor des Eintrags gehört.
*/
return static function (): void {
$wurzel = dirname(__DIR__);
$lies = static fn(string $pfad): string => (string)file_get_contents($wurzel . '/' . $pfad);
// ── Dialoge und aufgeklappte Auswahllisten ──────────────────────────
test('Der Dialog zählt seine aufgeklappten Ebenen zu sich', function () use ($lies) {
$js = $lies('public/assets/js/dialog.js');
assertTrue(str_contains($js, "var EBENE = '[data-dialog-layer]'"), 'Keine Kennzeichnung für Dialog-Ebenen');
assertTrue(str_contains($js, 'function gehoertDazu'), 'Ohne Zugehörigkeitsprüfung bleibt der Fokuswächter blind');
assertTrue(str_contains($js, 'if (gehoertDazu(dialog, e.target)) { return; }'),
'Der Fokuswächter holt den Fokus aus einer Dialog-Ebene zurück');
assertTrue(str_contains($js, "el.closest(AUSWAHL) || el.closest(EBENE)"),
'Eine Dialog-Ebene gilt als Herkunft des Fokus');
});
test('Die durchsuchbare Auswahl weist sich als Dialog-Ebene aus', function () use ($lies) {
$js = $lies('public/assets/js/app.js');
assertTrue(str_contains($js, "dropdown.setAttribute('data-dialog-layer', '')"),
'Ohne Kennzeichnung ist das Suchfeld in Dialogen nicht erreichbar');
assertTrue(str_contains($js, "if (e.key === 'Escape') { e.stopPropagation();"),
'Escape in der Liste schließt sonst den ganzen Dialog');
});
test('Die Anzeige folgt nachträglich neu aufgebauten Optionen', function () use ($lies) {
$js = $lies('public/assets/js/app.js');
assertTrue(str_contains($js, 'new MutationObserver(function () { self.sync(); })'),
'Ohne Beobachtung bleibt die Anzeige auf dem alten Eintrag stehen');
assertTrue(str_contains($js, ".observe(this.native, { childList: true, subtree: true })"),
'Die Beobachtung muss am Select selbst hängen');
});
test('Jeder Tresor-Dialog hat eine Schließen-Schaltfläche', function () use ($lies) {
foreach (['templates/vault/index.php', 'templates/vault/team_show.php'] as $pfad) {
$html = $lies($pfad);
preg_match_all('/<div class="modal-overlay[^"]*" id="([A-Za-z]+)"/', $html, $treffer);
foreach ($treffer[1] as $id) {
// Der Abschnitt bis zum nächsten Dialog enthält den Kopfbereich.
$start = (int)strpos($html, 'id="' . $id . '"');
$naechster = strpos($html, 'class="modal-overlay', $start + 1);
$block = substr($html, $start, ($naechster === false ? strlen($html) : $naechster) - $start);
assertTrue(str_contains($block, 'modal-close'),
'Dialog ohne Schließen-Schaltfläche: ' . $id . ' in ' . $pfad);
}
}
});
// ── Verschieben-Dialog des Team-Tresors ─────────────────────────────
test('Der Verschieben-Dialog spricht die Schnittstelle des Servers', function () use ($lies) {
$html = $lies('templates/vault/team_show.php');
assertTrue(str_contains($html, "form.append('target_team_id'"), 'Zielbereich wird unter falschem Namen gesendet');
assertTrue(str_contains($html, "form.append('target_folder_id'"), 'Zielordner wird unter falschem Namen gesendet');
assertFalse(str_contains($html, "form.append('team_id'"), 'Alter Feldname ist noch im Spiel');
assertTrue(str_contains($html, 'if (data.success)'), 'Die Antwort trägt success, nicht ok');
assertFalse(str_contains($html, 'if (data.ok)'), 'Die Antwort wird falsch ausgewertet');
});
test('Der Server liest genau diese Felder', function () use ($lies) {
$php = $lies('src/Controllers/VaultController.php');
assertTrue(str_contains($php, "\$request->post('target_team_id'"), 'Zielbereich wird nicht gelesen');
assertTrue(str_contains($php, "\$request->post('target_folder_id'"), 'Zielordner wird nicht gelesen');
});
// ── Ordner gehören zu genau einem Tresor ────────────────────────────
test('Die Ordnerwahl wird gegen den Tresor geprüft', function () use ($lies) {
$php = $lies('src/Controllers/VaultController.php');
assertTrue(str_contains($php, 'private function folderInVault('), 'Keine Prüfung der Ordnerzugehörigkeit');
assertSame(4, substr_count($php, '$this->folderInVault('),
'Alle vier Schreibwege (anlegen/bearbeiten, persönlich/Team) müssen prüfen');
assertTrue(str_contains($php, "WHERE id = ? AND team_id = ?'"), 'Team-Ordner werden nicht am Team geprüft');
assertTrue(str_contains($php, "WHERE id = ? AND user_id = ?'"), 'Eigene Ordner werden nicht am Nutzer geprüft');
});
test('Der Anlege-Dialog belegt den Ordner unabhängig vom Datentyp vor', function () use ($lies) {
$html = $lies('templates/vault/index.php');
assertTrue(str_contains($html, "'id' => (int)\$f['id']"), 'Ordner-Kennungen müssen als Zahl im Skript landen');
assertTrue(str_contains($html, "if (ziel !== null && String(f.id) === ziel)"),
'Die Vorauswahl muss den Datentyp der Kennung überbrücken');
});
test('Bearbeiten im Dialog behält Tresor und Ordner des Eintrags', function () use ($lies) {
$html = $lies('templates/vault/index.php');
$php = $lies('src/Vault/VaultManager.php');
assertTrue(str_contains($html, 'vaultApplyCreateTarget(eTeam,'),
'Der Dialog übernimmt den Tresor des Eintrags nicht');
assertTrue(str_contains($html, "e.folder_id"), 'Der Ordner des Eintrags wird nicht übernommen');
assertTrue(str_contains($php, "'team_id' => \$row['team_id']"),
'Ohne Tresor im Datensatz kann der Dialog ihn nicht setzen');
});
// ── Löschen heißt Papierkorb ────────────────────────────────────────
test('Löschen eines Eintrags wird als Papierkorb protokolliert', function () use ($lies) {
$php = $lies('src/Controllers/VaultController.php');
$html = $lies('templates/vault/index.php');
assertFalse(str_contains($php, 'unwiderruflich gelöscht'),
'Der Eintrag liegt 30 Tage im Papierkorb – das Protokoll darf nichts anderes behaupten');
assertTrue(str_contains($php, 'in den Papierkorb verschoben'), 'Keine Angabe zum Papierkorb');
assertFalse(str_contains($html, 'unwiderruflich löschen?'), 'Die Rückfrage verspricht zu viel');
});
};