feat(web/api): playlists helper + types + query keys (M7 #352 slice 1)
Wire shapes mirror the server's snake_case envelope. URL helpers use the existing apiFetch + ApiError surface so error codes (not_found, not_authorized, bad_request, server_error) propagate via the same copyForCode chain as everything else.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
import {
|
||||
listPlaylists,
|
||||
getPlaylist,
|
||||
createPlaylist,
|
||||
reorderPlaylist,
|
||||
removePlaylistTrack
|
||||
} from './playlists';
|
||||
|
||||
function stubFetch(status: number, body: unknown, init: Partial<Response> = {}) {
|
||||
const res = new Response(
|
||||
body === null ? null : JSON.stringify(body),
|
||||
{ status, headers: { 'Content-Type': 'application/json' }, ...init }
|
||||
);
|
||||
const spy = vi.fn().mockResolvedValue(res);
|
||||
vi.stubGlobal('fetch', spy);
|
||||
return spy;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe('playlists API helper', () => {
|
||||
test('listPlaylists GETs /api/playlists', async () => {
|
||||
const spy = stubFetch(200, { owned: [], public: [] });
|
||||
const r = await listPlaylists();
|
||||
expect(r.owned).toEqual([]);
|
||||
expect(r.public).toEqual([]);
|
||||
const call = spy.mock.calls[0];
|
||||
expect(call[0]).toBe('/api/playlists');
|
||||
expect((call[1] as RequestInit).method).toBe('GET');
|
||||
});
|
||||
|
||||
test('createPlaylist POSTs JSON body', async () => {
|
||||
const spy = stubFetch(200, { id: 'p1', name: 'Test' });
|
||||
const r = await createPlaylist({ name: 'Test' });
|
||||
expect(r.id).toBe('p1');
|
||||
const call = spy.mock.calls[0];
|
||||
expect((call[1] as RequestInit).method).toBe('POST');
|
||||
expect(JSON.parse((call[1] as RequestInit).body as string)).toEqual({ name: 'Test' });
|
||||
});
|
||||
|
||||
test('reorderPlaylist PUTs ordered_positions', async () => {
|
||||
const spy = stubFetch(200, { id: 'p1', tracks: [] });
|
||||
await reorderPlaylist('p1', [2, 1, 0]);
|
||||
const call = spy.mock.calls[0];
|
||||
expect(call[0]).toBe('/api/playlists/p1/tracks');
|
||||
expect((call[1] as RequestInit).method).toBe('PUT');
|
||||
expect(JSON.parse((call[1] as RequestInit).body as string)).toEqual({
|
||||
ordered_positions: [2, 1, 0]
|
||||
});
|
||||
});
|
||||
|
||||
test('removePlaylistTrack DELETEs by position', async () => {
|
||||
const spy = stubFetch(200, { id: 'p1', tracks: [] });
|
||||
await removePlaylistTrack('p1', 3);
|
||||
const call = spy.mock.calls[0];
|
||||
expect(call[0]).toBe('/api/playlists/p1/tracks/3');
|
||||
expect((call[1] as RequestInit).method).toBe('DELETE');
|
||||
});
|
||||
|
||||
test('not_found surfaces as ApiError', async () => {
|
||||
stubFetch(404, { error: 'not_found' });
|
||||
await expect(getPlaylist('missing')).rejects.toMatchObject({ code: 'not_found' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user