Files
siftlode/frontend/src/components/Scheduler.menu.dom.test.tsx
T
peter aac79ffa53 fix(i18n): numbers, units, dates and order in the UI language, and after a switch (E30 S3)
Every locale formatter takes the language as an argument. Under the React
Compiler a formatter that read the i18n singleton was memoized on its other
arguments, so a card kept "2 hr 4 min" after a switch to Hungarian; plexLang()
had the same flaw and left Plex queries in the old language. Components now
take the language from useTranslation, and the signature makes forgetting it
a type error.

- ratings, file sizes, stream stats: the language's decimal mark ("8,3")
- view and subscriber counts compact in the language ("1,2 E")
- seconds as the language's unit ("6 mp") in the sliders and the stats
- a person's birth and death dates as dates, not ISO strings
- A-Z lists by the shown name in the language's alphabet (compareText),
  the tag catalog re-sorted by its translated names
- lint local/locale-format (was date-format) also refuses Intl.NumberFormat
  outside lib/format.ts, localeCompare, and a component reading the language
  off the i18n singleton
2026-09-29 22:39:57 +02:00

125 lines
4.2 KiB
TypeScript

// @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<typeof import("react-i18next")>();
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> = {}): 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(
<QueryClientProvider client={qc}>
<Scheduler />
</QueryClientProvider>,
);
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",
]);
});
});