import { test, expect, type Page } from '@playwright/test'; const mangaId = '22222222-2222-2222-2222-222222222222'; const chapterId = 'c2222222-2222-2222-2222-222222222222'; const mangaFixture = { id: mangaId, title: 'Vagabond', author: 'Takehiko Inoue', description: null, cover_image_path: null, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z' }; const chapterFixture = { id: chapterId, manga_id: mangaId, number: 1, title: null, page_count: 3, created_at: '2026-01-01T00:00:00Z' }; const pagesFixture = [ { id: 'p1111111-2222-2222-2222-222222222222', chapter_id: chapterId, page_number: 1, storage_key: `mangas/${mangaId}/chapters/${chapterId}/pages/0001.png`, content_type: 'image/png' }, { id: 'p2222222-2222-2222-2222-222222222222', chapter_id: chapterId, page_number: 2, storage_key: `mangas/${mangaId}/chapters/${chapterId}/pages/0002.png`, content_type: 'image/png' }, { id: 'p3333333-2222-2222-2222-222222222222', chapter_id: chapterId, page_number: 3, storage_key: `mangas/${mangaId}/chapters/${chapterId}/pages/0003.png`, content_type: 'image/png' } ]; async function mockReaderApis(page: Page) { // Anonymous user — both `me` and preferences fall back to localStorage. await page.route('**/api/v1/auth/me', (route) => route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: { code: 'unauthenticated', message: '' } }) }) ); await page.route('**/api/v1/auth/me/preferences', (route) => route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: { code: 'unauthenticated', message: '' } }) }) ); await page.route('**/api/v1/me/bookmarks*', (route) => route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: { code: 'unauthenticated', message: '' } }) }) ); 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: [chapterFixture], 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: [chapterFixture], page: { limit: 50, offset: 0, total: null } }) }) ); await page.route(`**/api/v1/mangas/${mangaId}/chapters/${chapterId}`, (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(chapterFixture) }) ); await page.route( `**/api/v1/mangas/${mangaId}/chapters/${chapterId}/pages`, (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ pages: pagesFixture }) }) ); const png = Buffer.from( '89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c63000100000005000158a3b62a0000000049454e44ae426082', 'hex' ); await page.route('**/api/v1/files/**', (route) => route.fulfill({ status: 200, contentType: 'image/png', body: png }) ); } test.beforeEach(async ({ context }) => { // Clear the localStorage shadow so each test starts in the default // single-page mode regardless of order. await context.clearCookies(); await context.addInitScript(() => { try { localStorage.removeItem('mangalord-reader-mode'); localStorage.removeItem('mangalord-reader-gap'); } catch { // ignore — about:blank doesn't expose localStorage yet. } }); }); test('switching to continuous mode stacks all pages and hides chevrons', async ({ page }) => { await mockReaderApis(page); await page.goto(`/manga/${mangaId}/chapter/${chapterId}`); // Default single-page mode is active. await expect(page.getByTestId('reader-page')).toBeVisible(); await expect(page.getByTestId('page-indicator')).toHaveText('Page 1 / 3'); await page.getByTestId('reader-mode-continuous').click(); await expect(page.getByTestId('reader-continuous')).toBeVisible(); await expect(page.getByTestId('reader-page-1')).toBeVisible(); await expect(page.getByTestId('reader-page-3')).toBeVisible(); await expect(page.getByTestId('reader-prev')).toHaveCount(0); await expect(page.getByTestId('reader-next')).toHaveCount(0); await expect(page.getByTestId('page-indicator')).toHaveText('3 pages'); }); test('arrow keys do not paginate while in continuous mode', async ({ page }) => { await mockReaderApis(page); await page.goto(`/manga/${mangaId}/chapter/${chapterId}`); await page.getByTestId('reader-mode-continuous').click(); await expect(page.getByTestId('reader-continuous')).toBeVisible(); await page.keyboard.press('ArrowRight'); await page.keyboard.press('j'); // Still in continuous mode, still showing every page. await expect(page.getByTestId('reader-continuous')).toBeVisible(); await expect(page.getByTestId('reader-page-1')).toBeVisible(); await expect(page.getByTestId('reader-page-3')).toBeVisible(); }); test('gap select updates the inline gap on the continuous container', async ({ page }) => { await mockReaderApis(page); await page.goto(`/manga/${mangaId}/chapter/${chapterId}`); await page.getByTestId('reader-mode-continuous').click(); const container = page.getByTestId('reader-continuous'); // Default gap is "none" → 0px. await expect(container).toHaveAttribute('style', /gap:\s*0px/); await page.getByTestId('reader-gap').selectOption('medium'); await expect(container).toHaveAttribute('style', /gap:\s*32px/); await page.getByTestId('reader-gap').selectOption('large'); await expect(container).toHaveAttribute('style', /gap:\s*64px/); }); test('reader-mode preference set on one page is honored when the reader opens', async ({ page, context }) => { // Simulate a user who has already chosen continuous + medium on the // settings page (which writes through the preferences store to // localStorage). The reader should pick up that choice on first // render without any in-tab navigation. await context.addInitScript(() => { localStorage.setItem('mangalord-reader-mode', 'continuous'); localStorage.setItem('mangalord-reader-gap', 'medium'); }); await mockReaderApis(page); await page.goto(`/manga/${mangaId}/chapter/${chapterId}`); await expect(page.getByTestId('reader-continuous')).toBeVisible(); await expect(page.getByTestId('page-indicator')).toHaveText('3 pages'); await expect(page.getByTestId('reader-continuous')).toHaveAttribute( 'style', /gap:\s*32px/ ); }); test('preferences page hides the gap picker while in single-page mode', async ({ page }) => { // Visually verifies the conditional render. The radio-click semantics // are exercised in src/lib/preferences.svelte.test.ts; the visible // mode toggle in the reader top bar covers the cross-route propagation // path in the test above. await mockReaderApis(page); await page.goto('/profile/preferences'); await expect(page.getByTestId('reader-mode-radio-single')).toBeAttached(); await expect(page.getByTestId('reader-mode-radio-continuous')).toBeAttached(); await expect(page.getByTestId('reader-gap-radio-medium')).toHaveCount(0); });