Testing strategy — Vitest, Playwright and Lighthouse CI
Adopt a three-layer testing strategy — component tests with Vitest + Testing Library, E2E journeys with Playwright, and Lighthouse CI for performance and accessibility gates.
Aceito
27 de mai. de 2026
Context
#Phase 1 retired the Pages Router and 9 legacy dependencies. With the architecture simplified to a single App Router tree, the project has zero component tests, zero E2E coverage and no automated Lighthouse gates. The existing CI pipeline has 7 steps — lint, typecheck, unit test (2 files / 7 assertions), build, and two bundle budget checks.
A portfolio is both a product (recruiter / hiring-manager experience) and a demonstration of engineering craft. Missing test coverage and no measurable accessibility or performance contracts undermine both purposes.
Decision
#Add three quality layers in sequence:
### Layer 1 — Lighthouse CI (Phase 2a)
Run Lighthouse against the production build (`npm run build && npm run start`) in a dedicated CI job that gates on `quality` passing.
**Configuration file:** `.lighthouserc.js`
**URL audited:** `/v2` (home — heaviest bundle, WebGL background, largest reputational risk)
**Thresholds:**
| Category | Assertion | Min score | Rationale | |----------------|-----------|-----------|-----------| | Performance | warn | 0.55 | Measured baseline 0.60–0.63; WebGL shader compilation is the dominant cost (655 ms long task on 4× throttled CPU); see Performance baseline note below | | Accessibility | error | 0.95 | EcoVoz (a11y project) is the flagship case study | | SEO | error | 0.95 | `generateMetadata` + OG images are a portfolio signal | | Best Practices | error | 0.90 | Localhost in CI caps HTTPS score at ~0.96; 0.90 avoids false failures |
**Performance baseline note (measured 2026-05-27):**
Locally measured score: **0.60–0.63** (2 runs, Lighthouse 4× CPU throttle).
Root-cause analysis of the two main drivers:
1. **TBT 980–1180 ms** — `@react-three/fiber` (~1085–1310 ms JS execution) + WebGL context initialization (unattributable 655 ms long task). Even with Three.js fully deferred via `dynamic({ ssr: false })` at the shell level, this long task runs during the Lighthouse measurement window. It is inherent to having a WebGL background.
2. **LCP 4.5 s** (on throttled CPU) — The hero `<h1>` has `.jr-reveal` CSS class which sets `opacity: 0` as its initial state. Chrome's LCP algorithm does not count opacity-zero elements as candidates; the first *visible* element becomes LCP. On throttled hardware where JS blocking prevents paint for ~4 s, the small header brand link (`102×20 px`) becomes the LCP element at 4.5 s. In real production (unthrottled), LCP is estimated 1–2 s. This will be fixed in **Phase 3** when Framer Motion replaces `jr-reveal` with animations that start from `opacity: 1`.
The threshold of 0.55 catches genuine regressions (a new heavy synchronous import would push the score below 0.50) without flagging the baseline WebGL cost on every push. **Raise to 0.70 after Phase 3** once the opacity:0 LCP issue is resolved.
**Server ready detection:** `startServerReadyPattern: "Ready in"` — matches Next.js 15's `✓ Ready in Xms` output, stable across patch versions.
### Layer 2 — Component Tests with Vitest + Testing Library (Phase 2b)
Extend the existing Vitest setup with `@testing-library/react`, `@testing-library/user-event`, and `happy-dom`.
Target files: - `src/components/__tests__/Hero.test.tsx` - `src/components/__tests__/ProjectCard.test.tsx` - `src/components/__tests__/CommandPalette.test.tsx` - `src/features/v2/__tests__/shell-header-nav.test.tsx` - `src/i18n/__tests__/routing.test.ts` (expand existing coverage)
Target count: ≥ 40 component test assertions (from current 7).
### Layer 3 — E2E Journeys with Playwright (Phase 2c)
Install `@playwright/test` and `@axe-core/playwright`. Eight critical journey specs:
| Spec | What it guards | |------|---------------| | `navigation.spec.ts` | Home → Projetos → EcoVoz case study → back | | `locale-switching.spec.ts` | URL prefix changes, content updates | | `command-palette.spec.ts` | Ctrl+K opens, search navigates, Escape closes | | `theme-toggle.spec.ts` | Theme persists across routes via cookie | | `contact-links.spec.ts` | External links have correct href and target | | `accessibility.spec.ts` | axe-core on `/v2`, `/v2/projetos`, `/v2/projetos/ecovoz` | | `sitemap.spec.ts` | `/robots.txt` and `/sitemap.xml` return 200 | | `og-image.spec.ts` | `/api/og?title=test` returns 200 with image content-type |
Consequences
#- CI grows from 7 steps to 11 (+ Lighthouse job, + component test expansion, + Playwright job).
- Lighthouse Performance is warn-only, intentionally — this is reviewed rather than blocking. If WebGL is later removed or lazy-loaded, upgrade to `error`.
- The `numberOfRuns: 1` setting trades statistical reliability for speed. For a solo portfolio project this is acceptable; increase to 3 for a team project.
- `temporary-public-storage` upload gives shareable report URLs per CI run without requiring a Lighthouse CI server.
- Playwright adds ~5 minutes to CI; scope to chromium-only to keep wall time under 10 minutes total.
Alternatives Considered
#**Single test layer (Vitest only):** Rejected — component tests cannot catch routing regressions, locale switching bugs or accessibility violations that only manifest in a real browser.
**Jest instead of Vitest:** Rejected — project already uses Vitest; switching adds churn with no material benefit for a Next.js 15 + React 19 codebase.
**Cypress instead of Playwright:** Rejected — Playwright has better Next.js 15 + App Router compatibility, first-class TypeScript, and built-in axe-core integration via `@axe-core/playwright`.