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>
130 lines
6.3 KiB
JavaScript
130 lines
6.3 KiB
JavaScript
// Intensives UI-Audit. Findet Bugs die man visuell uebersieht:
|
|
// 1. Localization-Luecken: Text der bei DE->EN Wechsel identisch bleibt
|
|
// (potentiell hardcoded/untranslated), pro Tab + Modal.
|
|
// 2. Horizontal-Overflow: Elemente deren scrollWidth > clientWidth.
|
|
// 3. Zero-size sichtbare interaktive Elemente (Buttons/Inputs mit 0 Hoehe/Breite).
|
|
// 4. Off-screen positionierte sichtbare Elemente.
|
|
// 5. Text-Overflow (Inhalt groesser als Box, abgeschnitten ohne ellipsis).
|
|
//
|
|
// Run: node scripts/ui-audit.js
|
|
const { _electron: electron } = require('playwright');
|
|
|
|
const TABS = ['vods', 'clips', 'cutter', 'merge', 'stats', 'archive', 'settings'];
|
|
|
|
// Bekannte sprach-neutrale Texte (Namen, Zahlen, Marken) — keine Localization-Luecke.
|
|
const NEUTRAL = /^[\s\d.,:%/x+()-]*$|VOD|FPS|Twitch|Discord|YouTube|Apple|xQc|Ctrl|HH|MM|SS|@|http|\.mp4|\.json|MB|GB|KB|TB|B$/i;
|
|
|
|
async function collectVisibleText(win) {
|
|
return await win.evaluate(() => {
|
|
const out = {};
|
|
const els = document.querySelectorAll('button, label, span, h1, h2, h3, h4, p, option, .vod-btn, .nav-item, td, th, li');
|
|
let idx = 0;
|
|
for (const el of els) {
|
|
// Nur direkter Text, keine verschachtelten Element-Kinder einsammeln
|
|
const rect = el.getBoundingClientRect();
|
|
const visible = rect.width > 0 && rect.height > 0 && window.getComputedStyle(el).display !== 'none' && window.getComputedStyle(el).visibility !== 'hidden';
|
|
const directText = Array.from(el.childNodes)
|
|
.filter(n => n.nodeType === 3)
|
|
.map(n => n.textContent.trim())
|
|
.join(' ')
|
|
.trim();
|
|
if (!directText) continue;
|
|
const key = (el.id || el.className || el.tagName) + '#' + (idx++);
|
|
out[key] = { text: directText, visible };
|
|
}
|
|
return out;
|
|
});
|
|
}
|
|
|
|
async function checkLayout(win, label) {
|
|
return await win.evaluate((lbl) => {
|
|
const issues = [];
|
|
const all = document.querySelectorAll('*');
|
|
const vw = window.innerWidth, vh = window.innerHeight;
|
|
for (const el of all) {
|
|
const cs = window.getComputedStyle(el);
|
|
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') continue;
|
|
const rect = el.getBoundingClientRect();
|
|
if (rect.width === 0 && rect.height === 0) continue;
|
|
|
|
// Horizontal overflow innerhalb des Elements (Content breiter als Box, kein scroll/hidden)
|
|
if (el.scrollWidth > el.clientWidth + 2 && cs.overflowX === 'visible' && el.clientWidth > 0) {
|
|
const tag = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + String(el.className).split(' ')[0] : '');
|
|
issues.push(`overflow-x: ${tag} scrollW=${el.scrollWidth} clientW=${el.clientWidth}`);
|
|
}
|
|
// Interaktives Element mit 0-Groesse aber im Layout (display nicht none)
|
|
if ((el.tagName === 'BUTTON' || el.tagName === 'INPUT' || el.tagName === 'SELECT') && rect.width > 0 && rect.height === 0) {
|
|
issues.push(`zero-height interactive: ${el.tagName}#${el.id}`);
|
|
}
|
|
// Off-screen (komplett ausserhalb viewport, aber sichtbar gestylt) — nur grobe Faelle
|
|
if (rect.right < -5 || rect.left > vw + 5) {
|
|
const tag = el.tagName.toLowerCase() + (el.id ? '#' + el.id : '');
|
|
if (el.id && rect.width > 20) issues.push(`offscreen-x: ${tag} left=${Math.round(rect.left)} right=${Math.round(rect.right)}`);
|
|
}
|
|
}
|
|
return [...new Set(issues)];
|
|
}, label);
|
|
}
|
|
|
|
async function run() {
|
|
const electronPath = require('electron');
|
|
const app = await electron.launch({ executablePath: electronPath, args: ['.'], cwd: process.cwd() });
|
|
const win = await app.firstWindow();
|
|
await win.setViewportSize({ width: 1280, height: 900 });
|
|
await win.waitForTimeout(2500);
|
|
|
|
// Streamer laden fuer VODs-Kontext
|
|
await win.evaluate(async () => { await window.selectStreamer('xqc'); }).catch(() => {});
|
|
await win.waitForTimeout(4000);
|
|
|
|
const findings = { localization: [], layout: [] };
|
|
|
|
for (const tab of TABS) {
|
|
await win.evaluate((t) => window.showTab(t), tab);
|
|
await win.waitForTimeout(700);
|
|
|
|
// DE snapshot
|
|
await win.evaluate(() => window.changeLanguage && window.changeLanguage('de'));
|
|
await win.waitForTimeout(400);
|
|
const de = await collectVisibleText(win);
|
|
// EN snapshot
|
|
await win.evaluate(() => window.changeLanguage && window.changeLanguage('en'));
|
|
await win.waitForTimeout(400);
|
|
const en = await collectVisibleText(win);
|
|
|
|
// Diff: gleiche keys, identischer Text, sichtbar, nicht neutral, enthaelt Buchstaben
|
|
for (const key of Object.keys(de)) {
|
|
if (!en[key]) continue;
|
|
const dt = de[key].text, et = en[key].text;
|
|
if (!de[key].visible) continue;
|
|
if (dt !== et) continue; // hat sich geaendert -> uebersetzt, ok
|
|
if (!/[a-zA-ZäöüÄÖÜ]/.test(dt)) continue; // keine Buchstaben
|
|
if (NEUTRAL.test(dt)) continue;
|
|
if (dt.length < 3) continue;
|
|
// Heuristik: deutsche Marker ODER generell verdaechtig (mehr als 1 Wort + Buchstaben)
|
|
findings.localization.push(`[${tab}] "${dt}"`);
|
|
}
|
|
|
|
const layout = await checkLayout(win, tab);
|
|
for (const l of layout) findings.layout.push(`[${tab}] ${l}`);
|
|
}
|
|
|
|
// Dedup + nur potenziell-deutsche Strings (haben deutsche Muster)
|
|
const GERMAN = /(auswahlen|herunterladen|loeschen|schliessen|ausfuehren|hinzufugen|zusammenfugen|aufloesung|datei|einstellung|speichern|abbrechen|durchsuchen|fehlgeschlagen|wird|werden|zuerst|neueste|alteste|groesste|kleinste|alle|aktivieren|verschieben|aufnahme|laden|warteschlange|herunter|vorschau|qualitat|sprache|streamer hinzu|\bund\b|\boder\b|\bfur\b|\bmit\b|\bnicht\b|\bzum\b|\bzur\b)/i;
|
|
const locUnique = [...new Set(findings.localization)];
|
|
const locGerman = locUnique.filter(s => GERMAN.test(s));
|
|
const locOther = locUnique.filter(s => !GERMAN.test(s));
|
|
|
|
process.stdout.write('\n===== LOCALIZATION (German-pattern, likely untranslated) =====\n');
|
|
locGerman.forEach(s => process.stdout.write(' ' + s + '\n'));
|
|
process.stdout.write(`\n===== LOCALIZATION (other identical DE==EN, review) ===== (${locOther.length})\n`);
|
|
locOther.slice(0, 40).forEach(s => process.stdout.write(' ' + s + '\n'));
|
|
process.stdout.write('\n===== LAYOUT issues =====\n');
|
|
[...new Set(findings.layout)].forEach(s => process.stdout.write(' ' + s + '\n'));
|
|
process.stdout.write('\nDone audit.\n');
|
|
|
|
await app.close();
|
|
}
|
|
|
|
run().catch((e) => { process.stderr.write(String(e) + '\n'); process.exit(1); });
|