e8b0f1144b
Web UI rework. Highest-risk slice, part 1 of 2: - New components/ui/data-table.tsx: generic TanStack Table wrapper on the shadcn Table primitive. Controlled rowSelection/columnVisibility/ pagination, optional selection column (stopPropagation on cell click), row-click, column-visibility dropdown, manual-pagination support. Hard rule honored: NO getSortedRowModel, NO column resizing/sizing. - Migrate pages/FileBrowser.impl.tsx off @mui/x-data-grid + @mui/material onto DataTable: 5 columns (type/name/ext/size/modified), row-click -> ffprobe preview preserved, column-visibility toggle, no pagination. - DataTable + FileBrowser component tests (RED->GREEN). Gate: build + lint + test green (22 files / 58 tests).
128 lines
3.9 KiB
TypeScript
128 lines
3.9 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 { FileBrowser } from "../FileBrowser.impl";
|
|
import type { DirectoryListing, MonitoringMachine } from "../../types";
|
|
|
|
// usePersistentState (browserState) reads/writes localStorage; clear between tests
|
|
// so the selectedPath / currentDir state never leaks across cases.
|
|
beforeEach(() => {
|
|
window.localStorage.clear();
|
|
});
|
|
|
|
function machineFixture(
|
|
overrides: Partial<MonitoringMachine> = {},
|
|
): MonitoringMachine {
|
|
return {
|
|
id: "local",
|
|
name: "Local",
|
|
mode: "local",
|
|
enabled: true,
|
|
services: ["files", "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 listingFixture(
|
|
entries: {
|
|
name: string;
|
|
type: string;
|
|
size: number;
|
|
mtime: number;
|
|
}[],
|
|
): DirectoryListing {
|
|
return { path: "/", entries, count: entries.length };
|
|
}
|
|
|
|
let listing: DirectoryListing;
|
|
let machines: MonitoringMachine[];
|
|
|
|
vi.mock("react-router-dom", () => ({
|
|
useSearchParams: () => [new URLSearchParams(), vi.fn()],
|
|
useNavigate: () => vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../../hooks/useFiles", () => ({
|
|
useDirectoryListing: () => ({
|
|
data: listing,
|
|
isLoading: false,
|
|
error: null,
|
|
refetch: vi.fn(),
|
|
}),
|
|
useFfprobe: () => ({ data: undefined, isLoading: false, error: null }),
|
|
useJobTemplates: () => ({ data: [] }),
|
|
useRunJob: () => ({ isPending: false, mutate: vi.fn(), data: undefined }),
|
|
}));
|
|
|
|
vi.mock("../../hooks/useSettings", () => ({
|
|
useMonitoringSettings: () => ({ data: machines }),
|
|
}));
|
|
|
|
beforeEach(() => {
|
|
machines = [machineFixture()];
|
|
listing = listingFixture([
|
|
{ name: "movies", type: "d", size: 0, mtime: 1_700_000_000 },
|
|
{ name: "video.mkv", type: "f", size: 1_500_000_000, mtime: 1_700_000_000 },
|
|
{ name: "notes.txt", type: "f", size: 12, mtime: 1_700_000_000 },
|
|
]);
|
|
});
|
|
|
|
describe("FileBrowser (slice 7a — TanStack DataTable parity)", () => {
|
|
it("renders the 5 locked columns (type/name/ext/size/modified)", () => {
|
|
render(<FileBrowser />);
|
|
|
|
const headers = screen
|
|
.getAllByRole("columnheader")
|
|
.map((h) => h.textContent);
|
|
// The leading selection column header is empty (checkbox); the 5 data
|
|
// columns are Type, Name, Ext, Size, Modified in that order.
|
|
expect(headers).toEqual(
|
|
expect.arrayContaining(["Type", "Name", "Ext", "Size", "Modified"]),
|
|
);
|
|
expect(headers.filter((h) => h === "Type").length).toBe(1);
|
|
expect(headers.filter((h) => h === "Modified").length).toBe(1);
|
|
});
|
|
|
|
it("clicking a file row selects it for ffprobe preview (Media info)", async () => {
|
|
render(<FileBrowser />);
|
|
|
|
// The selected-file path surfaces in the Browser status caption once chosen.
|
|
expect(screen.queryByText(/Selected: \/video\.mkv/)).toBeNull();
|
|
|
|
await userEvent.click(screen.getByText("video.mkv"));
|
|
expect(screen.getByText(/Selected: \/video\.mkv/)).toBeInTheDocument();
|
|
|
|
// A recognized video file enters the ffprobe branch; with empty ffprobe
|
|
// data it shows the "No ffprobe data available." status (proving the
|
|
// selected file routed into the Media info preview flow).
|
|
expect(screen.getByText("No ffprobe data available.")).toBeInTheDocument();
|
|
});
|
|
|
|
it("clicking a directory row navigates into it (no ffprobe selection)", async () => {
|
|
render(<FileBrowser />);
|
|
|
|
await userEvent.click(screen.getByText("movies"));
|
|
// After navigating into /movies, the status caption shows the new cwd and
|
|
// NO "Selected:" segment (directories are opened, not selected for preview).
|
|
expect(screen.getByText(/Current: \/movies\b/)).toBeInTheDocument();
|
|
expect(screen.queryByText(/Selected:/)).toBeNull();
|
|
});
|
|
});
|