fd12e921fd
Slice 2 of jellyfin-service-registry. Removes the machine-level media_root/path_prefix fields from the frontend now that the backend no longer stores them. - types/index.ts: dropped media_root/path_prefix from MonitoringMachine and MonitoringMachineInput. - pages/Settings.tsx: removed the media_root form input, the read-only "Media root" detail (replaced with a local-hint field mirroring the editor), and media_root/path_prefix from emptyMachine() and both edit-handler reset mappings; updated the section description. - tests: removed media_root/path_prefix from Settings/Media/FileBrowser test fixtures. npm run build (tsc -b + vite) clean; 0 lint errors; 72 tests pass.
264 lines
6.5 KiB
TypeScript
264 lines
6.5 KiB
TypeScript
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> = {},
|
|
): 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,
|
|
notes: "",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function statusFixture(
|
|
overrides: Partial<MediaIndexStatus> = {},
|
|
): 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> = {}): 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(<Media />);
|
|
|
|
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(<Media />);
|
|
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(<Media />);
|
|
|
|
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(<Media />);
|
|
|
|
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(<Media />);
|
|
|
|
// 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(<Media />);
|
|
|
|
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();
|
|
});
|
|
});
|