Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
/**
* Die frei wählbaren Spalten der Ticketliste.
*
* Drei Zusagen stehen hier im Mittelpunkt. Die Auswahl gehört der einzelnen
* Person und niemandem sonst. Eine eingeblendete Spalte lässt sich filtern –
* und ein gesetzter Filter bleibt sichtbar, auch wenn seine Spalte fort ist,
* damit die Liste nie unsichtbar gefiltert wird. Und jeder Filter wirkt auf
* Liste *und* Zählung, sonst zeigte die Seitenzahl einen anderen Bestand als
* die Zeilen darunter.
*/
use OpenNIT\Tickets\TicketColumns;
return static function (): void {
$wurzel = dirname(__DIR__);
$quelle = static fn (string $pfad): string => (string)file_get_contents($wurzel . '/' . $pfad);
// ----------------------------------------------------------------
// Der Katalog
// ----------------------------------------------------------------
test('Jede Spalte ist vollständig beschrieben', function () {
foreach (TicketColumns::COLUMNS as $schluessel => $spalte) {
foreach (['label', 'hint', 'default', 'sort', 'filter', 'braucht'] as $feld) {
assertTrue(array_key_exists($feld, $spalte), 'Der Spalte ' . $schluessel . ' fehlt ' . $feld);
}
assertTrue(trim((string)$spalte['label']) !== '', 'Die Spalte ' . $schluessel . ' hat keine Überschrift');
assertTrue(
in_array($spalte['filter'], ['select', 'bucket', 'none'], true),
'Die Spalte ' . $schluessel . ' hat eine unbekannte Filterart'
);
assertTrue(
(bool)preg_match('/^[a-z][a-z_]{1,29}$/', (string)$schluessel),
'Der Schlüssel ' . $schluessel . ' taugt nicht als Feldname'
);
}
});
test('Jeder Filter mit fester Liste hat auch Werte', function () {
// Ein Auswahlfeld ohne Werte wäre ein Feld, das nichts tut.
foreach (TicketColumns::COLUMNS as $schluessel => $spalte) {
if ($spalte['filter'] === 'bucket') {
assertTrue(
!empty(TicketColumns::FILTER_OPTIONS[$schluessel]),
'Dem Zeitraum-Filter ' . $schluessel . ' fehlen die Werte'
);
}
}
// Umgekehrt: keine Werte ohne zugehörige Spalte.
foreach (array_keys(TicketColumns::FILTER_OPTIONS) as $schluessel) {
assertTrue(
isset(TicketColumns::COLUMNS[$schluessel]),
'Zu den Werten von ' . $schluessel . ' gibt es keine Spalte'
);
}
});
// ----------------------------------------------------------------
// Die persönliche Auswahl
// ----------------------------------------------------------------
test('Ohne eigene Auswahl gilt der Standard', function () {
$erwartet = array_keys(array_filter(
TicketColumns::COLUMNS,
static fn (array $s): bool => $s['default'] && $s['braucht'] === null
));
assertSame(array_values($erwartet), TicketColumns::sichtbar(null));
assertSame(array_values($erwartet), TicketColumns::sichtbar(''));
});
test('Mit eigener Auswahl zählt nur sie', function () {
// Auch dann, wenn dadurch eine Standard-Spalte verschwindet.
assertSame(['priority', 'due'], TicketColumns::sichtbar('due,priority'));
assertSame(['status'], TicketColumns::sichtbar('status'));
});
test('Keine Spalte ist eine gültige Auswahl', function () {
// Sonst ließe sich die letzte Spalte nie abwählen: Ein leerer Wert
// brächte den Standard zurück.
assertSame([], TicketColumns::sichtbar('-'));
assertSame([], TicketColumns::sichtbar('gibtesnicht'));
});
test('Die Reihenfolge kommt aus dem Katalog, nicht aus der Eingabe', function () {
// Sonst bestimmte die Reihenfolge der Häkchen im Formular, wie die
// Tabelle aussieht.
$katalog = array_keys(TicketColumns::COLUMNS);
$paar = array_slice($katalog, 0, 2);
assertSame($paar, TicketColumns::sichtbar(implode(',', array_reverse($paar))));
assertSame($paar, TicketColumns::normalisieren(array_reverse($paar)));
});
test('Eine abgeschaltete Funktion bietet ihre Spalte nicht an', function () {
// Eine Spalte, die immer leer bliebe, gehört nicht in die Auswahl.
$mitSchalter = ['sentiment' => true, 'time' => true];
assertFalse(TicketColumns::verfuegbar('sentiment'), 'Die Stimmung wird ohne die Funktion angeboten');
assertTrue(TicketColumns::verfuegbar('sentiment', $mitSchalter));
assertFalse(TicketColumns::verfuegbar('time'), 'Die erfasste Zeit wird ohne die Funktion angeboten');
assertTrue(TicketColumns::verfuegbar('time', $mitSchalter));
// Auch dann nicht, wenn sie ausdrücklich gewählt wurde.
assertSame([], TicketColumns::sichtbar('sentiment,time'));
assertSame(['sentiment', 'time'], TicketColumns::sichtbar('sentiment,time', $mitSchalter));
assertSame([], TicketColumns::normalisieren(['sentiment']));
});
test('Eine unbekannte Spalte überlebt das Speichern nicht', function () {
assertSame(['status'], TicketColumns::normalisieren(['status', 'boese', '../etc/passwd']));
});
// ----------------------------------------------------------------
// Filterleiste
// ----------------------------------------------------------------
test('Die Filterleiste folgt der Spaltenauswahl', function () {
$leiste = TicketColumns::filterleiste(['priority', 'due'], []);
assertSame(['priority', 'due'], $leiste, 'Die Leiste zeigt nicht die gewählten Spalten');
});
test('Eine Spalte ohne Filter taucht in der Leiste nicht auf', function () {
// Der Status hat seine eigenen Reiter über der Liste.
assertSame([], TicketColumns::filterleiste(['status'], []));
assertSame('none', TicketColumns::COLUMNS['status']['filter'], 'Der Status hat plötzlich ein eigenes Feld');
});
test('Ein gesetzter Filter bleibt sichtbar, auch ohne seine Spalte', function () {
// Sonst filterte die Liste weiter, ohne dass es noch jemand sähe.
$leiste = TicketColumns::filterleiste(['priority'], ['category' => '7']);
assertTrue(in_array('category', $leiste, true), 'Ein gesetzter Filter verschwindet mit seiner Spalte');
assertTrue(in_array('priority', $leiste, true), 'Die gewählte Spalte fehlt');
});
test('Ein leerer Wert hält keinen Filter in der Leiste', function () {
assertSame([], TicketColumns::filterleiste([], ['category' => '', 'due' => ' ']));
});
// ----------------------------------------------------------------
// Wirkung auf die Abfrage
// ----------------------------------------------------------------
test('Jeder Filter wirkt auf Liste und Zählung', function () use ($quelle) {
// Sonst zeigt die Liste gefilterte Zeilen und die Seitenzahl den
// ungefilterten Bestand.
$tm = $quelle('src/Tickets/TicketManager.php');
assertSame(
2,
substr_count($tm, '$this->applyColumnFilters($filters, $where, $params);'),
'Die Spaltenfilter greifen nicht in beiden Abfragen'
);
});
test('Nur bekannte Werte formen die Abfrage', function () use ($quelle) {
$tm = $quelle('src/Tickets/TicketManager.php');
$start = (int)strpos($tm, 'private function applyColumnFilters(');
$body = substr($tm, $start, 3200);
assertTrue(
str_contains($body, 'isset(TicketColumns::FILTER_OPTIONS[$schluessel][$w])'),
'Ein beliebiger Wert landet in der Abfrage'
);
// Jeder Filter des Katalogs wird auch wirklich umgesetzt.
foreach (['due', 'created', 'updated', 'sla', 'sentiment', 'time'] as $schluessel) {
assertTrue(str_contains($body, "'" . $schluessel . "'"), 'Der Filter ' . $schluessel . ' wird nicht umgesetzt');
}
});
test('Die Summe der Zeiten wird nur bei sichtbarer Spalte gelesen', function () use ($quelle) {
// Eine Gruppierung über alle Zeiteinträge kostet; sie gehört nicht in
// jede Listenabfrage.
$tm = $quelle('src/Tickets/TicketManager.php');
assertTrue(
str_contains($tm, "\$zeigtZeit = in_array('time', (array)(\$filters['_columns'] ?? []), true);"),
'Die Summe hängt nicht an der Spaltenauswahl'
);
assertTrue(str_contains($tm, "\$zeitJoin = \$zeigtZeit"), 'Die Verknüpfung läuft immer mit');
// Und der Filter kommt ohne sie aus.
$start = (int)strpos($tm, 'private function applyColumnFilters(');
$body = substr($tm, $start, 3200);
assertTrue(str_contains($body, 'EXISTS (SELECT 1 FROM ticket_time_entries'), 'Der Zeit-Filter braucht die Verknüpfung');
});
// ----------------------------------------------------------------
// Zugang und Oberfläche
// ----------------------------------------------------------------
test('Die Auswahl gehört der einzelnen Person', function () use ($quelle) {
$ctrl = $quelle('src/Controllers/TicketController.php');
assertTrue(
str_contains($ctrl, '$this->setUserSetting(TicketColumns::SETTING'),
'Die Auswahl wird nicht persönlich gespeichert'
);
assertTrue(
str_contains($ctrl, '$this->getUserSetting(TicketColumns::SETTING)'),
'Die Auswahl wird nicht persönlich gelesen'
);
assertTrue(
str_contains($ctrl, "\$this->authorize('view_tickets');"),
'Das Speichern prüft kein Recht'
);
// Der Rückweg kommt aus dem Formular und wird auf die eigene Anwendung begrenzt.
assertTrue(str_contains($ctrl, 'safeLocalReturn('), 'Der Rückweg wird nicht begrenzt');
$app = $quelle('src/Core/Application.php');
$zeile = strstr($app, "TicketController@saveColumns'");
assertTrue(is_string($zeile), 'Die Route fehlt');
assertTrue(
str_contains(substr($zeile, 0, 160), "'capability' => 'view_tickets'"),
'Die Route hängt an keinem Recht'
);
});
test('Eine gemerkte Standort-Spalte geht beim Umstieg nicht verloren', function () use ($quelle) {
// Die Spalte war bisher einzeln umschaltbar; wer sie eingeschaltet
// hatte, soll sie behalten.
$ctrl = $quelle('src/Controllers/TicketController.php');
assertTrue(
str_contains($ctrl, "\$this->getUserSetting('ticket_show_location_col', '0') === '1'"),
'Die frühere Einstellung wird nicht übernommen'
);
assertTrue(
str_contains($ctrl, "array_merge(TicketColumns::sichtbar(null, \$spaltenSchalter), ['location'])"),
'Die Standort-Spalte wird nicht zum Standard hinzugenommen'
);
});
test('Das Modal bietet jede verfügbare Spalte an', function () use ($quelle) {
$vorlage = $quelle('templates/tickets/index.php');
assertTrue(str_contains($vorlage, 'id="tkColsOverlay"'), 'Der Dialog fehlt');
assertTrue(str_contains($vorlage, 'action="/tickets/columns"'), 'Der Dialog speichert nirgends');
assertTrue(str_contains($vorlage, 'name="_csrf"'), 'Dem Formular fehlt der Token');
assertTrue(str_contains($vorlage, 'name="col_<?= $this->e((string)$cKey) ?>"'), 'Die Häkchen folgen nicht dem Katalog');
assertTrue(str_contains($vorlage, 'tkColsOpen()'), 'Der Dialog lässt sich nicht öffnen');
assertTrue(str_contains($vorlage, 'tkColsEsc'), 'Der Dialog lässt sich nicht per Escape schließen');
// Kopfzeile und Zellen folgen derselben Auswahl.
assertTrue(str_contains($vorlage, 'foreach ($columns as $colKey): $col = $column_catalog[$colKey];'), 'Die Kopfzeile folgt nicht der Auswahl');
assertTrue(str_contains($vorlage, 'foreach ($columns as $colKey): ?><?= $tkZelle('), 'Die Zellen folgen nicht der Auswahl');
});
test('Jede Spalte wird in der Zeile auch dargestellt', function () use ($quelle) {
// Eine Spalte ohne Fall in der Zellen-Funktion bliebe stumm leer.
$vorlage = $quelle('templates/tickets/index.php');
$start = (int)strpos($vorlage, '$tkZelle = function');
$body = substr($vorlage, $start, 4000);
foreach (array_keys(TicketColumns::COLUMNS) as $schluessel) {
assertTrue(
str_contains($body, "case '" . $schluessel . "':"),
'Für die Spalte ' . $schluessel . ' fehlt die Darstellung'
);
}
});
};