Files
deskhub/desktop/test/run.js
T
2026-10-23 17:22:55 +00:00

155 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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')`));
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')`));
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'); })()`));
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))`));
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.')`));
// 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 die Weboberfläche-Seiten, die der Server freigibt', await waitFor(`(() => { const t = document.getElementById('main').innerText; return t.includes('Backup') && t.includes('Einstellungen') && t.includes('im Browser'); })()`));
await run(`[...document.querySelectorAll('#main .tile')].find((x) => x.innerText.includes('Backup')).click()`);
check('Klick öffnet die Seite über die einmalige Anmeldung (nur /staff/-Pfad)', await waitFor(`window.__web === '/staff/backup.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));
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);
});