When the reader reaches within two pages of the end of a chapter (tracked in both modes off the furthest page reached), warm the next chapter's first few page images via hidden imgs so flipping over renders instantly. Fetched once per next-chapter id, guarded against mid-flight chapter changes, and retried on failure. e2e asserts the prefetch is absent at the chapter start and present (with the next chapter's URLs) near the end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
68 lines
3.6 KiB
TypeScript
68 lines
3.6 KiB
TypeScript
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';
|
|
|
|
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: '<svg xmlns="http://www.w3.org/2000/svg" width="800" height="1000"/>' });
|
|
}
|
|
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/${CH1}`)) return json(200, chapter(CH1, 1, 5));
|
|
if (pathname.endsWith(`/chapters/${CH2}`)) return json(200, chapter(CH2, 2, 4));
|
|
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)], page: { limit: 200, offset: 0, total: 2 } });
|
|
}
|
|
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/);
|
|
});
|