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>
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
// E2E for the admin Analysis section: scoped re-enqueue (all / manga /
|
||||
// chapter) with the include-already-analyzed toggle. Fully mocked.
|
||||
// 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',
|
||||
@@ -19,6 +23,29 @@ const systemStats = {
|
||||
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) {
|
||||
@@ -57,6 +84,28 @@ async function mockAdmin(page: Page, captured: Captured) {
|
||||
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({
|
||||
@@ -68,16 +117,13 @@ async function mockAdmin(page: Page, captured: Captured) {
|
||||
}
|
||||
|
||||
test.describe('/admin/analysis', () => {
|
||||
test('default whole-library re-enqueue posts only_unanalyzed=true', async ({
|
||||
page
|
||||
}) => {
|
||||
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 expect(page.getByTestId('admin-analysis-form')).toBeVisible();
|
||||
await page.getByTestId('admin-analysis-submit').click();
|
||||
await page.getByTestId('admin-analysis-enqueue-library').click();
|
||||
|
||||
await expect(page.getByTestId('admin-analysis-notice')).toContainText(
|
||||
'Enqueued 12 pages'
|
||||
@@ -85,40 +131,41 @@ test.describe('/admin/analysis', () => {
|
||||
expect(captured.body).toEqual({ only_unanalyzed: true });
|
||||
});
|
||||
|
||||
test('manga scope + include-analyzed posts manga_id and only_unanalyzed=false', async ({
|
||||
page
|
||||
}) => {
|
||||
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-scope-manga').check();
|
||||
await page
|
||||
.getByTestId('admin-analysis-manga-id')
|
||||
.fill('a9999999-9999-9999-9999-999999999999');
|
||||
await page.getByTestId('admin-analysis-include-analyzed').check();
|
||||
await page.getByTestId('admin-analysis-submit').click();
|
||||
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,
|
||||
manga_id: 'a9999999-9999-9999-9999-999999999999'
|
||||
chapter_id: chapterId
|
||||
});
|
||||
});
|
||||
|
||||
test('chapter scope requires an id before posting', 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-scope-chapter').check();
|
||||
await page.getByTestId('admin-analysis-submit').click();
|
||||
|
||||
await expect(page.getByTestId('admin-analysis-error')).toContainText(
|
||||
'Enter a chapter id'
|
||||
);
|
||||
expect(captured.body).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user