Files
deskhub/desktop/test/run.js
T
2026-10-23 11:14:56 +00:00

117 lines
12 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('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('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 || ''`));
}
// Abteilung: automatische Zuweisung neuer Tickets einstellen
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);
});