// @vitest-environment jsdom // // The Files rail's folder management: a new folder is named in place, a folder's right click opens its // ONE menu (the page's "…" opens the same — Files.menu.dom.test), and a rename happens in the entry. // A field left by Enter or Escape hands the keyboard back to what stands there now, never to . // The SidePanel chrome around the groups is stubbed; the writes are watched at the API. import { afterEach, beforeAll, 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 type { ReactNode } from "react"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ t: (k: string) => k, i18n: { language: "en" } }) }; }); vi.mock("./confirmContext", () => ({ useConfirm: () => async () => false })); vi.mock("./FilterRail", () => ({ default: ({ items, children, }: { items: { id: string; body: ReactNode }[]; children: ReactNode; }) => ( <> {items.map((i) => (
{i.body}
))} {children} ), })); import FilesRail from "./FilesRail"; import { api, type FileFolder } from "../lib/api"; import { defaultLayout } from "../lib/panelLayout"; beforeAll(() => { class Stub { observe() {} unobserve() {} disconnect() {} takeRecords() { return []; } } globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver; globalThis.IntersectionObserver ??= Stub as unknown as typeof IntersectionObserver; }); afterEach(() => { cleanup(); vi.restoreAllMocks(); }); const folder = (id: number, extra: Partial): FileFolder => ({ id, kind: "source", source_key: null, name: null, retention_days: 30, count: 1, size_bytes: 0, created_at: null, ...extra, }); const YT = folder(1, { source_key: "youtube" }); const OWN = folder(2, { kind: "own", name: "Receptek", retention_days: null }); async function mount(folders: FileFolder[] = [YT, OWN]) { vi.spyOn(api, "fileFolders").mockResolvedValue(folders); vi.spyOn(api, "filesUsage").mockResolvedValue({ unlimited: true, footprint_bytes: 0 } as never); render( , ); await screen.findByText("files.folder.new"); } const entry = (name: string) => screen.getByText(name).closest("button")!; const menuRows = () => screen.getAllByRole("menuitem", { hidden: true }).map((b) => b.textContent ?? ""); describe("FilesRail · folders", () => { it("names a new folder in place; Enter makes it and gives the keyboard back", async () => { await mount(); const create = vi.spyOn(api, "createFolder").mockResolvedValue(folder(3, { kind: "own" })); fireEvent.click(screen.getByText("files.folder.new")); const field = screen.getByRole("textbox", { name: "files.folder.newName" }); fireEvent.change(field, { target: { value: " Cimboráknak " } }); fireEvent.keyDown(field, { key: "Enter" }); await waitFor(() => expect(create).toHaveBeenCalledWith("Cimboráknak")); await waitFor(() => expect(document.activeElement).toBe(screen.getByText("files.folder.new").closest("button")), ); }); it("Escape makes nothing; an empty name makes nothing", async () => { await mount(); const create = vi.spyOn(api, "createFolder"); fireEvent.click(screen.getByText("files.folder.new")); const field = screen.getByRole("textbox", { name: "files.folder.newName" }); fireEvent.change(field, { target: { value: "Más" } }); fireEvent.keyDown(field, { key: "Escape" }); expect(screen.queryByRole("textbox")).toBeNull(); fireEvent.click(screen.getByText("files.folder.new")); fireEvent.keyDown(screen.getByRole("textbox", { name: "files.folder.newName" }), { key: "Enter", }); await act(async () => {}); expect(create).not.toHaveBeenCalled(); }); it("a folder's right click opens its menu; a source folder's holds only its retention", async () => { await mount(); act(() => { fireEvent.contextMenu(entry("Receptek")); }); await waitFor(() => expect(menuRows()).toEqual([ "files.folder.rename", "files.folder.retention", "files.folder.delete", ]), ); fireEvent.keyDown(document.activeElement!, { key: "Escape" }); act(() => { fireEvent.contextMenu(entry("files.source.youtube")); }); await waitFor(() => expect(menuRows()).toEqual(["files.folder.retention"])); }); it("renames in the entry, and Enter hands the keyboard back to it", async () => { await mount(); const update = vi.spyOn(api, "updateFolder").mockResolvedValue(OWN); act(() => { fireEvent.contextMenu(entry("Receptek")); }); await waitFor(() => expect(menuRows()).toContain("files.folder.rename")); fireEvent.click( screen .getAllByRole("menuitem", { hidden: true }) .find((b) => b.textContent === "files.folder.rename")!, ); const field = await screen.findByRole("textbox", { name: "files.folder.rename" }); fireEvent.change(field, { target: { value: "Ételek" } }); fireEvent.keyDown(field, { key: "Enter" }); await waitFor(() => expect(update).toHaveBeenCalledWith(2, { name: "Ételek" })); await waitFor(() => expect(document.activeElement).toBe(entry("Receptek"))); }); }); describe("FilesRail · what it says when a part is empty or missing", () => { it("says nothing was downloaded only when nothing is there — not when it all moved into own folders", async () => { await mount([folder(1, { source_key: "youtube", count: 0 }), { ...OWN, count: 3 }]); expect(screen.queryByText("files.rail.noSources")).toBeNull(); cleanup(); await mount([folder(1, { source_key: "youtube", count: 0 }), { ...OWN, count: 0 }]); expect(screen.getByText("files.rail.noSources")).toBeTruthy(); }); it("a storage figure that failed to load says so, rather than the meter quietly not being there", async () => { vi.spyOn(api, "fileFolders").mockResolvedValue([YT, OWN]); vi.spyOn(api, "filesUsage").mockRejectedValue(new Error("down")); render( , ); expect(await screen.findByText("files.usage.failed")).toBeTruthy(); }); });