Files
Mangalord/frontend/src/lib/components/AddToCollectionModal.svelte.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

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();
});
});