import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Media } from "../Media"; import type { MediaIndexStatus, MediaItem, MediaQueryResponse, MonitoringMachine, } from "../../types"; // Shared navigate mock so the row-click test can assert the call. The vi.mock // factory is hoisted above this const, but it only closes over `navigate` // lazily (the arrow runs at render time, well after init) — no TDZ access. const navigate = vi.fn(); function machineFixture( overrides: Partial = {}, ): MonitoringMachine { return { id: "local", name: "Local", mode: "local", enabled: true, services: ["jellyfin", "monitoring"], host: "", port: 22, username: "", key_directory: "", key_name: "", ssh_key_id: "", ssh_private_key_set: false, ssh_private_key_passphrase_set: false, password_set: false, media_root: "", path_prefix: "", jellyfin_url: "", jellyfin_user_id: "", jellyfin_api_key_set: false, jellyseerr_url: "", jellyseerr_api_key_set: false, notes: "", ...overrides, }; } function statusFixture( overrides: Partial = {}, ): MediaIndexStatus { return { exists: true, item_count: 2, updated_at: 1, updated_at_label: "now", build_duration_seconds: null, build_running: false, build_stage: "", build_message: "", build_progress: null, build_items_processed: 0, build_items_total: 0, build_current_library: "", build_library_index: 0, build_libraries_total: 0, build_library_progress: null, build_library_items_processed: 0, build_library_items_total: 0, build_elapsed_seconds: null, build_eta_seconds: null, build_library_elapsed_seconds: null, build_library_eta_seconds: null, build_cancel_requested: false, build_pid: null, build_error: "", ...overrides, }; } function mediaItem(overrides: Partial = {}): MediaItem { return { id: "1", title: "Inception", series: "", season: "", episode: null, type: "Movie", year: 2010, runtime_min: 148, size: "12.4 GB", bitrate: "35.0 Mbps", hdr: "HDR10", video: "HEVC", resolution: "4K", date_added: "2024-01-01", library: "Movies", path: "/media/movies/Inception.mkv", ...overrides, }; } let status: MediaIndexStatus; let queryResult: MediaQueryResponse; vi.mock("react-router-dom", () => ({ useNavigate: () => navigate, useSearchParams: () => [new URLSearchParams("jellyfin_service_id=jfs1"), vi.fn()], })); vi.mock("../../hooks/useMedia", () => ({ useMediaStatus: () => ({ data: status }), useMediaQuery: () => ({ data: queryResult, isLoading: false }), useBuildIndex: () => ({ isPending: false, mutate: vi.fn() }), useStopBuildIndex: () => ({ isPending: false, mutate: vi.fn() }), useForceStopBuildIndex: () => ({ isPending: false, mutate: vi.fn() }), })); vi.mock("../../hooks/useSettings", () => ({ useMonitoringSettings: () => ({ data: [machineFixture()] }), })); vi.mock("../../hooks/useServices", () => ({ useServiceInstances: () => ({ data: [{ id: "jfs1", service_type: "jellyfin", name: "Main", enabled: true }], }), })); vi.mock("../../hooks/useDashboard", () => ({ useCounts: () => ({ data: undefined }), useLibraries: () => ({ data: undefined }), })); // usePersistentState reads/writes localStorage; clear between tests so the // offset/pageSize/columnVisibility state never leaks across cases. beforeEach(() => { window.localStorage.clear(); navigate.mockClear(); status = statusFixture(); queryResult = { items: [ mediaItem({ id: "1", title: "Inception", path: "/media/movies/Inception.mkv", }), mediaItem({ id: "2", title: "Matrix", path: "/media/movies/Matrix.mkv", }), ], total: 2, limit: 100, offset: 0, }; }); describe("Media (slice 7b — TanStack DataTable + server-driven pagination)", () => { it("exposes exactly the 15 locked toggleable columns", async () => { render(); await userEvent.click(screen.getByRole("button", { name: /Columns/ })); const toggleable = screen .getAllByRole("menuitemcheckbox") .map((item) => (item.textContent ?? "").trim()); expect([...toggleable].sort()).toEqual( [ "title", "series", "season", "episode", "type", "year", "runtime_min", "size", "bitrate", "hdr", "video", "resolution", "date_added", "library", "path", ].sort(), ); // The leading selection column is never toggleable (enableHiding=false). expect(toggleable).toHaveLength(15); expect(toggleable).not.toContain("__select__"); }); it("renders the 15 data column headers", () => { render(); const headers = screen .getAllByRole("columnheader") .map((h) => (h.textContent ?? "").trim()); for (const expected of [ "Title", "Series", "Season", "Episode", "Type", "Year", "Runtime", "Size", "Bitrate", "HDR", "Video codec", "Resolution", "Date added", "Library", "Path", ]) { expect(headers).toContain(expected); } }); it("navigates to the file browser at the item path on row click", async () => { render(); await userEvent.click(screen.getByText("Inception")); expect(navigate).toHaveBeenCalledTimes(1); expect(navigate).toHaveBeenCalledWith( `/files?path=${encodeURIComponent("/media/movies/Inception.mkv")}`, ); }); it("does NOT navigate when toggling a row selection checkbox", async () => { render(); const firstCheckbox = screen.getAllByRole("checkbox", { name: "Select row", })[0]; await userEvent.click(firstCheckbox); expect(firstCheckbox).toBeChecked(); expect(navigate).not.toHaveBeenCalled(); }); it("renders the server-driven pagination total + page controls", () => { render(); // DataTable manual-pagination footer surfaces the server total + pager. // ("Page 1 of 1" also appears in the page caption, so match all and assert // the pager footer text is present alongside the unique total.) expect(screen.getByText("2 rows")).toBeInTheDocument(); expect(screen.getAllByText(/Page 1 of 1/).length).toBeGreaterThan(0); expect( screen.getByRole("button", { name: "Previous page" }), ).toBeDisabled(); }); it("disables Build index while a build is running", () => { status = statusFixture({ build_running: true }); render(); expect(screen.getByRole("button", { name: "Building..." })).toBeDisabled(); // Stop + Force stop surface only while running. expect( screen.getByRole("button", { name: "Stop build" }), ).toBeInTheDocument(); expect( screen.getByRole("button", { name: "Force stop" }), ).toBeInTheDocument(); }); });