End-to-End Testing with Playwright
Playwright, developed by Microsoft, has rapidly become the default choice for new E2E testing projects. It supports Chromium, Firefox, and WebKit with a unified API, runs tests in parallel without additional configuration, and includes tooling for debugging, tracing, and generating test code. Whether you are starting a new test suite or migrating from Selenium or Cypress, Playwright provides a modern foundation that addresses the most common pain points in browser-based testing.
Step 1: Install Playwright
The fastest way to get started is the Playwright initialization command. Run npm init playwright@latest in your project directory. The installer prompts you to choose between TypeScript and JavaScript, select a test directory name, and optionally add a GitHub Actions workflow file. It then installs the @playwright/test package and downloads browser binaries for Chromium, Firefox, and WebKit.
If you already have a project and want to add Playwright manually, run npm install -D @playwright/test followed by npx playwright install to download the browsers. The browser binaries are stored in a shared cache directory, so multiple projects on the same machine share the same browser installations.
After installation, your project will have a playwright.config.ts file (or .js if you chose JavaScript), a tests/ directory with an example test, and optionally a .github/workflows/ directory with a CI configuration.
Step 2: Configure the Test Runner
The playwright.config.ts file controls how tests are discovered, executed, and reported. Key settings include baseURL (the application URL, so tests can use relative paths), timeout (the maximum time a single test can run before failing), retries (how many times to retry a failed test), and workers (how many tests run in parallel).
The projects array defines which browsers to test against. Each project specifies a browser name and optional launch settings like viewport size, locale, or permissions. A typical configuration includes three projects for Chromium, Firefox, and WebKit, ensuring every test runs on all major browser engines.
The reporter setting controls output format. The default list reporter prints results to the terminal, while the html reporter generates an interactive report with screenshots and traces for failed tests. For CI environments, the junit or json reporters integrate with most CI platforms' test result dashboards.
Step 3: Write Your First Test
Playwright tests use the test function from @playwright/test. Each test receives a page object that represents a browser tab. The page object provides methods for navigation (page.goto()), element interaction (page.click(), page.fill()), and content reading (page.textContent(), page.title()).
A basic test navigates to a page, interacts with elements, and makes assertions. For example, a login test would call page.goto('/login'), use page.getByLabel('Email').fill('user@example.com') to enter credentials, click the submit button with page.getByRole('button', { name: 'Sign In' }).click(), and then assert that the dashboard loaded with await expect(page).toHaveURL('/dashboard').
Playwright's auto-waiting means you rarely need to add explicit wait statements. When you call page.click(), Playwright automatically waits for the element to be visible, enabled, and stable before clicking. Similarly, expect assertions retry until the condition is met or the timeout expires. This eliminates the most common source of test flakiness in older frameworks like Selenium.
Step 4: Use Locators and Assertions
Locators are the heart of Playwright's interaction model. A locator represents a way to find an element on the page, and Playwright evaluates it fresh each time it is used. The recommended locator strategies, in order of preference, are: getByRole (matches accessibility roles like button, link, heading), getByText (matches visible text content), getByLabel (matches form labels), getByPlaceholder (matches placeholder text), and getByTestId (matches data-testid attributes).
Role-based locators are preferred because they reflect how assistive technologies see the page, which means your tests also verify accessibility. A locator like page.getByRole('navigation').getByRole('link', { name: 'Products' }) finds the Products link within the navigation, which is both semantically clear and resistant to CSS class changes.
Playwright provides web-first assertions through the expect API. These assertions automatically retry until the condition is met: await expect(page.getByText('Welcome')).toBeVisible() keeps checking for the text until it appears or the timeout expires. Other useful assertions include toHaveText, toHaveValue, toHaveURL, toHaveCount, and toBeEnabled.
Step 5: Handle Authentication and State
Many E2E tests require a logged-in user. Rather than repeating the login flow in every test, Playwright supports saving and restoring browser state through the storageState feature. You create a global setup script that performs the login once, saves the cookies and localStorage to a JSON file, and then every test loads that state file before running.
In your configuration file, set the globalSetup option to point to a setup script. The setup script uses a browser context to navigate to the login page, enter credentials, submit the form, and then call context.storageState({ path: 'auth.json' }) to save the session. Each test project references this file with storageState: 'auth.json', giving every test a pre-authenticated session without the overhead of logging in each time.
For tests that require different user roles, you can create multiple storage state files (admin.json, editor.json, viewer.json) and assign them to different test projects or test groups. This pattern keeps tests fast while still covering permission-based scenarios.
Step 6: Run Tests in CI/CD
Playwright provides official Docker images that include all supported browsers, making CI setup straightforward. For GitHub Actions, Playwright offers a dedicated action that installs browser dependencies in a single step. For other CI platforms, pulling the mcr.microsoft.com/playwright Docker image provides a ready-to-run environment.
In your CI configuration, the test command is typically npx playwright test with the --reporter=html flag to generate a report artifact. Set the CI environment variable so Playwright optimizes for headless execution. Upload the HTML report and any trace files as pipeline artifacts so developers can debug failures without reproducing them locally.
For large test suites, use the --shard flag to split tests across multiple CI machines. Running npx playwright test --shard=1/4 on four parallel machines reduces total execution time by roughly 75%. Playwright also supports the --last-failed flag to re-run only tests that failed in the previous run, which speeds up debugging in CI.
Step 7: Debug and Trace Failures
When a test fails, Playwright offers several debugging tools. The --ui flag opens Playwright's interactive UI mode, where you can step through each test action, see the browser state at every point, and inspect the DOM. The --debug flag opens a headed browser with Playwright Inspector, which pauses at each action and lets you explore the page manually.
The trace viewer is especially valuable for CI failures that are hard to reproduce locally. Enable tracing in your configuration with trace: 'on-first-retry', which records a detailed trace (DOM snapshots, network requests, console logs) whenever a test is retried after a failure. Open the trace file with npx playwright show-trace trace.zip to see exactly what happened during the failed test run.
For generating new tests, Playwright's codegen tool opens a browser and records your interactions as test code. Run npx playwright codegen https://your-app.com to start recording. While codegen-produced code usually needs refinement, it provides a useful starting point and helps you discover the correct locator strategies for complex UI elements.
Playwright provides the most complete E2E testing experience available today, with cross-browser support, auto-waiting, parallel execution, and CI-ready tooling. Start with role-based locators and web-first assertions to write tests that are both reliable and easy to maintain.