// @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()), useTranslation: () => ({ i18n: { language: "en" }, t: (key: string) => key }), })); vi.mock("./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( , ); await screen.findByText("Phone 480"); } const rows = () => [...document.querySelectorAll("[data-nav-index]")]; const rowOf = (text: string) => screen.getByText(text).closest("[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" })), ); }); });