Files
Mangalord/frontend/e2e/mobile-chrome.spec.ts
MechaCat02 6dcb720a0f feat(frontend): mobile primitives + bottom-nav chrome (0.56.0)
Phase 1 of the mobile redesign: add the reusable primitives every
later phase plugs into, and switch the layout to a 4-tab bottom nav
+ compact AppBar on phone viewports while leaving the desktop header
untouched above 640px.

- New primitives: BottomNav, Sheet, AppBar, SegmentedControl, all
  with vitest unit tests.
- Layout swaps the desktop header for AppBar + BottomNav under the
  existing 640px breakpoint. Login / register / reader routes opt
  out of the mobile chrome.
- Library tab currently points at /bookmarks; the curated /library
  wrapper lands in Phase 5.
- Independent ResizeObservers publish --app-header-h,
  --mobile-app-bar-h, --app-bottom-nav-h, with a zero-height skip so
  hidden bars don't clobber the visible one's value.
- viewport-fit=cover + env(safe-area-inset-*) tokens for notched
  devices and the iOS home indicator.
- Playwright spec covers visibility at 390px / 1280px viewports and
  tab navigation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 19:46:16 +02:00

85 lines
3.4 KiB
TypeScript

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 /bookmarks and marks itself active', async ({ page }) => {
await mockAnonymous(page);
await page.setViewportSize(MOBILE);
await page.goto('/');
await page.getByTestId('bottom-nav-library').click();
await expect(page).toHaveURL(/\/bookmarks$/);
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();
});
});