import { beforeEach, describe, expect, it, vi } from 'vitest'; import { copyBlock, createBlock, createBlockGroup, deleteBlock, deleteBlockGroup, getBlock, getBlockGroups, getBlockItems, getBlockItemsWithMeta, getBlocks, previewBlock, replaceBlock, searchCollections, searchSmartCollections, searchTelevisionSeasons, searchTelevisionShows, type ReplaceBlockRequest } from './blocks'; function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { headers: { 'Content-Type': 'application/json' }, status }); } function noContent(): Response { return new Response(null, { status: 204 }); } const sampleReplace: ReplaceBlockRequest = { name: 'Morning', minutes: 60, stopScheduling: 'AfterDurationEnd', items: [ { collectionType: 'SearchQuery', collectionId: null, multiCollectionId: null, smartCollectionId: null, mediaItemId: null, searchTitle: 'News', searchQuery: 'news', playbackOrder: 'Shuffle', includeInProgramGuide: true, disableWatermarks: false, watermarkIds: [], graphicsElementIds: [] } ] }; describe('blocks api client', () => { beforeEach(() => { window.localStorage.clear(); vi.restoreAllMocks(); }); it('getBlockGroups fetches all block groups', async () => { const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([{ id: 1, name: 'Prime' }])); await expect(getBlockGroups()).resolves.toHaveLength(1); expect(fetchMock).toHaveBeenCalledWith('/api/blocks/groups', expect.objectContaining({ method: 'GET' })); }); it('createBlockGroup POSTs the name', async () => { const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 3, name: 'Prime' }, 201)); await createBlockGroup({ name: 'Prime' }); const [url, init] = fetchMock.mock.calls[0]; expect(url).toBe('/api/blocks/groups'); expect(init).toMatchObject({ method: 'POST' }); expect(JSON.parse(String(init?.body)).name).toBe('Prime'); }); it('deleteBlockGroup DELETEs by id', async () => { const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent()); await expect(deleteBlockGroup(3)).resolves.toBeUndefined(); expect(fetchMock).toHaveBeenCalledWith('/api/blocks/groups/3', expect.objectContaining({ method: 'DELETE' })); }); it('getBlocks fetches all blocks', async () => { const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([])); await getBlocks(); expect(fetchMock).toHaveBeenCalledWith('/api/blocks', expect.objectContaining({ method: 'GET' })); }); it('getBlock fetches a block by id', async () => { const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 4 })); await getBlock(4); expect(fetchMock).toHaveBeenCalledWith('/api/blocks/4', expect.objectContaining({ method: 'GET' })); }); it('createBlock POSTs group id and name', async () => { const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 8 }, 201)); await createBlock({ blockGroupId: 2, name: 'Morning' }); const [url, init] = fetchMock.mock.calls[0]; expect(url).toBe('/api/blocks'); expect(init).toMatchObject({ method: 'POST' }); expect(JSON.parse(String(init?.body))).toMatchObject({ blockGroupId: 2, name: 'Morning' }); }); it('deleteBlock DELETEs by id', async () => { const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent()); await deleteBlock(4); expect(fetchMock).toHaveBeenCalledWith('/api/blocks/4', expect.objectContaining({ method: 'DELETE' })); }); it('getBlockItems fetches items for a block', async () => { const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([])); await getBlockItems(4); expect(fetchMock).toHaveBeenCalledWith('/api/blocks/4/items', expect.objectContaining({ method: 'GET' })); }); it('replaceBlock PUTs the full block body', async () => { const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 4, items: [] })); await replaceBlock(4, sampleReplace); const [url, init] = fetchMock.mock.calls[0]; expect(url).toBe('/api/blocks/4'); expect(init).toMatchObject({ method: 'PUT' }); const body = JSON.parse(String(init?.body)); expect(body.minutes).toBe(60); expect(body.items).toHaveLength(1); }); it('getBlockItemsWithMeta returns the items and the ETag', async () => { vi.spyOn(window, 'fetch').mockResolvedValue( new Response(JSON.stringify([{ id: 1 }]), { headers: { ETag: '"5"', 'Content-Type': 'application/json' }, status: 200 }) ); const result = await getBlockItemsWithMeta(4); expect(result.etag).toBe('"5"'); expect(result.data).toHaveLength(1); }); it('replaceBlock sends If-Match when an ETag is supplied and returns the new ETag', async () => { const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue( new Response(JSON.stringify({ id: 4, items: [] }), { headers: { ETag: '"6"', 'Content-Type': 'application/json' }, status: 200 }) ); const result = await replaceBlock(4, sampleReplace, '"5"'); const [, init] = fetchMock.mock.calls[0]; expect(init?.headers).toMatchObject({ 'If-Match': '"5"' }); expect(result.etag).toBe('"6"'); }); it('replaceBlock omits If-Match when no ETag is supplied', async () => { const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 4, items: [] })); await replaceBlock(4, sampleReplace); const [, init] = fetchMock.mock.calls[0]; expect(init?.headers).not.toHaveProperty('If-Match'); }); it('previewBlock POSTs to the preview route', async () => { const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([])); await previewBlock(4, sampleReplace); const [url, init] = fetchMock.mock.calls[0]; expect(url).toBe('/api/blocks/4/preview'); expect(init).toMatchObject({ method: 'POST' }); }); it('copyBlock POSTs the target group and name to the copy route', async () => { const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 9 }, 201)); await copyBlock(4, { blockGroupId: 3, name: 'Morning Copy' }); const [url, init] = fetchMock.mock.calls[0]; expect(url).toBe('/api/blocks/4/copy'); expect(init).toMatchObject({ method: 'POST' }); expect(JSON.parse(String(init?.body))).toMatchObject({ blockGroupId: 3, name: 'Morning Copy' }); }); it('search pickers encode the query and hit the right routes', async () => { const fetchMock = vi.spyOn(window, 'fetch').mockImplementation(() => Promise.resolve(jsonResponse([]))); await searchCollections('a b'); await searchTelevisionShows('show'); await searchTelevisionSeasons('season'); await searchSmartCollections('smart'); expect(fetchMock.mock.calls.map((c) => c[0])).toEqual([ '/api/search/collections?query=a%20b', '/api/search/television-shows?query=show', '/api/search/television-seasons?query=season', '/api/search/smart-collections?query=smart' ]); }); });