import { test, expect, type Page } from '@playwright/test'; const userFixture = { id: 'u1', username: 'alice', created_at: '2026-01-01T00:00:00Z' }; const baseManga = { id: 'm1', title: 'Berserk', status: 'ongoing', alt_titles: ['Old Alt'], description: 'Original description', cover_image_path: null, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', authors: [{ id: 'a1', name: 'Kentaro Miura' }], genres: [], tags: [] }; async function stubAuthenticatedAndGenres(page: Page) { 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/genres', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([ { id: 'g-action', name: 'Action' }, { id: 'g-fantasy', name: 'Fantasy' } ]) }) ); } test('anonymous user sees sign-in prompt on /manga/[id]/edit', 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/genres', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: '[]' }) ); await page.route('**/api/v1/mangas/m1', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(baseManga) }) ); await page.goto('/manga/m1/edit'); await expect(page.getByTestId('edit-signin')).toBeVisible(); }); test('/manga/[id]/edit PATCHes the changed metadata and lands on the manga page', async ({ page }) => { await stubAuthenticatedAndGenres(page); let patchBody: Record | null = null; let mangaAfter = { ...baseManga }; await page.route('**/api/v1/mangas/m1', async (route) => { const method = route.request().method(); if (method === 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mangaAfter) }); } else if (method === 'PATCH') { patchBody = JSON.parse(route.request().postData() ?? '{}'); mangaAfter = { ...mangaAfter, title: (patchBody.title as string) ?? mangaAfter.title, description: 'description' in (patchBody as Record) ? ((patchBody.description as string | null) ?? null) : mangaAfter.description }; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mangaAfter) }); } else { await route.fallback(); } }); await page.route('**/api/v1/mangas/m1/chapters*', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [], page: { limit: 50, offset: 0, total: 0 } }) }) ); 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/me/read-progress/m1', (route) => route.fulfill({ status: 404, contentType: 'application/json', body: JSON.stringify({ error: { code: 'not_found', message: 'no progress' } }) }) ); await page.goto('/manga/m1'); // Edit link is gated on session.user — it should be visible to the // stubbed authenticated user. await page.getByTestId('edit-manga-link').click(); await expect(page).toHaveURL(/\/manga\/m1\/edit$/); const titleInput = page.getByTestId('manga-title'); await expect(titleInput).toHaveValue('Berserk'); await titleInput.fill('Berserk (Deluxe)'); await page.getByTestId('manga-edit-submit').click(); await expect(page).toHaveURL(/\/manga\/m1$/); await expect(page.getByTestId('manga-title')).toHaveText('Berserk (Deluxe)'); expect(patchBody).not.toBeNull(); expect((patchBody as Record).title).toBe('Berserk (Deluxe)'); });