Formatting was configured but never enforced: `bun run format:check` reported 199 unformatted files and ran in no workflow and in no git hook, so .prettierrc (printWidth 100, trailing commas) described an intention rather than the tree. This is the one-time sweep that makes the check gateable. Whitespace and token-reflow only -- no behavioural change: `bun run check` reports 0 errors and all 1053 frontend tests pass before and after. Kept out of every other commit on purpose. A 199-file diff mixed with real changes is unreviewable, and the next commit turns format:check into a hard CI gate so this cannot silently accumulate again.
720 lines
21 KiB
TypeScript
720 lines
21 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { render, screen, fireEvent, waitFor } from "@testing-library/svelte";
|
|
import GenericMediaListPage from "./GenericMediaListPage.svelte";
|
|
|
|
// Mock SvelteKit navigation
|
|
vi.mock("$app/navigation", () => ({
|
|
goto: vi.fn(),
|
|
}));
|
|
|
|
// Mock stores
|
|
vi.mock("$lib/stores/library", () => ({
|
|
currentLibrary: {
|
|
subscribe: vi.fn((fn) => {
|
|
fn({ id: "lib123", name: "Music" });
|
|
return vi.fn();
|
|
}),
|
|
},
|
|
// Consumed as a store ($viewMode) by LibraryGrid, which this page renders.
|
|
viewMode: {
|
|
subscribe: vi.fn((fn) => {
|
|
fn("grid");
|
|
return vi.fn();
|
|
}),
|
|
},
|
|
}));
|
|
|
|
vi.mock("$lib/stores/auth", () => ({
|
|
auth: {
|
|
getRepository: vi.fn(() => ({
|
|
getItems: vi.fn(),
|
|
search: vi.fn(),
|
|
})),
|
|
},
|
|
}));
|
|
|
|
vi.mock("$lib/composables/useServerReachabilityReload", () => ({
|
|
useServerReachabilityReload: vi.fn(() => ({
|
|
markLoaded: vi.fn(),
|
|
})),
|
|
}));
|
|
|
|
// The component lazily subscribes to the backend `search-event` when searching.
|
|
vi.mock("@tauri-apps/api/event", () => ({
|
|
listen: vi.fn(async () => () => {}),
|
|
}));
|
|
|
|
describe("GenericMediaListPage", () => {
|
|
// Component integration tests - core sorting/search/debouncing logic tested in backend-integration.test.ts
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllTimers();
|
|
});
|
|
|
|
describe("Component Initialization", () => {
|
|
it("should render with title and search bar", () => {
|
|
const config = {
|
|
itemType: "Audio" as const,
|
|
title: "Tracks",
|
|
backPath: "/library/music",
|
|
searchPlaceholder: "Search tracks...",
|
|
sortOptions: [{ key: "SortName", label: "Title" }],
|
|
defaultSort: "SortName",
|
|
displayComponent: "tracklist" as const,
|
|
};
|
|
|
|
const { container } = render(GenericMediaListPage, {
|
|
props: { config },
|
|
});
|
|
|
|
const heading = screen.getByText("Tracks");
|
|
expect(heading).toBeTruthy();
|
|
|
|
const searchInput = container.querySelector('input[type="text"]');
|
|
expect(searchInput).toBeTruthy();
|
|
});
|
|
|
|
it("should load items on mount", async () => {
|
|
const mockGetItemsFn = vi.fn().mockResolvedValue({
|
|
items: [],
|
|
totalRecordCount: 0,
|
|
});
|
|
|
|
vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue({
|
|
getItems: mockGetItemsFn,
|
|
search: vi.fn(),
|
|
} as any);
|
|
|
|
const config = {
|
|
itemType: "Audio" as const,
|
|
title: "Tracks",
|
|
backPath: "/library/music",
|
|
searchPlaceholder: "Search tracks...",
|
|
sortOptions: [{ key: "SortName", label: "Title" }],
|
|
defaultSort: "SortName",
|
|
displayComponent: "tracklist" as const,
|
|
};
|
|
|
|
render(GenericMediaListPage, {
|
|
props: { config },
|
|
});
|
|
|
|
await waitFor(() => expect(mockGetItemsFn).toHaveBeenCalledWith("lib123", expect.anything()));
|
|
});
|
|
|
|
it("should display sort options", () => {
|
|
const config = {
|
|
itemType: "MusicAlbum" as const,
|
|
title: "Albums",
|
|
backPath: "/library/music",
|
|
searchPlaceholder: "Search albums...",
|
|
sortOptions: [
|
|
{ key: "SortName", label: "Title" },
|
|
{ key: "Artist", label: "Artist" },
|
|
{ key: "ProductionYear", label: "Year" },
|
|
],
|
|
defaultSort: "SortName",
|
|
displayComponent: "grid" as const,
|
|
};
|
|
|
|
render(GenericMediaListPage, {
|
|
props: { config },
|
|
});
|
|
|
|
// Check that all sort options are rendered
|
|
const titleOption = screen.queryByText("Title");
|
|
expect(titleOption).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("Search Functionality", () => {
|
|
it("should debounce rapid keystrokes into a single search for the final value", async () => {
|
|
const mockSearchFn = vi.fn().mockResolvedValue({ items: [], totalRecordCount: 0 });
|
|
const mockGetItemsFn = vi.fn().mockResolvedValue({ items: [], totalRecordCount: 0 });
|
|
|
|
vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue({
|
|
getItems: mockGetItemsFn,
|
|
search: mockSearchFn,
|
|
} as any);
|
|
|
|
const config = {
|
|
itemType: "Audio" as const,
|
|
title: "Tracks",
|
|
backPath: "/library/music",
|
|
searchPlaceholder: "Search tracks...",
|
|
sortOptions: [{ key: "SortName", label: "Title" }],
|
|
defaultSort: "SortName",
|
|
displayComponent: "tracklist" as const,
|
|
};
|
|
|
|
const { container } = render(GenericMediaListPage, {
|
|
props: { config },
|
|
});
|
|
|
|
// Let the mount load settle so the debounce effect is armed.
|
|
await waitFor(() => expect(mockGetItemsFn).toHaveBeenCalled());
|
|
|
|
// Drive the debounce window deterministically with fake timers, flushing
|
|
// the async loadItems() microtasks after the timer fires.
|
|
vi.useFakeTimers();
|
|
const searchInput = container.querySelector("input") as HTMLInputElement;
|
|
|
|
fireEvent.input(searchInput, { target: { value: "t" } });
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
fireEvent.input(searchInput, { target: { value: "te" } });
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
fireEvent.input(searchInput, { target: { value: "tes" } });
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
fireEvent.input(searchInput, { target: { value: "test" } });
|
|
|
|
// 200ms after the final keystroke: still inside the 300ms window, so no
|
|
// search has fired despite four keystrokes.
|
|
await vi.advanceTimersByTimeAsync(200);
|
|
expect(mockSearchFn).not.toHaveBeenCalled();
|
|
|
|
// Cross the threshold: exactly one search, for the final value.
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
vi.useRealTimers();
|
|
|
|
expect(mockSearchFn).toHaveBeenCalledTimes(1);
|
|
expect(mockSearchFn).toHaveBeenCalledWith("test", expect.anything(), expect.any(Number));
|
|
});
|
|
|
|
it("should use backend search when search query is provided", async () => {
|
|
const mockSearchFn = vi.fn().mockResolvedValue({
|
|
items: [{ id: "item1", name: "Test Track" }],
|
|
totalRecordCount: 1,
|
|
});
|
|
|
|
const mockGetItemsFn = vi.fn().mockResolvedValue({
|
|
items: [],
|
|
totalRecordCount: 0,
|
|
});
|
|
|
|
const mockRepository = {
|
|
getItems: mockGetItemsFn,
|
|
search: mockSearchFn,
|
|
};
|
|
|
|
vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
|
|
mockRepository as any,
|
|
);
|
|
|
|
const config = {
|
|
itemType: "Audio" as const,
|
|
title: "Tracks",
|
|
backPath: "/library/music",
|
|
searchPlaceholder: "Search tracks...",
|
|
sortOptions: [{ key: "SortName", label: "Title" }],
|
|
defaultSort: "SortName",
|
|
displayComponent: "tracklist" as const,
|
|
};
|
|
|
|
const { container } = render(GenericMediaListPage, {
|
|
props: { config },
|
|
});
|
|
|
|
// Wait for the initial mount load so the debounced search effect is armed.
|
|
await waitFor(() => expect(mockGetItemsFn).toHaveBeenCalled());
|
|
|
|
const searchInput = container.querySelector("input") as HTMLInputElement;
|
|
fireEvent.input(searchInput, { target: { value: "test" } });
|
|
|
|
// search() is called as search(query, options, requestId).
|
|
await waitFor(() => {
|
|
expect(mockSearchFn).toHaveBeenCalledWith(
|
|
"test",
|
|
expect.objectContaining({
|
|
includeItemTypes: ["Audio"],
|
|
limit: 10000,
|
|
}),
|
|
expect.any(Number),
|
|
);
|
|
});
|
|
});
|
|
|
|
it("should use getItems without search for empty query", async () => {
|
|
const mockGetItemsFn = vi.fn().mockResolvedValue({
|
|
items: [],
|
|
totalRecordCount: 0,
|
|
});
|
|
|
|
const mockRepository = {
|
|
getItems: mockGetItemsFn,
|
|
search: vi.fn(),
|
|
};
|
|
|
|
vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
|
|
mockRepository as any,
|
|
);
|
|
|
|
const config = {
|
|
itemType: "Audio" as const,
|
|
title: "Tracks",
|
|
backPath: "/library/music",
|
|
searchPlaceholder: "Search tracks...",
|
|
sortOptions: [{ key: "SortName", label: "Title" }],
|
|
defaultSort: "SortName",
|
|
displayComponent: "tracklist" as const,
|
|
};
|
|
|
|
render(GenericMediaListPage, {
|
|
props: { config },
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(mockGetItemsFn).toHaveBeenCalledWith(
|
|
"lib123",
|
|
expect.objectContaining({
|
|
includeItemTypes: ["Audio"],
|
|
sortBy: "SortName",
|
|
sortOrder: "Ascending",
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
it("should clear previous search when input becomes empty", async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const mockSearchFn = vi.fn().mockResolvedValue({
|
|
items: [],
|
|
totalRecordCount: 0,
|
|
});
|
|
|
|
const mockGetItemsFn = vi.fn().mockResolvedValue({
|
|
items: [],
|
|
totalRecordCount: 0,
|
|
});
|
|
|
|
const mockRepository = {
|
|
getItems: mockGetItemsFn,
|
|
search: mockSearchFn,
|
|
};
|
|
|
|
vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
|
|
mockRepository as any,
|
|
);
|
|
|
|
const config = {
|
|
itemType: "Audio" as const,
|
|
title: "Tracks",
|
|
backPath: "/library/music",
|
|
searchPlaceholder: "Search tracks...",
|
|
sortOptions: [{ key: "SortName", label: "Title" }],
|
|
defaultSort: "SortName",
|
|
displayComponent: "tracklist" as const,
|
|
};
|
|
|
|
const { container } = render(GenericMediaListPage, {
|
|
props: { config },
|
|
});
|
|
|
|
const searchInput = container.querySelector("input") as HTMLInputElement;
|
|
|
|
// Type search query
|
|
fireEvent.input(searchInput, { target: { value: "test" } });
|
|
vi.advanceTimersByTime(300);
|
|
|
|
// Clear search
|
|
fireEvent.input(searchInput, { target: { value: "" } });
|
|
vi.advanceTimersByTime(300);
|
|
|
|
await waitFor(() => {
|
|
// Should call getItems when search is cleared
|
|
expect(mockGetItemsFn).toHaveBeenCalled();
|
|
});
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
});
|
|
|
|
describe("Sorting Functionality", () => {
|
|
it("should pass sortBy parameter to backend", async () => {
|
|
const mockGetItemsFn = vi.fn().mockResolvedValue({
|
|
items: [],
|
|
totalRecordCount: 0,
|
|
});
|
|
|
|
const mockRepository = {
|
|
getItems: mockGetItemsFn,
|
|
search: vi.fn(),
|
|
};
|
|
|
|
vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
|
|
mockRepository as any,
|
|
);
|
|
|
|
const config = {
|
|
itemType: "MusicAlbum" as const,
|
|
title: "Albums",
|
|
backPath: "/library/music",
|
|
searchPlaceholder: "Search albums...",
|
|
sortOptions: [
|
|
{ key: "SortName", label: "Title" },
|
|
{ key: "Artist", label: "Artist" },
|
|
],
|
|
defaultSort: "SortName",
|
|
displayComponent: "grid" as const,
|
|
};
|
|
|
|
render(GenericMediaListPage, {
|
|
props: { config },
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(mockGetItemsFn).toHaveBeenCalledWith(
|
|
"lib123",
|
|
expect.objectContaining({
|
|
sortBy: "SortName",
|
|
sortOrder: "Ascending",
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
it("should pass Jellyfin field names to backend (not custom compareFn)", async () => {
|
|
const mockGetItemsFn = vi.fn().mockResolvedValue({
|
|
items: [],
|
|
totalRecordCount: 0,
|
|
});
|
|
|
|
const mockRepository = {
|
|
getItems: mockGetItemsFn,
|
|
search: vi.fn(),
|
|
};
|
|
|
|
vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
|
|
mockRepository as any,
|
|
);
|
|
|
|
const config = {
|
|
itemType: "Audio" as const,
|
|
title: "Tracks",
|
|
backPath: "/library/music",
|
|
searchPlaceholder: "Search tracks...",
|
|
sortOptions: [
|
|
{ key: "SortName", label: "Title" },
|
|
{ key: "Artist", label: "Artist" },
|
|
{ key: "Album", label: "Album" },
|
|
{ key: "DatePlayed", label: "Recent" },
|
|
],
|
|
defaultSort: "SortName",
|
|
displayComponent: "tracklist" as const,
|
|
};
|
|
|
|
render(GenericMediaListPage, {
|
|
props: { config },
|
|
});
|
|
|
|
await waitFor(() => {
|
|
const lastCall = (mockGetItemsFn as any).mock.calls[0];
|
|
const options = lastCall[1];
|
|
|
|
// Should pass Jellyfin field names directly
|
|
expect(typeof options.sortBy).toBe("string");
|
|
expect(["SortName", "Artist", "Album", "DatePlayed"]).toContain(options.sortBy);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("ItemType Filtering", () => {
|
|
it("should include correct itemType in getItems request", async () => {
|
|
const mockGetItemsFn = vi.fn().mockResolvedValue({
|
|
items: [],
|
|
totalRecordCount: 0,
|
|
});
|
|
|
|
const mockRepository = {
|
|
getItems: mockGetItemsFn,
|
|
search: vi.fn(),
|
|
};
|
|
|
|
vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
|
|
mockRepository as any,
|
|
);
|
|
|
|
const config = {
|
|
itemType: "Audio" as const,
|
|
title: "Tracks",
|
|
backPath: "/library/music",
|
|
searchPlaceholder: "Search tracks...",
|
|
sortOptions: [{ key: "SortName", label: "Title" }],
|
|
defaultSort: "SortName",
|
|
displayComponent: "tracklist" as const,
|
|
};
|
|
|
|
render(GenericMediaListPage, {
|
|
props: { config },
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(mockGetItemsFn).toHaveBeenCalledWith(
|
|
"lib123",
|
|
expect.objectContaining({
|
|
includeItemTypes: ["Audio"],
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
it("should include correct itemType in search request", async () => {
|
|
const mockSearchFn = vi.fn().mockResolvedValue({
|
|
items: [],
|
|
totalRecordCount: 0,
|
|
});
|
|
|
|
const mockGetItemsFn = vi.fn().mockResolvedValue({
|
|
items: [],
|
|
totalRecordCount: 0,
|
|
});
|
|
|
|
const mockRepository = {
|
|
getItems: mockGetItemsFn,
|
|
search: mockSearchFn,
|
|
};
|
|
|
|
vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
|
|
mockRepository as any,
|
|
);
|
|
|
|
const config = {
|
|
itemType: "MusicAlbum" as const,
|
|
title: "Albums",
|
|
backPath: "/library/music",
|
|
searchPlaceholder: "Search albums...",
|
|
sortOptions: [{ key: "SortName", label: "Title" }],
|
|
defaultSort: "SortName",
|
|
displayComponent: "grid" as const,
|
|
};
|
|
|
|
const { container } = render(GenericMediaListPage, {
|
|
props: { config },
|
|
});
|
|
|
|
await waitFor(() => expect(mockGetItemsFn).toHaveBeenCalled());
|
|
|
|
const searchInput = container.querySelector("input") as HTMLInputElement;
|
|
fireEvent.input(searchInput, { target: { value: "album" } });
|
|
|
|
await waitFor(() => {
|
|
expect(mockSearchFn).toHaveBeenCalledWith(
|
|
"album",
|
|
expect.objectContaining({
|
|
includeItemTypes: ["MusicAlbum"],
|
|
}),
|
|
expect.any(Number),
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Loading State", () => {
|
|
it("should show loading indicator during data fetch", async () => {
|
|
const mockGetItemsFn = vi.fn(
|
|
() =>
|
|
new Promise((resolve) =>
|
|
setTimeout(() => resolve({ items: [], totalRecordCount: 0 }), 100),
|
|
),
|
|
);
|
|
|
|
const mockRepository = {
|
|
getItems: mockGetItemsFn,
|
|
search: vi.fn(),
|
|
};
|
|
|
|
vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
|
|
mockRepository as any,
|
|
);
|
|
|
|
const config = {
|
|
itemType: "Audio" as const,
|
|
title: "Tracks",
|
|
backPath: "/library/music",
|
|
searchPlaceholder: "Search tracks...",
|
|
sortOptions: [{ key: "SortName", label: "Title" }],
|
|
defaultSort: "SortName",
|
|
displayComponent: "tracklist" as const,
|
|
};
|
|
|
|
vi.useFakeTimers();
|
|
const { container } = render(GenericMediaListPage, {
|
|
props: { config },
|
|
});
|
|
|
|
// Component should be rendering (will show loading state internally)
|
|
expect(container).toBeTruthy();
|
|
|
|
vi.advanceTimersByTime(100);
|
|
vi.useRealTimers();
|
|
});
|
|
});
|
|
|
|
describe("Error Handling", () => {
|
|
it("should handle backend errors gracefully", async () => {
|
|
const mockGetItemsFn = vi.fn().mockRejectedValue(new Error("Network error"));
|
|
|
|
const mockRepository = {
|
|
getItems: mockGetItemsFn,
|
|
search: vi.fn(),
|
|
};
|
|
|
|
vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
|
|
mockRepository as any,
|
|
);
|
|
|
|
const config = {
|
|
itemType: "Audio" as const,
|
|
title: "Tracks",
|
|
backPath: "/library/music",
|
|
searchPlaceholder: "Search tracks...",
|
|
sortOptions: [{ key: "SortName", label: "Title" }],
|
|
defaultSort: "SortName",
|
|
displayComponent: "tracklist" as const,
|
|
};
|
|
|
|
render(GenericMediaListPage, {
|
|
props: { config },
|
|
});
|
|
|
|
await waitFor(() => {
|
|
// Should handle error without throwing
|
|
expect(mockGetItemsFn).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it("should handle missing library gracefully", async () => {
|
|
const { goto } = await import("$app/navigation");
|
|
vi.mocked(goto).mockClear();
|
|
|
|
const mockGetItemsFn = vi.fn();
|
|
|
|
const mockRepository = {
|
|
getItems: mockGetItemsFn,
|
|
search: vi.fn(),
|
|
};
|
|
|
|
vi.mocked((await import("$lib/stores/auth")).auth.getRepository).mockReturnValue(
|
|
mockRepository as any,
|
|
);
|
|
|
|
// Deliver a null current library for this test only.
|
|
const currentLibrary = vi.mocked((await import("$lib/stores/library")).currentLibrary);
|
|
currentLibrary.subscribe.mockImplementation((fn: any) => {
|
|
fn(null);
|
|
return vi.fn();
|
|
});
|
|
|
|
const config = {
|
|
itemType: "Audio" as const,
|
|
title: "Tracks",
|
|
backPath: "/library/music",
|
|
searchPlaceholder: "Search tracks...",
|
|
sortOptions: [{ key: "SortName", label: "Title" }],
|
|
defaultSort: "SortName",
|
|
displayComponent: "tracklist" as const,
|
|
};
|
|
|
|
render(GenericMediaListPage, {
|
|
props: { config },
|
|
});
|
|
|
|
// With no current library, loadItems bails out to the back path and never
|
|
// queries the repository.
|
|
await waitFor(() => expect(goto).toHaveBeenCalledWith("/library/music"));
|
|
expect(mockGetItemsFn).not.toHaveBeenCalled();
|
|
|
|
// Restore the default (non-null) library for subsequent tests.
|
|
currentLibrary.subscribe.mockImplementation((fn: any) => {
|
|
fn({ id: "lib123", name: "Music" });
|
|
return vi.fn();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Display Component Props", () => {
|
|
it("should support grid display component", () => {
|
|
const config = {
|
|
itemType: "MusicAlbum" as const,
|
|
title: "Albums",
|
|
backPath: "/library/music",
|
|
searchPlaceholder: "Search albums...",
|
|
sortOptions: [{ key: "SortName", label: "Title" }],
|
|
defaultSort: "SortName",
|
|
displayComponent: "grid" as const,
|
|
};
|
|
|
|
const { container } = render(GenericMediaListPage, {
|
|
props: { config },
|
|
});
|
|
|
|
expect(container).toBeTruthy();
|
|
});
|
|
|
|
it("should support tracklist display component", () => {
|
|
const config = {
|
|
itemType: "Audio" as const,
|
|
title: "Tracks",
|
|
backPath: "/library/music",
|
|
searchPlaceholder: "Search tracks...",
|
|
sortOptions: [{ key: "SortName", label: "Title" }],
|
|
defaultSort: "SortName",
|
|
displayComponent: "tracklist" as const,
|
|
};
|
|
|
|
const { container } = render(GenericMediaListPage, {
|
|
props: { config },
|
|
});
|
|
|
|
expect(container).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("Config Simplification", () => {
|
|
it("should not require searchFields in config", () => {
|
|
const config = {
|
|
itemType: "Audio" as const,
|
|
title: "Tracks",
|
|
backPath: "/library/music",
|
|
searchPlaceholder: "Search tracks...",
|
|
sortOptions: [{ key: "SortName", label: "Title" }],
|
|
defaultSort: "SortName",
|
|
displayComponent: "tracklist" as const,
|
|
// Note: searchFields is NOT present
|
|
};
|
|
|
|
// Should render without searchFields
|
|
expect(() => {
|
|
render(GenericMediaListPage, {
|
|
props: { config },
|
|
});
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it("should not require compareFn in sort options", () => {
|
|
const config = {
|
|
itemType: "Audio" as const,
|
|
title: "Tracks",
|
|
backPath: "/library/music",
|
|
searchPlaceholder: "Search tracks...",
|
|
sortOptions: [
|
|
{ key: "SortName", label: "Title" },
|
|
// Note: no compareFn property
|
|
],
|
|
defaultSort: "SortName",
|
|
displayComponent: "tracklist" as const,
|
|
};
|
|
|
|
// Should render without compareFn in sort options
|
|
expect(() => {
|
|
render(GenericMediaListPage, {
|
|
props: { config },
|
|
});
|
|
}).not.toThrow();
|
|
});
|
|
});
|
|
});
|