import { test, expect, type Page } from './fixtures'; // As the reader nears the end of a chapter, the next chapter's first page // images are prefetched (hidden) so flipping over is instant. They must NOT // be present at the start of the chapter. const MANGA_ID = 'm1'; const CH1 = 'c1'; const CH2 = 'c2'; const CH3 = 'c3'; function pages(prefix: string, n: number) { return Array.from({ length: n }, (_, i) => ({ id: `${prefix}p${i + 1}`, chapter_id: prefix, page_number: i + 1, storage_key: `${prefix}/${i + 1}`, content_type: 'image/svg+xml' })); } function chapter(id: string, number: number, count: number) { return { id, manga_id: MANGA_ID, number, title: null, page_count: count, created_at: `2026-0${number}-01T00:00:00Z`, size_bytes: 0 }; } async function mockReader(page: Page) { await page.route('**/api/v1/**', async (route) => { const { pathname } = new URL(route.request().url()); const json = (status: number, body: unknown) => route.fulfill({ status, contentType: 'application/json', body: JSON.stringify(body) }); if (pathname.includes('/files/')) { return route.fulfill({ status: 200, contentType: 'image/svg+xml', body: '' }); } if (pathname.endsWith('/auth/config')) return json(200, { self_register_enabled: true, private_mode: false }); if (pathname.endsWith('/auth/me')) return json(401, { error: { code: 'unauthenticated', message: 'no' } }); if (pathname.endsWith('/auth/me/preferences')) return json(401, { error: { code: 'unauthenticated', message: 'no' } }); if (pathname.endsWith(`/chapters/${CH1}/pages`)) return json(200, { pages: pages(CH1, 5) }); if (pathname.endsWith(`/chapters/${CH2}/pages`)) return json(200, { pages: pages(CH2, 4) }); if (pathname.endsWith(`/chapters/${CH3}/pages`)) return json(200, { pages: pages(CH3, 3) }); if (pathname.endsWith(`/chapters/${CH1}`)) return json(200, chapter(CH1, 1, 5)); if (pathname.endsWith(`/chapters/${CH2}`)) return json(200, chapter(CH2, 2, 4)); if (pathname.endsWith(`/chapters/${CH3}`)) return json(200, chapter(CH3, 3, 3)); if (pathname.includes(`/mangas/${MANGA_ID}/chapters`)) { // Oldest-first, as the reader expects (next = index + 1). return json(200, { items: [chapter(CH1, 1, 5), chapter(CH2, 2, 4), chapter(CH3, 3, 3)], page: { limit: 200, offset: 0, total: 3 } }); } if (pathname.endsWith(`/mangas/${MANGA_ID}`)) return json(200, { id: MANGA_ID, title: 'Berserk', status: 'ongoing', alt_titles: [], description: null, cover_image_path: null, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', authors: [], genres: [], tags: [], content_warnings: [], chapter_storage_bytes: 0 }); if (pathname.includes('/me/read-progress')) return json(401, { error: { code: 'unauthenticated', message: 'no' } }); return json(503, { error: { code: 'e2e_unmocked', message: pathname } }); }); } test('prefetches the next chapter first pages only when near the end', async ({ page }) => { await mockReader(page); await page.goto(`/manga/${MANGA_ID}/chapter/${CH1}`); await expect(page.getByTestId('reader-page')).toBeVisible(); // Start of a 5-page chapter → not near the end → no next-chapter preload. await expect(page.getByTestId('reader-next-preload')).toHaveCount(0); // Jump to the last page. await page.keyboard.press('End'); // Next chapter's first pages are now prefetched. const preloads = page.getByTestId('reader-next-preload'); await expect(preloads.first()).toBeAttached(); await expect(preloads).toHaveCount(3); await expect(preloads.first()).toHaveAttribute('src', /c2\/1/); }); test('clears stale prefetch when navigating to the next chapter', async ({ page }) => { await mockReader(page); await page.goto(`/manga/${MANGA_ID}/chapter/${CH1}`); await expect(page.getByTestId('reader-page')).toBeVisible(); // Warm chapter 2's pages near the end of chapter 1. await page.keyboard.press('End'); await expect(page.getByTestId('reader-next-preload')).toHaveCount(3); // Navigate to chapter 2. At its start (not near the end) the previous // prefetch must be cleared, even though chapter 2 has its own next (3). await page.goto(`/manga/${MANGA_ID}/chapter/${CH2}`); await expect(page.getByTestId('reader-page')).toBeVisible(); await expect(page.getByTestId('reader-next-preload')).toHaveCount(0); });