import { test, expect, type Page } from '@playwright/test'; // E2E for the admin Analysis section: enqueue the whole library, or // search a manga and enqueue it / one of its chapters, with the // include-already-analyzed toggle. Fully mocked. const DESKTOP = { width: 1280, height: 720 } as const; const mangaId = 'a9999999-9999-9999-9999-999999999999'; const chapterId = 'c9999999-9999-9999-9999-999999999999'; const adminUser = { id: 'u11111111-1111-1111-1111-111111111111', username: 'admin', created_at: '2026-01-01T00:00:00Z', is_admin: true }; const systemStats = { disk: null, memory: { total_bytes: 1, used_bytes: 0, percent_used: 0 }, cpu: { percent_used: 0 }, alerts: [] }; const mangaRow = { id: mangaId, title: 'Berserk', status: 'ongoing', cover_image_path: null, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', sync_state: 'synced', chapter_count: 1, latest_seen_at: null }; const chapterRow = { id: chapterId, manga_id: mangaId, number: 1, title: 'The Brand', page_count: 20, created_at: '2026-01-01T00:00:00Z', sync_state: 'synced', latest_seen_at: null }; type Captured = { body: Record | null }; async function mockAdmin(page: Page, captured: Captured) { await page.route('**/api/v1/auth/config', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ self_register_enabled: true, private_mode: false }) }) ); await page.route('**/api/v1/auth/me', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ user: adminUser }) }) ); await page.route('**/api/v1/auth/me/preferences', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ reader_mode: 'single', reader_page_gap: 'small' }) }) ); await page.route('**/api/v1/me/bookmarks*', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [], page: { limit: 50, offset: 0, total: 0 } }) }) ); await page.route('**/api/v1/admin/system', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(systemStats) }) ); // Manga search. Registered before the chapters route so the more // specific chapters glob (added later) wins for chapter URLs. await page.route('**/api/v1/admin/mangas**', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [mangaRow], page: { limit: 20, offset: 0, total: 1 } }) }) ); await page.route('**/api/v1/admin/mangas/*/chapters**', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [chapterRow], page: { limit: 500, offset: 0, total: 1 } }) }) ); await page.route('**/api/v1/admin/analysis/reenqueue', (route) => { captured.body = JSON.parse(route.request().postData() ?? '{}'); return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ enqueued: 12 }) }); }); } test.describe('/admin/analysis', () => { test('queue the whole library posts only_unanalyzed=true', async ({ page }) => { const captured: Captured = { body: null }; await mockAdmin(page, captured); await page.setViewportSize(DESKTOP); await page.goto('/admin/analysis'); await page.getByTestId('admin-analysis-enqueue-library').click(); await expect(page.getByTestId('admin-analysis-notice')).toContainText( 'Enqueued 12 pages' ); expect(captured.body).toEqual({ only_unanalyzed: true }); }); test('search a manga and queue it', async ({ page }) => { const captured: Captured = { body: null }; await mockAdmin(page, captured); await page.setViewportSize(DESKTOP); await page.goto('/admin/analysis'); await page.getByTestId('admin-analysis-search').fill('ber'); await expect( page.getByTestId(`admin-analysis-manga-${mangaId}`) ).toBeVisible(); await page.getByTestId(`admin-analysis-enqueue-manga-${mangaId}`).click(); await expect(page.getByTestId('admin-analysis-notice')).toContainText( 'Berserk' ); expect(captured.body).toEqual({ only_unanalyzed: true, manga_id: mangaId }); }); test('expand chapters and queue one with include-analyzed on', async ({ page }) => { const captured: Captured = { body: null }; await mockAdmin(page, captured); await page.setViewportSize(DESKTOP); await page.goto('/admin/analysis'); await page.getByTestId('admin-analysis-include-analyzed').check(); await page.getByTestId('admin-analysis-search').fill('ber'); await page.getByTestId(`admin-analysis-expand-${mangaId}`).click(); await page.getByTestId(`admin-analysis-enqueue-chapter-${chapterId}`).click(); await expect(page.getByTestId('admin-analysis-notice')).toBeVisible(); expect(captured.body).toEqual({ only_unanalyzed: false, chapter_id: chapterId }); }); });