Dateien
AegisSight-Monitor/tests/test_lauf.js
claude-dev f8d32b1d18 fix(studio): Lauf-Anzeige ohne geöffneten Fall zurücksetzen
Nachtrag zu ffbdd51. _renderLauf lief nur über _stagesIdle, und das wiederum
nur beim Laden eines Falls. Schloss man den Fall oder öffnete das Studio ohne
Fall, blieb der Stand-Kasten mit den Angaben des zuletzt geöffneten Falls
stehen, beim allerersten Aufruf war er leer.

Zwei Absicherungen. Der Leerzustand setzt Fall und Frischedaten zurück und
zeichnet die Spalte einmal neu, und das Dokument bringt den Ausgangstext
bereits mit.

Sechs Prüfungen dafür.

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

338 Zeilen
14 KiB
JavaScript

/**
* 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'),
},
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();