// @vitest-environment jsdom // // E28 C1 on the Scheduler page: Run all and Pause stay in the status strip; the destructive Purge // discovery left it for the strip's "…", which holds the page's whole list and asks before purging. import { describe, it, expect, vi, afterEach, beforeEach } from "vitest"; import { render, screen, fireEvent, cleanup, act, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const h = vi.hoisted(() => ({ confirm: vi.fn(async () => true) })); vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ i18n: { language: "en" }, t: (k: string) => k }) }; }); vi.mock("./confirmContext", () => ({ useConfirm: () => h.confirm })); import Scheduler from "./Scheduler"; import { api, type SchedulerStatus } from "../lib/api"; const status = (over: Partial = {}): SchedulerStatus => ({ running_here: true, enabled: true, paused: false, jobs: [ { id: "sync", interval_minutes: 30, next_run: null, running: false, status: "ok", last_started: null, last_finished: null, last_result: null, result: null, last_error: null, progress: null, }, ], queue: { channels_recent_pending: 0, channels_deep_pending: 0, deep_eta_seconds: 0, videos_pending_enrich: 0, videos_pending_shorts: 0, videos_live_refresh: 0, }, quota: { used_today: 0, remaining_today: 100, daily_budget: 100, backfill_reserve: 0 }, maintenance: { revalidate_batch: 5000, revalidate_batch_default: 5000, min: 1000, max: 50000 }, ...over, }); let qc: QueryClient; let current: SchedulerStatus; async function mount(initial = status()) { current = initial; qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render( , ); await screen.findByText("scheduler.maintenance.batchLabel"); } beforeEach(() => { vi.spyOn(api, "schedulerStatus").mockImplementation(() => Promise.resolve(current)); }); afterEach(() => { cleanup(); vi.restoreAllMocks(); h.confirm.mockClear(); }); const menuRows = () => screen.getAllByRole("menuitem", { hidden: true }).map((b) => b.textContent ?? ""); async function openMenu() { act(() => { fireEvent.click(screen.getByRole("button", { name: "scheduler.menu" })); }); await waitFor(() => expect(screen.getAllByRole("menuitem", { hidden: true }).length).toBeGreaterThan(0), ); } const choose = (label: string) => fireEvent.click( screen.getAllByRole("menuitem", { hidden: true }).find((b) => b.textContent === label)!, ); describe("Scheduler · the status strip's '…'", () => { it("keeps Run all and Pause in sight and Purge discovery only in the menu", async () => { await mount(); expect(screen.getByRole("button", { name: /scheduler\.runAll$/ })).toBeTruthy(); expect(screen.getByRole("button", { name: /scheduler\.pause$/ })).toBeTruthy(); expect(screen.queryByRole("button", { name: /scheduler\.purgeDiscovery/ })).toBeNull(); await openMenu(); expect(menuRows()).toEqual(["scheduler.runAll", "scheduler.pause", "scheduler.purgeDiscovery"]); }); it("purges only after the confirm says yes", async () => { const purge = vi.spyOn(api, "purgeDiscovery").mockResolvedValue({ search_videos_deleted: 1, videos_deleted: 0, channels_deleted: 0, } as never); await mount(); h.confirm.mockResolvedValueOnce(false); await openMenu(); choose("scheduler.purgeDiscovery"); await waitFor(() => expect(h.confirm).toHaveBeenCalledTimes(1)); expect(h.confirm).toHaveBeenCalledWith(expect.objectContaining({ danger: true })); expect(purge).not.toHaveBeenCalled(); await openMenu(); choose("scheduler.purgeDiscovery"); await waitFor(() => expect(purge).toHaveBeenCalledTimes(1)); }); it("says why Run all is off while the scheduler is paused", async () => { await mount(status({ paused: true })); await openMenu(); expect(menuRows()).toEqual([ "scheduler.runAllscheduler.runAllPausedHint", "scheduler.resume", "scheduler.purgeDiscovery", ]); }); });