/** * Testet den Einstellungen-Reiter des Studios 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_falleinstellungen.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(); return { id: id, value: '', checked: false, textContent: '', innerHTML: '', hidden: false, disabled: false, min: 0, _fokus: 0, focus() { this._fokus++; }, classList: { toggle(name, an) { if (an) klassen.add(name); else klassen.delete(name); }, contains(name) { return klassen.has(name); }, }, }; } const FELDER = [ 'set-title', 'set-description', 'set-international', 'set-telegram', 'set-x', 'set-x-hint', 'set-visibility', 'set-visibility-text', 'set-ai-backend', 'set-ai-warn', 'set-refresh-mode', 'set-refresh-value', 'set-refresh-unit', 'set-interval-field', 'set-starttime-field', 'set-refresh-starttime', 'set-retention', 'set-notify-summary', 'set-notify-new-articles', 'set-notify-status-change', 'set-save', 'set-state', 'set-running-note', ]; const elemente = {}; const meldungen = []; const aufrufe = { update: [], abo: [] }; let aboAntwort = null; let aboWirft = false; let updateWirft = false; let xKonten = [{ active: true }]; const umgebung = { document: { getElementById: (id) => elemente[id] || null }, UI: { showToast: (text, art) => meldungen.push({ text, art }) }, API: { getSubscription: async () => { if (aboWirft) throw new Error('kein Abo'); return aboAntwort; }, updateIncident: async (id, daten) => { aufrufe.update.push({ id, daten }); if (updateWirft) throw new Error('keine Berechtigung'); return Object.assign({ id }, daten); }, updateSubscription: async (id, daten) => { aufrufe.abo.push({ id, daten }); }, listXAccounts: async () => xKonten, }, console: console, }; umgebung.window = umgebung; // --- Die Methoden aus studio.js herausloesen ------------------------------- const quelle = fs.readFileSync( path.join(__dirname, '..', 'src', 'static', 'js', 'studio.js'), 'utf8'); const von = quelle.indexOf(' // === Einstellungen des Falls ==='); const bis = quelle.indexOf(' _tabTitle(key) {', von); if (von === -1 || bis === -1) { console.log('FEHLER: Einstellungen-Block in studio.js nicht gefunden'); process.exit(1); } const kontext = vm.createContext(umgebung); vm.runInContext('var Studio = {\n' + quelle.slice(von, bis) + '\n' + ' incident: null, incidents: [], _runningStage: null,\n' + ' _kopf: 0, _liste: 0,\n' + ' renderHeader() { this._kopf++; },\n' + ' renderCases() { this._liste++; },\n' + '};', kontext); const Studio = kontext.Studio; const LAGE = { id: 7, title: 'Ceuta', description: 'Grenzsituation', type: 'adhoc', status: 'active', visibility: 'public', international_sources: true, include_telegram: false, include_x: false, ai_backend: 'bedrock', refresh_mode: 'auto', refresh_interval: 1440, refresh_start_time: '06:30', retention_days: 30, }; function neu(ueber) { for (const id of FELDER) elemente[id] = element(id); Studio.incident = Object.assign({}, LAGE, ueber || {}); Studio.incidents = [Object.assign({}, Studio.incident)]; Studio._runningStage = null; Studio._settingsAlt = null; Studio._settingsAboAlt = null; Studio._kopf = 0; Studio._liste = 0; aufrufe.update.length = 0; aufrufe.abo.length = 0; meldungen.length = 0; aboAntwort = null; aboWirft = false; updateWirft = false; xKonten = [{ active: true }]; } const g = (id) => elemente[id]; async function main() { console.log('\nA) Das Formular wird aus dem Fall gefüllt'); neu(); await Studio.loadSettings(); pruefe('A1 Titel', g('set-title').value === 'Ceuta'); pruefe('A2 Beschreibung', g('set-description').value === 'Grenzsituation'); pruefe('A3 internationale Quellen an', g('set-international').checked === true); pruefe('A4 Telegram aus', g('set-telegram').checked === false); pruefe('A5 öffentlich', g('set-visibility').checked === true); pruefe('A6 KI-Weg steht auf EU', g('set-ai-backend').value === 'bedrock'); pruefe('A7 Aufbewahrung', String(g('set-retention').value) === '30'); pruefe('A8 Startzeit', g('set-refresh-starttime').value === '06:30'); console.log('\nB) Das Intervall wird in der größten glatten Einheit gezeigt'); pruefe('B1 1440 Minuten werden zu 1 Tag', g('set-refresh-unit').value === '1440' && String(g('set-refresh-value').value) === '1', [g('set-refresh-unit').value, g('set-refresh-value').value]); neu({ refresh_interval: 90 }); await Studio.loadSettings(); pruefe('B2 90 Minuten bleiben Minuten', g('set-refresh-unit').value === '1' && String(g('set-refresh-value').value) === '90', [g('set-refresh-unit').value, g('set-refresh-value').value]); neu({ refresh_interval: 120 }); await Studio.loadSettings(); pruefe('B3 120 Minuten werden zu 2 Stunden', g('set-refresh-unit').value === '60' && String(g('set-refresh-value').value) === '2'); neu({ refresh_interval: 10080 }); await Studio.loadSettings(); pruefe('B4 10080 Minuten werden zu 1 Woche', g('set-refresh-unit').value === '10080' && String(g('set-refresh-value').value) === '1'); console.log('\nC) Intervall und Startzeit nur bei automatischer Aktualisierung'); neu({ refresh_mode: 'manual' }); await Studio.loadSettings(); pruefe('C1 bei manuell verborgen', g('set-interval-field').classList.contains('visible') === false && g('set-starttime-field').classList.contains('visible') === false); g('set-refresh-mode').value = 'auto'; Studio.settingsRefreshToggle(); pruefe('C2 bei automatisch sichtbar', g('set-interval-field').classList.contains('visible') === true && g('set-starttime-field').classList.contains('visible') === true); console.log('\nD) Gespeichert wird nur, was sich geändert hat'); neu(); await Studio.loadSettings(); await Studio.saveSettings(); pruefe('D1 ohne Änderung kein Aufruf', aufrufe.update.length === 0, aufrufe.update); pruefe('D2 der Zustand sagt es', g('set-state').textContent === 'Nichts geändert', g('set-state').textContent); g('set-title').value = 'Ceuta und Melilla'; await Studio.saveSettings(); pruefe('D3 genau ein Aufruf', aufrufe.update.length === 1); pruefe('D4 nur der Titel im Patch', JSON.stringify(Object.keys(aufrufe.update[0].daten)) === '["title"]', aufrufe.update[0].daten); pruefe('D5 richtige Lage', aufrufe.update[0].id === 7); pruefe('D6 Liste und Kopfzeile neu gezeichnet', Studio._kopf === 1 && Studio._liste === 1); pruefe('D7 Rückmeldung an den Nutzer', meldungen.length === 1 && meldungen[0].art === 'success', meldungen); await Studio.saveSettings(); pruefe('D8 direkt danach ist wieder nichts offen', aufrufe.update.length === 1, aufrufe.update); console.log('\nE) Der KI-Weg lässt sich umstellen'); neu(); await Studio.loadSettings(); g('set-ai-backend').value = 'cli'; await Studio.saveSettings(); pruefe('E1 nur der KI-Weg im Patch', JSON.stringify(aufrufe.update[0].daten) === '{"ai_backend":"cli"}', aufrufe.update[0].daten); neu(); await Studio.loadSettings(); g('set-ai-backend').value = ''; await Studio.saveSettings(); pruefe('E2 Zurücksetzen auf die Org-Vorgabe schickt den Leerstring', aufrufe.update.length === 1 && aufrufe.update[0].daten.ai_backend === '', aufrufe.update[0] && aufrufe.update[0].daten); console.log('\nF) Der Warnhinweis folgt der Änderung'); neu(); await Studio.loadSettings(); pruefe('F1 zunächst verborgen', g('set-ai-warn').hidden === true); g('set-ai-backend').value = 'cli'; Studio.settingsAiChanged(); pruefe('F2 nach der Umstellung sichtbar', g('set-ai-warn').hidden === false); g('set-ai-backend').value = 'bedrock'; Studio.settingsAiChanged(); pruefe('F3 zurückgestellt wieder verborgen', g('set-ai-warn').hidden === true); g('set-ai-backend').value = 'cli'; Studio.settingsAiChanged(); await Studio.saveSettings(); pruefe('F4 nach dem Speichern ist der neue Weg der Normalfall', g('set-ai-warn').hidden === true); console.log('\nG) Der Hinweis auf einen laufenden Schritt'); neu(); Studio._runningStage = 'collect'; await Studio.loadSettings(); pruefe('G1 bei laufendem Schritt sichtbar', g('set-running-note').hidden === false); neu(); await Studio.loadSettings(); pruefe('G2 sonst verborgen', g('set-running-note').hidden === true); console.log('\nH) E-Mail-Benachrichtigungen'); neu(); aboAntwort = { notify_email_summary: true, notify_email_new_articles: false, notify_email_status_change: true }; await Studio.loadSettings(); pruefe('H1 Stand wird geladen', g('set-notify-summary').checked === true && g('set-notify-new-articles').checked === false && g('set-notify-status-change').checked === true); await Studio.saveSettings(); pruefe('H2 ohne Änderung kein Abo-Aufruf', aufrufe.abo.length === 0); g('set-notify-new-articles').checked = true; await Studio.saveSettings(); pruefe('H3 Änderung wird geschickt', aufrufe.abo.length === 1); pruefe('H4 alle drei Werte im Aufruf', aufrufe.abo[0].daten.notify_email_new_articles === true && aufrufe.abo[0].daten.notify_email_summary === true, aufrufe.abo[0].daten); pruefe('H5 die Lage selbst wurde nicht angefasst', aufrufe.update.length === 0); neu(); aboWirft = true; await Studio.loadSettings(); pruefe('H6 ohne Abo bleiben die Haken aus', g('set-notify-summary').checked === false && g('set-notify-status-change').checked === false); console.log('\nI) Randfälle'); neu(); await Studio.loadSettings(); g('set-title').value = ' '; await Studio.saveSettings(); pruefe('I1 ohne Titel wird nicht gespeichert', aufrufe.update.length === 0); pruefe('I2 Fehlermeldung erscheint', meldungen.length === 1 && meldungen[0].art === 'error', meldungen); pruefe('I3 das Titelfeld bekommt den Fokus', g('set-title')._fokus === 1); neu(); await Studio.loadSettings(); updateWirft = true; g('set-title').value = 'Anderer Titel'; await Studio.saveSettings(); pruefe('I4 Fehler beim Speichern wird gemeldet', meldungen.some(m => m.art === 'error'), meldungen); pruefe('I5 der Zustand sagt es auch', g('set-state').textContent === 'Nicht gespeichert', g('set-state').textContent); pruefe('I6 der Knopf ist wieder bedienbar', g('set-save').disabled === false); neu(); Studio.incident = null; await Studio.loadSettings(); await Studio.saveSettings(); pruefe('I7 ohne offenen Fall passiert nichts', aufrufe.update.length === 0); console.log('\nJ) Der X-Schalter hängt an den hinterlegten Zugängen'); neu(); xKonten = []; await Studio.loadSettings(); pruefe('J1 ohne Zugang gesperrt', g('set-x').disabled === true); pruefe('J2 mit Hinweis', g('set-x-hint').hidden === false); neu({ include_x: true }); xKonten = []; await Studio.loadSettings(); pruefe('J3 nutzt der Fall X bereits, bleibt er bedienbar', g('set-x').disabled === false); neu(); xKonten = [{ active: true }]; await Studio.loadSettings(); pruefe('J4 mit Zugang frei und ohne Hinweis', g('set-x').disabled === false && g('set-x-hint').hidden === true); console.log('\nK) Die Sichtbarkeit beschriftet sich selbst'); neu(); await Studio.loadSettings(); pruefe('K1 öffentlich', g('set-visibility-text').textContent.startsWith('Öffentlich'), g('set-visibility-text').textContent); g('set-visibility').checked = false; Studio.settingsVisibilityHint(); pruefe('K2 privat', g('set-visibility-text').textContent.startsWith('Privat'), g('set-visibility-text').textContent); console.log('\nL) Das Kennzeichen des KI-Wegs'); pruefe('L1 EU', Studio._aiTag({ ai_backend: 'bedrock' }, 'x').includes('>EU<')); pruefe('L2 EU trägt die Klasse', Studio._aiTag({ ai_backend: 'bedrock' }, 'x').includes('x ai-eu')); pruefe('L3 Anthropic', Studio._aiTag({ ai_backend: 'cli' }, 'x').includes('>Anthropic<')); pruefe('L4 ohne Angabe bleibt es leer', Studio._aiTag({ ai_backend: null }, 'x') === ''); pruefe('L5 auch ohne Lage', Studio._aiTag(null, 'x') === ''); pruefe('L6 unbekannter Wert erzeugt nichts', Studio._aiTag({ ai_backend: 'irgendwas' }, 'x') === ''); console.log('\nM) Die Untergrenze des Intervalls'); neu(); await Studio.loadSettings(); g('set-refresh-unit').value = '1'; g('set-refresh-value').value = '3'; Studio.settingsIntervalMin(); pruefe('M1 in Minuten sind 10 das Minimum', String(g('set-refresh-value').value) === '10', g('set-refresh-value').value); g('set-refresh-unit').value = '60'; g('set-refresh-value').value = '1'; Studio.settingsIntervalMin(); pruefe('M2 in Stunden ist 1 erlaubt', String(g('set-refresh-value').value) === '1'); console.log('\nErgebnis: ' + ok + ' bestanden, ' + fail + ' fehlgeschlagen'); process.exit(fail ? 1 : 0); } main();