Das Studio konnte Fälle bisher nur anlegen, nicht bearbeiten. Es gab kein Einstellungs-Panel, alle Felder waren nach dem Anlegen eingefroren. Wer etwas ändern wollte, musste zurück in die klassische Ansicht. Für die geplante Ablösung der klassischen Ansicht ist das der erste Blocker. Neuer Reiter Einstellungen in der Fallnavigation, unter Export. Er führt alle Felder des klassischen Bearbeiten-Modals: Titel, Beschreibung, die drei Quellenschalter, Sichtbarkeit, KI-Verarbeitung, Aktualisierung mit Intervall und Startzeit, Aufbewahrung und die drei E-Mail-Benachrichtigungen. Gespeichert wird nur, was sich geändert hat. Zwei Gründe. Der Server aktualisiert feldweise, unverändert zurückgereichte Werte würden trotzdem geschrieben. Und eine unbeabsichtigte Änderung am Intervall kann einen laufenden Zeitplan verstellen. Die Wahl des KI-Wegs, also Anthropic gegen die EU-Fassung über Bedrock Frankfurt, fehlte im Studio vollständig. Sie steht jetzt im Reiter und im Anlege-Dialog, wortgleich zur klassischen Ansicht. Beim Umstellen erscheint ein Hinweis, dass bereits erzeugte Inhalte auf dem bisherigen Weg entstanden sind und die Änderung ab dem nächsten Lauf greift. Läuft gerade ein Schritt, sagt der Reiter das ebenfalls. Dazu die Kennzeichen EU und Anthropic in Fallliste und Kopfzeile. Sie erscheinen nur, wenn der Fall ausdrücklich auf einen Weg gestellt ist. Ohne Angabe gilt die Vorgabe der Organisation, dann bleibt es unbeschriftet. Zwei Feinheiten. Das Intervall wird in der größten glatt aufgehenden Einheit gezeigt, 1440 Minuten also als ein Tag. Und der X-Schalter bleibt bedienbar, wenn der Fall X bereits nutzt, sonst ließe er sich nach dem Wegfall des letzten Zugangs nicht mehr abschalten. Prüfungen ohne Browser und ohne Netzzugriff. 57 Fälle für den Reiter, dazu 29 für die Struktur beider Oberflächen, die prüfen, dass jedes onclick auf eine vorhandene Methode zeigt, dass keine ID doppelt vergeben ist und dass jedes Design-Token ohne Fallback auch definiert ist. Cache-Buster auf 20260802b. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
360 Zeilen
14 KiB
JavaScript
360 Zeilen
14 KiB
JavaScript
/**
|
|
* 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();
|