Files
Psychotoxical-psysonic/src/api/subsonicGenres.test.ts
T
Frank Stellmacher e734a8fc43 feat(genre): play, shuffle and queue buttons on the genre view (#926)
* feat(genre): add paginated songs-by-genre API

Wraps the Subsonic getSongsByGenre endpoint plus a fetchAllSongsByGenre
helper that paginates until exhausted, capped to keep the queue and the
burst of requests bounded for very large genres.

* refactor(playback): extract shared bulk play/shuffle/enqueue helper

A single fetchTracks-driven core (loading flag, empty guard, canonical
shuffleArray) so async detail-page play buttons stop growing divergent
copies. Artist detail now reuses it, dropping its weaker sort-random
shuffle.

* feat(genre): play, shuffle and queue buttons on the genre view

Header buttons load the genre's songs and start ordered or shuffled
playback, or append them to the queue. The slice is bounded to stay
within the queue resolver's cache budget so every row resolves instead
of rendering as a placeholder. Strings added across all nine locales.

* docs(changelog): genre play/shuffle buttons (#926)
2026-05-30 12:59:44 +02:00

51 lines
1.9 KiB
TypeScript

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);
});
});