import { test, expect, type Page } from '@playwright/test'; // E2E for the admin Analysis section: coverage overview + badges, drill // manga → chapter → page, the page-detail modal, and the enqueue actions. // 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 pageDone = 'p1111111-1111-1111-1111-111111111111'; const pageNone = 'p2222222-2222-2222-2222-222222222222'; 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: [] }; type Captured = { reenqueue: Record | null; analyzeCalls: number }; async function mockAdmin(page: Page, cap: Captured) { await page.route('**/api/v1/auth/config', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ self_register_enabled: true, private_mode: false }) }) ); await page.route('**/api/v1/auth/me', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ user: adminUser }) }) ); await page.route('**/api/v1/auth/me/preferences', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ reader_mode: 'single', reader_page_gap: 'small' }) }) ); await page.route('**/api/v1/me/bookmarks*', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [], page: { limit: 50, offset: 0, total: 0 } }) }) ); await page.route('**/api/v1/admin/system', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(systemStats) }) ); // Coverage overview. Registered before the more specific routes below // so the later-registered (more specific) globs win for their URLs. await page.route('**/api/v1/admin/analysis/mangas**', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [ { manga_id: mangaId, title: 'Berserk', total_pages: 2, analyzed_pages: 1 } ], page: { limit: 25, offset: 0, total: 1 } }) }) ); await page.route('**/api/v1/admin/analysis/mangas/*/chapters', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [ { chapter_id: chapterId, number: 1, title: 'The Brand', total_pages: 2, analyzed_pages: 1 } ] }) }) ); await page.route('**/api/v1/admin/analysis/chapters/*/pages', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [ { page_id: pageDone, page_number: 1, status: 'done' }, { page_id: pageNone, page_number: 2, status: 'none' } ] }) }) ); await page.route(`**/api/v1/admin/analysis/pages/${pageDone}`, (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ page_id: pageDone, page_number: 1, chapter_id: chapterId, manga_id: mangaId, status: 'done', is_nsfw: true, scene_description: 'A rainy street at night.', model: 'test-model', error: null, analyzed_at: '2026-06-13T12:00:00Z', ocr: [{ kind: 'speech', text: 'Hello there' }], tags: ['action', 'city'], content_warnings: ['gore'] }) }) ); await page.route(`**/api/v1/admin/analysis/pages/${pageNone}`, (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ page_id: pageNone, page_number: 2, chapter_id: chapterId, manga_id: mangaId, status: 'none', is_nsfw: false, scene_description: null, model: null, error: null, analyzed_at: null, ocr: [], tags: [], content_warnings: [] }) }) ); await page.route('**/api/v1/admin/analysis/reenqueue', (r) => { cap.reenqueue = JSON.parse(r.request().postData() ?? '{}'); return r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ enqueued: 12 }) }); }); await page.route(`**/api/v1/admin/pages/${pageNone}/analyze`, (r) => { cap.analyzeCalls += 1; return r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ enqueued: true }) }); }); } test.describe('/admin/analysis', () => { test('overview shows coverage badges and queues the whole library', async ({ page }) => { const cap: Captured = { reenqueue: null, analyzeCalls: 0 }; await mockAdmin(page, cap); await page.setViewportSize(DESKTOP); await page.goto('/admin/analysis'); const badge = page.getByTestId(`admin-analysis-coverage-manga-${mangaId}`); await expect(badge).toBeVisible(); await expect(badge).toContainText('Partial 1/2'); await page.getByTestId('admin-analysis-enqueue-library').click(); await expect(page.getByTestId('admin-analysis-notice')).toContainText( 'Enqueued 12 pages' ); expect(cap.reenqueue).toEqual({ only_unanalyzed: true }); }); test('drill manga → chapter → page and inspect the result', async ({ page }) => { const cap: Captured = { reenqueue: null, analyzeCalls: 0 }; await mockAdmin(page, cap); await page.setViewportSize(DESKTOP); await page.goto('/admin/analysis'); await page.getByTestId(`admin-analysis-expand-${mangaId}`).click(); await expect( page.getByTestId(`admin-analysis-coverage-chapter-${chapterId}`) ).toContainText('Partial 1/2'); await page.getByTestId(`admin-analysis-expand-chapter-${chapterId}`).click(); const doneChip = page.getByTestId(`admin-analysis-page-${pageDone}`); await expect(doneChip).toHaveAttribute('data-status', 'done'); await doneChip.click(); const modal = page.getByTestId('admin-analysis-detail'); await expect(modal).toBeVisible(); await expect( page.getByTestId('admin-analysis-detail-status') ).toContainText('Analyzed'); await expect(modal).toContainText('Hello there'); await expect(modal).toContainText('action'); await expect(page.getByTestId('admin-analysis-detail-warnings')).toContainText( 'gore' ); }); test('queue an unanalyzed page from its detail modal', async ({ page }) => { const cap: Captured = { reenqueue: null, analyzeCalls: 0 }; await mockAdmin(page, cap); await page.setViewportSize(DESKTOP); await page.goto('/admin/analysis'); await page.getByTestId(`admin-analysis-expand-${mangaId}`).click(); await page.getByTestId(`admin-analysis-expand-chapter-${chapterId}`).click(); await page.getByTestId(`admin-analysis-page-${pageNone}`).click(); await expect(page.getByTestId('admin-analysis-detail-status')).toContainText( 'Not analyzed' ); await page.getByTestId('admin-analysis-detail-queue').click(); await expect(page.getByTestId('admin-analysis-notice')).toContainText( 'Queued page 2' ); expect(cap.analyzeCalls).toBe(1); }); });