import { test, expect, type Page } from '@playwright/test'; // Regression spec for the reader-back-loop bug: previously the reader's // back arrow was a plain ``, which PUSHED a new // history entry every tap, so browser-back kept ping-ponging between // detail and reader instead of walking out to home. The reader now // intercepts left-click and does `history.back()` when there's // same-origin history to pop. const MID = 'a1111111-1111-1111-1111-111111111111'; const CID = 'c1111111-1111-1111-1111-111111111111'; const mangaBody = { id: MID, title: 'Berserk', status: 'ongoing', alt_titles: [], description: 'Short.', cover_image_path: null, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', authors: [], genres: [], tags: [] }; async function mockApis(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: 'x', message: 'x' } }) }) ); await page.route('**/api/v1/auth/me/preferences', (r) => r.fulfill({ status: 401, contentType: 'application/json', body: '{}' }) ); await page.route('**/api/v1/me/bookmarks*', (r) => r.fulfill({ status: 401, contentType: 'application/json', body: '{}' }) ); await page.route('**/api/v1/me/read-progress*', (r) => r.fulfill({ status: 404, contentType: 'application/json', body: '{}' }) ); await page.route('**/api/v1/genres*', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: '[]' }) ); // Catalog returns a single card whose href points at MID so the // SPA-click walk lands on the manga we've mocked downstream. await page.route('**/api/v1/mangas', (r) => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [mangaBody], page: { limit: 50, offset: 0, total: 1 } }) }) ); await page.route('**/api/v1/mangas/**', (r) => { const u = new URL(r.request().url()); const last = u.pathname.split('/').pop(); if (last === CID) return r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ id: CID, manga_id: MID, number: 1, title: 'Ch.1', page_count: 1, created_at: '2026-01-01T00:00:00Z' }) }); if (last === 'pages') return r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ pages: [ { id: 'p', chapter_id: CID, page_number: 1, storage_key: 'x.png', content_type: 'image/png' } ] }) }); if (u.pathname.endsWith('/chapters')) return r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [ { id: CID, manga_id: MID, number: 1, title: 'Ch.1', page_count: 1, created_at: '2026-01-01T00:00:00Z' } ], page: { limit: 50, offset: 0, total: 1 } }) }); return r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mangaBody) }); }); const png = Buffer.from( '89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c63000100000005000158a3b62a0000000049454e44ae426082', 'hex' ); await page.route('**/api/v1/files/**', (r) => r.fulfill({ status: 200, contentType: 'image/png', body: png }) ); } test.describe('back-nav flow', () => { test('phone viewport: reader back pops history (does not push), then detail back returns to home', async ({ page }) => { await mockApis(page); await page.setViewportSize({ width: 390, height: 844 }); await page.goto('/'); const initialLen = await page.evaluate(() => window.history.length); // Walk: home → detail → reader USING SPA NAVIGATION (link clicks, // not window.location.href). The bug only manifests on SPA nav // because `document.referrer` stays empty across pushState — so // a hard navigation would silently mask the regression. await page.locator('[data-testid="manga-list"] a').first().click(); await page.waitForURL(/\/manga\/[^/]+$/); await page.locator('[data-testid="chapter-list"] a').first().click(); await page.waitForURL(/\/chapter\//); const lenAtReader = await page.evaluate(() => window.history.length); expect(lenAtReader).toBe(initialLen + 2); // Tap reader back → goes back to detail WITHOUT pushing a new entry. await page.getByTestId('back-to-manga').click(); await page.waitForURL(/\/manga\/[^/]+$/); const lenAtDetail = await page.evaluate(() => window.history.length); expect(lenAtDetail).toBe(lenAtReader); // Tap detail back → walks out to home. await page.getByTestId('detail-back').click(); await page.waitForURL((url) => url.pathname === '/'); }); });