feat(search): frontend content search (text + warnings) + manga warning banner
Surfaces the analysis pipeline in the UI: - api clients: searchPages() + PageSearchItem/ContentWarning types; listMangas cwInclude/cwExclude; MangaDetail.content_warnings. - /search gains a content-search mode: free-text over OCR+scene and tri-state content-warning toggles (include/exclude), driven by URL params (text, cw_include, cw_exclude) and the new /me/page-search endpoint. Tag browsing is preserved when no content filter is active. - manga detail renders a deduped content-warning banner. Tests: vitest param serialization (searchPages CSV/text/cw, listMangas cw); Playwright text-search + cw-toggle flows (route-mocked). svelte-check + build clean; full vitest (258) + search e2e (7) green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,25 @@ const mangasResponse = {
|
||||
page: { limit: 100, offset: 0, total: 1 }
|
||||
};
|
||||
|
||||
const pageSearchResponse = {
|
||||
items: [
|
||||
{
|
||||
page_id: pageId,
|
||||
chapter_id: chapterId,
|
||||
manga_id: mangaId,
|
||||
page_number: 5,
|
||||
chapter_number: 1,
|
||||
chapter_title: null,
|
||||
manga_title: 'Berserk',
|
||||
storage_key: `mangas/${mangaId}/chapters/${chapterId}/pages/0005.png`,
|
||||
is_nsfw: true,
|
||||
content_warnings: ['gore'],
|
||||
rank: 0.9
|
||||
}
|
||||
],
|
||||
page: { limit: 100, offset: 0, total: 1 }
|
||||
};
|
||||
|
||||
async function mockSearch(page: Page) {
|
||||
await page.route('**/api/v1/auth/config', (route) =>
|
||||
route.fulfill({
|
||||
@@ -125,6 +144,13 @@ async function mockSearch(page: Page) {
|
||||
body: JSON.stringify({ items: distinct })
|
||||
})
|
||||
);
|
||||
await page.route('**/api/v1/me/page-search*', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(pageSearchResponse)
|
||||
})
|
||||
);
|
||||
await page.route('**/api/v1/me/page-tags?**', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
@@ -248,4 +274,32 @@ test.describe('/search', () => {
|
||||
`/manga/${mangaId}`
|
||||
);
|
||||
});
|
||||
|
||||
test('text search sets ?text= and renders page results', async ({ page }) => {
|
||||
await mockSearch(page);
|
||||
await page.setViewportSize(DESKTOP);
|
||||
await page.goto('/search');
|
||||
|
||||
await page.getByTestId('search-text-input').fill('dragon');
|
||||
await page.getByTestId('search-text-input').press('Enter');
|
||||
|
||||
await expect(page).toHaveURL(/text=dragon/);
|
||||
await expect(page.getByTestId('search-content-list')).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId(`search-result-${pageId}`)
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('content-warning toggle sets ?cw_include= and searches', async ({
|
||||
page
|
||||
}) => {
|
||||
await mockSearch(page);
|
||||
await page.setViewportSize(DESKTOP);
|
||||
await page.goto('/search');
|
||||
|
||||
await page.getByTestId('cw-toggle-gore').click();
|
||||
|
||||
await expect(page).toHaveURL(/cw_include=gore/);
|
||||
await expect(page.getByTestId('search-content-list')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user