[Skip to main content](https://www.pkgpulse.com/blog/vitest-jest-playwright-complete-testing-stack-2026#main-content)
## [TL;DR](https://www.pkgpulse.com/blog/vitest-jest-playwright-complete-testing-stack-2026\#tldr)
**The modern testing stack: Vitest (unit/integration) + Playwright (E2E).** Jest is still running millions of tests across the npm ecosystem, but new projects default to Vitest because it shares Vite's config, runs tests in parallel by default, and is 5-10x faster. Playwright replaced Cypress as the E2E tool of choice — better multi-tab support, less flakiness, and first-class TypeScript. The old stack (Jest + Enzyme/React Testing Library + Cypress) still works, but the new stack (Vitest + Testing Library + Playwright) is faster, simpler, and better.
## [Key Takeaways](https://www.pkgpulse.com/blog/vitest-jest-playwright-complete-testing-stack-2026\#key-takeaways)
- **Vitest**: Jest-compatible API, Vite-native, ~10x faster, TypeScript without setup
- **Jest**: 40M+ weekly downloads (legacy), still excellent, no reason to migrate working tests
- **Playwright**: multi-browser E2E, trace viewer, 80%+ market share in new projects
- **Cypress**: real-time browser view is great DX but slower and less capable than Playwright
- **Testing Library**: the default React component testing approach — framework-agnostic
* * *
## [Unit Testing: Vitest vs Jest](https://www.pkgpulse.com/blog/vitest-jest-playwright-complete-testing-stack-2026\#unit-testing-vitest-vs-jest)
```typescript
// The APIs are nearly identical — migration is usually find-and-replace:
// ─── Jest ───
// jest.config.js
module.exports = {
transform: { '^.+\\.tsx?$': ['ts-jest', {}] }, // setup required
testEnvironment: 'jsdom',
};
// test file:
import { sum } from './math';
describe('math utils', () => {
test('adds two numbers', () => {
expect(sum(1, 2)).toBe(3);
});
it('handles negatives', () => {
expect(sum(-1, -2)).toBe(-3);
});
});
// ─── Vitest ───
// vite.config.ts (reuses existing Vite config!)
import { defineConfig } from 'vite';
export default defineConfig({
test: {
environment: 'jsdom',
globals: true, // optional: makes describe/test/expect global without import
},
});
// test file — identical to Jest:
import { sum } from './math';
describe('math utils', () => {
test('adds two numbers', () => {
expect(sum(1, 2)).toBe(3);
});
});
// Performance comparison (500 unit tests, React project):
// Jest (with ts-jest): 8.4s
// Jest (with babel-jest): 11.2s
// Vitest: 1.8s 🏆
// Why Vitest is faster:
// → Uses esbuild for transforms (same as Vite dev server)
// → Parallel by default (worker threads, one per test file)
// → No separate config for TS — shares Vite's esbuild config
// → Module resolution uses Vite's resolver (no duplicate setup)
```
* * *
## [Component Testing with React Testing Library](https://www.pkgpulse.com/blog/vitest-jest-playwright-complete-testing-stack-2026\#component-testing-with-react-testing-library)
```typescript
// Testing Library works with both Jest and Vitest — same API:
// Setup (Vitest):
// package.json:
{
"test": "vitest",
"dependencies": {
"@testing-library/react": "^15",
"@testing-library/user-event": "^14",
"@testing-library/jest-dom": "^6"
}
}
// vitest.setup.ts:
import '@testing-library/jest-dom/vitest'; // extends expect with toBeInDocument etc.
// vite.config.ts:
export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
},
});
// Component test:
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('submits with email and password', async () => {
const mockSubmit = vi.fn(); // vi.fn() instead of jest.fn()
render();
await userEvent.type(screen.getByLabelText('Email'), 'user@example.com');
await userEvent.type(screen.getByLabelText('Password'), 'password123');
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'password123',
});
});
});
it('shows error for invalid email', async () => {
render();
await userEvent.type(screen.getByLabelText('Email'), 'not-an-email');
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));
expect(screen.getByText(/invalid email/i)).toBeInTheDocument();
});
});
```
* * *
## [E2E Testing: Playwright vs Cypress](https://www.pkgpulse.com/blog/vitest-jest-playwright-complete-testing-stack-2026\#e2e-testing-playwright-vs-cypress)
```typescript
// ─── Playwright ───
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry', // Capture traces on failure
},
projects: [\
{ name: 'chromium', use: { browserName: 'chromium' } },\
{ name: 'firefox', use: { browserName: 'firefox' } },\
{ name: 'safari', use: { browserName: 'webkit' } },\
{ name: 'mobile', use: { ...devices['iPhone 13'] } },\
],
});
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test';
test('user can log in', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'user@example.com');
await page.fill('[data-testid="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toHaveText('Welcome back');
});
// Multi-tab test (Playwright exclusive):
test('cart persists across tabs', async ({ context }) => {
const page1 = await context.newPage();
const page2 = await context.newPage();
await page1.goto('/product/1');
await page1.click('button:text("Add to Cart")');
await page2.goto('/cart');
await expect(page2.locator('.cart-item')).toHaveCount(1);
});
// API mocking in tests:
test('shows error when API fails', async ({ page }) => {
await page.route('**/api/users', route => route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Server error' }),
}));
await page.goto('/users');
await expect(page.locator('.error-message')).toBeVisible();
});
```
* * *
## [Playwright Trace Viewer: Debugging E2E Failures](https://www.pkgpulse.com/blog/vitest-jest-playwright-complete-testing-stack-2026\#playwright-trace-viewer-debugging-e2e-failures)
```bash
# Run tests with trace on failure:
npx playwright test --trace on
# Or configure in playwright.config.ts:
use: { trace: 'on-first-retry' }
# After a failure, view the trace:
npx playwright show-trace test-results/trace.zip
# The trace viewer shows:
# → Screenshot at each action
# → Network requests and responses
# → Console logs and errors
# → DOM snapshots you can inspect
# → Timeline of the test execution
# This replaces hours of debugging with 5 minutes of trace review
# Run specific test in headed mode (see the browser):
npx playwright test --headed auth.spec.ts
# Generate test code by recording browser actions:
npx playwright codegen http://localhost:3000
# → Opens browser, records your clicks, generates test code
```
* * *
## [Complete Testing Stack Setup](https://www.pkgpulse.com/blog/vitest-jest-playwright-complete-testing-stack-2026\#complete-testing-stack-setup)
```bash
# Install everything for the modern stack:
npm install --save-dev \
vitest \
@vitest/ui \ # visual test runner UI
jsdom \ # browser environment for unit tests
@testing-library/react \
@testing-library/user-event \
@testing-library/jest-dom \
@playwright/test
# Install Playwright browsers (one-time):
npx playwright install
# package.json scripts:
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:watch": "vitest --watch",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:all": "vitest run && playwright test"
}
}
# File structure:
src/
components/
Button/
Button.tsx
Button.test.tsx ← unit/integration test (Vitest + Testing Library)
utils/
math.test.ts ← unit test
e2e/
auth.spec.ts ← E2E test (Playwright)
checkout.spec.ts
playwright.config.ts
vite.config.ts ← Vitest config lives here
```
* * *
## [When to Keep Jest](https://www.pkgpulse.com/blog/vitest-jest-playwright-complete-testing-stack-2026\#when-to-keep-jest)
```
Keep Jest when:
→ Existing test suite works — don't migrate for the sake of it
→ Your project doesn't use Vite (Create React App, custom Webpack setup)
→ You use Jest-specific features (jest.spyOn, jest.useFakeTimers) extensively
→ Your team knows Jest deeply and migration would cause disruption
Migrate to Vitest when:
→ New project (always use Vitest)
→ Test suite is slow and painful (10+ second runs for unit tests)
→ You've already migrated to Vite for bundling
→ TypeScript setup with ts-jest is causing friction
Migration process (from Jest to Vitest):
1. npx vitest-migration # automated codemods available
2. Replace jest.fn() → vi.fn()
3. Replace jest.mock() → vi.mock()
4. Update jest.config.js → vitest config in vite.config.ts
5. Run tests: expect ~95% to pass without changes
The compatibility is excellent — most Jest tests run on Vitest unchanged.
```
* * *
_Compare Vitest, Jest, Playwright, and other testing library trends at [PkgPulse](https://www.pkgpulse.com/compare/vitest-vs-jest)._
See the live comparison
[View vitest vs. jest on PkgPulse →](https://www.pkgpulse.com/compare/vitest-vs-jest)
## Comments
### The 2026 JavaScript Stack Cheatsheet
One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.
Get the Free Cheatsheet