Playwright with Jest: Getting Started with E2E Tests

Update, November 2026. I wrote this post in 2020 with Jest and jest-playwright-preset. Today I would start with Playwright’s own runner, @playwright/test. The jest-playwright repository was archived in September 2025, and its README recommends the official runner. The first half of this post now shows the modern setup. The Jest setup is still here, rewritten and checked against Jest 29, for teams that already have a Jest suite.

This post continues First steps with test automation, where I answered the beginner questions: which language, which framework, what to automate first.

Playwright vs Jest: what each one does

Playwright controls a browser: it opens pages, clicks, types and reads what is on the screen. Jest runs tests and checks results: it finds test files, runs them, and reports passes and failures. You need both jobs done, and @playwright/test does both. Jest only does the second one, so it needs Playwright next to it.

@playwright/test Jest + jest-playwright-preset
Controls the browser Yes Yes (Playwright library)
Test runner Built in Jest
Assertions that wait and retry Yes (expect(locator)) No, you wait by hand
Parallel runs Built in, per file Jest workers
Fresh browser context per test Yes, page fixture Shared page unless configured
Multi-browser config projects in the config browsers in jest-playwright.config.js
Status Maintained by the Playwright team Package works, repository archived, peer dependency is Jest 29

The Playwright docs say it directly: for end-to-end testing you want @playwright/test, not the bare playwright library, because the runner adds web-first assertions that auto-wait, isolated page and context fixtures, and configuration. In 2020 I wrote that I had never used Playwright’s assertions. The ones that auto-wait are the reason I switched.

Puppeteer is a similar browser library from Google. The LogRocket comparison covers the differences.

What we will test

A short flow on the Playwright website, because its page structure is stable and documented:

  • load the home page,
  • check that the title mentions Playwright,
  • check that a “Get started” link is visible,
  • click it and check that the Installation page opens.

I used my own blog as the target in the 2020 version. Its markup has changed since, so the old selectors no longer apply. That is also a lesson in selectors that survive changes.

You should know basic JavaScript or TypeScript, be able to use the CLI and npm, and be familiar with HTML.

Project setup with @playwright/test

The quickest route is the official initializer:

npm init playwright@latest

It asks about TypeScript, the test folder and whether to add a GitHub Actions workflow, then installs the browsers. To do it by hand:

npm init -y
npm install -D @playwright/test
npx playwright install

Add a config in the project root:

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});

projects replaces the browsers array from the Jest config. Each project runs every test once.

Writing the first test

Create tests/example.spec.ts:

import { test, expect } from '@playwright/test';

test.describe('playwright.dev', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://playwright.dev/');
  });

  test('has a title that mentions Playwright', async ({ page }) => {
    await expect(page).toHaveTitle(/Playwright/);
  });

  test('has a Get started link', async ({ page }) => {
    await expect(page.getByRole('link', { name: 'Get started' })).toBeVisible();
  });

  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();
  });
});

Three things differ from the 2020 version:

  • Locators such as getByRole find elements the way a user would, by role and visible name, instead of css=.post-title. The Playwright best practices say tests should interact with what the user sees, not with implementation details such as CSS classes.
  • expect(...) assertions retry until they pass or time out, so there is no manual waitForLoadState('networkidle'). Playwright’s docs discourage networkidle for tests.
  • page is a fixture. Every test gets a fresh browser context, so tests do not leak cookies into each other.

Run it:

npx playwright test

I ran this exact file against playwright.dev with Playwright 1.64 and all three tests passed in Chromium. npx playwright test --ui opens the UI mode with a timeline, and npx playwright show-report opens the HTML report after a run.

Bonus: record tests with codegen

The recorder I linked in 2020, playwright-cli, has been folded into the main package. Run:

npx playwright codegen https://playwright.dev/

A browser and an inspector window open. Click through the flow and Playwright writes the test code, using the same role-based locators. Treat the output as a draft. Rename things, add assertions on what matters, and delete clicks that were accidents.

The Jest setup (for existing Jest suites)

If your team already runs Jest and you want to add browser tests, this still works. Keep in mind that the preset’s peer dependency is Jest 29, and that the repository is archived.

npm install -D jest@29 jest-playwright-preset playwright
npx playwright install
// jest.config.js
module.exports = {
  preset: 'jest-playwright-preset',
  testMatch: ['**/tests/**/*.test.js'],
};
// jest-playwright.config.js
module.exports = {
  browsers: ['chromium', 'firefox', 'webkit'],
  launchOptions: {
    headless: true,
  },
};

Add "test": "jest" to scripts in package.json. The preset exposes a global page, so the same flow looks like this:

// tests/example.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);
  });
});

Note the extra waitFor(). isVisible() answers immediately and does not wait, which is the exact thing Playwright’s own expect handles for you. I checked this version with Jest 29.7, jest-playwright-preset 4.0.0 and Playwright 1.64 in Chromium. The 2020 selectors and page.$$eval('css=...') calls are gone.

If you use Puppeteer with jest-puppeteer instead, the API differs slightly: drop the css= prefix and use waitForNavigation in place of waitForLoadState.

Conclusion

Use @playwright/test for a new project. It gives you the browser control, the runner, and assertions that wait, with one config file. Keep Jest for the suites that already have it. Once the first test runs, the next questions are where to keep the tests (in the same repository or a separate one) and what kind of check each one is (UI, API, screenshot or performance).

The original 2020 Jest version of this code is in Kodziak/playwright-first-steps. It targets my old blog markup, so treat it as history.

Have a nice day!