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>
101 lines
3.3 KiB
TypeScript
101 lines
3.3 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import { render, screen, cleanup } from '@testing-library/svelte';
|
|
import { tick } from 'svelte';
|
|
import AddToCollectionModal from './AddToCollectionModal.svelte';
|
|
|
|
// Stub the API modules at the import-graph level so the modal exercises
|
|
// its dispatch (manga vs page) without hitting fetch.
|
|
const collectionsApi = vi.hoisted(() => ({
|
|
listMyCollections: vi.fn(),
|
|
getMyCollectionsContaining: vi.fn(),
|
|
addMangaToCollection: vi.fn(),
|
|
removeMangaFromCollection: vi.fn(),
|
|
createCollection: vi.fn()
|
|
}));
|
|
const pageCollectionsApi = vi.hoisted(() => ({
|
|
getMyCollectionsContainingPage: vi.fn(),
|
|
addPageToCollection: vi.fn(),
|
|
removePageFromCollection: vi.fn()
|
|
}));
|
|
|
|
vi.mock('$lib/api/collections', () => collectionsApi);
|
|
vi.mock('$lib/api/page_collections', () => pageCollectionsApi);
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
function summary(id: string, name: string, containing: string[] = []) {
|
|
return {
|
|
id,
|
|
user_id: 'u1',
|
|
name,
|
|
description: null,
|
|
created_at: '2026-01-01T00:00:00Z',
|
|
updated_at: '2026-01-01T00:00:00Z',
|
|
manga_count: 0,
|
|
sample_covers: [],
|
|
_containing: containing
|
|
};
|
|
}
|
|
|
|
describe('AddToCollectionModal', () => {
|
|
it('manga target loads via getMyCollectionsContaining', async () => {
|
|
collectionsApi.listMyCollections.mockResolvedValueOnce({
|
|
items: [summary('c1', 'Favorites')],
|
|
page: { limit: 200, offset: 0, total: 1 }
|
|
});
|
|
collectionsApi.getMyCollectionsContaining.mockResolvedValueOnce(['c1']);
|
|
|
|
render(AddToCollectionModal, {
|
|
props: {
|
|
open: true,
|
|
target: { kind: 'manga', id: 'm1' },
|
|
onClose: () => {}
|
|
}
|
|
});
|
|
// Wait for the $effect → load() → await chain to settle.
|
|
await tick();
|
|
await Promise.resolve();
|
|
await tick();
|
|
|
|
expect(collectionsApi.getMyCollectionsContaining).toHaveBeenCalledWith('m1');
|
|
expect(pageCollectionsApi.getMyCollectionsContainingPage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('page target loads via getMyCollectionsContainingPage', async () => {
|
|
collectionsApi.listMyCollections.mockResolvedValueOnce({
|
|
items: [summary('c1', 'Favorite panels')],
|
|
page: { limit: 200, offset: 0, total: 1 }
|
|
});
|
|
pageCollectionsApi.getMyCollectionsContainingPage.mockResolvedValueOnce([]);
|
|
|
|
render(AddToCollectionModal, {
|
|
props: {
|
|
open: true,
|
|
target: { kind: 'page', id: 'p1' },
|
|
onClose: () => {}
|
|
}
|
|
});
|
|
await tick();
|
|
await Promise.resolve();
|
|
await tick();
|
|
|
|
expect(pageCollectionsApi.getMyCollectionsContainingPage).toHaveBeenCalledWith('p1');
|
|
expect(collectionsApi.getMyCollectionsContaining).not.toHaveBeenCalled();
|
|
expect(screen.getByText('Favorite panels')).toBeTruthy();
|
|
});
|
|
|
|
it('does not load when closed', () => {
|
|
render(AddToCollectionModal, {
|
|
props: {
|
|
open: false,
|
|
target: { kind: 'manga', id: 'm1' },
|
|
onClose: () => {}
|
|
}
|
|
});
|
|
expect(collectionsApi.listMyCollections).not.toHaveBeenCalled();
|
|
});
|
|
});
|