Twitch-VOD-Manager/scripts/ui-screenshot.js
xRangerDE 951158fe5a release: 5.0.13 — VOD-Card Button-Alignment + UI-Polish-Pass
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>
2026-05-23 17:00:08 +02:00

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); });