/** * 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();