Dateien
AegisSight-Monitor/tests/test_sammelaktionen.js
claude-dev c09c2a49b9 feat(ui): Sammelaktionen auch in der klassischen Ansicht, mit Lucide-Icons
Im Studio lassen sich mehrere Faelle auf einmal archivieren oder loeschen, in
der klassischen Ansicht ging das nur einzeln ueber die Aktionsleiste. Wer dort
zehn alte Lagen wegraeumen wollte, musste jede einzeln oeffnen.

Die Seitenleiste bekommt deshalb dieselbe Bedienung wie das Studio. Unten
stehen zwei Knoepfe, Archivieren und Loeschen. Der erste Klick schaltet den
Auswahlmodus ein, erst dann erscheinen Haken in der Lagenliste, an Stelle des
Statuspunktes. Ein Klick auf eine Zeile waehlt dann aus, statt die Lage zu
oeffnen. Bestaetigt wird in der Leiste darunter, die mitzaehlt und nur
anbietet, was zur Auswahl passt. Sind nur aktive Lagen gewaehlt, erscheint
kein Aktivieren-Knopf, im Loeschmodus keiner der beiden anderen.

"Alle sichtbaren" beruecksichtigt den Filter der Seitenleiste, waehlt also bei
aktivem Eigene-Filter nur die eigenen Lagen aus. Das Loeschen fragt vorher nach
und nennt Titel und Artikelzahl, weil es nicht rueckgaengig zu machen ist. Die
Aktionen laufen nacheinander ueber die normalen Endpunkte, damit deren
Rechtepruefung greift, und Fehlschlaege werden mit Nummer gemeldet statt still
verschluckt. War die offene Lage dabei, springt die Ansicht zurueck.

Dazu Lucide-Icons fuer Archivieren, Aktivieren, Loeschen und Abbrechen, in
beiden Ansichten. Im Studio setzte _renderBulkBar die Beschriftung bisher ueber
textContent des Knopfes, was das Icon bei jedem Neuzeichnen entfernt haette.
Die Beschriftung liegt jetzt in einer eigenen span.

Cache-Buster auf 20260802a, sonst laden Browser die alten Skripte weiter.

Neu sind 34 Pruefungen, sie laufen ohne Browser gegen ein DOM-Double.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 12:19:52 +00:00

238 Zeilen
9.4 KiB
JavaScript

