Layout and search fix
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 2m4s
🏗️ Build and Test JellyTau / Android Compile Check (push) Has been skipped
Traceability Validation / Check Requirement Traces (push) Successful in 23s
Build & Release / Run Tests (push) Failing after 2m45s
Build & Release / Build Linux (push) Has been skipped
Build & Release / Build Android (push) Has been skipped
Build & Release / Create Release (push) Has been skipped
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 2m4s
🏗️ Build and Test JellyTau / Android Compile Check (push) Has been skipped
Traceability Validation / Check Requirement Traces (push) Successful in 23s
Build & Release / Run Tests (push) Failing after 2m45s
Build & Release / Build Linux (push) Has been skipped
Build & Release / Build Android (push) Has been skipped
Build & Release / Create Release (push) Has been skipped
This commit is contained in:
@@ -32,7 +32,12 @@ vi.mock("$lib/composables/useServerReachabilityReload", () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
describe.skip("GenericMediaListPage", () => {
|
||||
// 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();
|
||||
@@ -66,6 +71,16 @@ describe.skip("GenericMediaListPage", () => {
|
||||
});
|
||||
|
||||
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",
|
||||
@@ -80,9 +95,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
props: { config },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// loadItems should have been called
|
||||
});
|
||||
await waitFor(() => expect(mockGetItemsFn).toHaveBeenCalledWith("lib123", expect.anything()));
|
||||
});
|
||||
|
||||
it("should display sort options", () => {
|
||||
@@ -111,8 +124,14 @@ describe.skip("GenericMediaListPage", () => {
|
||||
});
|
||||
|
||||
describe("Search Functionality", () => {
|
||||
it("should debounce search input for 300ms", async () => {
|
||||
vi.useFakeTimers();
|
||||
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,
|
||||
@@ -128,29 +147,33 @@ describe.skip("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;
|
||||
|
||||
// Type into search
|
||||
fireEvent.input(searchInput, { target: { value: "t" } });
|
||||
expect(searchInput.value).toBe("t");
|
||||
|
||||
// Search should not trigger immediately
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
// Add more characters
|
||||
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" } });
|
||||
|
||||
// Still shouldn't trigger (only 100ms passed total)
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
// Now advance to 300ms total - search should trigger
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
await waitFor(() => {
|
||||
// Search should have been debounced
|
||||
});
|
||||
// 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 () => {
|
||||
@@ -159,8 +182,13 @@ describe.skip("GenericMediaListPage", () => {
|
||||
totalRecordCount: 1,
|
||||
});
|
||||
|
||||
const mockGetItemsFn = vi.fn().mockResolvedValue({
|
||||
items: [],
|
||||
totalRecordCount: 0,
|
||||
});
|
||||
|
||||
const mockRepository = {
|
||||
getItems: vi.fn(),
|
||||
getItems: mockGetItemsFn,
|
||||
search: mockSearchFn,
|
||||
};
|
||||
|
||||
@@ -178,25 +206,27 @@ describe.skip("GenericMediaListPage", () => {
|
||||
displayComponent: "tracklist" as const,
|
||||
};
|
||||
|
||||
vi.useFakeTimers();
|
||||
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" } });
|
||||
|
||||
// Advance timer to trigger debounced search
|
||||
vi.advanceTimersByTime(300);
|
||||
|
||||
// search() is called as search(query, options, requestId).
|
||||
await waitFor(() => {
|
||||
expect(mockSearchFn).toHaveBeenCalledWith("test", expect.objectContaining({
|
||||
includeItemTypes: ["Audio"],
|
||||
limit: 10000,
|
||||
}));
|
||||
expect(mockSearchFn).toHaveBeenCalledWith(
|
||||
"test",
|
||||
expect.objectContaining({
|
||||
includeItemTypes: ["Audio"],
|
||||
limit: 10000,
|
||||
}),
|
||||
expect.any(Number)
|
||||
);
|
||||
});
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should use getItems without search for empty query", async () => {
|
||||
@@ -416,15 +446,18 @@ describe.skip("GenericMediaListPage", () => {
|
||||
});
|
||||
|
||||
it("should include correct itemType in search request", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const mockSearchFn = vi.fn().mockResolvedValue({
|
||||
items: [],
|
||||
totalRecordCount: 0,
|
||||
});
|
||||
|
||||
const mockGetItemsFn = vi.fn().mockResolvedValue({
|
||||
items: [],
|
||||
totalRecordCount: 0,
|
||||
});
|
||||
|
||||
const mockRepository = {
|
||||
getItems: vi.fn(),
|
||||
getItems: mockGetItemsFn,
|
||||
search: mockSearchFn,
|
||||
};
|
||||
|
||||
@@ -446,17 +479,20 @@ describe.skip("GenericMediaListPage", () => {
|
||||
props: { config },
|
||||
});
|
||||
|
||||
await waitFor(() => expect(mockGetItemsFn).toHaveBeenCalled());
|
||||
|
||||
const searchInput = container.querySelector("input") as HTMLInputElement;
|
||||
fireEvent.input(searchInput, { target: { value: "album" } });
|
||||
vi.advanceTimersByTime(300);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearchFn).toHaveBeenCalledWith("album", expect.objectContaining({
|
||||
includeItemTypes: ["MusicAlbum"],
|
||||
}));
|
||||
expect(mockSearchFn).toHaveBeenCalledWith(
|
||||
"album",
|
||||
expect.objectContaining({
|
||||
includeItemTypes: ["MusicAlbum"],
|
||||
}),
|
||||
expect.any(Number)
|
||||
);
|
||||
});
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -536,6 +572,7 @@ describe.skip("GenericMediaListPage", () => {
|
||||
|
||||
it("should handle missing library gracefully", async () => {
|
||||
const { goto } = await import("$app/navigation");
|
||||
vi.mocked(goto).mockClear();
|
||||
|
||||
const mockGetItemsFn = vi.fn();
|
||||
|
||||
@@ -548,14 +585,12 @@ describe.skip("GenericMediaListPage", () => {
|
||||
mockRepository as any
|
||||
);
|
||||
|
||||
// Mock currentLibrary to return null
|
||||
vi.resetModules();
|
||||
vi.mocked((await import("$lib/stores/library")).currentLibrary.subscribe).mockImplementation(
|
||||
(fn: any) => {
|
||||
fn(null);
|
||||
return vi.fn();
|
||||
}
|
||||
);
|
||||
// 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,
|
||||
@@ -571,9 +606,15 @@ describe.skip("GenericMediaListPage", () => {
|
||||
props: { config },
|
||||
});
|
||||
|
||||
// Should navigate to back path when library is missing
|
||||
await waitFor(() => {
|
||||
// goto would be called with config.backPath
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user