🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m29s
Traceability Validation / Check Requirement Traces (push) Successful in 19s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 5m4s
Build & Release / Run Tests (push) Successful in 4m52s
Build & Release / Build Linux (push) Successful in 17m55s
Build & Release / Build Android (push) Successful in 22m13s
Build & Release / Create Release (push) Successful in 13s
711 lines
21 KiB
TypeScript
711 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();
|
|
});
|
|
});
|
|
});
|