import { mkdir, writeFile } from "node:fs/promises"; import { dirname } from "node:path"; import { test, expect } from "@playwright/test"; import { resetData, resolvePath, jobRowByKind } from "./helpers"; import { baseUrl } from "./baseUrl"; const SLUG = "test-transcribe"; const SNAPSHOT_REL = `test-transcripts/channels/${SLUG}/snapshot.json`; async function writeJson(relPath: string, value: unknown): Promise { const full = resolvePath(relPath); await mkdir(dirname(full), { recursive: true }); await writeFile(full, JSON.stringify(value, null, 2) + "\n"); } // Seed a snapshot with the two cleanup buckets populated. transcribedWithAudio // drives the "cleanable transcribed audio" section; multipleAudioFormats drives // the "extra audio formats" section. // // Both sections render on /cleanup now — they are the only channel-work // sections that belong to no operation, because what they are about is bytes. // The file keeps its name: it is still the cleanup-actionable pair. async function seedCleanupSnapshot(): Promise { await writeJson(SNAPSHOT_REL, { generatedAt: "2026-05-27T12:00:00.000Z", totals: { videos: 3, transcribed: 2, downloaded: 3 }, buckets: { noTranscript: [], downloadedNoTranscript: [], untranscoded: [], multipleAudioFormats: ["vidA"], transcribedWithAudio: ["vidA", "vidB"], untranscribable: [], noMetadata: [], failedListed: [], missingFromArchive: [], duplicateDirs: [], partialDownloads: [], }, undownloadedIds: [], // 2.5 MB transcribed-audio cleanup, 1 MB extra-format cleanup → exercises // the "Est. reclaim" column formatting. cleanupBytes: { transcribedWithAudio: 2_621_440, multipleAudioFormats: 1_048_576, }, }); await fetch(`${baseUrl}/api/test/invalidate-cache`).catch(() => {}); } test("surfaces the two cleanup sections with per-channel counts", async ({ page, }) => { await resetData("one-transcribe-channel-with-audio"); await seedCleanupSnapshot(); await page.goto("/cleanup"); const transcribed = page.getByRole("region", { name: "clean-transcribed-audio", exact: true, }); await expect(transcribed).toBeVisible(); const transcribedRow = transcribed.getByLabel( `clean-transcribed-audio row ${SLUG}`, ); await expect(transcribedRow).toBeVisible(); await expect(transcribedRow).toContainText("2"); // 2,621,440 bytes → 2.5 MB, shown in the new "Est. reclaim" column. await expect(transcribedRow).toContainText("~2.5 MB"); const extras = page.getByRole("region", { name: "clean-extra-formats", exact: true, }); await expect(extras).toBeVisible(); const extrasRow = extras.getByLabel(`clean-extra-formats row ${SLUG}`); await expect(extrasRow).toContainText("1"); // 1,048,576 bytes → 1.0 MB. await expect(extrasRow).toContainText("~1.0 MB"); }); test("inline 'Clean audio' queues a clean-audio-transcribed job when confirmed", async ({ page, }) => { await resetData("one-transcribe-channel-with-audio"); await seedCleanupSnapshot(); await page.goto("/cleanup"); page.on("dialog", (d) => d.accept()); await page.getByRole("button", { name: `clean audio ${SLUG}` }).click(); await expect(page.getByLabel(`clean audio ${SLUG} job`)).toBeVisible({ timeout: 10_000, }); await page.goto("/jobs"); await expect( jobRowByKind(page, "clean-audio-transcribed").first(), ).toBeVisible({ timeout: 10_000 }); }); test("inline 'Clean extra formats' queues a clean-extra-audio-formats job when confirmed", async ({ page, }) => { await resetData("one-transcribe-channel-with-audio"); await seedCleanupSnapshot(); await page.goto("/cleanup"); page.on("dialog", (d) => d.accept()); await page .getByRole("button", { name: `clean extra formats ${SLUG}` }) .click(); await expect( page.getByLabel(`clean extra formats ${SLUG} job`), ).toBeVisible({ timeout: 10_000 }); await page.goto("/jobs"); await expect( page .getByRole("row") .filter({ hasText: "clean-extra-audio-formats" }) .first(), ).toBeVisible({ timeout: 10_000 }); }); test("dismissing the confirm dialog does not queue a cleanup job", async ({ page, }) => { await resetData("one-transcribe-channel-with-audio"); await seedCleanupSnapshot(); await page.goto("/cleanup"); page.on("dialog", (d) => d.dismiss()); await page.getByRole("button", { name: `clean audio ${SLUG}` }).click(); // No job indicator should appear after a dismissed confirm. await expect(page.getByLabel(`clean audio ${SLUG} job`)).toHaveCount(0); await page.goto("/jobs"); await expect( page.getByRole("row").filter({ hasText: "clean-audio-transcribed" }), ).toHaveCount(0); });