import { beforeEach, describe, expect, it, vi } from 'vitest'; import { fetchAllSongsByGenre, getSongsByGenre } from './subsonicGenres'; import type { SubsonicSong } from './subsonicTypes'; const { apiMock } = vi.hoisted(() => ({ apiMock: vi.fn() })); vi.mock('./subsonicClient', () => ({ api: apiMock, libraryFilterParams: () => ({}), })); function songs(n: number, startId = 0): SubsonicSong[] { return Array.from({ length: n }, (_, i) => ({ id: String(startId + i), title: `t${startId + i}` }) as SubsonicSong); } describe('getSongsByGenre', () => { beforeEach(() => apiMock.mockReset()); it('normalizes a single-song response into an array', async () => { apiMock.mockResolvedValue({ songsByGenre: { song: { id: '1', title: 'only' } } }); expect(await getSongsByGenre('Metal')).toEqual([{ id: '1', title: 'only' }]); }); it('returns an empty array when the genre has no songs', async () => { apiMock.mockResolvedValue({ songsByGenre: {} }); expect(await getSongsByGenre('Empty')).toEqual([]); }); }); describe('fetchAllSongsByGenre', () => { beforeEach(() => apiMock.mockReset()); it('paginates until a short page signals the end', async () => { apiMock .mockResolvedValueOnce({ songsByGenre: { song: songs(500) } }) .mockResolvedValueOnce({ songsByGenre: { song: songs(30, 500) } }); const all = await fetchAllSongsByGenre('Metal'); expect(all).toHaveLength(530); expect(apiMock).toHaveBeenCalledTimes(2); expect(apiMock.mock.calls[0][1]).toMatchObject({ offset: 0, count: 500 }); expect(apiMock.mock.calls[1][1]).toMatchObject({ offset: 500 }); }); it('stops at the cap without fetching further pages', async () => { apiMock.mockResolvedValue({ songsByGenre: { song: songs(500) } }); const all = await fetchAllSongsByGenre('Huge', 10); expect(all).toHaveLength(10); expect(apiMock).toHaveBeenCalledTimes(1); }); });