WAS: `npm run klick` startet das ECHTE Rippy (Electron, Haupt, Kern, Fenster, Datenbank) mit eigenem Profil (--profil, --klick-smoke: ohne Tray, Updater, Toasts), einem nachgebauten Laufwerk (RIPPY_FAKE_LAUFWERK) und nachgebauten Werkzeugen (bau/klick/fake-*.cjs) — und klickt sich durch: Rippen → Dialog → Sprachen je Titel (jpn statt deu) → Rollen auf Folge → Staffel 1, ab Folge 3 → Vorschau → Rip → Warteschlange → Kompression → Ablage als S01E03/S01E04 unter Serien\…\Season 01 → Einstellungen/Roh-Dateien → Roh loeschen → Ereignisse. Bilder liegen in beweise/klick/. Die Naht dafuer (kern/werkzeuge/aufruf.ts): Zeigt ein Werkzeug-Pfad auf ein Node-Skript, laeuft es ueber die eigene Laufzeit (ELECTRON_RUN_AS_NODE) — fuer EXE-Pfade aendert sich nichts. WARUM: Beide Fehler vom 01.09.2026 waren Verkabelung; 261 Unit-Tests sahen keinen. Der Klick-Beweis haette beide gefunden. NEBENBEFUND: Die CSP blockte eingebettete Schrift-Teile (data:-Fonts der fontsource-Pakete) — Konsolenfehler im gebauten Fenster, jetzt font-src. Dazu test/messung.spuren.test.ts: HandBrake-Scan einer echten Roh-Datei auf Verlangen (RIPPY_MESSUNG_MKV). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
193 lines
8.2 KiB
JavaScript
193 lines
8.2 KiB
JavaScript
// Der Klick-Beweis (5.4.0, Punkt 7 der Übergabe vom 01.09.2026): startet
|
|
// das ECHTE Rippy (Electron, Haupt, Kern, Fenster, Datenbank) mit einem
|
|
// frischen Profil, einem nachgebauten Laufwerk und nachgebauten Werkzeugen
|
|
// — und KLICKT sich durch: Rippen → Titel-Dialog → Sprachen je Titel →
|
|
// Rollen auf Folge → Staffel/Folge → Rip → Warteschlange → Kompression →
|
|
// Ablage → Roh-Dateien → löschen.
|
|
//
|
|
// Warum: Beide Fehler vom 01.09.2026 waren Verkabelung (Nachrichtentyp
|
|
// nicht im Prüfer, zwei gleichzeitige Titel-Läufe). 261 Unit-Tests sahen
|
|
// keinen davon; ein Klick hätte beide gefunden. Playwright treibt Electron
|
|
// (Doku »Electron« bei playwright.dev, _electron.launch).
|
|
//
|
|
// npm run klick (baut vorher)
|
|
// Bilder: ../beweise/klick/*.png — Ausgang 0 (grün) oder 2.
|
|
import { _electron as electron } from 'playwright'
|
|
import { existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs'
|
|
import { DatabaseSync } from 'node:sqlite'
|
|
import { tmpdir } from 'node:os'
|
|
import { dirname, join, resolve } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const wurzel = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
const profil = join(tmpdir(), 'rippy-klick')
|
|
const ablage = join(profil, 'ablage')
|
|
const bilder = resolve(wurzel, '..', 'beweise', 'klick')
|
|
|
|
rmSync(profil, { recursive: true, force: true })
|
|
mkdirSync(profil, { recursive: true })
|
|
mkdirSync(bilder, { recursive: true })
|
|
|
|
// Das Profil vorbereiten: Einrichtung erledigt, Ablage im Profil, die
|
|
// Werkzeuge sind die Nachbauten (kern/werkzeuge/aufruf.ts startet Skripte
|
|
// über die eigene Node-Laufzeit). Die Datenbank legt der Kern selbst an —
|
|
// hier nur die Einstellungen (Tabelle wie in kern/speicher/db.ts).
|
|
{
|
|
const db = new DatabaseSync(join(profil, 'rippy.db'))
|
|
db.exec('CREATE TABLE IF NOT EXISTS einstellungen (schluessel TEXT PRIMARY KEY, wert TEXT NOT NULL)')
|
|
const setzen = db.prepare('INSERT OR REPLACE INTO einstellungen (schluessel, wert) VALUES (?, ?)')
|
|
setzen.run('setup.done', 'true')
|
|
setzen.run('ablage', ablage)
|
|
setzen.run('werkzeug.makemkv', join(wurzel, 'bau', 'klick', 'fake-makemkvcon.cjs'))
|
|
setzen.run('werkzeug.handbrake', join(wurzel, 'bau', 'klick', 'fake-handbrake.cjs'))
|
|
setzen.run('audioSprachen', 'deu,eng')
|
|
setzen.run('untertitelSprachen', 'deu')
|
|
setzen.run('rohAufbewahrung', 'behalten')
|
|
setzen.run('autoAuswurf', 'false')
|
|
db.close()
|
|
}
|
|
|
|
const app = await electron.launch({
|
|
executablePath: join(wurzel, 'node_modules', 'electron', 'dist', 'electron.exe'),
|
|
args: [wurzel, `--profil=${profil}`, '--klick-smoke'],
|
|
env: { ...process.env, RIPPY_FAKE_LAUFWERK: 'Q', RIPPY_OHNE_SCHLUESSEL: '1' },
|
|
timeout: 60_000,
|
|
})
|
|
const seite = await app.firstWindow()
|
|
seite.setDefaultTimeout(30_000)
|
|
seite.on('console', (m) => {
|
|
if (m.type() === 'error') console.log(`KLICK fenster: ${m.text().slice(0, 200)}`)
|
|
})
|
|
|
|
let nr = 0
|
|
async function bild(name) {
|
|
nr += 1
|
|
const pfad = join(bilder, `${String(nr).padStart(2, '0')}-${name}.png`)
|
|
await seite.screenshot({ path: pfad, fullPage: true })
|
|
console.log(`KLICK bild: ${pfad}`)
|
|
}
|
|
|
|
async function schritt(name, tun) {
|
|
try {
|
|
await tun()
|
|
console.log(`KLICK ✓ ${name}`)
|
|
} catch (fehler) {
|
|
console.error(`KLICK ✗ ${name}: ${String(fehler.message ?? fehler).split('\n')[0]}`)
|
|
await seite.screenshot({ path: join(bilder, `fehler-${name.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}.png`), fullPage: true }).catch(() => {})
|
|
await app.close().catch(() => {})
|
|
process.exit(2)
|
|
}
|
|
}
|
|
|
|
const dialog = () => seite.locator('div.fixed.inset-0').last()
|
|
|
|
await schritt('Fenster offen, nachgebautes Laufwerk Q: sichtbar', async () => {
|
|
await seite.getByText('LAUFWERK Q:').waitFor()
|
|
await seite.getByText('NACHGEBAUT (KLICK-BEWEIS)').waitFor()
|
|
})
|
|
await bild('uebersicht')
|
|
|
|
await schritt('Rippen → Dialog öffnet sich', async () => {
|
|
await seite.getByRole('button', { name: 'Rippen', exact: true }).click()
|
|
await seite.getByText('Was soll gerippt werden?').waitFor()
|
|
})
|
|
|
|
await schritt('Titel-Lauf liefert drei Titel', async () => {
|
|
await dialog().getByText('Titel 0', { exact: true }).waitFor()
|
|
await dialog().getByText('Titel 1', { exact: true }).waitFor()
|
|
await dialog().getByText('Titel 2', { exact: true }).waitFor()
|
|
})
|
|
|
|
await schritt('Sprachen je Titel: jpn/deu Ton, deu/eng Untertitel als Chips', async () => {
|
|
await dialog().getByRole('button', { name: /jpn/ }).first().waitFor()
|
|
await dialog().getByRole('button', { name: /✓ deu/ }).first().waitFor()
|
|
})
|
|
|
|
await schritt('Titel 0 bekommt japanischen Ton (deu abwählen, jpn anwählen)', async () => {
|
|
const chips = dialog().locator('button', { hasText: /^✓ deu$|^deu$/ })
|
|
await chips.first().click()
|
|
await dialog().getByRole('button', { name: /^jpn$/ }).first().click()
|
|
await dialog().getByRole('button', { name: /^✓ jpn$/ }).first().waitFor()
|
|
})
|
|
|
|
await schritt('„Hauptinhalt + Extras" wählt beide langen Titel, der Schnipsel bleibt draußen', async () => {
|
|
// Rippy wählt bei zwei fast gleich langen Titeln nur EINEN vor („bitte
|
|
// nachsehen") — der Knopf holt den zweiten dazu, Titel 2 (35 s) nicht.
|
|
await dialog().getByRole('button', { name: 'Hauptinhalt + Extras' }).click()
|
|
await dialog().getByText(/2 von 3 gewählt/).waitFor()
|
|
})
|
|
|
|
await schritt('Rollen auf Folge → Staffel/Folge-Felder erscheinen', async () => {
|
|
const rollen = dialog().locator('select')
|
|
await rollen.nth(0).selectOption('folge')
|
|
await rollen.nth(1).selectOption('folge')
|
|
await dialog().getByText('AB FOLGE').waitFor()
|
|
await dialog().getByPlaceholder('automatisch').fill('3')
|
|
})
|
|
|
|
await schritt('Vorschau nennt Serien\\…\\Season 01 und S01E03', async () => {
|
|
await dialog().getByText(/Serien\\/).waitFor()
|
|
await dialog().getByText(/Season 01/).waitFor()
|
|
await dialog().getByText(/S01E03/).waitFor()
|
|
})
|
|
await bild('dialog')
|
|
|
|
await schritt('Rip starten → Phase RIPPT', async () => {
|
|
await dialog().getByRole('button', { name: /Titel rippen/ }).click()
|
|
await seite.getByText(/RIPPT/).first().waitFor()
|
|
})
|
|
|
|
await schritt('Rip fertig, Kompression eingereiht, Laufwerk frei', async () => {
|
|
await seite.getByText(/Kompression eingereiht/).waitFor({ timeout: 60_000 })
|
|
await seite.getByRole('button', { name: 'Rippen', exact: true }).waitFor()
|
|
})
|
|
|
|
await schritt('Kompressions-Karte läuft und die Ablage endet', async () => {
|
|
await seite.getByText('KOMPRESSION', { exact: true }).waitFor({ timeout: 30_000 })
|
|
await seite.getByText(/Komprimiert und abgelegt/).waitFor({ timeout: 60_000 })
|
|
})
|
|
await bild('fertig')
|
|
|
|
await schritt('Ergebnis liegt als S01E03/S01E04 unter Serien\\…\\Season 01', async () => {
|
|
const serien = join(ablage, 'Serien')
|
|
if (!existsSync(serien)) throw new Error(`kein Ordner ${serien}`)
|
|
const gefunden = []
|
|
const gehen = (o) => {
|
|
for (const e of readdirSync(o, { withFileTypes: true })) {
|
|
const p = join(o, e.name)
|
|
if (e.isDirectory()) gehen(p)
|
|
else if (e.name.endsWith('.mkv') && statSync(p).size > 0) gefunden.push(p)
|
|
}
|
|
}
|
|
gehen(serien)
|
|
const namen = gefunden.map((p) => p.split(/[\\/]/).pop())
|
|
if (!namen.some((n) => n.endsWith('S01E03.mkv')) || !namen.some((n) => n.endsWith('S01E04.mkv'))) {
|
|
throw new Error(`gefunden: ${namen.join(', ')}`)
|
|
}
|
|
if (!gefunden[0].includes('Season 01')) throw new Error(`kein Season-01-Ordner: ${gefunden[0]}`)
|
|
console.log(`KLICK dateien: ${gefunden.join(' | ')}`)
|
|
})
|
|
|
|
await schritt('Einstellungen → Roh-Dateien zeigt den Vorgang mit Belegung', async () => {
|
|
await seite.getByRole('button', { name: 'Einstellungen' }).click()
|
|
await seite.locator('#roh').waitFor()
|
|
await seite.getByText(/Belegung jetzt/).waitFor()
|
|
await seite.getByText(/Roh: \d/).first().waitFor()
|
|
})
|
|
await bild('roh-dateien')
|
|
|
|
await schritt('Roh-Dateien löschen → „gelöscht"', async () => {
|
|
await seite.getByRole('button', { name: 'Roh-Dateien löschen' }).first().click()
|
|
await seite.getByText(/Roh: gelöscht/).first().waitFor()
|
|
})
|
|
|
|
await schritt('Ereignisse nennen die Kompression', async () => {
|
|
await seite.getByRole('button', { name: 'Übersicht' }).click()
|
|
await seite.getByText(/Kompression „/).first().waitFor()
|
|
})
|
|
await bild('ereignisse')
|
|
|
|
console.log('KLICK: OK — Rippen, Dialog, Sprachen, Folgen, Rip, Warteschlange, Kompression, Ablage, Roh-Dateien: alles geklickt.')
|
|
await app.close().catch(() => {})
|
|
process.exit(0)
|