/**
* Testet die Sammelaktionen der Seitenleiste ohne Browser.
*
* Die Methoden werden aus app.js herausgeloest und gegen ein schlankes
* DOM-Double laufen gelassen. Kein Netzzugriff, keine Kosten.
*
* Aufruf aus dem Projektstamm:
* node tests/test_sammelaktionen.js
*/
'use strict';
const fs = require('fs');
const path = require('path');
const vm = require('vm');
let ok = 0;
let fail = 0;
function pruefe(name, bedingung, extra) {
if (bedingung) {
ok++;
console.log(' OK ' + name);
} else {
fail++;
console.log(' FEHL ' + name + ' '
+ (extra === undefined ? '' : JSON.stringify(extra)));
}
}
// --- DOM-Double, nur was die Sammelaktionen anfassen ---
function element(id) {
return {
id: id,
hidden: false,
checked: false,
textContent: '',
style: {},
_span: { textContent: '' },
querySelector(sel) { return sel === 'span' ? this._span : null; },
};
}
const elemente = {};
for (const id of ['sidebar-bulk-tools', 'sidebar-bulk-bar', 'sidebar-bulk-count',
'sidebar-bulk-all', 'sidebar-bulk-archive', 'sidebar-bulk-activate',
'sidebar-bulk-delete', 'empty-state', 'incident-view']) {
elemente[id] = element(id);
}
const meldungen = [];
const aufrufe = { update: [], delete: [] };
const umgebung = {
document: { getElementById: (id) => elemente[id] || null },
UI: { showToast: (text, art) => meldungen.push({ text, art }) },
API: {
updateIncident: async (id, daten) => { aufrufe.update.push({ id, daten }); },
deleteIncident: async (id) => { aufrufe.delete.push(id); },
},
confirm: () => umgebung._bestaetigen,
_bestaetigen: true,
console: console,
};
umgebung.window = umgebung;
// --- Die Sammelaktionen aus app.js herausloesen ---
const quelle = fs.readFileSync(
path.join(__dirname, '..', 'src', 'static', 'js', 'app.js'), 'utf8');
const von = quelle.indexOf(' // === Sammelaktionen in der Seitenleiste ===');
const bis = quelle.indexOf(' renderSidebar() {', von);
if (von === -1 || bis === -1) {
console.log('FEHLER: Sammelaktionen in app.js nicht gefunden');
process.exit(1);
}
const kontext = vm.createContext(umgebung);
vm.runInContext('var App = {\n' + quelle.slice(von, bis) + '\n'
+ ' incidents: [], currentIncidentId: null, _sidebarFilter: "all",'
+ ' _currentUsername: "ich",\n'
+ ' renderSidebar() { this._renderBulkBar(); },\n'
+ ' async loadIncidents() { this._geladen = (this._geladen || 0) + 1; },\n'
+ '};', kontext);
const App = kontext.App;
const LAGEN = [
{ id: 1, title: 'Ceuta', status: 'active', article_count: 40, created_by_username: 'ich' },
{ id: 2, title: 'IMK 2026', status: 'active', article_count: 20, created_by_username: 'wer' },
{ id: 3, title: 'Altfall', status: 'archived', article_count: 5, created_by_username: 'ich' },
];
function neu() {
App.incidents = LAGEN.map(l => Object.assign({}, l));
App._bulkMode = null;
App._bulkSel = new Set();
App._sidebarFilter = 'all';
App.currentIncidentId = null;
aufrufe.update.length = 0;
aufrufe.delete.length = 0;
meldungen.length = 0;
umgebung._bestaetigen = true;
for (const el of Object.values(elemente)) {
el.hidden = false;
el.checked = false;
el.textContent = '';
el.style = {};
el._span.textContent = '';
}
}
async function main() {
console.log('\nA) Der Auswahlmodus schaltet die Leisten um');
neu();
pruefe('A1 normal ist der Auswahlmodus aus', App._bulkMode === null);
App.startBulkMode('archive');
pruefe('A2 Startleiste verschwindet', elemente['sidebar-bulk-tools'].hidden === true);
pruefe('A3 Bestaetigungsleiste erscheint', elemente['sidebar-bulk-bar'].hidden === false);
pruefe('A4 Hinweis fordert zum Anhaken auf',
elemente['sidebar-bulk-count'].textContent.includes('anhaken'),
elemente['sidebar-bulk-count'].textContent);
App.endBulkMode();
pruefe('A5 Abbrechen stellt den Ausgangszustand her',
App._bulkMode === null && elemente['sidebar-bulk-tools'].hidden === false);
pruefe('A6 Auswahl wird dabei verworfen', App._bulkSel.size === 0);
console.log('\nB) Angeboten wird nur, was zur Auswahl passt');
neu();
App.startBulkMode('archive');
App.toggleBulkSel(1, true);
pruefe('B1 Archivieren sichtbar bei aktiver Lage',
elemente['sidebar-bulk-archive'].hidden === false);
pruefe('B2 Aktivieren verborgen ohne archivierte Lage',
elemente['sidebar-bulk-activate'].hidden === true);
pruefe('B3 Loeschen im Archivmodus verborgen',
elemente['sidebar-bulk-delete'].hidden === true);
pruefe('B4 Zaehler im Knopf stimmt',
elemente['sidebar-bulk-archive']._span.textContent === 'Archivieren (1)',
elemente['sidebar-bulk-archive']._span.textContent);
App.toggleBulkSel(3, true);
pruefe('B5 Aktivieren erscheint bei archivierter Lage',
elemente['sidebar-bulk-activate'].hidden === false);
pruefe('B6 beide Zaehler stimmen',
elemente['sidebar-bulk-archive']._span.textContent === 'Archivieren (1)'
&& elemente['sidebar-bulk-activate']._span.textContent === 'Aktivieren (1)');
pruefe('B7 Auswahl wird gezaehlt',
elemente['sidebar-bulk-count'].textContent === '2 Lagen ausgewählt',
elemente['sidebar-bulk-count'].textContent);
neu();
App.startBulkMode('delete');
App.toggleBulkSel(1, true);
pruefe('B8 im Loeschmodus nur Loeschen',
elemente['sidebar-bulk-delete'].hidden === false
&& elemente['sidebar-bulk-archive'].hidden === true);
pruefe('B9 Zaehler im Loeschknopf',
elemente['sidebar-bulk-delete']._span.textContent === 'Löschen (1)',
elemente['sidebar-bulk-delete']._span.textContent);
console.log('\nC) Alle sichtbaren auswaehlen achtet auf den Filter');
neu();
App.startBulkMode('archive');
App.selectAllVisible(true);
pruefe('C1 ohne Filter sind alle drei gewaehlt', App._bulkSel.size === 3, App._bulkSel.size);
pruefe('C2 der Alle-Haken spiegelt das', elemente['sidebar-bulk-all'].checked === true);
App.selectAllVisible(false);
pruefe('C3 abwaehlen leert die Auswahl', App._bulkSel.size === 0);
App._sidebarFilter = 'mine';
App.selectAllVisible(true);
pruefe('C4 mit Filter nur die eigenen', App._bulkSel.size === 2, [...App._bulkSel]);
pruefe('C5 der fremde Fall fehlt', !App._bulkSel.has(2));
console.log('\nD) Archivieren wirkt nur auf aktive Lagen');
neu();
App.startBulkMode('archive');
App._bulkSel = new Set([1, 3]);
await App.bulkStatus('archived');
pruefe('D1 nur die aktive Lage wurde archiviert',
aufrufe.update.length === 1 && aufrufe.update[0].id === 1, aufrufe.update);
pruefe('D2 Status korrekt gesetzt',
aufrufe.update[0].daten.status === 'archived', aufrufe.update[0]);
pruefe('D3 Auswahlmodus endet danach', App._bulkMode === null);
pruefe('D4 Rueckmeldung an den Nutzer',
meldungen.length === 1 && meldungen[0].art === 'success', meldungen);
console.log('\nE) Aktivieren wirkt nur auf archivierte Lagen');
neu();
App.startBulkMode('archive');
App._bulkSel = new Set([1, 3]);
await App.bulkStatus('active');
pruefe('E1 nur die archivierte Lage wurde aktiviert',
aufrufe.update.length === 1 && aufrufe.update[0].id === 3, aufrufe.update);
console.log('\nF) Loeschen fragt nach und raeumt auf');
neu();
App.startBulkMode('delete');
App._bulkSel = new Set([1, 2]);
umgebung._bestaetigen = false;
await App.bulkDelete();
pruefe('F1 ohne Bestaetigung wird nichts geloescht', aufrufe.delete.length === 0);
pruefe('F2 der Auswahlmodus bleibt bestehen', App._bulkMode === 'delete');
umgebung._bestaetigen = true;
App.currentIncidentId = 1;
await App.bulkDelete();
pruefe('F3 beide Lagen geloescht',
aufrufe.delete.length === 2 && aufrufe.delete.includes(1), aufrufe.delete);
pruefe('F4 die offene Lage wird geschlossen', App.currentIncidentId === null);
pruefe('F5 die Leeransicht erscheint',
elemente['empty-state'].style.display === 'flex'
&& elemente['incident-view'].style.display === 'none',
[elemente['empty-state'].style.display, elemente['incident-view'].style.display]);
console.log('\nG) Fehlschlaege werden gemeldet statt verschluckt');
neu();
App.startBulkMode('archive');
App._bulkSel = new Set([1]);
const alteFunktion = umgebung.API.updateIncident;
umgebung.API.updateIncident = async () => { throw new Error('kein Recht'); };
await App.bulkStatus('archived');
umgebung.API.updateIncident = alteFunktion;
pruefe('G1 Fehlermeldung erscheint',
meldungen.length === 1 && meldungen[0].art === 'error', meldungen);
pruefe('G2 die betroffene Nummer steht darin',
meldungen[0].text.includes('1'), meldungen[0].text);
console.log('\nH) Leere Auswahl loest nichts aus');
neu();
App.startBulkMode('archive');
await App.bulkStatus('archived');
await App.bulkDelete();
pruefe('H1 keine Aufrufe ohne Auswahl',
aufrufe.update.length === 0 && aufrufe.delete.length === 0);
pruefe('H2 keine Rueckmeldung ohne Auswahl', meldungen.length === 0, meldungen);
console.log('\nErgebnis: ' + ok + ' bestanden, ' + fail + ' fehlgeschlagen');
process.exit(fail ? 1 : 0);
}
main();