Files
Mangalord/frontend/src/lib/api/page_collections.test.ts
MechaCat02 6c901e64c9 feat(search): tag-based page search surface + per-page tags & collections
Add the /search surface (Pages / Chapters / Mangas tabs) backed by
per-user page tags and per-page collections: schema (migration 0023),
backend endpoints for page tags/collections and tagged-page aggregations
(with the OCR text-search param reserved at 501), plus the frontend API
clients, library Page-tags tab, collection page sections, page context
menu / AddTagsSheet, and reader long-press wiring. Includes the
continuous-reader navigation fixes (?page=N handling, chapter-reset
timing, back-button pops history) and tag-normalization hardening
accumulated on the branch.

Bump version 0.60.2 -> 0.62.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 15:51:38 +02:00

102 lines
3.3 KiB
TypeScript

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<string, unknown> = {}) {
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<typeof globalThis.fetch>;
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$/);
});
});