Twitch-VOD-Manager/scripts/ui-audit-modals.js
xRangerDE de1b3c9a91 test(ui): add programmatic UI audit tooling
scripts/ui-audit.js — laedt die App, schaltet pro Tab DE<->EN und
diffed sichtbaren Text (findet hardcoded/untranslated Strings), prueft
Horizontal-Overflow, Zero-size-Interactives, Off-screen-Elemente.

scripts/ui-audit-modals.js — gleiche Checks fuer Modals (Trim-Dialog,
Template-Guide, Command-Palette) inkl. Viewport-Clipping-Check bei
min-height.

Intensive Verifikation 5.0.14: 0 Localization-Leaks ueber alle Tabs +
Modals, 0 Layout-Overflows, alle Modals fit viewport (auch @700px
min-height), Button-Alignment 0px spread (xqc/papaplatte/xrohat),
empty-states + 1200/1280/1920px + alle 5 Themes sauber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:19:24 +02:00

99 lines
5.0 KiB
JavaScript

// Modal-fokussiertes Audit: oeffnet jedes Modal, prueft DE/EN-Localization
// + Layout-Overflow + ob das Modal in den Viewport passt (nicht abgeschnitten).
const { _electron: electron } = require('playwright');
const NEUTRAL = /^[\s\d.,:%/x+()|-]*$|VOD|FPS|Twitch|Ctrl|HH|MM|SS|@|http|\.mp4|\.json|MB|GB|KB|TB|TAB|^x$/i;
const GERMAN = /(auswahlen|herunterladen|loeschen|schliessen|ausfuehren|hinzufugen|zusammenfugen|aufloesung|datei|einstellung|speichern|abbrechen|durchsuchen|fehlgeschlagen|zuerst|neueste|alteste|vorschau|qualitat|sprache|\bund\b|\boder\b|\bfur\b|\bnicht\b|\bzum\b|\bzur\b|\bdie\b|\bder\b|\bdas\b|wahle|reihenfolge|geandert|unterstutzte)/i;
async function modalText(win) {
return win.evaluate(() => {
const open = document.querySelector('.modal-overlay.show, .modal-overlay[style*="flex"], #commandPaletteModal.show');
const root = open || document.querySelector('.modal-overlay.show');
if (!root) return null;
const txts = [];
for (const el of root.querySelectorAll('button, label, span, h1, h2, h3, h4, p, option')) {
const r = el.getBoundingClientRect();
if (r.width === 0 || r.height === 0) continue;
const t = Array.from(el.childNodes).filter(n => n.nodeType === 3).map(n => n.textContent.trim()).join(' ').trim();
if (t) txts.push(t);
}
const rect = root.querySelector('.modal') ? root.querySelector('.modal').getBoundingClientRect() : root.getBoundingClientRect();
return { texts: txts, modalRect: { top: Math.round(rect.top), bottom: Math.round(rect.bottom), left: Math.round(rect.left), right: Math.round(rect.right) }, vh: window.innerHeight, vw: window.innerWidth };
});
}
async function auditModal(win, name, openFn) {
await win.evaluate(() => { ['de'].forEach(() => window.changeLanguage && window.changeLanguage('de')); });
await win.waitForTimeout(300);
const opened = await win.evaluate(openFn);
if (!opened) { process.stdout.write(` ${name}: could not open\n`); return; }
await win.waitForTimeout(700);
const de = await modalText(win);
// close, switch en, reopen
await win.keyboard.press('Escape'); await win.waitForTimeout(300);
await win.evaluate(() => window.changeLanguage && window.changeLanguage('en'));
await win.waitForTimeout(300);
await win.evaluate(openFn);
await win.waitForTimeout(700);
const en = await modalText(win);
await win.keyboard.press('Escape'); await win.waitForTimeout(300);
if (!de || !en) { process.stdout.write(` ${name}: no modal captured\n`); return; }
// Localization: deutsche Strings die in EN identisch blieben
const enSet = new Set(en.texts);
const leaks = de.texts.filter(t => enSet.has(t) && GERMAN.test(t) && !NEUTRAL.test(t) && t.length >= 3);
// Modal off-screen check (EN)
const m = en.modalRect;
const clipped = m.top < 0 || m.left < 0 || m.bottom > en.vh || m.right > en.vw;
process.stdout.write(` ${name}: ${leaks.length ? 'LOCALIZATION LEAK -> ' + JSON.stringify(leaks) : 'loc OK'} | ${clipped ? `CLIPPED ${JSON.stringify(m)} vh=${en.vh} vw=${en.vw}` : 'fits viewport'}\n`);
}
async function run() {
const app = await electron.launch({ executablePath: require('electron'), args: ['.'], cwd: process.cwd() });
const win = await app.firstWindow();
await win.setViewportSize({ width: 1280, height: 800 });
await win.waitForTimeout(2500);
await win.evaluate(async () => { await window.selectStreamer('xqc'); }).catch(() => {});
await win.waitForTimeout(4000);
process.stdout.write('\n===== MODAL AUDIT (1280x800) =====\n');
// Trim/Clip dialog — braucht eine VOD-Card
await auditModal(win, 'trim-dialog', () => {
const btn = document.querySelector('.vod-card .vod-btn.secondary');
if (btn) { btn.click(); return true; }
return false;
});
await auditModal(win, 'template-guide', () => {
if (typeof window.openTemplateGuide === 'function') { window.openTemplateGuide(); return true; }
return false;
});
await auditModal(win, 'command-palette', () => {
// Ctrl+K simulieren via direkter open falls exposed, sonst keydown
const ev = new KeyboardEvent('keydown', { key: 'k', ctrlKey: true, bubbles: true });
document.dispatchEvent(ev);
return document.getElementById('commandPaletteModal')?.classList.contains('show') || false;
});
// Kleines Fenster — Template Guide ist das hoechste Modal, bei 700px Hoehe testen
await win.setViewportSize({ width: 1280, height: 700 });
await win.waitForTimeout(500);
process.stdout.write('\n===== MODAL AUDIT (1280x700, min-height) =====\n');
await auditModal(win, 'template-guide@700', () => {
if (typeof window.openTemplateGuide === 'function') { window.openTemplateGuide(); return true; }
return false;
});
await auditModal(win, 'trim-dialog@700', () => {
const btn = document.querySelector('.vod-card .vod-btn.secondary');
if (btn) { btn.click(); return true; }
return false;
});
await app.close();
process.stdout.write('\nDone modal audit.\n');
}
run().catch(e => { process.stderr.write(String(e) + '\n'); process.exit(1); });