import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from 'vitest'; import { addPageToCollection, removePageFromCollection, listCollectionPages, getMyCollectionsContainingPage } from './page_collections'; function ok(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } }); } function noContent(): Response { return new Response(null, { status: 204 }); } function pageItemFixture(extra: Record = {}) { return { page_id: 'p1', chapter_id: 'ch1', manga_id: 'm1', page_number: 1, chapter_number: 1, chapter_title: null, manga_title: 'Berserk', storage_key: 'mangas/m1/chapters/ch1/pages/0001.png', added_at: '2026-01-01T00:00:00Z', ...extra }; } describe('page_collections api client', () => { let fetchSpy: MockInstance; beforeEach(() => { fetchSpy = vi.spyOn(globalThis, 'fetch'); }); afterEach(() => { vi.restoreAllMocks(); }); it('listCollectionPages hits the breadcrumb endpoint', async () => { fetchSpy.mockResolvedValueOnce( ok({ items: [pageItemFixture()], page: { limit: 50, offset: 0, total: 1 } }) ); const r = await listCollectionPages('c1'); expect(r.items[0].manga_title).toBe('Berserk'); const url = fetchSpy.mock.calls[0][0] as string; expect(url).toMatch(/\/v1\/collections\/c1\/pages$/); }); it('listCollectionPages forwards pagination params', async () => { fetchSpy.mockResolvedValueOnce( ok({ items: [], page: { limit: 10, offset: 20, total: 0 } }) ); await listCollectionPages('c1', { limit: 10, offset: 20 }); const url = fetchSpy.mock.calls[0][0] as string; expect(url).toContain('limit=10'); expect(url).toContain('offset=20'); }); it('addPageToCollection POSTs { page_id }', async () => { fetchSpy.mockResolvedValueOnce(ok({}, 201)); await addPageToCollection('c1', 'p1'); const init = fetchSpy.mock.calls[0][1] as RequestInit; expect(init.method).toBe('POST'); expect(JSON.parse(init.body as string)).toEqual({ page_id: 'p1' }); }); it('removePageFromCollection DELETEs with both ids encoded', async () => { fetchSpy.mockResolvedValueOnce(noContent()); await removePageFromCollection('c with space', 'p1'); const url = fetchSpy.mock.calls[0][0] as string; // Path includes encoded "c with space" + page id. expect(url).toContain('/v1/collections/c%20with%20space/pages/p1'); const init = fetchSpy.mock.calls[0][1] as RequestInit; expect(init.method).toBe('DELETE'); }); it('getMyCollectionsContainingPage unwraps `collection_ids`', async () => { fetchSpy.mockResolvedValueOnce(ok({ collection_ids: ['c1', 'c2'] })); const ids = await getMyCollectionsContainingPage('p1'); expect(ids).toEqual(['c1', 'c2']); const url = fetchSpy.mock.calls[0][0] as string; expect(url).toMatch(/\/v1\/pages\/p1\/my-collections$/); }); });