// UI-Tests der Desktop-App: lädt test/mock.html in einem unsichtbaren Electron-Fenster
// (mit Mock-API statt Server) und steuert die echte Oberfläche per Skript.
// Start: npm test
const { app, BrowserWindow } = require('electron');
const path = require('path');
const failures = [];
let passed = 0;
const consoleErrors = [];
function check(name, ok, detail) {
if (ok) { passed++; console.log(' ok ' + name); }
else { failures.push(name); console.log(' FAIL ' + name + (detail ? ' – ' + detail : '')); }
}
app.whenReady().then(async () => {
const win = new BrowserWindow({ show: false, width: 1280, height: 800, webPreferences: { contextIsolation: false, nodeIntegration: false } });
win.webContents.on('console-message', (_e, level, message) => { if (level >= 3) consoleErrors.push(message); });
win.webContents.on('render-process-gone', (_e, d) => { failures.push('Renderer abgestürzt: ' + d.reason); });
const run = (code) => win.webContents.executeJavaScript(code);
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const waitFor = async (code, ms = 4000) => {
const end = Date.now() + ms;
while (Date.now() < end) { if (await run(code).catch(() => false)) return true; await wait(50); }
return false;
};
try {
await win.loadFile(path.join(__dirname, 'mock.html'));
// Mock-Routen, die für die Admin-Seiten fehlen, liefern eine leere Liste statt eines Fehlers
await run(`(() => { const o = window.deskhub.api; window.deskhub.api = async (r) => { const res = await o(r); return res.error && /^Mock:/.test(res.error.message) ? { data: { items: [], rules: [], holidays: [], hours: [], staffOptions: [], departments: [], permissionKeys: [], fieldOptions: [], operatorOptions: [], actionOptions: [] } } : res; }; })()`);
console.log('Start & Navigation');
check('App startet und zeigt das Hauptgerüst', await waitFor(`!!document.querySelector('.side') && !!document.getElementById('main')`));
check('Seitenleiste enthält Hauptpunkte', await run(`['Übersicht','Tickets','Kunden'].every(t => document.querySelector('.side').innerText.includes(t))`));
await run(`go('dashboard')`);
check('Dashboard zeigt Kennzahlen', await waitFor(`document.getElementById('main').innerText.includes('Drucker druckt nicht')`));
console.log('Programm-Update');
check('Ohne neue Version kein Update-Banner', await run(`!document.getElementById('banners').innerText.includes('Update verfügbar')`));
await run(`window.__update = { available: true, packaged: true, version: '9.9.9', current: '1.1.0', notes: '' }; checkUpdate()`);
check('Neue Version zeigt Banner mit Button', await waitFor(`document.getElementById('banners').innerText.includes('Update verfügbar: DeskHub 9.9.9') && document.getElementById('banners').innerText.includes('Jetzt aktualisieren')`));
await run(`[...document.querySelectorAll('#banners button')].find(b => b.textContent === 'Später').click()`);
check('„Später“ blendet das Banner aus', await run(`!document.getElementById('banners').innerText.includes('Update verfügbar')`));
await run(`window.__update = undefined; state.update = undefined; paintBanners()`);
await run(`go('tickets')`);
check('Ticketliste zeigt alle Tickets', await waitFor(`['Drucker druckt nicht','Passwort zurücksetzen','Rechnung fehlt','VPN Verbindung bricht ab'].every(t => document.getElementById('main').innerText.includes(t))`));
check('Reiter „Wiedervorlage“, „Pausiert“ und „Beobachtet“ sind da', await run(`(() => { const t = [...document.querySelectorAll('#main .tabs button')].map((b) => b.innerText).join('|'); return t.includes('Wiedervorlage') && t.includes('Pausiert') && t.includes('Beobachtet'); })()`));
check('Gespeicherte Ansicht erscheint als Chip', await run(`document.getElementById('main').innerText.includes('Meine überfälligen')`));
// E-Mail-Abruf von Hand und Nachladen der Mail-Anhänge
check('Kopfleiste hat den Knopf „E-Mails abrufen“', await run(`!![...document.querySelectorAll('.topbar button')].find(b => b.textContent.includes('E-Mails abrufen'))`));
await run(`[...document.querySelectorAll('.topbar button')].find(b => b.textContent.includes('E-Mails abrufen')).click()`);
check('Abruf von Hand sendet die Anfrage und zeigt das Ergebnis', await waitFor(`(window.__mailFetchPosts || []).length === 1 && document.body.innerText.includes('Abruf fertig: 2 E-Mail(s) verarbeitet.')`));
check('Knopf ist nach dem Abruf wieder bedienbar', await waitFor(`(() => { const b = [...document.querySelectorAll('.topbar button')].find(b => b.textContent.includes('E-Mails abrufen')); return !!b && !b.disabled; })()`));
// Neue Rechte (Server 1.0.85): Knöpfe und Bereiche erscheinen nur mit dem Recht
const withPerm = async (perm, on, fn) => { await run(`state.staff.permissions['${perm}'] = ${on}; renderShell(); go('tickets')`); await wait(400); return fn(); };
check('Ohne das Recht „E-Mails jetzt abrufen“ fehlt der Knopf', await withPerm('fetch_mail', false, () => run(`![...document.querySelectorAll('.topbar button')].find(b => b.textContent.includes('E-Mails abrufen'))`)));
check('Mit dem Recht ist der Knopf wieder da', await withPerm('fetch_mail', true, () => run(`!![...document.querySelectorAll('.topbar button')].find(b => b.textContent.includes('E-Mails abrufen'))`)));
await run(`state.staff.permissions.manage_banlist = false; state.staff.permissions.manage_teams = true; go('admin')`);
check('Sperrliste ist ohne Recht nicht in der Verwaltung', await waitFor(`document.getElementById('main').innerText.includes('Teams') && !document.getElementById('main').innerText.includes('Sperrliste')`));
await run(`state.staff.permissions.manage_banlist = true; go('admin')`);
check('Sperrliste erscheint mit dem Recht „Sperrliste“ (ohne Administrator zu sein)', await waitFor(`document.getElementById('main').innerText.includes('Sperrliste')`));
// Rollen-Editor: Gruppen und Beschriftungen vom Server, wirksame Rechte (Standardwerte) angekreuzt
await run(`go('admin', 'roles')`);
check('Rollen: Tabelle zeigt die Rolle', await waitFor(`document.getElementById('main').innerText.includes('Teamleitung')`));
await run(`[...document.querySelectorAll('#main tr.click')][0].click()`);
check('Rollen-Editor zeigt Gruppen und neue Rechte mit Beschriftung vom Server', await waitFor(`(() => { const t = document.body.innerText.toLowerCase(); return t.includes('tickets bearbeiten') && t.includes('team und sicherheit') && t.includes('tickets zusammenführen und teilen'); })()`));
check('Rechte, die für die Rolle als Standard gelten, sind angekreuzt (nicht „aus“)', await run(`(() => { const l = [...document.querySelectorAll('.modal label.check, .modal-box label.check, label.check')].find(x => x.textContent.includes('zusammenführen und teilen')); return !!l && l.querySelector('input').checked; })()`));
await run(`document.querySelector('.modal button.ghost, .modal .btn.ghost, .modal-box .btn.ghost')?.click()`);
await run(`window.__source = 'email'; go('ticket', 1)`);
check('E-Mail-Ticket: Knopf „Anhänge laden“ ist da', await waitFor(`!![...document.querySelectorAll('#main button')].find(b => b.textContent.includes('Anhänge laden'))`));
await run(`[...document.querySelectorAll('#main button')].find(b => b.textContent.includes('Anhänge laden')).click()`);
check('Nachladen sendet die Ticketnummer', await waitFor(`(window.__mailReloadPosts || []).length === 1 && !!window.__mailReloadPosts[0].number`));
await run(`window.__source = 'web'; go('ticket', 1)`);
check('Web-Ticket: kein Knopf „Anhänge laden“', await waitFor(`document.getElementById('main').innerText.includes('Drucker druckt nicht') && ![...document.querySelectorAll('#main button')].find(b => b.textContent.includes('Anhänge laden'))`));
await run(`go('ticket', 1)`);
check('Ticketdetail zeigt Betreff und Verlauf', await waitFor(`['Drucker druckt nicht', 'Mein Drucker druckt nichts mehr', 'Treiber prüfen'].every(x => document.getElementById('main').innerText.includes(x))`));
check('Ticketdetail zeigt Eigenschaften und Aktionen', await run(`['eigenschaften', 'schließen'].every(x => document.getElementById('main').innerText.toLowerCase().includes(x))`));
check('Ticketdetail: Wiedervorlage- und Beobachten-Knopf sind da', await run(`(() => { const t = document.getElementById('main').innerText; return t.includes('Auf später') && t.includes('Beobachten'); })()`));
check('Ticketdetail: „Dazu passend“ zeigt Artikel und ähnliches Ticket', await run(`(() => { const t = document.getElementById('main').innerText; return t.toLowerCase().includes('dazu passend') && t.includes('Drucker einrichten') && t.includes('T-77'); })()`));
check('Ticketdetail: Bewertung des Kunden (Sterne und Kommentar)', await run(`(() => { const t = document.getElementById('main').textContent; return t.includes('Kundenzufriedenheit') && t.includes('★★★★☆') && t.includes('4 von 5') && t.includes('Schnell und freundlich'); })()`));
check('Ticketdetail: Beobachter und Cc-Empfänger werden gezeigt', await run(`(() => { const t = document.getElementById('main').innerText; return t.includes('Eva Beispiel') && t.includes('Cc Person') && t.includes('cc@firma.de'); })()`));
await run(`window.__bodies = {}; (() => { const i = [...document.querySelectorAll('#main input')].find((x) => x.placeholder === 'Cc-Adresse hinzufügen'); i.value = 'neu@kunde.de'; [...document.querySelectorAll('#main button')].find((b) => b.innerText === 'Hinzufügen').click(); })()`);
check('Cc hinzufügen sendet die Adresse an den Server und zeigt sie an', await waitFor(`(window.__bodies['ticket_collaborators.php'] || {}).action === 'add' && window.__bodies['ticket_collaborators.php'].email === 'neu@kunde.de' && document.getElementById('main').innerText.includes('neu@kunde.de')`));
await run(`window.__bodies = {}; [...document.querySelectorAll('#main .mini.row button')][0].click()`);
check('Cc entfernen sendet die Kennung an den Server', await waitFor(`(window.__bodies['ticket_collaborators.php'] || {}).action === 'remove' && window.__bodies['ticket_collaborators.php'].id === 1 && !document.getElementById('main').innerText.includes('Cc Person')`));
await run(`window.__collabs = null`);
await run(`window.__bodies = {}; [...document.querySelectorAll('#main button')].find((b) => b.innerText.includes('Beobachten') && !b.innerText.includes('Nicht')).click()`);
await run(`window.__web = ''; [...document.querySelectorAll('#main button')].find((b) => b.innerText.includes('Im Browser')).click()`);
check('Ticket: „Im Browser“ öffnet das Ticket in der Weboberfläche', await waitFor(`String(window.__web).startsWith('/staff/ticket_view.php?n=')`));
check('Beobachten sendet following=true an den Server', await waitFor(`(window.__bodies['ticket_update.php'] || {}).following === true`));
await run(`go('ticket', 1)`);
await waitFor(`document.getElementById('main').innerText.includes('Drucker druckt nicht')`);
check('Interne Notiz ist gekennzeichnet', await run(`!!document.querySelector('.thread.note')`));
// Textbausteine nur für die Abteilung des Tickets, Signatur-Auswahl mit Vorschau
await waitFor(`!!document.querySelector('.composer select')`);
check('Textbausteine: Abteilung des Tickets und „für alle“ sind da, fremde Abteilungen nicht', await run(`(() => { const s = [...document.querySelectorAll('.composer select')].find((x) => x.options[0] && x.options[0].text.startsWith('Textbaustein')); const t = [...s.options].map((o) => o.text); return t.includes('Nur Technik') && t.includes('Gruß') && !t.includes('Nur Vertrieb'); })()`));
check('Textbausteine sind nach Abteilung gruppiert', await run(`(() => { const g = [...document.querySelectorAll('.composer optgroup')].map((x) => x.label); return g.includes('Abteilung Technik') && g.includes('Für alle Abteilungen'); })()`));
check('Signatur-Auswahl mit Voreinstellung „Meine Signatur“ und Vorschau', await run(`(() => { const b = document.querySelector('.composer .sigbox'); return !!b && b.querySelector('select').value === 'own' && b.querySelector('.sigpreview').textContent.includes('Max persönlich'); })()`));
check('Angaben des Kunden (Zusatzfelder) werden angezeigt', await run(`(() => { const t = document.body.textContent; return t.includes('Angaben des Kunden') && t.includes('Titel der Geschichte') && t.includes('Der kleine Bär') && t.includes('Nutzerbedingungen'); })()`));
check('Absätze der Signatur bleiben getrennt (nicht „GrüßeMax“)', await run(`(() => { const t = document.querySelector('.composer .sigpreview'); return t.children.length >= 2 && t.innerText.includes('Grüße' + String.fromCharCode(10)) && !t.innerText.includes('GrüßeMax'); })()`));
await run(`(() => { const s = document.querySelector('.composer .sigbox select'); s.value = 'dept'; s.dispatchEvent(new Event('change')); })()`);
check('Abteilungs-Signatur zeigt Vorschau mit Text und Bild', await run(`(() => { const b = document.querySelector('.composer .sigpreview'); const i = b.querySelector('img'); return b.textContent.includes('Technik-Team Support') && !!i && i.getAttribute('src').startsWith('https://') && i.getAttribute('width') === '120'; })()`));
check('Vorschau enthält nichts Gefährliches (Skripte, Handler, Stile, javascript-Links)', await run(`(() => { const b = document.querySelector('.composer .sigpreview'); return !b.querySelector('script') && !b.querySelector('[onerror]') && !b.querySelector('[style]') && !b.querySelector('a[href]'); })()`));
await run(`(() => { const s = document.querySelector('.composer .sigbox select'); s.value = 'none'; s.dispatchEvent(new Event('change')); })()`);
check('„Keine Signatur“ wird in der Vorschau erklärt', await run(`document.querySelector('.composer .sigpreview').textContent.includes('keine Signatur')`));
check('Signatur-Auswahl erscheint nur bei Antworten, nicht bei internen Notizen', await run(`(() => { const tab = [...document.querySelectorAll('.composer .tab')].find((x) => x.textContent.includes('Notiz')); if (!tab) return true; tab.click(); const hidden = document.querySelector('.composer .sigbox').classList.contains('hidden'); [...document.querySelectorAll('.composer .tab')].find((x) => x.textContent.includes('Antwort')).click(); return hidden; })()`));
// Administrator öffnet ein von anderen bearbeitetes Ticket: Hinweis mit Knopf zum Aufheben der Sperre
await run(`window.__lockedBy = 'Florian'; go('ticket', 1)`);
check('Gesperrtes Ticket: Admin sieht den Inhalt und den Hinweis mit dem Namen', await waitFor(`document.getElementById('main').innerText.includes('Drucker druckt nicht') && document.getElementById('main').innerText.includes('„Florian“ bearbeitet')`));
check('Gesperrtes Ticket: Knopf „Sperre aufheben und übernehmen“ vorhanden', await run(`!![...document.querySelectorAll('#main button')].find((b) => b.innerText.includes('Sperre aufheben'))`));
await run(`[...document.querySelectorAll('#main button')].find((b) => b.innerText.includes('Sperre aufheben')).click()`);
check('Sperre aufheben ruft den Server auf und der Hinweis verschwindet', await waitFor(`window.__calls.includes('ticket_unlock.php') && !document.getElementById('main').innerText.includes('„Florian“ bearbeitet')`));
await run(`go('reports')`);
check('Berichte laden Tabelle mit Summen', await waitFor(`['Technik', 'Vertrieb', '59'].every(x => document.getElementById('main').innerText.includes(x))`));
check('Berichte: Kundenzufriedenheit mit Durchschnitt, Rücklauf und Kommentar', await waitFor(`(() => { const t = document.getElementById('main').textContent; return t.includes('Kundenzufriedenheit') && t.includes('4,2 ★') && t.includes('6 Antworten von 10') && t.includes('Max Muster 4,5') && t.includes('Top'); })()`));
await run(`go('newticket')`);
check('Neues Ticket: Vorlagenauswahl erscheint', await waitFor(`(() => { const s = [...document.querySelectorAll('#main select')][0]; return !!s && [...s.options].some((o) => o.text === 'Neuer Mitarbeiter'); })()`));
await run(`(() => { const s = [...document.querySelectorAll('#main select')][0]; s.value = '1'; s.dispatchEvent(new Event('change')); })()`);
check('Vorlage füllt Betreff, Text und Priorität vor', await run(`(() => { const inp = [...document.querySelectorAll('#main input')].map((x) => x.value); const ta = document.querySelector('#main textarea').value; const sel = [...document.querySelectorAll('#main select')].map((x) => x.value); return inp.includes('Neuer Mitarbeiter: ') && ta === 'Name:\\nStartdatum:' && sel.includes('1'); })()`));
console.log('Verwaltung');
await run(`state.staff.role = 'admin'`);
const sections = await run(`adminSections().map(s => s.key)`);
check('Verwaltungsbereiche vorhanden', sections.length >= 10, 'gefunden: ' + sections.length);
for (const key of sections) {
await run(`go('admin', '${key}')`);
const ok = await waitFor(`document.getElementById('main').children.length > 0 && !document.querySelector('#main .warnbox')`, 3000);
check('Verwaltung „' + key + '“ rendert ohne Fehler', ok, await run(`(document.querySelector('#main .warnbox')||{}).innerText || ''`));
}
// Digitaler Assistent: Reiter, Einstellungen, Antworten, Fragen-Log, Synonyme, Ausprobieren
await run(`go('admin', 'assistant')`);
const txt = () => run(`document.getElementById('main').innerText`);
const tabBtn = (label) => run(`[...document.querySelectorAll('#main .tabs button')].find((b) => b.innerText.includes(${JSON.stringify(label)})).click()`);
check('Assistent: Einstellungen zeigen Zustand und Felder', await waitFor(`(() => { const t = document.getElementById('main').innerText; return t.includes('Zustand jetzt') && t.includes('aktiv') && t.includes('Aufbewahrung'); })()`));
await run(`window.__assistantPosts = []; [...document.querySelectorAll('#main button')].find((b) => b.innerText === 'Speichern').click()`);
check('Assistent: Speichern sendet die Einstellungen', await waitFor(`(window.__assistantPosts[0] || {}).action === 'save_settings' && window.__assistantPosts[0].logDays === 90`));
await tabBtn('Zusätzliche Antworten');
check('Assistent: Zusätzliche Antworten werden aufgelistet', await waitFor(`document.getElementById('main').innerText.includes('Wie lange dauert der Versand?') && document.getElementById('main').innerText.includes('3×')`));
await tabBtn('Fragen-Log');
check('Assistent: Fragen-Log zeigt Kennzahlen, Frage und häufige Fragen ohne Antwort', await waitFor(`(() => { const t = document.getElementById('main').innerText; return t.includes('Wie kündige ich?') && t.includes('Häufige Fragen ohne Antwort') && t.includes('Hat geholfen'); })()`));
await tabBtn('Gleichbedeutende Wörter');
check('Assistent: Synonyme sind sichtbar und speicherbar', await waitFor(`!!document.querySelector('#main textarea') && document.querySelector('#main textarea').value.includes('Einladung')`));
await run(`window.__assistantPosts = []; [...document.querySelectorAll('#main button')].find((b) => b.innerText === 'Speichern').click()`);
check('Assistent: Synonyme senden save_synonyms', await waitFor(`(window.__assistantPosts[0] || {}).action === 'save_synonyms'`));
await tabBtn('Frage ausprobieren');
await run(`window.__assistantPosts = []; document.querySelector('#main input').value = 'Wann kommt das Paket?'; [...document.querySelectorAll('#main button')].find((b) => b.innerText === 'Testen').click()`);
check('Assistent: Ausprobieren zeigt die Antwort', await waitFor(`document.getElementById('main').innerText.includes('Zwei bis drei Werktage.')`));
// Statusseite: Zustand der Dienste, Einschalten, Meldung veröffentlichen
await run(`go('admin', 'statuspage')`);
check('Statusseite: Dienste und Gesamtzustand werden gezeigt', await waitFor(`(() => { const t = document.getElementById('main').textContent; return t.includes('Alle Systeme laufen') && t.includes('Webseite') && t.includes('Neue Meldung'); })()`));
await run(`window.__statusPosts = []; document.querySelector('#main input[type=checkbox]').click()`);
check('Statusseite: Einschalten sendet set_enabled', await waitFor(`(window.__statusPosts[0] || {}).action === 'set_enabled' && window.__statusPosts[0].enabled === true`));
await run(`window.__statusPosts = []; (() => { const t = [...document.querySelectorAll('#main input')].find((x) => x.placeholder.includes('E-Mail-Versand')); t.value = 'Portal langsam'; [...document.querySelectorAll('#main button')].find((b) => b.innerText === 'Veröffentlichen').click(); })()`);
check('Statusseite: Meldung veröffentlichen sendet Titel und Stufe', await waitFor(`(window.__statusPosts[0] || {}).action === 'create_incident' && window.__statusPosts[0].title === 'Portal langsam' && window.__statusPosts[0].status === 'investigating'`));
await run(`window.__statusPosts = []; (() => { const s = [...document.querySelectorAll('#main select')][0]; s.value = 'outage'; s.dispatchEvent(new Event('change')); })()`);
check('Statusseite: Zustand eines Dienstes ändern sendet set_component', await waitFor(`(window.__statusPosts[0] || {}).action === 'set_component' && window.__statusPosts[0].status === 'outage' && window.__statusPosts[0].id === 1`));
// Rechte: ohne das Recht ist der Bereich nicht in der Verwaltung
await run(`state.staff.role = 'agent'; state.staff.permissions = {}`);
check('Assistent: ohne Recht nicht in den Verwaltungsbereichen', await run(`!adminSections().some((s) => s.key === 'assistant')`));
await run(`state.staff.permissions = { manage_assistant: true }`);
check('Assistent: mit dem Recht „manage_assistant“ ist er da, andere Bereiche nicht', await run(`adminSections().some((s) => s.key === 'assistant') && !adminSections().some((s) => s.key === 'departments')`));
await run(`state.staff.role = 'admin'`);
// Abteilung: automatische Zuweisung neuer Tickets einstellen
await run(`go('admin')`);
check('Verwaltung zeigt nur Bereiche der App, keine Seiten der Weboberfläche mehr', await waitFor(`(() => { const t = document.getElementById('main').innerText; return t.includes('Abteilungen') && !t.includes('(im Browser)') && !t.includes('Backup') && !document.querySelector('#main .tile[data-web]'); })()`));
check('Verwaltung ruft die Seiten der Weboberfläche nicht mehr ab', await run(`!window.__calls.includes('web_pages.php')`));
await run(`go('admin', 'departments')`);
check('Abteilungen: Liste lädt', await waitFor(`document.getElementById('main').innerText.includes('Technik')`));
await run(`document.querySelector('#main tr.click').click()`);
check('Abteilung bearbeiten: Einstellung „Neue Tickets automatisch zuweisen“ ist da und zeigt den gespeicherten Wert', await waitFor(`(() => { const l = [...document.querySelectorAll('.modal label')].find((x) => x.innerText.includes('Neue Tickets automatisch zuweisen')); return !!l && l.nextElementSibling.value === 'staff'; })()`));
check('Abteilung bearbeiten: Mitarbeiter-Auswahl zeigt den gespeicherten Mitarbeiter', await run(`(() => { const l = [...document.querySelectorAll('.modal label')].find((x) => x.innerText.startsWith('Mitarbeiter (nur bei')); return !!l && l.nextElementSibling.value === '2'; })()`));
await run(`(() => { const l = [...document.querySelectorAll('.modal label')].find((x) => x.innerText.includes('Neue Tickets automatisch zuweisen')); l.nextElementSibling.value = 'round_robin'; })()`);
await run(`[...document.querySelectorAll('.modal button')].find((b) => b.innerText === 'Speichern').click()`);
check('Abteilung speichern sendet die Zuweisungs-Einstellung an den Server', await waitFor(`!!(window.__bodies['departments.php'] && window.__bodies['departments.php'].action === 'save')`), JSON.stringify(await run(`window.__bodies['departments.php']`)));
check('… mit „Reihum“ und dem Mitarbeiter', await run(`(() => { const b = window.__bodies['departments.php'] || {}; return b.autoAssignMode === 'round_robin' && b.autoAssignStaffId === 2; })()`));
console.log('Theme');
await run(`window.deskhub.setSettings({ theme: 'light' }).then(() => { state.prefs = { ...state.prefs, theme: 'light' }; applyTheme(); })`);
check('Hell-Modus setzt data-theme', await waitFor(`document.documentElement.dataset.theme === 'light'`));
await run(`window.deskhub.setSettings({ theme: 'dark' }).then(() => { state.prefs = { ...state.prefs, theme: 'dark' }; applyTheme(); })`);
check('Dunkel-Modus setzt data-theme', await waitFor(`document.documentElement.dataset.theme === 'dark'`));
console.log('Verbindung & Überwachung');
await run(`go('dashboard')`);
await run(`window.__origApi = window.deskhub.api; window.deskhub.api = async () => ({ error: { code: 'NETWORK', status: 0, message: 'Server nicht erreichbar.' } }); pollActivity()`);
check('Fehlende Verbindung zeigt Banner', await waitFor(`document.getElementById('banners').innerText.includes('Keine Verbindung zum Server')`, 6000));
check('Banner bietet „Jetzt erneut versuchen“', await run(`document.getElementById('banners').innerText.includes('Jetzt erneut versuchen')`));
await run(`window.deskhub.api = window.__origApi; retryNow()`);
check('Banner verschwindet nach Wiederherstellung', await waitFor(`!document.getElementById('banners').innerText.includes('Keine Verbindung zum Server')`, 6000));
await run(`window.__health = [{ level: 'error', code: 'mailbox_down', message: 'Postfach „support@example.de“ ist nicht erreichbar.' }]; pollActivity()`);
check('Serverstörung (Mailabruf) wird als Banner gezeigt', await waitFor(`document.getElementById('banners').innerText.includes('support@example.de')`, 6000));
console.log('Sprache');
check('Standard (Windows-Sprache im Test: Deutsch): Flaggen sind in der Kopfleiste', await run(`document.querySelectorAll('.topbar .lang-flag').length === 2 && document.querySelector('.topbar .lang-flag.on').title === 'Deutsch'`));
check('Deutsch: Seitenleiste deutsch', await run(`document.querySelector('.side').innerText.includes('Übersicht')`));
await run(`localStorage.setItem('lang', 'en')`);
await win.loadFile(path.join(__dirname, 'mock.html'));
check('English: App startet', await waitFor(`!!document.querySelector('.side') && !!document.getElementById('main')`));
check('English: Seitenleiste und Kopfleiste übersetzt', await run(`(() => { const t = document.querySelector('.side').innerText + document.querySelector('.topbar').innerText; return t.includes('Overview') && t.includes('Tasks') && t.includes('Sign out') && !t.includes('Abmelden'); })()`));
check('English: Flagge English ist aktiv, passt', await run(`document.querySelector('.topbar .lang-flag.on').title === 'English' && document.documentElement.lang === 'en'`));
await run(`go('ticket', 1)`);
check('English: Ticket übersetzt (Eigenschaften, Aktionen, Texte mit Zahlen)', await waitFor(`(() => { const t = document.getElementById('main').textContent; return t.includes('Properties') && t.includes('Resolve') && t.includes('Customer satisfaction') && t.includes('Mein Drucker druckt nichts mehr') && t.includes('Drucker druckt nicht'); })()`));
check('English: Kundendaten (Ticketinhalt) bleiben unübersetzt', await run(`document.getElementById('main').textContent.includes('Mein Drucker druckt nichts mehr')`));
await run(`go('newticket')`);
check('English: Neues Ticket (Beschriftungen und Platzhalter)', await waitFor(`(() => { const t = document.getElementById('main').textContent; return t.includes('New ticket') && t.includes('Customer name') && t.includes('Subject') && t.includes('Create ticket'); })()`));
await run(`go('tickets')`);
check('English: Ticketliste hat Reiter und Suche auf English', await waitFor(`(() => { const t = document.getElementById('main').textContent; return t.includes('Watched') || t.includes('Snoozed') || t.includes('Open'); })()`));
check('English: Datumsformat englisch', await run(`fmtDate('2026-10-24 19:42:00').includes('24/10/2026')`));
await run(`state.staff.role = 'admin'; go('admin')`);
check('English: Verwaltung (Kacheln, Hinweis ohne Web-Verweis)', await waitFor(`(() => { const t = document.getElementById('main').textContent; return t.includes('Administration') && t.includes('Departments') && t.includes('Only areas you are authorized for.'); })()`));
await run(`[...document.querySelectorAll('.topbar .lang-flag')].find((b) => b.title === 'Deutsch').click()`);
await waitFor(`localStorage.getItem('lang') === 'de'`);
check('Klick auf die deutsche Flagge speichert die Sprache', await run(`localStorage.getItem('lang') === 'de'`));
await run(`localStorage.removeItem('lang')`);
console.log('Zähler nach echtem Stand');
await run(`go('dashboard')`);
await run(`window.__notified = []; const on = window.deskhub.notify; window.deskhub.notify = (t, m) => { window.__notified.push(m); return on && on(t, m); }; window.__badgeCalls = []; const ob = window.deskhub.badge; window.deskhub.badge = (c, d) => { window.__badgeCalls.push(c); return ob && ob(c, d); }; window.__attention = { unassigned: [5], mine: [7] }; state.knownIds = null; state.badge = 0;`);
await run(`pollActivity()`);
check('Zähler zeigt die Zahl der Tickets, die Aufmerksamkeit brauchen', await waitFor(`state.badge === 2 && document.getElementById('badge') && document.getElementById('badge').textContent === '2'`));
check('Erster Abruf meldet nichts (nur Bezugspunkt)', await run(`window.__notified.length === 0`));
await run(`window.__attention = { unassigned: [], mine: [] }; pollActivity()`);
check('Zähler sinkt von selbst auf 0, wenn nichts mehr offen ist', await waitFor(`state.badge === 0 && document.getElementById('badge').classList.contains('hidden')`));
await run(`window.__attention = { unassigned: [9], mine: [] }; pollActivity()`);
check('Neues Ticket: Zähler steigt und es kommt eine Meldung', await waitFor(`state.badge === 1 && window.__notified.length === 1 && window.__notified[0].includes('nicht zugewiesen')`));
await run(`pollActivity()`);
await wait(200);
check('Gleicher Stand: keine zweite Meldung', await run(`window.__notified.length === 1`));
await run(`go('tickets')`);
await wait(300);
check('Öffnen der Ticketliste leert den Zähler nicht mehr', await run(`state.badge === 1`));
await run(`delete window.__attention`);
check('Keine JavaScript-Fehler in der Konsole', consoleErrors.length === 0, consoleErrors.slice(0, 3).join(' | '));
} catch (e) {
failures.push('Testlauf abgebrochen: ' + e.message);
console.log(' FAIL ' + e.message);
}
console.log(`\n${passed} bestanden, ${failures.length} fehlgeschlagen`);
app.exit(failures.length ? 1 : 0);
});