Files
siftlode/frontend/src/components/ProfileEditor.dom.test.tsx
T
peter 8b2260c9a8 fix(files): review round 1, the page — honest states, the keyboard, the tray's words
- A library past the list's cap is searched and sorted on the server; the count says "the newest".
- Each folder and each search opens on its first page (DataTable pageScope, useCardPager scope).
- A file dropped where the page takes none never reaches the browser (which would unload the app).
- The viewer's arrows wait while a layer opened from it is on top, and scroll a wide text line;
  a text shows its first 5000 lines and says where the rest is.
- Confirms name the file (no more "your downloads"); the asking menu rows end in "…".
- The tray names a running step beside its bar, shows a pause mark for paused work, polls slowly
  while only paused work waits, and forgets a finished file that was deleted.
- An unchanged rename sends nothing; a paste opens the download dialog only with nothing on top;
  the link box never replaces what was typed.
- Honest states: folders that failed to load, a file no longer there, the storage figure; the rail
  says "nothing downloaded" only when there is nothing.
- Focus after deleting the open folder and when the format form opens; the format name's label.
- ui/Meter (one danger line for both quota bars) and ui/RenameField in the platform layer.
- Copy fixes, stale comments and the speculative hub key; a contract test for the list's
  parameters; e2e: the Files page and the paste intake.
2026-10-13 00:14:09 +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("./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" })),
);
});
});