import { test, expect, type Page } from '@playwright/test'; const mangaId = '22222222-2222-2222-2222-222222222222'; const userFixture = { id: 'u1', username: 'alice', created_at: '2026-01-01T00:00:00Z' }; const mangaFixture = { id: mangaId, title: 'Berserk', author: 'Kentaro Miura', description: null, cover_image_path: null, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z' }; const bookmarkFixture = { id: 'b1', user_id: 'u1', manga_id: mangaId, chapter_id: null, page: null, created_at: '2026-01-01T00:00:00Z' }; async function setupAuthenticatedBookmarkFlow(page: Page) { let bookmarks: typeof bookmarkFixture[] = []; await page.route('**/api/v1/auth/me', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ user: userFixture }) }) ); await page.route(`**/api/v1/mangas/${mangaId}`, (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mangaFixture) }) ); await page.route(`**/api/v1/mangas/${mangaId}/chapters?*`, (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [], page: { limit: 50, offset: 0, total: null } }) }) ); await page.route(`**/api/v1/mangas/${mangaId}/chapters`, (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [], page: { limit: 50, offset: 0, total: null } }) }) ); await page.route('**/api/v1/me/bookmarks*', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: bookmarks, page: { limit: 50, offset: 0, total: null } }) }) ); await page.route('**/api/v1/bookmarks', (route) => { if (route.request().method() === 'POST') { bookmarks = [bookmarkFixture, ...bookmarks]; route.fulfill({ status: 201, contentType: 'application/json', body: JSON.stringify(bookmarkFixture) }); } else { route.fallback(); } }); await page.route('**/api/v1/bookmarks/b1', (route) => { if (route.request().method() === 'DELETE') { bookmarks = bookmarks.filter((b) => b.id !== 'b1'); route.fulfill({ status: 204 }); } else { route.fallback(); } }); } test('authed user toggles a manga bookmark and sees it in /bookmarks', async ({ page }) => { await setupAuthenticatedBookmarkFlow(page); await page.goto(`/manga/${mangaId}`); const toggle = page.getByTestId('bookmark-toggle'); await expect(toggle).toHaveText('☆ Bookmark'); await expect(toggle).toHaveAttribute('aria-pressed', 'false'); await toggle.click(); await expect(toggle).toHaveText('★ Bookmarked'); await expect(toggle).toHaveAttribute('aria-pressed', 'true'); // The /bookmarks list reflects it. await page.goto('/bookmarks'); await expect(page.getByTestId('bookmark-list')).toContainText('Manga bookmark'); // Toggle off from the manga page. await page.goto(`/manga/${mangaId}`); const toggle2 = page.getByTestId('bookmark-toggle'); await expect(toggle2).toHaveText('★ Bookmarked'); await toggle2.click(); await expect(toggle2).toHaveText('☆ Bookmark'); }); test('anonymous user sees a sign-in CTA instead of a toggle', async ({ page }) => { await page.route('**/api/v1/auth/me', (route) => route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: { code: 'unauthenticated', message: 'unauthenticated' } }) }) ); await page.route(`**/api/v1/mangas/${mangaId}`, (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mangaFixture) }) ); await page.route(`**/api/v1/mangas/${mangaId}/chapters?*`, (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [], page: { limit: 50, offset: 0, total: null } }) }) ); await page.route(`**/api/v1/mangas/${mangaId}/chapters`, (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [], page: { limit: 50, offset: 0, total: null } }) }) ); await page.route('**/api/v1/me/bookmarks*', (route) => route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: { code: 'unauthenticated', message: 'unauthenticated' } }) }) ); await page.goto(`/manga/${mangaId}`); await expect(page.getByTestId('bookmark-signin')).toBeVisible(); await expect(page.getByTestId('bookmark-toggle')).toHaveCount(0); }); test('/bookmarks page prompts anonymous users to sign in', async ({ page }) => { await page.route('**/api/v1/auth/me', (route) => route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: { code: 'unauthenticated', message: 'unauthenticated' } }) }) ); await page.route('**/api/v1/me/bookmarks*', (route) => route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: { code: 'unauthenticated', message: 'unauthenticated' } }) }) ); await page.goto('/bookmarks'); await expect(page.getByTestId('bookmarks-signin')).toBeVisible(); });