import { test, expect, type Page } from '@playwright/test'; // Mobile chrome contract: the AppBar + BottomNav are visible on phone // viewports and the desktop header is hidden. On desktop, the reverse. // On the reader route, both mobile bars are hidden so the reader's own // chrome owns the screen. The 640px cutover is the project's single // existing breakpoint and is also used here. const MOBILE = { width: 390, height: 844 } as const; const DESKTOP = { width: 1280, height: 720 } as const; async function mockAnonymous(page: Page) { await page.route('**/api/v1/auth/config', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ self_register_enabled: true, private_mode: false }) }); }); await page.route('**/api/v1/auth/me', async (route) => { await route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: { code: 'unauthenticated', message: 'unauthenticated' } }) }); }); await page.route('**/api/v1/mangas*', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [], page: { limit: 50, offset: 0, total: 0 } }) }); }); } test.describe('mobile chrome', () => { test('phone viewport on /: bottom nav visible, desktop header hidden', async ({ page }) => { await mockAnonymous(page); await page.setViewportSize(MOBILE); await page.goto('/'); await expect(page.getByTestId('bottom-nav')).toBeVisible(); await expect(page.getByTestId('mobile-app-bar')).toBeVisible(); await expect(page.locator('header.desktop-header')).toBeHidden(); }); test('desktop viewport on /: desktop header visible, mobile chrome hidden', async ({ page }) => { await mockAnonymous(page); await page.setViewportSize(DESKTOP); await page.goto('/'); await expect(page.locator('header.desktop-header')).toBeVisible(); await expect(page.getByTestId('bottom-nav')).toBeHidden(); await expect(page.getByTestId('mobile-app-bar')).toBeHidden(); }); test('phone viewport: Home tab carries aria-current on /', async ({ page }) => { await mockAnonymous(page); await page.setViewportSize(MOBILE); await page.goto('/'); const home = page.getByTestId('bottom-nav-home'); await expect(home).toHaveAttribute('aria-current', 'page'); }); test('phone viewport: tapping Library navigates to /library and marks itself active', async ({ page }) => { await mockAnonymous(page); await page.route('**/api/v1/me/bookmarks*', (route) => route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: { code: 'x', message: 'x' } }) }) ); await page.route('**/api/v1/me/collections*', (route) => route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: { code: 'x', message: 'x' } }) }) ); await page.route('**/api/v1/me/read-progress*', (route) => route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: { code: 'x', message: 'x' } }) }) ); await page.setViewportSize(MOBILE); await page.goto('/'); await page.getByTestId('bottom-nav-library').click(); await expect(page).toHaveURL(/\/library$/); await expect(page.getByTestId('bottom-nav-library')).toHaveAttribute('aria-current', 'page'); }); test('phone viewport: login route hides the mobile chrome (auth gateway pattern)', async ({ page }) => { await mockAnonymous(page); await page.setViewportSize(MOBILE); await page.goto('/login'); await expect(page.getByTestId('bottom-nav')).toBeHidden(); await expect(page.getByTestId('mobile-app-bar')).toBeHidden(); }); });