Eager-load every page of the current chapter rather than a bounded rolling window, so pages are fetched up front (the browser paces them over its per-origin connection limit, top-to-bottom = reading order) instead of filling in as they're scrolled onto. This drops the scrollLeadIdx/eagerThrough machinery in favour of a plain `loading="eager"`. The first few pages of the *next* chapter are already warmed when the reader nears the end of the current one (the next-chapter preloader), so flipping over stays instant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
77 lines
4.2 KiB
TypeScript
77 lines
4.2 KiB
TypeScript
import { test, expect, type Page } from './fixtures';
|
|
|
|
// Continuous mode eager-loads a rolling window of pages ahead of the reading
|
|
// position, so pages are fetched (and sized) before they scroll into view
|
|
// instead of popping in / reflowing on scroll. Pages far beyond the window
|
|
// stay lazy so opening a long chapter doesn't fire every request at once.
|
|
|
|
const MANGA_ID = 'm1';
|
|
const CH1 = 'c1';
|
|
const PAGE_COUNT = 10;
|
|
|
|
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-01-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/')) {
|
|
// A viewBox + rect gives the <img> a real intrinsic size so pages
|
|
// get real (tall) heights — offscreen pages then stay below the
|
|
// fold instead of collapsing to 0 and piling into the viewport.
|
|
return route.fulfill({ status: 200, contentType: 'image/svg+xml', body: '<svg xmlns="http://www.w3.org/2000/svg" width="800" height="1200" viewBox="0 0 800 1200"><rect width="800" height="1200" fill="#888"/></svg>' });
|
|
}
|
|
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, PAGE_COUNT) });
|
|
if (pathname.endsWith(`/chapters/${CH1}`)) return json(200, chapter(CH1, 1, PAGE_COUNT));
|
|
if (pathname.includes(`/mangas/${MANGA_ID}/chapters`)) {
|
|
return json(200, { items: [chapter(CH1, 1, PAGE_COUNT)], page: { limit: 200, offset: 0, total: 1 } });
|
|
}
|
|
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 } });
|
|
});
|
|
}
|
|
|
|
async function goContinuous(page: Page, path: string) {
|
|
await page.addInitScript(() => localStorage.setItem('mangalord-reader-mode', 'continuous'));
|
|
await page.goto(path);
|
|
await expect(page.getByTestId('reader-continuous')).toBeVisible();
|
|
}
|
|
|
|
test('eager-loads the whole current chapter up front, not just the visible pages', async ({ page }) => {
|
|
await mockReader(page);
|
|
await goContinuous(page, `/manga/${MANGA_ID}/chapter/${CH1}`);
|
|
|
|
// Every page — including ones far below the fold — is eager, so the
|
|
// browser fetches the whole chapter up front instead of on scroll.
|
|
for (let n = 1; n <= PAGE_COUNT; n++) {
|
|
await expect(page.getByTestId(`reader-page-${n}`)).toHaveAttribute('loading', 'eager');
|
|
}
|
|
|
|
// ... and a page well below the fold actually finishes loading without
|
|
// ever scrolling to it — the observable payoff of preloading.
|
|
const lastPage = page.getByTestId(`reader-page-${PAGE_COUNT}`);
|
|
await expect(async () => {
|
|
const loaded = await lastPage.evaluate((img: HTMLImageElement) => img.complete && img.naturalWidth > 0);
|
|
expect(loaded).toBe(true);
|
|
}).toPass();
|
|
expect(await page.evaluate(() => window.scrollY)).toBe(0);
|
|
});
|