The 43 react-refresh/only-export-components warnings the gate let through meant an edit to a provider (or AnchoredPopover, Toaster, plexDetailUi, …) could not be hot-swapped: Fast Refresh reloads a module that also exports hooks or helpers. - Each provider's contexts, the types its hooks return and the hooks themselves live in a sibling context module (navigationContext.ts, channelsContext.ts, …); the provider file keeps the component. loadAccountFilters moved next to the feed filters' defaults (feedFiltersInit.ts), following navigationInit/playlistsInit. - Helpers that sat in component files have their own modules: welcomeMode, downloadTitle, plexPlayable, channelSync, ui/anchoredPopoverHooks, ui/pageToolbarSlot, ui/pagerContent, ui/toastLevels, ui/overlayOrder, ui/tableAlign, lib/plexDetail; the cells of the channel columns and the registry's page wrappers are components files of their own (channelCells, modulePages). - usePersistedTab, an alias of useAccountPersistedState, is gone; its callers use the original. npm run lint is now eslint --max-warnings 0, so a warning fails the gate like an error does.
104 lines
3.6 KiB
TypeScript
104 lines
3.6 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// The Download Center's confirm dialogs name their ACTION on the confirm button (E11.5): the shared
|
|
// `confirmThen` once labelled it with the dialog's question — "Re-download this file?" on the button,
|
|
// and the cancel dialog's own "Cancel" beside "Cancel". i18n is key-passthrough, so labels read as keys.
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import type { DownloadJob } from "../lib/api";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
jobs: [] as unknown[],
|
|
confirm: vi.fn(async (_opts: { title: string; confirmLabel?: string }) => false),
|
|
}));
|
|
|
|
vi.mock("react-i18next", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("react-i18next")>();
|
|
return { ...actual, useTranslation: () => ({ t: (key: string) => key }) };
|
|
});
|
|
vi.mock("./confirmContext", () => ({ useConfirm: () => mocks.confirm }));
|
|
vi.mock("../lib/useLiveQuery", () => ({
|
|
useLiveQuery: (key: readonly unknown[]) => ({
|
|
data: key[0] === "downloads" ? mocks.jobs : undefined,
|
|
isError: false,
|
|
isLoading: false,
|
|
refetch: vi.fn(),
|
|
}),
|
|
}));
|
|
vi.mock("../lib/useMe", () => ({ useMe: () => ({ role: "user" }) }));
|
|
// The page toolbar (title + tabs) renders into the app shell's slot, which a bare render has none of.
|
|
vi.mock("./ui/PageShell", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("./ui/PageShell")>()),
|
|
PageToolbar: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
}));
|
|
|
|
import DownloadCenter from "./DownloadCenter";
|
|
|
|
const job = (id: number, status: DownloadJob["status"]): DownloadJob => ({
|
|
id,
|
|
status,
|
|
progress: 0,
|
|
speed_bps: null,
|
|
eta_s: null,
|
|
phase: null,
|
|
error: null,
|
|
error_code: null,
|
|
error_detail: null,
|
|
queue_pos: 0,
|
|
created_at: null,
|
|
source_kind: "youtube",
|
|
source_ref: `v${id}`,
|
|
source_url: null,
|
|
poster_url: null,
|
|
profile_id: null,
|
|
spec: {
|
|
mode: "av",
|
|
max_height: null,
|
|
container: null,
|
|
audio_format: null,
|
|
vcodec: null,
|
|
embed_subs: false,
|
|
embed_chapters: false,
|
|
embed_thumbnail: false,
|
|
sponsorblock: false,
|
|
},
|
|
display_name: `Clip ${id}`,
|
|
display_uploader: null,
|
|
display_uploader_url: null,
|
|
extra_links: [],
|
|
can_download: status === "done",
|
|
expired: false,
|
|
asset: null,
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
mocks.confirm.mockClear();
|
|
});
|
|
|
|
describe("DownloadCenter confirm dialogs", () => {
|
|
it("label the confirm button with the action, not the dialog's question", async () => {
|
|
mocks.jobs = [job(1, "running"), job(2, "done")];
|
|
render(
|
|
<QueryClientProvider client={new QueryClient()}>
|
|
<DownloadCenter />
|
|
</QueryClientProvider>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByTitle("downloads.actions.cancel"));
|
|
await waitFor(() => expect(mocks.confirm).toHaveBeenCalledTimes(1));
|
|
// The tab's label shares its button with a count badge.
|
|
fireEvent.click(screen.getByRole("button", { name: /downloads\.tabs\.done/ }));
|
|
fireEvent.click(await screen.findByTitle("downloads.actions.redownload"));
|
|
fireEvent.click(screen.getByTitle("downloads.actions.delete"));
|
|
await waitFor(() => expect(mocks.confirm).toHaveBeenCalledTimes(3));
|
|
|
|
expect(mocks.confirm.mock.calls.map(([o]) => [o.title, o.confirmLabel])).toEqual([
|
|
["downloads.confirm.cancelTitle", "downloads.confirm.cancelAction"],
|
|
["downloads.confirm.redownloadTitle", "downloads.confirm.redownloadAction"],
|
|
["downloads.confirm.deleteTitle", "downloads.actions.delete"],
|
|
]);
|
|
});
|
|
});
|