E2E Testing in Real Browsers
|
This page documents end-to-end browser testing with Selenium, Playwright, Cypress, and several adjacent tools. Like the other tool-survey pages on this site, no single reference book underpins it: the content was assembled with the assistance of AI from each tool’s own official documentation — Selenium, Playwright, and Cypress — and should be verified against those docs before relying on it in production. |
End-to-end (e2e) browser testing sits at the top of the test pyramid: it drives a real (or real-engine) browser against a running application the way an actual user would, exercising the full stack — UI, network, backend, and any third-party integrations — in a single test run. This page is a deeper, tool-focused reference covering Selenium, Playwright, and Cypress in detail, plus a short survey of adjacent tools. Framework-specific, lightweight e2e examples already live on each framework’s own testing page — React, Next.js, Vue.js, Angular, Vaadin, Thymeleaf, JSF, GWT, JSP and Struts — and are not duplicated here.
Selenium
Selenium is the oldest and most widely supported of the three, and the reference implementation of the W3C WebDriver protocol.
Architecture
Selenium drives a real browser out-of-process: your test code (in whatever language binding you use) sends
WebDriver-protocol commands over HTTP to a browser-specific driver process (chromedriver, geckodriver,
msedgedriver, safaridriver), which in turn controls the actual browser. This is the same architectural
pattern later adopted, in spirit, by WebdriverIO (see Other Options).
Installation
Selenium ships official bindings for Java (Maven/Gradle), Python (pip), JavaScript/Node (npm), C# (NuGet),
Ruby (gem), and Kotlin. Since Selenium 4.6, Selenium Manager is bundled with every binding and
automatically resolves and downloads the driver matching the installed browser version, removing the need to
manage driver binaries by hand. For remote or parallel execution, Selenium Grid (or its Docker images,
selenium/standalone-chrome, or a selenium/hub plus node containers) lets tests run against browsers hosted on
a different machine or distributed across several.
Headless vs. Headed
Headless execution is opt-in via browser-specific flags passed through the language binding’s options object,
e.g. --headless=new for Chrome/Edge (ChromeOptions/EdgeOptions) or -headless for Firefox
(FirefoxOptions). Headed (visible) execution is the default and is normally used only for local debugging.
Evidence Capture
Screenshot capture is manual and on-demand: driver.get_screenshot_as_file(…) in Python, or
((TakesScreenshot) driver).getScreenshotAs(…) in Java. Selenium has no built-in video recording; teams
typically wire screenshot capture into a test-failure hook (e.g. a JUnit/pytest teardown) rather than capturing
evidence unconditionally on every step.
Running Locally
Selenium tests run through whatever test runner the language binding uses — mvn test for Java/JUnit or
TestNG, pytest for Python, dotnet test for C#, and so on.
Running in CI/CD
name: Selenium E2E
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm ci
- run: npm test -- --headless # runs Chrome with --headless=new
- uses: actions/upload-artifact@v4
if: failure()
with:
name: selenium-screenshots
path: screenshots/
A selenium/standalone-chrome service container is a common alternative to installing Chrome directly on the
runner, particularly for non-JS bindings.
Playwright
Playwright, from Microsoft, is a newer entrant built around a single automation protocol shared across browser engines.
Architecture
Playwright talks directly to each browser engine’s own low-level protocol — the Chrome DevTools Protocol (CDP) for Chromium, and each engine’s equivalent for Firefox and WebKit — from a single library, with no separate per-browser driver process to manage. Bindings are available for Node/TypeScript, Python, Java, and .NET.
Installation
npm init playwright@latest scaffolds a config file and example tests in one step; npx playwright install
then downloads Playwright’s own bundled Chromium, Firefox, and WebKit builds (pinned to versions it has tested
against, rather than relying on whatever browsers happen to be installed on the machine). Equivalent bindings
exist for Python (pytest-playwright), Java, and .NET.
Headless vs. Headed
Headless is the default. Headed execution is opt-in per-run (npx playwright test --headed) or per-context
({ headless: false }). UI Mode (npx playwright test --ui) runs tests interactively with a live,
watchable browser and timeline. The Trace Viewer replays a completed run step by step after the fact, with
the full DOM snapshot, network log, and console output available for each step.
Evidence Capture
page.screenshot() captures a screenshot on demand at any point in a test. Beyond that, Playwright’s config
supports capturing evidence automatically and conditionally: screenshot: 'only-on-failure',
video: 'retain-on-failure', and trace: 'on-first-retry' — so passing runs stay lightweight while a failure
or retry keeps everything needed to diagnose it, viewable afterwards in the Trace Viewer.
Running in CI/CD
name: Playwright E2E
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.48.0-noble
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm ci
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
The official mcr.microsoft.com/playwright Docker image ships with every browser and system dependency
preinstalled; without it, npx playwright install --with-deps installs both the browsers and their OS-level
dependencies on a plain runner.
Cypress
Cypress takes a fundamentally different architectural approach from the other two.
Architecture
Cypress runs inside the browser, in the same run-loop as the application under test, rather than driving the browser from an external, out-of-process automation layer. This gives it direct access to the DOM, network layer, and JavaScript runtime of the page under test, at the cost of being JavaScript/TypeScript-only and having historically weaker support for multiple browser tabs and cross-origin navigation (see Comparison).
Installation
A single package, npm install cypress --save-dev, bundles the test runner together with its own Electron
browser; Cypress also detects and can launch locally installed Chrome, Edge, or Firefox. There are no separate
driver binaries or engine downloads to manage.
Headless vs. Headed
cypress run executes headlessly by default and is what CI pipelines use. cypress open launches the
interactive Test Runner headed, with a live, re-playable command log — the primary way Cypress tests are
written and debugged locally.
Evidence Capture
Out of the box, with no configuration: an automatic screenshot on test failure, and a full video recording of the entire run. Optional Cypress Cloud centralizes and shares those artifacts (plus timing and flake data) across CI runs.
Running Locally
npx cypress open # interactive, headed
npx cypress run # headless, e.g. for a pre-push check
Running in CI/CD
name: Cypress E2E
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: cypress-io/github-action@v6
with:
build: npm run build
start: npm start
- uses: actions/upload-artifact@v4
if: always()
with:
name: cypress-evidence
path: |
cypress/screenshots
cypress/videos
The official cypress-io/github-action handles installing Cypress (with caching), starting the app under test,
and waiting for it to be ready, in addition to running the tests themselves.
Comparison
| Aspect | Selenium | Playwright | Cypress |
|---|---|---|---|
Browser/engine coverage |
Broadest: every major browser (Chrome, Edge, Firefox, Safari) plus legacy/older versions via driver support. |
Chromium, Firefox, and WebKit, each Playwright’s own bundled build. |
Chromium-family first (Chrome, Edge, Electron); stable Firefox support since v4.0, but WebKit/Safari support remains experimental. |
Language support |
Multi-language: Java, Python, JavaScript, C#, Ruby (Kotlin projects use the Java binding via JVM interop — there is no separate Kotlin package). |
Multi-language: JavaScript/TypeScript, Python, Java, .NET. |
JavaScript/TypeScript only. |
Execution model, speed, flakiness |
Out-of-process via a driver; generally the slowest of the three; auto-waiting is more limited, so tests lean more on explicit waits. |
Out-of-process via native browser protocols; fast; strong built-in auto-waiting for elements/network significantly reduces flakiness. |
In-browser, in the same run loop as the app; typically the fastest to author against; strong built-in auto-retry/auto-waiting. |
Built-in evidence/debugging tooling |
Manual screenshots only; no built-in video; debugging leans on external tooling and IDE integrations. |
Trace Viewer (full step-by-step replay with DOM/network/console) and UI Mode. |
Time-Travel debugger (a snapshot per command) plus automatic video, in the interactive Test Runner. |
Multi-tab / multi-origin support |
Full native support — switching windows/tabs and navigating across origins is unrestricted. |
Full native support, including multiple browser contexts and cross-origin navigation in one test. |
Historically limited: a single test traditionally could not navigate across origins or drive more than one tab; Cypress has added incremental multi-origin support but it remains less complete than the other two. |
Parallelization / Grid story |
Selenium Grid (self-hosted or Dockerized) for distributing runs across machines/browsers. |
Built-in test sharding ( |
Built-in parallelization via Cypress Cloud (paid for larger usage), or manual sharding across CI jobs. |
Ecosystem maturity / community size |
Oldest and largest ecosystem; the de facto standard for over a decade, with the broadest base of tutorials, third-party integrations, and existing test suites. |
Younger but growing fast; strong first-party documentation and increasingly the default recommendation for new projects. |
Mature and JS-ecosystem-focused; very strong developer experience and community within JS/TS shops specifically. |
For a new project with no existing constraints, Playwright is generally the best default: it combines multi-language support, strong built-in auto-waiting, and best-in-class debugging tooling (Trace Viewer, UI Mode) without Cypress’s single-tab/single-origin limitations. Cypress remains an excellent choice for JS/TS-only teams that value its interactive developer experience and are comfortable with its browser-engine and multi-tab constraints. Selenium is still the right call when a project needs coverage of legacy or niche browsers Playwright and Cypress don’t target, needs a language binding neither of the others offers, or already has significant investment in Selenium Grid infrastructure.
Other Options
A few adjacent tools worth knowing about, without a full treatment here. For end-to-end testing of a mobile app on simulators, emulators and real devices — Detox, Maestro and Appium — see Testing in the React Native section.
Puppeteer (pptr.dev) is Google’s own Node.js library for driving Chromium/Chrome over the DevTools Protocol; it has also shipped stable Firefox support via WebDriver BiDi since v21 (2023), though Chromium remains its primary, more mature target. It’s lower-level than the other tools on this page, and is often the foundation other tools — including, historically, parts of Playwright’s own lineage — build on rather than a typical end-user e2e framework.
WebdriverIO (webdriver.io) is WebDriver-protocol-based like Selenium, but built specifically as a JavaScript-first testing framework with a more modern developer experience layered on top; it can also drive browsers via Puppeteer/CDP as an alternative to the WebDriver protocol.
TestCafe (testcafe.io) requires no WebDriver installation or browser plugin at all — it works by injecting a proxy between the test and the browser, which lets it run against any installed browser without matching driver binaries.
Robot Framework with the Browser library (robotframework.org, built on Playwright under the hood) pairs a keyword-driven, largely non-code test syntax with Playwright’s automation engine — a good fit for cross-language/cross-discipline teams (e.g. QA engineers alongside developers) who want Playwright’s reliability without writing tests as JavaScript/Python/Java/.NET code directly.