diff --git a/web/src/lib/api/history.test.ts b/web/src/lib/api/history.test.ts new file mode 100644 index 00000000..47fabccf --- /dev/null +++ b/web/src/lib/api/history.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + fetchHistoryPage, + getNextPageParam, + HISTORY_PAGE_SIZE, + type HistoryResponse +} from './history'; + +vi.mock('./client', () => ({ + api: { + get: vi.fn() + } +})); + +import { api } from './client'; + +describe('history API helpers', () => { + beforeEach(() => { + vi.mocked(api.get).mockReset(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('HISTORY_PAGE_SIZE is 50', () => { + expect(HISTORY_PAGE_SIZE).toBe(50); + }); + + it('fetchHistoryPage hits /api/me/history?offset=N&limit=50', async () => { + vi.mocked(api.get).mockResolvedValue({ events: [], has_more: false }); + await fetchHistoryPage(50); + expect(api.get).toHaveBeenCalledWith('/api/me/history?offset=50&limit=50'); + }); + + it('fetchHistoryPage uses offset=0 by default', async () => { + vi.mocked(api.get).mockResolvedValue({ events: [], has_more: false }); + await fetchHistoryPage(); + expect(api.get).toHaveBeenCalledWith('/api/me/history?offset=0&limit=50'); + }); + + it('getNextPageParam returns next offset when has_more is true', () => { + const page: HistoryResponse = { events: [], has_more: true }; + expect(getNextPageParam(page, [], 0)).toBe(50); + expect(getNextPageParam(page, [], 50)).toBe(100); + }); + + it('getNextPageParam returns undefined when has_more is false', () => { + const page: HistoryResponse = { events: [], has_more: false }; + expect(getNextPageParam(page, [], 50)).toBeUndefined(); + }); +}); diff --git a/web/src/lib/api/history.ts b/web/src/lib/api/history.ts new file mode 100644 index 00000000..0b66f7d6 --- /dev/null +++ b/web/src/lib/api/history.ts @@ -0,0 +1,43 @@ +import { createInfiniteQuery } from '@tanstack/svelte-query'; +import { api } from './client'; +import { qk } from './queries'; +import type { TrackRef } from './types'; + +export const HISTORY_PAGE_SIZE = 50; + +export type HistoryEvent = { + id: string; + played_at: string; // RFC3339 + track: TrackRef; +}; + +export type HistoryResponse = { + events: HistoryEvent[]; + has_more: boolean; +}; + +// Exposed for direct testability (the createInfiniteQuery wrapper is harder +// to drive in unit tests without a full QueryClient harness). +export async function fetchHistoryPage(offset = 0): Promise { + return api.get( + `/api/me/history?offset=${offset}&limit=${HISTORY_PAGE_SIZE}` + ); +} + +export function getNextPageParam( + lastPage: HistoryResponse, + _allPages: HistoryResponse[], + lastPageParam: number +): number | undefined { + return lastPage.has_more ? lastPageParam + HISTORY_PAGE_SIZE : undefined; +} + +export function createHistoryQuery() { + return createInfiniteQuery({ + queryKey: qk.history(), + queryFn: ({ pageParam }) => fetchHistoryPage(pageParam as number), + initialPageParam: 0, + getNextPageParam: (lastPage, allPages, lastPageParam) => + getNextPageParam(lastPage, allPages, lastPageParam as number) + }); +} diff --git a/web/src/lib/api/queries.ts b/web/src/lib/api/queries.ts index 08d03597..bbf5b421 100644 --- a/web/src/lib/api/queries.ts +++ b/web/src/lib/api/queries.ts @@ -19,6 +19,7 @@ export const qk = { likedTracks: () => ['likedTracks'] as const, likedAlbums: () => ['likedAlbums'] as const, likedArtists: () => ['likedArtists'] as const, + history: () => ['history'] as const, // Lidarr / requests / admin. lidarrSearch: (q: string, kind: string) => ['lidarrSearch', { q, kind }] as const,