Voltar para engenharia

ADR-006

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.

testingqualitycilighthouseplaywrightvitest

Status

Aceito

Publicado

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–9 s** (on throttled CPU) — Phase 3 investigation (2026-05-29) revealed the actual chain:

(a) `.jr-reveal { opacity: 0 }` excluded `p.jr-hero-summary` from LCP candidacy, making Chrome measure the smaller `102×20 px` brand link instead. This masked the real bottleneck.

(b) Once `opacity: 0` was removed, `p.jr-hero-summary` became the LCP candidate with a 4–9 s render delay. `next/font/google` is already in use (font-display score: 1, no issues). Font loading is **not** the bottleneck.

(c) Lighthouse long-task data shows the real cause: the WebGL background's lazy-loaded Three.js chunks (`@react-three/fiber`) fire long tasks at **t=4930 ms** (629 ms) and **t=5844 ms** (235 ms). When these chunks initialize, the browser recomposes the entire page. Chrome's LCP algorithm re-records the timestamp for all visible elements — including `p.jr-hero-summary` — at the time of that recomposition. The 4–9 s render delay *is* the WebGL recomposition latency.

This was always true; `opacity: 0` was accidentally hiding it by providing an earlier LCP candidate.

**Phase 3.3 — IntersectionObserver gating (measured 2026-05-29, 3 runs):**

After `requestIdleCallback` (partial improvement) and then `IntersectionObserver` on a fixed full-screen sentinel, both approaches showed the same local Lighthouse pattern — median score 53, range 53–72, long tasks still at t≈5250 ms.

Root cause of remaining variance: Three.js execution at 4× CPU throttle takes ~4–5 s. IO/rIC defer the *start* of execution but the shader compilation itself takes so long that the recomposition always lands at t≈5 s. The score variance (53 vs 72) correlates with FCP: when the throttle simulation produces FCP 0.8 s, Chrome closes the LCP window before the recomposition. When FCP 1.1 s, it doesn't. This is inherent to the local throttle simulation, not the loading strategy.

**In production (unthrottled):** Three.js executes in ~1 s instead of ~5 s; IO consistently defers it past the LCP window. Performance score on real hardware is estimated 85+.

The IntersectionObserver approach is kept: saves ~600 kB of Three.js for bounce visitors, correct architecture (canvas loads only when in viewport), and improves real-user experience even when local scores don't reflect it.

The threshold of 0.55 catches genuine regressions without flagging the throttle-specific baseline. **Raise to 0.70+** requires running LHCI against a deployed URL (Vercel preview — no localhost overhead, no 4× throttle on real network) or accepting that the local metric is not representative for this specific pattern.

**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`.