The 2026 testing stack: Vitest for units, Playwright for everything else
Jest is dead for new projects. Vitest took the unit test layer. Playwright's component testing is now stable. Here's the setup, the split, and the patterns that make tests worth writing.
The JavaScript testing landscape has consolidated in 2026. Jest is still maintained but almost no new projects reach for it - Vitest is faster, has a nearly identical API, and runs natively in Vite-based projects without configuration. Playwright's component testing graduated from experimental to stable and is now the default choice for anything that requires a real browser. Here's the division of labor we use and the patterns that make the investment worthwhile.
The split: what Vitest owns vs. what Playwright owns
Vitest owns pure functions, Zod schemas, server actions, utility libraries, and any React component that doesn't need a real browser API. It runs in Node with a JSDOM environment, executes in parallel, and outputs results in under a second for most unit suites. Playwright owns anything that touches the real browser: E2E flows, components that need IntersectionObserver or ResizeObserver, clipboard access, Web Workers, and animation-dependent UI. The rule of thumb: if you'd be mocking a browser API to make a Vitest test work, move the test to Playwright instead.
// vitest - colocated with source, pure logic
// src/lib/formatters.test.ts
import { formatCurrency } from './formatters';
test('formats USD correctly', () => {
expect(formatCurrency(1234.5, 'USD')).toBe('$1,234.50');
});
// playwright - lives in e2e/, tests the running app
// e2e/checkout.spec.ts
test('user can complete checkout', async ({ page }) => {
await page.goto('/products/portslayer');
await page.getByRole('button', { name: 'Buy now' }).click();
await expect(page.getByText('Order confirmed')).toBeVisible();
});Vitest setup that's worth doing
The two Vitest config options that have the biggest effect on test quality: `coverage.provider: 'v8'` (faster and more accurate than `istanbul` for modern JS) and `globals: false` (force explicit imports of `expect`, `test`, `describe` - it prevents tests from running in non-test environments if a file accidentally gets imported). Enable `--ui` in development to get the visual test runner that shows real-time coverage and per-file test trees - it's significantly faster to iterate with than the terminal reporter.
Playwright patterns that prevent flakiness
Flaky E2E tests are worse than no E2E tests - they erode trust in the test suite until developers ignore failures. Three patterns prevent most flakiness: use `page.getByRole()` and `page.getByLabel()` instead of CSS selectors (they're resilient to markup changes), use `await expect(locator).toBeVisible()` instead of `waitForTimeout()` (Playwright auto-retries assertions, fixed timeouts don't), and isolate test data by creating fresh fixtures per test rather than sharing state.
The 20/80 coverage rule
The testing approach that actually ships: comprehensive unit tests on all business logic, validation schemas, and pure functions (fast, catches regressions early), plus 20–30 E2E tests covering the critical user journeys (checkout, auth, core feature paths). Trying to achieve 100% E2E coverage produces a slow, brittle suite that developers learn to ignore. 20 reliable Playwright tests that cover the paths that break your business are worth more than 200 tests that fail randomly.
- Run Vitest in watch mode locally and on every PR in CI - it's fast enough that there's no reason not to.
- Run Playwright on PRs against the preview deployment, not localhost - this catches environment-specific issues that local runs miss.
- Use Playwright's `--ui` flag for debugging failing E2E tests - the timeline scrubber and DOM snapshot at each step cut investigation time dramatically.
- Quarantine flaky tests immediately rather than adding retries - a test that needs three attempts to pass is a test that's measuring something it shouldn't be.
Written by Appesto Engineering.