diff --git a/frontend/e2e/admin-analysis.spec.ts b/frontend/e2e/admin-analysis.spec.ts index 6d5702a..4bd66a2 100644 --- a/frontend/e2e/admin-analysis.spec.ts +++ b/frontend/e2e/admin-analysis.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; // E2E for the admin Analysis section: coverage overview + badges, drill // manga → chapter → page, the page-detail modal, and the enqueue actions. diff --git a/frontend/e2e/admin-crawler.spec.ts b/frontend/e2e/admin-crawler.spec.ts index 7d0f31d..dabad8e 100644 --- a/frontend/e2e/admin-crawler.spec.ts +++ b/frontend/e2e/admin-crawler.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; // E2E for the admin Crawler "History" tab: the Live/History toggle, the // searchable/filterable job log, and inline requeue of a dead job. The diff --git a/frontend/e2e/auth-flow.spec.ts b/frontend/e2e/auth-flow.spec.ts index a7f4ce0..3268a00 100644 --- a/frontend/e2e/auth-flow.spec.ts +++ b/frontend/e2e/auth-flow.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; // Mocks the auth endpoints at the network level so the journey is // deterministic and doesn't require a live backend. diff --git a/frontend/e2e/back-nav-flow.spec.ts b/frontend/e2e/back-nav-flow.spec.ts index 6a92042..c542693 100644 --- a/frontend/e2e/back-nav-flow.spec.ts +++ b/frontend/e2e/back-nav-flow.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; // Regression spec for the reader-back-loop bug: previously the reader's // back arrow was a plain ``, which PUSHED a new diff --git a/frontend/e2e/bookmarks.spec.ts b/frontend/e2e/bookmarks.spec.ts index 5388241..971ba5f 100644 --- a/frontend/e2e/bookmarks.spec.ts +++ b/frontend/e2e/bookmarks.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; const mangaId = '22222222-2222-2222-2222-222222222222'; const userFixture = { diff --git a/frontend/e2e/fixtures.ts b/frontend/e2e/fixtures.ts new file mode 100644 index 0000000..de0b347 --- /dev/null +++ b/frontend/e2e/fixtures.ts @@ -0,0 +1,43 @@ +import { test as base, expect } from '@playwright/test'; + +export { expect }; +export type { Page } from '@playwright/test'; + +/** + * Shared Playwright `test` for the E2E suite. + * + * Adds an auto-use fallback for the `/api` surface: any request a spec + * forgot to mock is fulfilled here with a fast 503 instead of falling + * through to the SvelteKit dev proxy. That proxy targets a backend that + * isn't running under E2E (`BACKEND_URL` in `.env`), so an unmocked call + * otherwise incurs a proxy round-trip + Vite error logging — which, under + * 8 parallel workers at cold start, piles up and flakes the first tests. + * + * Because this route is registered during fixture setup (before the test + * body runs), any `page.route(...)` a spec registers later takes + * precedence — only genuinely-unmocked calls land here. The 503 also + * surfaces mock gaps loudly (a clear warning + a fast, attributable + * failure) instead of a 30s "element never appeared" timeout. + */ +export const test = base.extend<{ apiFallback: void }>({ + apiFallback: [ + async ({ page }, use) => { + // Scope to `/api/v1/**` (the real backend surface). A broader + // `**/api/**` would also swallow Vite's own dev module requests + // under `/src/lib/api/*.ts` and break the app bundle. + await page.route('**/api/v1/**', (route) => { + const { pathname } = new URL(route.request().url()); + console.warn(`[e2e] unmocked API call: ${route.request().method()} ${pathname}`); + return route.fulfill({ + status: 503, + contentType: 'application/json', + body: JSON.stringify({ + error: { code: 'e2e_unmocked', message: `unmocked in test: ${pathname}` } + }) + }); + }); + await use(); + }, + { auto: true } + ] +}); diff --git a/frontend/e2e/manga-edit.spec.ts b/frontend/e2e/manga-edit.spec.ts index ff5f95c..c523de6 100644 --- a/frontend/e2e/manga-edit.spec.ts +++ b/frontend/e2e/manga-edit.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; const userFixture = { id: 'u1', diff --git a/frontend/e2e/manga-list.spec.ts b/frontend/e2e/manga-list.spec.ts index 752cae3..70d2493 100644 --- a/frontend/e2e/manga-list.spec.ts +++ b/frontend/e2e/manga-list.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; import { SORT_FIELD_LABELS } from '../src/lib/mangaSort'; // These E2E tests run against the SvelteKit dev server, which proxies /api diff --git a/frontend/e2e/mobile-account-library.spec.ts b/frontend/e2e/mobile-account-library.spec.ts index 70831cd..95f5aa0 100644 --- a/frontend/e2e/mobile-account-library.spec.ts +++ b/frontend/e2e/mobile-account-library.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; // Phase 5: Account becomes an inset-grouped hub on mobile (Profile / // Preferences / Change password + red Log out at the bottom) and diff --git a/frontend/e2e/mobile-chrome.spec.ts b/frontend/e2e/mobile-chrome.spec.ts index 7036150..dc0879d 100644 --- a/frontend/e2e/mobile-chrome.spec.ts +++ b/frontend/e2e/mobile-chrome.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +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. diff --git a/frontend/e2e/mobile-list-search.spec.ts b/frontend/e2e/mobile-list-search.spec.ts index 3638d11..e162ebd 100644 --- a/frontend/e2e/mobile-list-search.spec.ts +++ b/frontend/e2e/mobile-list-search.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; // Phase 2: the catalog (/) gets a mobile chrome — search input full // width, Filter and Sort as chip buttons that open bottom sheets, and diff --git a/frontend/e2e/mobile-manga-detail.spec.ts b/frontend/e2e/mobile-manga-detail.spec.ts index d71b8cd..a38f23d 100644 --- a/frontend/e2e/mobile-manga-detail.spec.ts +++ b/frontend/e2e/mobile-manga-detail.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; // Phase 3: the manga detail page gains a mobile hero (blurred backdrop // + transparent app bar), a sticky bottom CTA whose wording reflects diff --git a/frontend/e2e/mobile-reader.spec.ts b/frontend/e2e/mobile-reader.spec.ts index a76e734..765dfa8 100644 --- a/frontend/e2e/mobile-reader.spec.ts +++ b/frontend/e2e/mobile-reader.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; // Phase 4: the reader gains a mobile chrome — invisible tap zones for // prev/next/toggle, a chapter-jump bottom sheet, a reader-settings diff --git a/frontend/e2e/page-context-menu.spec.ts b/frontend/e2e/page-context-menu.spec.ts index f42255c..d43de13 100644 --- a/frontend/e2e/page-context-menu.spec.ts +++ b/frontend/e2e/page-context-menu.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; // E2E for the per-page collection + tag flow added in v0.61.0. Mocks // the entire API so the spec runs without a backend. The same diff --git a/frontend/e2e/page-title.spec.ts b/frontend/e2e/page-title.spec.ts index ccf16c2..dd5bfb9 100644 --- a/frontend/e2e/page-title.spec.ts +++ b/frontend/e2e/page-title.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; // Guards the title-on-nav behavior: without this, a stale title from // the last manga / author page lingers when the user navigates to a diff --git a/frontend/e2e/private-mode.spec.ts b/frontend/e2e/private-mode.spec.ts index c4ebb41..33e90d1 100644 --- a/frontend/e2e/private-mode.spec.ts +++ b/frontend/e2e/private-mode.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; // Network-level mocks for the private-mode UX. The backend integration // tests (api_private_mode.rs) cover the actual gate; here we only diff --git a/frontend/e2e/profile.spec.ts b/frontend/e2e/profile.spec.ts index 7a63283..76e7bc9 100644 --- a/frontend/e2e/profile.spec.ts +++ b/frontend/e2e/profile.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; const userFixture = { id: 'u1', diff --git a/frontend/e2e/reader-chapter-select.spec.ts b/frontend/e2e/reader-chapter-select.spec.ts index 7e6944a..8323192 100644 --- a/frontend/e2e/reader-chapter-select.spec.ts +++ b/frontend/e2e/reader-chapter-select.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; const mangaId = '33333333-3333-3333-3333-333333333333'; const chapter1Id = 'c1111111-3333-3333-3333-333333333333'; diff --git a/frontend/e2e/reader-mode.spec.ts b/frontend/e2e/reader-mode.spec.ts index 012bc90..1a6e4bd 100644 --- a/frontend/e2e/reader-mode.spec.ts +++ b/frontend/e2e/reader-mode.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; const mangaId = '22222222-2222-2222-2222-222222222222'; const chapterId = 'c2222222-2222-2222-2222-222222222222'; diff --git a/frontend/e2e/reader-page-deep-link.spec.ts b/frontend/e2e/reader-page-deep-link.spec.ts index 8ce3cd3..bd82292 100644 --- a/frontend/e2e/reader-page-deep-link.spec.ts +++ b/frontend/e2e/reader-page-deep-link.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; // E2E for the `?page=N` deep-link path in both reader modes. The // single-mode case has been working since v0.x; the continuous-mode diff --git a/frontend/e2e/reader.spec.ts b/frontend/e2e/reader.spec.ts index a98bd47..02ed820 100644 --- a/frontend/e2e/reader.spec.ts +++ b/frontend/e2e/reader.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; const mangaId = '11111111-1111-1111-1111-111111111111'; const chapterId = 'c1111111-1111-1111-1111-111111111111'; diff --git a/frontend/e2e/search.spec.ts b/frontend/e2e/search.spec.ts index 460feab..55f34d8 100644 --- a/frontend/e2e/search.spec.ts +++ b/frontend/e2e/search.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; // E2E for the /search page shipped in v0.62.0. Five scenarios against // mocked endpoints — no backend needed. diff --git a/frontend/e2e/upload.spec.ts b/frontend/e2e/upload.spec.ts index 0b3c161..6143196 100644 --- a/frontend/e2e/upload.spec.ts +++ b/frontend/e2e/upload.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect, type Page } from './fixtures'; const userFixture = { id: 'u1', diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index bd489b5..3bb4a2d 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -11,6 +11,13 @@ const E2E_PORT = 5174; export default defineConfig({ testDir: 'e2e', timeout: 30_000, + // The specs are deterministic in isolation, but a full parallel run + // has a cold-start window: Vite compiles each route on first request, + // so the first tests across 8 workers contend on the dev server and + // can trip the timeout. One retry re-runs those on the now-warm server. + // (The `/api/v1` fallback in e2e/fixtures.ts removes the other flake + // source — unmocked calls stalling against the dead dev proxy.) + retries: 1, use: { baseURL: process.env.E2E_BASE_URL ?? `http://localhost:${E2E_PORT}`, trace: 'retain-on-failure'