Files
Mangalord/frontend/e2e/admin-analysis.spec.ts
MechaCat02 7e675b72cc feat(analysis): admin analysis section uses manga search + chapter picker
Replaces the raw manga-id / chapter-id UUID inputs with a real picker:

- Search a manga (debounced, reuses listAdminMangas) → result rows show
  title + chapter count; "Queue manga" enqueues the whole manga.
- Expand a result to load its chapters (listAdminChapters) and "Queue
  chapter" enqueues a single chapter.
- Keeps the "Queue all (whole library)" action and a single global
  "include already-analyzed" toggle applied to every action; per-action
  busy state + a result message naming what was queued.

Tests: rewrote the Playwright spec to drive the search → manga/chapter
flow (whole library, search+queue manga, expand+queue chapter with
include-analyzed). svelte-check + build clean.

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

172 lines
5.6 KiB
TypeScript

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<string, unknown> | 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
});
});
});