Angular E2E Testing in 2026: Playwright vs Cypress vs WebdriverIO [2026]
Unit and component tests tell you that the pieces work. They can't tell you that the login redirect lands on the right page, that the production build serves the lazy chunk the checkout route needs, or that the cookie banner doesn't cover the "Pay" button on a small screen. End-to-end (E2E) tests drive the real app in a real browser, the way a user does, and they catch exactly the failures that slip between well-tested units. They're also slower and more brittle than anything else in your suite, so the choice of tool and the choice of what to test matter more here than anywhere.
This is lesson 11.5 of the Angular Tutorial, and the last lesson of Module 11. It follows lesson 11.4 on testing pipes, directives and HTTP services. You'll learn how ng e2e works, how Playwright, Cypress and WebdriverIO compare for Angular apps, how to add each one, which scenarios belong in E2E rather than in component tests, and how to keep an E2E suite fast and trustworthy in CI.
Where E2E fits #
| Test type | Runs | Knows about | Typical speed | Catches |
|---|---|---|---|---|
| Unit (11.2, 11.4) | Node, DOM emulation | One function, service or pipe | Milliseconds | Logic errors |
| Component (11.3) | Node or Vitest browser mode | One component and its template | Milliseconds to tens of ms | Rendering, inputs/outputs, interactions |
| E2E | A real browser against a running app | Only what's on screen and the URL | Seconds per test | Routing, integration, build and configuration problems, real user flows |
The practical rule is the familiar pyramid: many unit and component tests, a focused set of E2E tests for the flows that make or lose you money. If a scenario can be verified by rendering one component, it doesn't need E2E.
How ng e2e works #
ng e2e runs whatever builder is configured as the project's e2e target. New projects don't include one, so the first time you run it, the CLI asks which tool you want and installs it:
| Tool | Installed with |
|---|---|
| Playwright | ng add playwright-ng-schematics |
| Cypress | ng add @cypress/schematic |
| WebdriverIO | ng add @wdio/schematics |
| Nightwatch | ng add @nightwatch/schematics |
| Puppeteer | ng add @puppeteer/ng-schematics |
You can run the ng add command directly instead of going through the prompt. Each schematic adds its config file, an example test and an e2e target in angular.json, so ng e2e builds or serves the app and runs the suite. These schematics are maintained by each tool's own team or community, not by the Angular team, so check the schematic's repository for compatibility with your Angular version.
You don't have to use ng e2e at all. Playwright and Cypress both work well with their own CLIs pointed at a running dev server or a deployed preview URL, which is often simpler in CI.
The three main options #
Playwright #
Playwright drives Chromium, Firefox and WebKit from a test process outside the browser. Its test runner (@playwright/test) has parallel workers, retries, sharding, fixtures and HTML reports built in, and its trace viewer records a timeline of each test with DOM snapshots, network calls and console output.
npm init playwright@latest # or: ng add playwright-ng-schematics
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
retries: process.env.CI ? 2 : 0,
use: { baseURL: 'http://localhost:4200', trace: 'on-first-retry' },
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile', use: { ...devices['Pixel 7'] } },
],
webServer: {
command: 'npm run start',
url: 'http://localhost:4200',
reuseExistingServer: !process.env.CI,
},
});
// e2e/checkout.spec.ts
import { test, expect } from '@playwright/test';
test('a guest can buy a product', async ({ page }) => {
await page.goto('/products/desk-lamp');
await page.getByRole('button', { name: 'Add to cart' }).click();
await page.getByRole('link', { name: 'Cart (1)' }).click();
await page.getByRole('button', { name: 'Checkout' }).click();
await page.getByLabel('Email').fill('guest@example.com');
await page.getByRole('button', { name: 'Place order' }).click();
await expect(page).toHaveURL(/\/orders\/\w+/);
await expect(page.getByRole('heading', { name: 'Thank you' })).toBeVisible();
});
Locators such as getByRole and getByLabel, and web-first assertions like toBeVisible(), retry automatically until they pass or time out. That removes most of the manual waiting that makes E2E tests flaky. Playwright is also what Vitest browser mode uses under the hood when you install @vitest/browser-playwright (lesson 11.1), so one browser install can serve both.
Cypress #
Cypress runs your tests inside the browser alongside the app, with an interactive runner that shows each command, the DOM at that step and time-travel debugging. Its commands are chained and retried automatically.
ng add @cypress/schematic
// cypress/e2e/checkout.cy.ts
describe('checkout', () => {
it('lets a guest buy a product', () => {
cy.visit('/products/desk-lamp');
cy.contains('button', 'Add to cart').click();
cy.contains('a', 'Cart (1)').click();
cy.contains('button', 'Checkout').click();
cy.get('input[name=email]').type('guest@example.com');
cy.contains('button', 'Place order').click();
cy.url().should('match', /\/orders\/\w+/);
cy.contains('h1', 'Thank you').should('be.visible');
});
});
Cypress also offers component testing for Angular: it mounts a single component in a real browser with its own runner. That's an alternative to Vitest browser mode if your team already standardises on Cypress. Its in-browser architecture is the source of both its strengths (direct access to the app, network stubbing with cy.intercept, excellent interactive debugging) and its limits (multi-tab and multi-origin flows need extra handling). Parallelisation across CI machines and recorded dashboards are part of its paid Cypress Cloud service.
WebdriverIO #
WebdriverIO automates browsers through the WebDriver standard and WebDriver BiDi, and it can drive mobile apps through Appium. That makes it a strong fit when your test matrix includes real devices, cloud browser grids, or native and hybrid mobile apps alongside the Angular web app.
ng add @wdio/schematics
// e2e/checkout.e2e.ts
describe('checkout', () => {
it('lets a guest buy a product', async () => {
await browser.url('/products/desk-lamp');
await $('button=Add to cart').click();
await $('a*=Cart').click();
await $('button=Checkout').click();
await $('input[name=email]').setValue('guest@example.com');
await $('button=Place order').click();
await expect(browser).toHaveUrl(expect.stringContaining('/orders/'));
});
});
Comparison #
| Playwright | Cypress | WebdriverIO | |
|---|---|---|---|
| Architecture | Out-of-process, browser automation protocols | In-browser, runs next to the app | WebDriver / WebDriver BiDi |
| Browsers | Chromium, Firefox, WebKit | Chrome-family, Firefox, Electron; WebKit experimental | Any browser with a WebDriver implementation; mobile via Appium |
| Parallel runs | Built-in workers and sharding | Via Cypress Cloud, or third-party tooling | Built-in, plus cloud grids |
| Debugging | Trace viewer, UI mode, inspector | Interactive runner with time travel | Logs, REPL, reporter ecosystem |
| Multi-tab / multi-origin | Supported directly | Possible with cy.origin, more constraints |
Supported |
| Component tests for Angular | Use Vitest browser mode with the Playwright provider | Yes, built in | Use Vitest browser mode with the WebdriverIO provider |
| Best fit | Cross-browser web E2E with fast CI | Teams that value the interactive runner | Mixed web and mobile, existing Selenium-style grids |
For a new Angular web app without existing constraints, Playwright is a sensible default because it pairs with the Vitest browser mode you may already use. If your team already has a working Cypress or WebdriverIO setup, that's usually a better reason to keep it than any feature difference.
What to put in E2E tests #
Good E2E candidates share one property: they cross boundaries that no smaller test crosses.
- Critical journeys: sign-up, login, checkout, the main "create" flow of your product.
- Routing and guards: deep links, redirects after login, a guard that blocks an unauthenticated user (see lesson 5.3).
- Production-only behaviour: lazy chunks, service worker caching, SSR and hydration (run against
ng buildoutput, not the dev server). - Third-party integration points: payment widgets and identity providers in their sandbox modes, or stubbed at the network level.
Poor candidates: validation messages for every form field, formatting of each table cell, every branch of a component's @if. Those are component tests, and they'll run thousands of times faster.
Keeping E2E reliable in CI #
| Practice | Why |
|---|---|
Select by role, label or data-testid |
Survives styling and markup refactors; role and label selectors also check accessibility |
| Never use fixed sleeps | Rely on auto-waiting locators and assertions; sleeps are both slow and flaky |
| Seed test data per test | Create the user or order through an API call in setup, so tests don't depend on each other or on run order |
| Stub only what you don't own | Keep your own backend real (or a realistic test instance); stub external services at the network layer |
| Run against a production build | ng build output behaves differently from the dev server; test what you ship |
| Capture artifacts on failure | Traces, screenshots and videos turn "flaky in CI" into a diagnosable failure |
| Shard large suites | Split across CI machines once the suite takes more than a few minutes |
Retries are a safety net, not a fix. Track tests that pass only on retry and treat them as bugs, either in the test or in the app.
Gotchas #
| Symptom | Cause | Fix |
|---|---|---|
| Tests pass locally, time out in CI | Dev server not ready, or browsers not installed | Use the runner's webServer/start-server option; install browsers in the CI image |
| Random failures on clicks | Fixed waits or clicking before the element is actionable | Auto-waiting locators and web-first assertions; remove sleep |
| Tests fail when run in a different order | Shared data between tests | Seed data per test via API; isolate browser storage |
| Works on dev server, breaks in production | Different build, base href or SSR | Run E2E against ng build output or a preview deployment |
| Selectors break after a redesign | CSS-structure selectors | Role, label or data-testid selectors |
ng e2e says no target configured |
No E2E builder installed | Run ng e2e and choose a tool, or ng add one directly |
What's next #
That completes Module 11. You now have the full testing stack: Vitest and ng test, signal and effect tests, TestBed and harnesses for components, focused tests for pipes, directives and HTTP, and an E2E layer for the journeys that matter. Module 12 changes direction and looks at AI-assisted Angular development, starting with what the Angular CLI's MCP server does and how coding assistants use it.
Try it yourself #
Write down the three user journeys that would hurt most if they broke tonight. Add one E2E test for the most important one with the tool your team prefers, using only role and label selectors, and run it against a production build. Then check whether any existing E2E test only verifies something a component test could cover.
list_projectsYour workspace has one application, storefront, with no e2e target, and its unit tests already use Vitest with --browsers=chromiumHeadless for a few specs. That makes Playwright the lower-friction choice: you already install its Chromium build, and you need Safari coverage, which Playwright gives you through WebKit. Run ng add playwright-ng-schematics to get an e2e target, or npm init playwright@latest if you’d rather run it with its own CLI. In playwright.config.ts, set webServer to serve the production build, enable trace: 'on-first-retry', and add a WebKit project. Start with the checkout journey only, selected by role and label. Cypress would also work; choose it instead if your team values its interactive runner more than WebKit coverage.Up next in Angular
More from this topic
Enjoyed this article?
Get new Angular tutorials delivered. No spam — just code-first articles when they ship.
1 comment



Pingback: Angular Animations: animate.enter and animate.leave [2026]