Hauptfix (User-Report): in der VOD-Grid sass der Trim/Queue-Button bei Cards mit 1-zeiligem Titel hoeher als bei 2-zeiligen Nachbarn. Ursache: .vod-card war ein Block, Buttons flossen mit dem Content. Grid streckt zwar alle Cards einer Reihe gleich hoch, aber der Leerraum landete unten. Fix: .vod-card = flex column, .vod-actions = margin-top:auto -> Buttons docken am Boden an. Verifiziert per Playwright ueber xqc (16 Reihen), papaplatte (6), xrohat (12): maxButtonTopSpread = 0px in allen. Weitere Funde aus dem Screenshot-Pass (scripts/ui-screenshot.js): - Globale Basis-Dark-Theme-Regel fuer alle text-Inputs + textarea, damit bare Inputs ohne .form-group/.form-stack Wrapper nie OS-weiss durchkommen (#cutterFilePath war weiss im Dark-Theme). - Cutter-Preview-Placeholder 'Video auswahlen um Vorschau zu sehen' war hardcoded Deutsch ohne id -> id + Locale-Key + Wiring (zeigt jetzt 'Select a video to see a preview' im EN-Mode). - Clips-Button '#btnClip' wurde nie lokalisiert (zeigte immer 'Clip herunterladen') -> setText-Wiring ergaenzt, nutzt existierenden clips.downloadButton key. scripts/ui-screenshot.js: neues Harness das die App startet, durch Tabs/Streamer/Themes navigiert, Screenshots macht + Button-Alignment programmatisch misst. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
120 lines
4.6 KiB
JavaScript
120 lines
4.6 KiB
JavaScript
// UI-Screenshot-Harness — laedt die App, navigiert durch Tabs + Streamer
|
|
// + Themes und schreibt PNGs nach tmp_ui_shots/. Kein Test-Assert, nur
|
|
// visuelle Forensik fuer UI-Polishing.
|
|
//
|
|
// Run: node scripts/ui-screenshot.js
|
|
const { _electron: electron } = require('playwright');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const OUT_DIR = path.join(process.cwd(), 'tmp_ui_shots');
|
|
|
|
async function run() {
|
|
fs.rmSync(OUT_DIR, { recursive: true, force: true });
|
|
fs.mkdirSync(OUT_DIR, { recursive: true });
|
|
|
|
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);
|
|
|
|
const shot = async (name) => {
|
|
const file = path.join(OUT_DIR, `${name}.png`);
|
|
try {
|
|
await win.screenshot({ path: file, animations: 'disabled', timeout: 8000 });
|
|
process.stdout.write(`shot: ${name}\n`);
|
|
} catch (e) {
|
|
process.stdout.write(`shot FAILED: ${name} — ${String(e).split('\n')[0]}\n`);
|
|
}
|
|
};
|
|
|
|
// Misst die Y-Position aller .vod-actions relativ zu ihrer Grid-Reihe.
|
|
// Buttons in derselben Reihe muessen die gleiche bottom-Y haben.
|
|
const checkButtonAlignment = async (label) => {
|
|
const rows = await win.evaluate(() => {
|
|
const cards = Array.from(document.querySelectorAll('.vod-card'));
|
|
const byRow = {};
|
|
for (const card of cards) {
|
|
const actions = card.querySelector('.vod-actions');
|
|
if (!actions) continue;
|
|
const cardRect = card.getBoundingClientRect();
|
|
const actRect = actions.getBoundingClientRect();
|
|
const rowKey = Math.round(cardRect.top / 10) * 10; // Reihe nach top gruppieren
|
|
(byRow[rowKey] = byRow[rowKey] || []).push(Math.round(actRect.top));
|
|
}
|
|
// Pro Reihe: max-min der button-tops (sollte ~0 sein wenn aligned)
|
|
const result = [];
|
|
for (const [row, tops] of Object.entries(byRow)) {
|
|
if (tops.length < 2) continue;
|
|
result.push({ row: Number(row), spread: Math.max(...tops) - Math.min(...tops), count: tops.length });
|
|
}
|
|
return result;
|
|
});
|
|
const maxSpread = rows.reduce((m, r) => Math.max(m, r.spread), 0);
|
|
process.stdout.write(` [align ${label}] rows=${rows.length} maxButtonTopSpread=${maxSpread}px ${maxSpread <= 2 ? 'OK' : 'MISALIGNED'}\n`);
|
|
return maxSpread;
|
|
};
|
|
|
|
// 1. Settings tab (alle Inputs/Selects/Checkboxes sichtbar)
|
|
await win.evaluate(() => window.showTab('settings'));
|
|
await win.waitForTimeout(600);
|
|
await shot('01-settings');
|
|
|
|
// 2. Streamer mit vielen VODs + variablen Titel-Laengen
|
|
const streamers = ['xqc', 'papaplatte', 'xrohat'];
|
|
for (const s of streamers) {
|
|
await win.evaluate(() => window.showTab('vods'));
|
|
await win.evaluate(async (name) => { await window.selectStreamer(name); }, s);
|
|
await win.waitForTimeout(4500);
|
|
const count = await win.locator('.vod-card').count();
|
|
process.stdout.write(` ${s}: ${count} vod-cards\n`);
|
|
await checkButtonAlignment(s);
|
|
await shot(`02-vods-${s}`);
|
|
}
|
|
|
|
// 3. Queue (mit einem item drin)
|
|
await win.evaluate(() => window.showTab('vods'));
|
|
const vodCount = await win.locator('.vod-card').count();
|
|
if (vodCount > 0) {
|
|
await win.locator('.vod-card .vod-btn.primary').first().click().catch(() => {});
|
|
await win.waitForTimeout(800);
|
|
}
|
|
await win.evaluate(() => window.showTab('queue'));
|
|
await win.waitForTimeout(500);
|
|
await shot('03-queue');
|
|
|
|
// 4. Clips / Cutter / Merge / Stats / Archive tabs
|
|
for (const tab of ['clips', 'cutter', 'merge', 'stats', 'archive']) {
|
|
await win.evaluate((t) => window.showTab(t), tab);
|
|
await win.waitForTimeout(600);
|
|
await shot(`04-tab-${tab}`);
|
|
}
|
|
|
|
// 5. Themes durchschalten — auf VODs-Tab mit xqc geladen
|
|
await win.evaluate(() => window.showTab('vods'));
|
|
await win.evaluate(async () => { await window.selectStreamer('xqc'); });
|
|
await win.waitForTimeout(3500);
|
|
for (const theme of ['discord', 'youtube', 'apple', 'light', 'twitch']) {
|
|
await win.evaluate((t) => window.changeTheme(t), theme);
|
|
await win.waitForTimeout(500);
|
|
await shot(`05-theme-${theme}`);
|
|
}
|
|
|
|
// 6. Settings im Light-Theme (Input-Backgrounds pruefen)
|
|
await win.evaluate(() => window.changeTheme('light'));
|
|
await win.evaluate(() => window.showTab('settings'));
|
|
await win.waitForTimeout(600);
|
|
await shot('06-settings-light');
|
|
await win.evaluate(() => window.changeTheme('twitch'));
|
|
|
|
await app.close();
|
|
process.stdout.write(`\nDone. PNGs in ${OUT_DIR}\n`);
|
|
}
|
|
|
|
run().catch((e) => { process.stderr.write(String(e) + '\n'); process.exit(1); });
|