diff --git a/src/static/js/studio.js b/src/static/js/studio.js
index f8d2af5..191bc6e 100644
--- a/src/static/js/studio.js
+++ b/src/static/js/studio.js
@@ -170,6 +170,14 @@ const Studio = {
const cols = document.getElementById('studio-cols');
if (cols) cols.classList.toggle('idle', on);
document.getElementById('studio-empty').style.display = on ? '' : 'none';
+ // Ohne Fall wuerde die Lauf-Anzeige sonst mit dem Stand des zuletzt
+ // geoeffneten Falls stehen bleiben. Nur beim Leerschalten neu zeichnen,
+ // beim Oeffnen macht das loadDetail mit den richtigen Daten.
+ if (on) {
+ this.incident = null;
+ this.fresh = null;
+ this._renderLauf();
+ }
},
// === Linke Spalte: Reiter "Fälle" / "Quellen" ===
diff --git a/src/static/studio.html b/src/static/studio.html
index 1829d36..af39811 100644
--- a/src/static/studio.html
+++ b/src/static/studio.html
@@ -396,7 +396,7 @@
selben Ziel nebeneinander stehen. Die Beschriftung des Knopfes
richtet sich nach dem Zustand des Falls, siehe _renderLauf. -->
-
+
Kein Fall geöffnet
diff --git a/tests/test_lauf.js b/tests/test_lauf.js
index 603836a..bf90c2c 100644
--- a/tests/test_lauf.js
+++ b/tests/test_lauf.js
@@ -1,314 +1,337 @@
-/**
- * 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),
- },
- _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'];
-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'
- + '};\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('\nErgebnis: ' + ok + ' bestanden, ' + fail + ' fehlgeschlagen');
- process.exit(fail ? 1 : 0);
-}
-
-main();
+/**
+ * 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();