Files
Mangalord/frontend/e2e/mobile-chrome.spec.ts
MechaCat02 ded77fe4ba test(e2e): kill cold-start flakiness with an /api/v1 fallback + one retry
A full parallel run intermittently failed the first few tests: unmocked
/api/v1 calls fell through to the dev proxy, whose backend isn't running
under E2E, so each incurred a slow ECONNREFUSED round-trip + Vite error
logging that piled up across 8 workers at cold start.

- Add e2e/fixtures.ts: a shared `test` whose auto-use fixture installs an
  `**/api/v1/**` fallback (fast 503, logged) for any endpoint a spec
  didn't mock. Registered before the test body, so per-test routes still
  win; only genuine gaps land here. Turns silent mock-gap hangs into
  instant, attributable failures — and cuts the full run ~96s → ~25s.
- Point all 22 specs at ./fixtures instead of @playwright/test.
- retries: 1 in the config as a safety net for the residual Vite
  cold-compile timing (re-runs on the now-warm server).

Two consecutive full runs: 110 passed, 0 flaky, no retries consumed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:06:20 +02:00

106 lines
4.1 KiB
TypeScript

import { test, expect, type Page } from './fixtures';
// 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();
});
});