Files
siftlode/frontend/src/modules/settings/ProfileEditor.dom.test.tsx
T
peter 63a0fecc8b refactor(frontend): the flat components/ folder moves into shell/, modules/<page>/ and features/<name>/
A pure move (git mv, history kept): 333 files leave components/, which now holds only the platform
layer (ui/) and the legal pages. The shell is the frame (App, header, nav rail, palette, boot); each
page lives in modules/<page>/ with its rail, store and tests; what several pages share sits in
features/<name>/; the shared-link page is watch/. Domain-free helpers went straight to ui/
(ViewSwitcher, FilterRail, the confirm dialog, the language switcher, the copy rows).

Every relative specifier was rewritten (imports, dynamic imports, vi.mock, new URL), and the
contract tests that scan sources now scan the whole app source, not only components/ — each glob
was counted before and after so none went green over fewer files. The locale-format rule matched
`/src/components/` only, so the move would have taken ~300 files out from under its i18n-singleton
check; it now covers every folder that holds components, with a test per folder.
2026-10-22 23:12:45 +02:00

142 lines
5.5 KiB
TypeScript

// @vitest-environment jsdom
//
// The download formats (Settings → Downloads, and the download dialog's "Manage formats"): one aligned
// list, built-ins first, each row saying what it saves. A row's one menu: a built-in's only
// Duplicate; the user's own Edit, Duplicate and Delete (behind a confirm). A duplicate is a NEW format
// with the original's settings and a name of its own. i18n is key-passthrough.
import { afterEach, describe, expect, it, vi } from "vitest";
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { api, type DownloadProfile } from "../../lib/api";
const mocks = vi.hoisted(() => ({ confirm: vi.fn(async (_o?: unknown) => true) }));
vi.mock("react-i18next", async (importOriginal) => ({
...(await importOriginal<typeof import("react-i18next")>()),
useTranslation: () => ({ i18n: { language: "en" }, t: (key: string) => key }),
}));
vi.mock("../../components/ui/confirmContext", () => ({ useConfirm: () => mocks.confirm }));
import { DownloadFormats } from "./ProfileEditor";
const SPEC = {
mode: "av",
max_height: 480,
container: "mp4",
audio_format: "m4a",
vcodec: null,
embed_subs: true,
embed_chapters: false,
embed_thumbnail: false,
sponsorblock: true,
};
const PROFILES = [
{ id: 9, name: "Phone 480", is_builtin: false, spec: SPEC },
{ id: 1, name: "best", is_builtin: true, spec: { mode: "a", audio_format: "mp3" } },
] as unknown as DownloadProfile[];
afterEach(() => {
cleanup();
vi.restoreAllMocks();
mocks.confirm.mockClear();
});
async function mount() {
vi.spyOn(api, "downloadProfiles").mockResolvedValue(PROFILES);
render(
<QueryClientProvider client={new QueryClient()}>
<DownloadFormats />
</QueryClientProvider>,
);
await screen.findByText("Phone 480");
}
const rows = () => [...document.querySelectorAll<HTMLElement>("[data-nav-index]")];
const rowOf = (text: string) => screen.getByText(text).closest<HTMLElement>("[data-nav-index]")!;
const menuRows = () =>
screen.getAllByRole("menuitem", { hidden: true }).map((b) => b.textContent ?? "");
const pick = (label: string) =>
fireEvent.click(
screen.getAllByRole("menuitem", { hidden: true }).find((b) => b.textContent === label)!,
);
async function openMenu(row: HTMLElement) {
act(() => {
fireEvent.contextMenu(row);
});
await waitFor(() =>
expect(screen.getAllByRole("menuitem", { hidden: true }).length).toBeGreaterThan(0),
);
}
describe("DownloadFormats", () => {
it("lists the built-ins first, each saying what it saves", async () => {
await mount();
expect(rows().map((r) => r.textContent)).toEqual([
expect.stringContaining("downloads.profiles.modeA · MP3"),
expect.stringContaining(
"downloads.profiles.modeAv · ≤480p · downloads.profiles.short.subs · downloads.profiles.short.sponsorblock",
),
]);
});
it("a built-in's menu only duplicates; the user's own edits, duplicates and deletes", async () => {
await mount();
await openMenu(rows()[0]!);
expect(menuRows()).toEqual(["downloads.profiles.duplicate"]);
fireEvent.keyDown(document.activeElement!, { key: "Escape" });
await openMenu(rowOf("Phone 480"));
expect(menuRows()).toEqual([
"downloads.profiles.edit",
"downloads.profiles.duplicate",
"downloads.profiles.delete",
]);
});
it("deletes only through the confirm", async () => {
const del = vi.spyOn(api, "deleteDownloadProfile").mockResolvedValue({} as never);
await mount();
mocks.confirm.mockResolvedValueOnce(false);
await openMenu(rowOf("Phone 480"));
pick("downloads.profiles.delete");
await waitFor(() => expect(mocks.confirm).toHaveBeenCalledTimes(1));
expect(del).not.toHaveBeenCalled();
await openMenu(rowOf("Phone 480"));
pick("downloads.profiles.delete");
await waitFor(() => expect(del).toHaveBeenCalledWith(9));
});
it("a duplicate is a new format with the original's settings and a name of its own", async () => {
const create = vi.spyOn(api, "createDownloadProfile").mockResolvedValue({} as never);
const update = vi.spyOn(api, "updateDownloadProfile").mockResolvedValue({} as never);
await mount();
await openMenu(rowOf("Phone 480"));
pick("downloads.profiles.duplicate");
// The form opens with the keyboard in its name, the field named by its label.
const name = await screen.findByRole("textbox", { name: "downloads.profiles.name" });
await waitFor(() => expect(document.activeElement).toBe(name));
expect((name as HTMLInputElement).value).toBe("");
fireEvent.change(name, { target: { value: "Phone copy" } });
fireEvent.click(screen.getByText("downloads.profiles.create"));
await waitFor(() =>
expect(create).toHaveBeenCalledWith({
name: "Phone copy",
spec: expect.objectContaining({ max_height: 480, embed_subs: true, sponsorblock: true }),
}),
);
expect(update).not.toHaveBeenCalled();
});
it("Edit saves over the format itself", async () => {
const update = vi.spyOn(api, "updateDownloadProfile").mockResolvedValue({} as never);
await mount();
await openMenu(rowOf("Phone 480"));
pick("downloads.profiles.edit");
const name = await screen.findByDisplayValue("Phone 480");
fireEvent.change(name, { target: { value: "Phone" } });
fireEvent.click(screen.getByText("downloads.profiles.save"));
await waitFor(() =>
expect(update).toHaveBeenCalledWith(9, expect.objectContaining({ name: "Phone" })),
);
});
});