// Granular queue control: the ↑/↓/Promote buttons on a QUEUED job's row on // /jobs reorder it within its queue. This asserts the EFFECT rather than the // listing order: a promoted job runs next when the head is cancelled, ahead of // the job that was FIFO-first. import { mkdir, writeFile } from "node:fs/promises"; import { test, expect } from "@playwright/test"; import { channelStage, generateReport, resetData, resolvePath, } from "./helpers"; import { baseUrl } from "./baseUrl"; async function invalidateCache() { await fetch(`${baseUrl}/api/test/invalidate-cache`).catch(() => {}); } // A channel whose Sync runs slowly (fake-ytdlp honors --test-slow), so it stays // "running"/"queued" long enough to reorder. async function makeSlowChannel(slug: string, name: string) { const root = resolvePath(`test-transcripts/channels/${slug}`); await mkdir(root, { recursive: true }); await writeFile( `${root}/config.json`, JSON.stringify({ handling: "youtube", name, url: `https://www.youtube.com/@${slug}/videos`, ytdlpExtraArgs: ["--test-slow"], }), ); } async function setQueueViaCustom( page: import("@playwright/test").Page, actionLabel: string, value: string, ) { await page .getByLabel(`queue for ${actionLabel}`, { exact: true }) .selectOption({ label: "Custom…" }); await page .getByLabel(`custom queue for ${actionLabel}`, { exact: true }) .fill(value); await page .getByRole("button", { name: `save custom queue for ${actionLabel}` }) .click(); } async function syncToQueue( page: import("@playwright/test").Page, slug: string, queue: string, ) { await generateReport(page, slug); await page.goto(channelStage(slug, "playlist")); await setQueueViaCustom(page, "Sync", queue); await page.getByRole("button", { name: "Sync" }).click(); } test("Promote makes a queued job run next, ahead of the FIFO-first one", async ({ page, }) => { test.setTimeout(90_000); await resetData(null); await makeSlowChannel("reorder-a", "Reorder A"); await makeSlowChannel("reorder-b", "Reorder B"); await makeSlowChannel("reorder-c", "Reorder C"); await invalidateCache(); // A runs; B (FIFO-first) then C queue behind it on the shared queue. await syncToQueue(page, "reorder-a", "qReorder"); await expect(page.getByLabel("Sync output")).toContainText("test-slow", { timeout: 15_000, }); await syncToQueue(page, "reorder-b", "qReorder"); await expect(page.getByLabel("Sync queue status")).toBeVisible({ timeout: 10_000, }); await syncToQueue(page, "reorder-c", "qReorder"); await expect(page.getByLabel("Sync queue status")).toBeVisible({ timeout: 10_000, }); await page.goto("/jobs"); const rowA = page.getByRole("row").filter({ hasText: "reorder-a" }); const rowB = page.getByRole("row").filter({ hasText: "reorder-b" }); const rowC = page.getByRole("row").filter({ hasText: "reorder-c" }); await expect(rowA.getByText("running", { exact: true })).toBeVisible({ timeout: 15_000, }); await expect(rowB.getByText("queued", { exact: true })).toBeVisible(); await expect(rowC.getByText("queued", { exact: true })).toBeVisible(); // C is last in the queue, so its row offers Promote; B (first queued) does not. await expect( rowB.getByRole("button", { name: /^promote job/ }), ).toHaveCount(0); await rowC.getByRole("button", { name: /^promote job/ }).click(); // Cancel the running A; the PROMOTED C runs next, not the FIFO-first B. await rowA.getByRole("button", { name: /^Cancel$/ }).click(); await expect(rowC.getByText("running", { exact: true })).toBeVisible({ timeout: 15_000, }); await expect(rowB.getByText("queued", { exact: true })).toBeVisible(); });