- Replace .eslintrc.cjs with eslint.config.mjs (ESLint 9 flat config)
using direct eslint-plugin-solid + @typescript-eslint/parser approach
- Add @typescript-eslint/parser to root devDependencies
- Add main/module/types top-level fields to packages/core/package.json
- Add resolve.conditions to packages/core/vite.config.ts
- Create packages/core/tsconfig.test.json for test type-checking
- Remove empty paths:{} from packages/core/tsconfig.json
302 lines
9.9 KiB
Markdown
302 lines
9.9 KiB
Markdown
[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(<LoginForm onSubmit={mockSubmit} />);
|
|
|
|
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(<LoginForm onSubmit={vi.fn()} />);
|
|
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 |