/** * Testet die Lauf-Anzeige der Bausteinspalte ohne Browser. * * Die Methoden werden aus studio.js herausgeloest und gegen ein schlankes * DOM-Double laufen gelassen. Kein Netzzugriff, keine Kosten. * * Aufruf aus dem Projektstamm: * node tests/test_lauf.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 ------------------------------------------------------------ function element(id) { const klassen = new Set(); const el = { id: id, innerHTML: '', textContent: '', title: '', hidden: false, disabled: false, classList: { add: (n) => klassen.add(n), remove: (n) => klassen.delete(n), toggle: (n, an) => { if (an) klassen.add(n); else klassen.delete(n); }, contains: (n) => klassen.has(n), }, style: {}, _kinder: {}, querySelector(sel) { const k = sel.replace('.', ''); if (!this._kinder[k]) this._kinder[k] = element(k); return this._kinder[k]; }, }; return el; } const IDS = ['lauf-stand', 'ls-haupt', 'ls-neben', 'ls-btn', 'ls-hinweis', 'lauf-kette', 'lauf-fortschritt', 'lauf-schritte', 'start-panel', 'art-summary-meta', 'art-fc-meta', 'art-snap-meta', 'art-tl-meta', 'studio-cols', 'studio-empty']; const elemente = {}; const meldungen = []; const aufrufe = []; const umgebung = { document: { getElementById: (id) => elemente[id] || null, querySelectorAll: () => [], }, UI: { showToast: (t, a) => meldungen.push({ t, a }), escape: (s) => String(s === undefined || s === null ? '' : s) .replace(/&/g, '&').replace(//g, '>'), }, CSS: { escape: (s) => s }, localStorage: { _d: {}, setItem(k, v) { this._d[k] = v; }, getItem(k) { return this._d[k]; } }, console: console, }; umgebung.window = umgebung; // --- Den Lauf-Block aus studio.js herausloesen ----------------------------- const quelle = fs.readFileSync( path.join(__dirname, '..', 'src', 'static', 'js', 'studio.js'), 'utf8'); const von = quelle.indexOf(' // === Der Lauf (Spalte 3) ==='); const bis = quelle.indexOf(' async runStage(stage) {', von); if (von === -1 || bis === -1) { console.log('FEHLER: Lauf-Block in studio.js nicht gefunden'); process.exit(1); } const kontext = vm.createContext(umgebung); const Studio = vm.runInContext( 'var Studio = {\n' + quelle.slice(von, bis) + '\n' + ' incident: null, fresh: null, _runningStage: null,\n' + ' async runStage(s) { this._letzterStart = s; },\n' // _showEmpty steht ausserhalb des herausgeloesten Blocks, deshalb hier // wortgleich nachgebildet. + ' _showEmpty(on) {\n' + ' const c = document.getElementById("studio-cols");\n' + ' if (c) c.classList.toggle("idle", on);\n' + ' document.getElementById("studio-empty").style.display = on ? "" : "none";\n' + ' if (on) { this.incident = null; this.fresh = null; this._renderLauf(); }\n' + ' },\n' + '};\nStudio;', kontext); const LAGE = { id: 7, title: 'Ceuta', type: 'adhoc' }; // Vier Zustaende, wie sie im Betrieb wirklich vorkommen const FRISCH = { leer: { articles: 0, summary: {}, factcheck: {}, geoparse: {} }, veraltet: { articles: 85, summary: { exists: true, last: '2026-08-02 18:10', pending: 3 }, factcheck: { exists: true, last: '2026-08-02 16:00', facts: 19, pending: 3 }, geoparse: { pending: 0 }, snapshots: 4, events: 12 }, aktuell: { articles: 85, summary: { exists: true, last: '2026-08-02 18:40', pending: 0 }, factcheck: { exists: true, last: '2026-08-02 18:41', facts: 19, pending: 0 }, geoparse: { pending: 0 }, snapshots: 4, events: 12 }, halb: { articles: 85, summary: { exists: false }, factcheck: { exists: false }, geoparse: { pending: 85 } }, }; function neu(zustand, ueber) { for (const id of IDS) elemente[id] = element(id); Studio.incident = Object.assign({}, LAGE, ueber || {}); Studio.fresh = FRISCH[zustand] ? JSON.parse(JSON.stringify(FRISCH[zustand])) : null; Studio._runningStage = null; Studio._schrittOffen = null; Studio._letzterStart = null; meldungen.length = 0; aufrufe.length = 0; } const g = (id) => elemente[id]; const knopfText = () => g('ls-btn').querySelector('.ls-text').textContent; async function main() { console.log('\nA) Der Startknopf richtet sich nach dem Zustand'); neu('leer'); Studio._renderLauf(); pruefe('A1 leerer Fall lädt zum Start ein', knopfText() === 'Lauf starten', knopfText()); pruefe('A2 der Stand sagt warum', g('ls-haupt').textContent.includes('Noch keine Artikel'), g('ls-haupt').textContent); pruefe('A3 der Kasten ist hervorgehoben', g('lauf-stand').classList.contains('betont')); pruefe('A4 mit einem Hinweis, was danach kommt', g('ls-hinweis').textContent.length > 10 && g('ls-hinweis').hidden === false); neu('leer', { type: 'research' }); Studio._renderLauf(); pruefe('A5 bei einer Recherche heißt es Recherche starten', knopfText() === 'Recherche starten', knopfText()); neu('veraltet'); Studio._renderLauf(); pruefe('A6 bei neuen Artikeln heißt es aktualisieren', knopfText() === 'Jetzt aktualisieren', knopfText()); pruefe('A7 der Stand nennt die Zahl', g('ls-haupt').textContent.includes('3 neue Artikel'), g('ls-haupt').textContent); pruefe('A8 und warnt', g('ls-haupt').classList.contains('warn')); neu('aktuell'); Studio._renderLauf(); pruefe('A9 wenn alles frisch ist heißt es erneut durchlaufen', knopfText() === 'Erneut durchlaufen', knopfText()); pruefe('A10 der Kasten ist dann ruhig', !g('lauf-stand').classList.contains('betont')); pruefe('A11 und der Hinweis verschwindet', g('ls-hinweis').hidden === true); pruefe('A12 der Bestand steht daneben', g('ls-neben').textContent === '85 Artikel im Bestand', g('ls-neben').textContent); console.log('\nB) Während eines Laufs'); neu('veraltet'); Studio._runningStage = 'full'; Studio._renderLauf(); pruefe('B1 der Knopf bricht ab', knopfText() === 'Abbrechen', knopfText()); pruefe('B2 er ist rot abgesetzt', g('ls-btn').classList.contains('abbrechen')); pruefe('B3 er bleibt klickbar', g('ls-btn').disabled === false); pruefe('B4 der Stand sagt, was läuft', g('ls-haupt').textContent.includes('Kompletter Lauf'), g('ls-haupt').textContent); neu('veraltet'); Studio._runningStage = 'analyze'; Studio._renderLauf(); pruefe('B5 ein nicht abbrechbarer Schritt sperrt den Knopf', g('ls-btn').disabled === true); pruefe('B6 und wird nicht als Abbruch angeboten', !g('ls-btn').classList.contains('abbrechen')); pruefe('B7 der Stand nennt den Schritt beim Ergebnisnamen', g('ls-haupt').textContent.includes('Lagebild schreiben'), g('ls-haupt').textContent); console.log('\nC) Der Knopf trifft den richtigen Schritt'); neu('veraltet'); await Studio.laufKnopf(); pruefe('C1 ohne Lauf startet er den kompletten Lauf', Studio._letzterStart === 'full'); neu('veraltet'); Studio._runningStage = 'collect'; await Studio.laufKnopf(); pruefe('C2 ein laufendes Sammeln wird auch als Sammeln abgebrochen', Studio._letzterStart === 'collect', Studio._letzterStart); neu('veraltet'); Studio._runningStage = 'analyze'; await Studio.laufKnopf(); pruefe('C3 ein nicht abbrechbarer Schritt löst nichts aus', Studio._letzterStart === null, Studio._letzterStart); pruefe('C4 und sagt das auch', meldungen.length === 1 && meldungen[0].a === 'info', meldungen); console.log('\nD) Die Kette'); neu('aktuell'); Studio._renderLauf(); const kette = g('lauf-kette').innerHTML; pruefe('D1 vier Knoten', (kette.match(/class="kn /g) || []).length === 4, (kette.match(/class="kn /g) || []).length); pruefe('D2 drei Verbindungen', (kette.match(/kn-linie/g) || []).length === 3); pruefe('D3 alle vier erledigt', (kette.match(/kn-fertig/g) || []).length === 4); pruefe('D4 der Zähler stimmt', g('lauf-fortschritt').textContent === '4 von 4 Schritten auf dem neuesten Stand', g('lauf-fortschritt').textContent); neu('veraltet'); Studio._renderLauf(); pruefe('D5 bei veralteten Ergebnissen sind nicht alle grün', (g('lauf-kette').innerHTML.match(/kn-fertig/g) || []).length === 2, (g('lauf-kette').innerHTML.match(/kn-fertig/g) || []).length); pruefe('D6 zwei stehen auf veraltet', (g('lauf-kette').innerHTML.match(/kn-veraltet/g) || []).length === 2); pruefe('D7 der Zähler zählt nur die fertigen', g('lauf-fortschritt').textContent.startsWith('2 von 4'), g('lauf-fortschritt').textContent); neu('leer'); Studio._renderLauf(); pruefe('D8 ohne Artikel sind drei Schritte gesperrt', (g('lauf-kette').innerHTML.match(/kn-gesperrt/g) || []).length === 3, (g('lauf-kette').innerHTML.match(/kn-gesperrt/g) || []).length); console.log('\nE) Die Schritte'); neu('halb'); Studio._renderLauf(); const l = g('lauf-schritte').innerHTML; pruefe('E1 vier Schritte', (l.match(/class="schritt /g) || []).length === 4, (l.match(/class="schritt /g) || []).length); pruefe('E2 Ergebnisnamen statt Erzeugernamen', l.includes('Artikel holen') && l.includes('Lagebild schreiben') && l.includes('Fakten prüfen') && l.includes('Orte erkennen')); pruefe('E3 keine Erzeugernamen mehr', !l.includes('Geoparsing') && !l.includes('>Sammeln<') && !l.includes('>Analyse<')); pruefe('E4 der erste Schritt ist erledigt', l.includes('schritt schritt-fertig')); pruefe('E5 die noch nicht erzeugten sind offen', (l.match(/schritt-offen/g) || []).length === 2, (l.match(/schritt-offen/g) || []).length); pruefe('E6 die Karte meldet offene Artikel', l.includes('85 Artikel noch nicht verortet')); neu('aktuell', { type: 'research' }); Studio._renderLauf(); pruefe('E7 bei einer Recherche heißt Schritt zwei Recherchebericht', g('lauf-schritte').innerHTML.includes('Recherchebericht schreiben')); console.log('\nF) Kein Schritt startet etwas'); for (const z of ['leer', 'veraltet', 'aktuell', 'halb']) { neu(z); Studio._renderLauf(); const h = g('lauf-schritte').innerHTML; pruefe('F ' + z + ' enthält keinen Startaufruf', !h.includes('runStage') && !h.includes('laufKnopf')); } console.log('\nG) Aufklappen erklärt nur'); neu('aktuell'); Studio._renderLauf(); pruefe('G1 anfangs ist nichts offen', !g('lauf-schritte').innerHTML.includes('schritt offen') && !g('lauf-schritte').innerHTML.includes(' offen"')); Studio.klappSchritt(1); pruefe('G2 ein Klick öffnet den Schritt', g('lauf-schritte').innerHTML.includes(' offen"'), Studio._schrittOffen); pruefe('G3 und zeigt die Erklärung', g('lauf-schritte').innerHTML.includes('Wertet alle Artikel aus')); Studio.klappSchritt(1); pruefe('G4 nochmal klicken schließt ihn', Studio._schrittOffen === null); Studio.klappSchritt(0); Studio.klappSchritt(2); pruefe('G5 es ist immer nur einer offen', Studio._schrittOffen === 2); console.log('\nH) Nebenangaben an den Ansichten'); neu('aktuell'); Studio._renderLauf(); pruefe('H1 Lagebild nennt die Artikelzahl', g('art-summary-meta').textContent.includes('85 Artikel ausgewertet'), g('art-summary-meta').textContent); pruefe('H2 Faktencheck nennt die Fakten', g('art-fc-meta').textContent.includes('19 Fakten'), g('art-fc-meta').textContent); pruefe('H3 frühere Berichte werden gezählt', g('art-snap-meta').textContent === '4'); pruefe('H4 Ereignisse werden gezählt', g('art-tl-meta').textContent === '12 Ereignisse', g('art-tl-meta').textContent); neu('leer'); Studio._renderLauf(); pruefe('H5 ohne Lauf steht noch nicht erzeugt', g('art-summary-meta').textContent === 'noch nicht erzeugt', g('art-summary-meta').textContent); console.log('\nI) Ohne offenen Fall'); neu('leer'); Studio.incident = null; Studio.fresh = null; Studio._renderLauf(); pruefe('I1 der Knopf ist gesperrt', g('ls-btn').disabled === true); pruefe('I2 der Stand sagt es', g('ls-haupt').textContent === 'Kein Fall geöffnet', g('ls-haupt').textContent); pruefe('I3 die Kette ist leer', g('lauf-kette').innerHTML === ''); pruefe('I4 die Schrittliste ist leer', g('lauf-schritte').innerHTML === ''); pruefe('I5 nichts stürzt ab', true); console.log('\nJ) Ohne Frischedaten'); neu('leer'); Studio.fresh = null; Studio._renderLauf(); pruefe('J1 die Anzeige bleibt bedienbar', g('ls-btn').disabled === false); pruefe('J2 und behandelt den Fall wie leer', g('ls-haupt').textContent.includes('Noch keine Artikel')); console.log('\nK) Der Leerzustand räumt die Anzeige auf'); neu('aktuell'); Studio._renderLauf(); pruefe('K1 zunächst steht der Stand des Falls da', g('ls-haupt').textContent.includes('neuesten Stand')); Studio._showEmpty(true); pruefe('K2 danach steht dort kein alter Stand mehr', g('ls-haupt').textContent === 'Kein Fall geöffnet', g('ls-haupt').textContent); pruefe('K3 die Kette ist geleert', g('lauf-kette').innerHTML === ''); pruefe('K4 die Schrittliste ist geleert', g('lauf-schritte').innerHTML === ''); pruefe('K5 der Knopf ist gesperrt', g('ls-btn').disabled === true); pruefe('K6 der Fall ist wirklich losgelassen', Studio.incident === null); console.log('\nErgebnis: ' + ok + ' bestanden, ' + fail + ' fehlgeschlagen'); process.exit(fail ? 1 : 0); } main();