import { test, expect, type Page } from './fixtures'; // The global nav progress bar (rendered once in the root layout) is the only // feedback most data routes have while a client-side navigation is pending: // they run `load` with `ssr = false`, so SvelteKit holds the previous page on // screen with no indication anything is happening. This drives a real // home -> detail navigation, holds the detail's `getManga` open, and asserts // the bar activates during the pending load and clears once it settles. const mangaId = 'm1111111-1111-1111-1111-111111111111'; const listItem = { id: mangaId, 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: [] }; const mangaDetail = { id: mangaId, 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 }; async function mockCommon(page: Page) { await page.route('**/api/v1/auth/config', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ self_register_enabled: true, private_mode: false }) }) ); await page.route('**/api/v1/auth/me', (r) => r.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: { code: 'unauthenticated', message: 'no' } }) }) ); await page.route('**/api/v1/auth/me/preferences', (r) => r.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: { code: 'unauthenticated', message: 'no' } }) }) ); await page.route('**/api/v1/genres*', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: '[]' }) ); // Catalog list (Pattern B). Registered before the detail routes so the // more-specific handlers below win for the /mangas/:id URLs. await page.route('**/api/v1/mangas*', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [listItem], page: { limit: 50, offset: 0, total: 1 } }) }) ); // Detail load's non-manga calls. await page.route('**/api/v1/me/bookmarks*', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [], page: { limit: 50, offset: 0, total: 0 } }) }) ); await page.route(`**/api/v1/mangas/${mangaId}/chapters*`, (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [], page: { limit: 50, offset: 0, total: 0 } }) }) ); await page.route(`**/api/v1/mangas/${mangaId}/similar`, (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [] }) }) ); await page.route(`**/api/v1/me/read-progress/${mangaId}`, (r) => r.fulfill({ status: 404, contentType: 'application/json', body: JSON.stringify({ error: { code: 'not_found', message: 'no' } }) }) ); await page.route(`**/api/v1/me/reactions/${mangaId}`, (r) => r.fulfill({ status: 404, contentType: 'application/json', body: JSON.stringify({ error: { code: 'not_found', message: 'no' } }) }) ); } test('shows the nav progress bar during a pending navigation and clears it after', async ({ page }) => { await mockCommon(page); // Hold the detail's getManga open so the navigation stays pending long // enough to observe the bar. Registered last => wins for the exact // /mangas/:id URL over the catalog glob. let releaseDetail: () => void = () => {}; const gate = new Promise((resolve) => { releaseDetail = resolve; }); await page.route(`**/api/v1/mangas/${mangaId}`, async (route) => { await gate; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mangaDetail) }); }); await page.goto('/'); await expect(page.getByTestId('manga-list')).toBeVisible(); const bar = page.getByTestId('nav-progress'); // Idle on a settled page. await expect(bar).toHaveAttribute('data-phase', 'idle'); // Client-side navigate into the (gated) detail page. await page.locator(`a[href="/manga/${mangaId}"]`).first().click(); // Bar enters the loading phase while the detail load is in flight. await expect(bar).toHaveAttribute('data-phase', 'loading'); // Let the load resolve; the detail renders and the bar settles back to // idle (via a brief done phase). releaseDetail(); await expect(page.getByTestId('manga-title')).toHaveText('Berserk'); await expect(bar).toHaveAttribute('data-phase', 'idle'); });