Angular End-to-End Testing with Playwright: Test Like a User

Angular Tutorial: Beginner to Advanced · 18. Quality, security and delivery · 5 min read · Updated

Unit tests check pieces. End-to-end (e2e) tests open your real app in a real browser and click through it like a user. They catch problems no unit test can see: a broken route, a missing provider, a form that does not submit. Playwright is a popular tool for this, with auto-waiting and good debugging.

What you will learn

  • How to install and configure Playwright for Angular
  • How to write stable tests with accessible locators
  • How to isolate tests and run them in CI

Setup

In your Angular project:

npm init playwright@latest

Choose a folder named e2e, and say yes to installing browsers. Then edit playwright.config.ts so Playwright starts your app and knows its address:

// File: playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  use: { baseURL: 'http://localhost:4200', trace: 'on-first-retry' },
  webServer: {
    command: 'npm start',
    url: 'http://localhost:4200',
    reuseExistingServer: !process.env['CI'],
  },
});

webServer runs ng serve for you and waits until the URL answers. Run the tests with:

npx playwright test

Tests for the Task Manager

These tests run against the Task Manager project from earlier. We ran them against that app: all five passed.

// File: e2e/tasks.spec.ts
import { expect, test } from '@playwright/test';

test.beforeEach(async ({ page }) => {
  await page.goto('/');
  await page.evaluate(() => localStorage.clear());
  await page.reload();
});

async function addTask(page: import('@playwright/test').Page, title: string, priority = 'medium') {
  await page.getByLabel('New task').fill(title);
  await page.getByLabel('Priority').selectOption(priority);
  await page.getByRole('button', { name: 'Add task' }).click();
}

test('shows the page title and an empty list', async ({ page }) => {
  await expect(page.getByRole('heading', { name: 'Task Manager' })).toBeVisible();
  await expect(page.getByText('No tasks to show.')).toBeVisible();
});

test('rejects a title that is too short', async ({ page }) => {
  await addTask(page, 'ab');
  await expect(page.getByRole('alert')).toContainText('at least 3 characters');
  await expect(page.getByText('No tasks to show.')).toBeVisible();
});

test('adds a task, completes it and filters the list', async ({ page }) => {
  await addTask(page, 'Write e2e tests', 'high');
  await addTask(page, 'Deploy the app');

  await expect(page.getByRole('status')).toHaveText('2 open of 2 tasks');

  await page.getByRole('checkbox', { name: 'Write e2e tests' }).check();
  await expect(page.getByRole('status')).toHaveText('1 open of 2 tasks');

  await page.getByRole('button', { name: 'Done', exact: true }).click();
  await expect(page.getByRole('listitem')).toHaveCount(1);
  await expect(page.getByRole('listitem')).toContainText('Write e2e tests');
});

test('keeps tasks after a reload', async ({ page }) => {
  await addTask(page, 'Remember me');
  await page.reload();
  await expect(page.getByText('Remember me')).toBeVisible();
});

test('deletes a task', async ({ page }) => {
  await addTask(page, 'Temporary task');
  await page.getByRole('button', { name: 'Delete Temporary task' }).click();
  await expect(page.getByText('Temporary task')).toHaveCount(0);
});

Why these tests are stable

  • Locators by role and label (getByRole, getByLabel) match what users and screen readers see. They survive CSS and markup changes, and they quietly check your accessibility: if a control has no label, the locator fails.
  • Auto-waiting. await expect(...) retries until the condition is true or it times out. Do not add sleep or fixed waits.
  • Isolation. Each test gets a fresh browser context. We also clear localStorage in beforeEach, so tests do not depend on each other or on order.
  • Strict mode. If a locator matches more than one element, Playwright stops with an error. In our first run getByLabel('Write e2e tests') matched both the checkbox and the Delete button (its aria-label contained the same words). Using getByRole('checkbox', { name: ... }) fixed it, and that is the habit to copy.

Debug

npx playwright test --ui          # time-travel UI
npx playwright test --debug       # step through
npx playwright show-report        # HTML report after a run
npx playwright codegen localhost:4200   # record clicks as test code

Use trace: 'on-first-retry' so failed CI runs come with a trace file you can open and inspect.

Test with a real or fake backend

  • For a fast, reliable suite, intercept network calls with page.route() and return fixed JSON.
  • Keep a few tests against a real test backend to catch contract mismatches.
  • Never run e2e tests against production data, and never put real passwords in test code. Use a seeded test user.
// File: e2e/mocked-api.spec.ts
import { expect, test } from '@playwright/test';

test('shows posts from a mocked API', async ({ page }) => {
  await page.route('**/posts', (route) =>
    route.fulfill({ json: [{ id: 1, userId: 1, title: 'Mocked post', body: 'Hi' }] }),
  );
  await page.goto('/posts');
  await expect(page.getByText('Mocked post')).toBeVisible();
});

What to cover with e2e tests

Pick the journeys that make or lose money or trust: sign in, search, checkout, create and save. Do not test every detail. A suite of 10 to 30 reliable tests is better than 300 slow, flaky ones.

Try it yourself

  1. Install Playwright in one of your projects and run the generated example.
  2. Write a test for your own sign-in flow with a mocked API.
  3. Run the suite twice in a row. If any test fails the second time, fix its isolation.

Quick check

  1. Why prefer getByRole over CSS selectors?
  2. Why do you not need sleep in Playwright tests?
  3. How do you keep tests independent?

Next, protect your users in Angular security best practices. See the Playwright documentation.

Frequently asked questions

Playwright or Cypress?

Both work well with Angular. Playwright supports several browsers, parallel runs and multiple tabs out of the box. Pick the one your team knows, and keep the suite small.

My tests pass locally but fail in CI. Why?

Common reasons are slower machines, missing browsers, a server that is not ready, or tests that depend on each other. Use auto-waiting assertions, install browsers in CI, and use traces to see what happened.