// The dashboard mission-control cockpit (app/page.tsx → DashboardCockpit). SSRs // initial payloads then polls the same /api endpoints the monitor widget uses: // a live Pipeline band (jobs/workers/sync + global pause/sync controls) over a // Needs-work / Quick-add split and an enriched channels table. Also covers the // persisted downloads pause blocking a manual sync. import { test, expect } from "@playwright/test"; import { resetData, writeSettings, readJson, generateReport} from "./helpers"; const ONE_WORKER = { workers: [ { id: "gpu", name: "GPU", kind: "local", enabled: true, priority: 0, appId: "whisper-cpp", config: {}, }, ], }; test("empty state: pipeline idle, nothing to do, no channels table", async ({ page, }) => { await resetData("empty"); await writeSettings(ONE_WORKER); await page.goto("/"); await expect(page.getByRole("region", { name: "Pipeline" })).toBeVisible(); await expect(page.getByLabel("pipeline idle")).toBeVisible(); await expect(page.getByLabel("needs work empty")).toBeVisible(); // No channels → the Channels table is omitted entirely. await expect(page.getByRole("columnheader", { name: "Slug" })).toHaveCount(0); // The add-a-channel entry point is always present. await expect( page.getByRole("link", { name: "+ Add a channel" }), ).toHaveAttribute("href", "/channels/new"); }); test("renders the pipeline band, needs-work panel and enriched channels table", async ({ page, }) => { await resetData("youtube-with-playlist"); await writeSettings(ONE_WORKER); // Visiting the channel detail page auto-generates the snapshot that feeds the // undownloaded counts. await generateReport(page, "test-youtube"); await page.goto("/channels/test-youtube"); await page.goto("/"); const pipeline = page.getByRole("region", { name: "Pipeline" }); await expect(pipeline).toBeVisible(); await expect( page.getByRole("button", { name: "Pause Transcriptions" }), ).toBeVisible(); await expect( page.getByRole("button", { name: "Pause Downloads" }), ).toBeVisible(); await expect( page.getByRole("button", { name: "sync all channels" }), ).toBeVisible(); // Needs-work panel lists the channel with pending downloads. await expect(page.getByLabel("needs work test-youtube")).toBeVisible(); // Enriched channels table: the row plus its inline actions. const row = page.getByRole("row", { name: "channel row test-youtube" }); await expect(row).toBeVisible(); await expect( row.getByRole("button", { name: "sync test-youtube" }), ).toBeVisible(); await expect( row.getByRole("button", { name: "prioritize test-youtube for download" }), ).toBeVisible(); }); test("pausing downloads persists and blocks a manual sync", async ({ page }) => { await resetData("youtube-with-playlist"); await writeSettings(ONE_WORKER); await generateReport(page, "test-youtube"); await page.goto("/channels/test-youtube"); await page.goto("/"); await page.getByRole("button", { name: "Pause Downloads" }).click(); // The toggle flips (the workers poll re-reads the persisted flag). await expect( page.getByRole("button", { name: "Resume Downloads" }), ).toBeVisible(); // The gate is written to settings.json (survives a restart) — on the LANE, // `autoQueue.download.held`, which is where slice 1.4 moved it from the loose // `downloadsPaused` flag. await expect .poll(async () => { const s = await readJson<{ autoQueue?: { download?: { held?: boolean } }; }>("test-settings.json"); return s.autoQueue?.download?.held; }) .toBe(true); // A manual per-channel Sync is now refused with the paused notice. const row = page.getByRole("row", { name: "channel row test-youtube" }); await row.getByRole("button", { name: "sync test-youtube" }).click(); await expect(row.getByRole("alert")).toContainText("Downloads are paused"); }); test("sidebar links to Channels, Sites, Jobs, Settings", async ({ page }) => { await resetData(); await page.goto("/"); const sidebar = page.locator("aside"); await expect(sidebar.getByRole("link", { name: /channels/i })).toHaveAttribute( "href", "/channels", ); await expect(sidebar.getByRole("link", { name: /sites/i })).toHaveAttribute( "href", "/sites", ); await expect(sidebar.getByRole("link", { name: /jobs/i })).toHaveAttribute( "href", "/jobs", ); await expect( sidebar.getByRole("link", { name: /settings/i }), ).toHaveAttribute("href", "/settings"); }); test("settings page lists getPaths() values", async ({ page }) => { await resetData(); await page.goto("/settings"); await page.locator("summary").filter({ hasText: "System paths" }).click(); // Match the table-row headers, not stray mentions in nearby blurbs. await expect( page.getByRole("rowheader", { name: "transcriptsDir" }), ).toBeVisible(); await expect(page.getByRole("rowheader", { name: "ytdlpBin" })).toBeVisible(); await expect(page.getByRole("rowheader", { name: "whisperBin" })).toBeVisible(); });