Playwright does not need Jest. @playwright/test is its own runner, with parallel workers, retries, fixtures, assertions and reports built in. Jest is still the right tool for unit and component tests of your code. For browser tests, use Playwright Test, and keep Jest only if you already run a suite on jest-playwright-preset.
In 2020 I wrote a tutorial that combined Jest and Playwright. It worked then, and the setup in it still runs. This post is the decision page for anyone reading that tutorial today and asking whether to follow it or skip to the built-in runner.
Playwright Test vs Jest plus jest-playwright
Facts as of November 2026, checked on npm and in the docs: @playwright/test 1.64.1, Jest 30.5.2 (latest) with Jest 29.7.0 as the line the preset supports, jest-playwright-preset 4.0.0.
@playwright/test |
Jest + jest-playwright-preset |
|
|---|---|---|
| Role | Runner built for browser tests | General JavaScript runner plus a Playwright environment |
| Browser access | page fixture, one fresh context per test |
Global page shared by the file, you reset it |
| Assertions | expect with retrying web matchers such as toHaveTitle, toBeVisible |
Jest expect, no retrying; you wait yourself |
| Parallelism | Files run in parallel by default, fullyParallel for tests |
Files run in parallel by Jest workers; browser lifecycle is yours |
| Multiple browsers | projects in config |
browsers in jest-playwright.config.js |
| Retries | retries in config |
jest.retryTimes |
| Traces and report | Trace viewer, HTML report, screenshots and video on failure | Wire up yourself |
| Sharding | --shard=1/4 |
Jest --shard splits files; browsers are still yours |
| Maintenance | Released with Playwright | npm marks jest-playwright-preset as deprecated and points to @playwright/test; peer dependency is Jest 29 |
| Good at | End-to-end and API tests | Unit tests, module mocks, snapshots, jsdom |
Two rows matter most. The preset’s own npm page now recommends migrating to the built-in runner. And Jest 30 is out while the preset still declares Jest 29, so a Jest-based E2E suite pins you to the older Jest line.
Does Playwright use Jest?
No. Playwright ships a separate runner, @playwright/test. The assertion API looks like Jest’s expect(...).toBe(...), so the syntax feels familiar, but the code underneath is Playwright’s. The structure carries over too: test, describe, beforeAll and beforeEach exist, as test.describe and test.beforeAll.
The older pairing existed because in 2020 Playwright was a library without a runner. You needed something to find test files and run them. That gap closed.
Migration
Here is the test from the 2020 tutorial’s Jest section, moved to Playwright Test. I ran both versions against playwright.dev and both pass.
Before, with Jest:
// jest.config.js
module.exports = {
preset: 'jest-playwright-preset',
testMatch: ['**/jt/**/*.test.js'],
};
// jest-playwright.config.js
module.exports = {
browsers: ['chromium'],
launchOptions: { headless: true },
};
// jt/docs.test.js
describe('playwright.dev', () => {
beforeAll(async () => {
await page.goto('https://playwright.dev/');
});
test('title mentions Playwright', async () => {
expect(await page.title()).toMatch(/Playwright/);
});
test('Get started opens the Installation page', async () => {
await page.getByRole('link', { name: 'Get started' }).click();
await page.waitForURL(/.*intro/);
const heading = page.getByRole('heading', { name: 'Installation' });
await heading.waitFor();
expect(await heading.isVisible()).toBe(true);
});
});
After, with Playwright Test:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
use: { baseURL: 'https://playwright.dev', trace: 'on-first-retry' },
projects: [{ name: 'chromium', use: { browserName: 'chromium' } }],
});
// tests/docs.spec.ts
import { test, expect } from '@playwright/test';
test.describe('playwright.dev', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('title mentions Playwright', async ({ page }) => {
await expect(page).toHaveTitle(/Playwright/);
});
test('Get started opens the Installation page', async ({ page }) => {
await page.getByRole('link', { name: 'Get started' }).click();
await expect(page).toHaveURL(/.*intro/);
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});
});
Run it with npx playwright test. The changes, line by line:
pageis no longer a global. Each test asks for it as a fixture and gets a new browser context, so tests cannot leak cookies into each other. That also removes the reason forbeforeAllnavigation: the 2020 version shared one page across tests in a file, and the second test depended on where the first left it. I changed it tobeforeEach.expect(await page.title()).toMatch(...)becameawait expect(page).toHaveTitle(...). The first reads once. The second retries until the title matches or the timeout ends, which removes a common source of flaky E2E tests (more in Flaky end-to-end tests: the five causes and how to fix each).waitForURLpluswaitForplusisVisiblecollapsed into two assertions. Playwright already waits for the click to settle, so the explicit waits are gone.jest-playwright.config.jsbecameprojects. Adding Firefox and WebKit is two more entries, not a separate preset option.- Retries and tracing are config instead of per-file calls.
Config mapping
| Jest and the preset | Playwright Test |
|---|---|
testMatch |
testDir, testMatch |
browsers: [...] |
projects |
launchOptions |
use: { launchOptions } |
--maxWorkers |
--workers |
jest.setTimeout(ms) |
timeout in config |
jest.retryTimes(n) |
retries |
global page |
page fixture |
shared login code in beforeAll |
a fixture, or storageState |
Run both side by side
During a gradual migration, both runners can live in one repo. Jest’s default test patterns match *.spec.ts and *.test.ts, which is the same thing Playwright looks for, so keep them apart. Put Playwright tests in tests/ or e2e/, and tell Jest to ignore that folder:
// jest.config.js
module.exports = {
testPathIgnorePatterns: ['/node_modules/', '/e2e/'],
};
Then give them separate scripts: "test:unit": "jest" and "test:e2e": "playwright test". Move one file at a time and delete the old one when the new one has been green for a while.
Fixtures instead of globals
The biggest change in daily work is fixtures. A Jest suite on the preset tends to build shared helpers on globals. In Playwright you extend test with what each test needs:
import { test as base, expect, type Page } from '@playwright/test';
export const test = base.extend<{ loggedInPage: Page }>({
loggedInPage: async ({ page }, use) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('demo-password');
await page.getByRole('button', { name: 'Sign in' }).click();
await use(page);
},
});
export { expect };
Tests that need a signed-in user ask for loggedInPage; tests that do not, skip the cost. For larger suites, log in once in a setup project and reuse storageState.
When Jest still fits
- Unit tests. Pure functions, services, reducers. Jest’s module mocking (
jest.mock), fake timers and snapshots are mature, and Playwright does not try to replace them. - Component tests with jsdom through Testing Library. Playwright has an experimental component testing package, which runs components in a real browser. Treat it as an option to try, not a drop-in replacement for jsdom tests.
- A working Jest + preset suite that nobody is fighting. Migrate when you touch it for another reason: you need traces, the Jest 29 pin is in your way, or flakiness is eating time. A rewrite for its own sake costs more than it returns.
- A monorepo where Jest is the single runner by policy. Check whether that policy is worth keeping for browser tests.
The usual split I’d set up for a new project: Jest (or the runner your framework ships with) for unit tests, Playwright Test for anything that needs a browser.
What to do about Puppeteer
If you came from the 2020 tutorial’s Puppeteer note, the comparison there is separate: Puppeteer vs Playwright for end-to-end tests. For test design either way, How to make your UI automation tests resilient to changes still applies unchanged.
FAQ
Do I need Jest to use Playwright? No. Install @playwright/test and run npx playwright test. Jest is only needed if you want one runner for unit and E2E tests, and the preset for that is deprecated.
Is jest-playwright still maintained? npm marks jest-playwright-preset as deprecated and points to @playwright/test. Its peer dependency is Jest 29. Existing suites keep working.
How do I wait properly in a Jest and jest-playwright test? Wait on the element, not on a timer: await locator.waitFor() before you assert, or page.waitForURL after navigation. Plain Jest assertions read once, so the wait has to come first.
Which is faster, Playwright Test or Jest? It depends on the suite and the machine, so measure your own. Browser startup and test count dominate. Playwright’s per-test contexts are cheaper than launching a browser per worker.
Take one Jest browser test file from your repo, port it using the mapping table above, and run both versions for a week before you delete the original.