DEV Community

Cover image for Testing Angular Components by Properties with Playwright
Vitali Haradkou
Vitali Haradkou

Posted on

Testing Angular Components by Properties with Playwright

Most Angular E2E tests look like this:

await page.locator('[data-testid="submit-btn"]').click();
await page.locator('.user-card:nth-child(2) h2').textContent();
Enter fullscreen mode Exit fullscreen mode

You end up fighting CSS specificity and fragile DOM structure instead of testing Angular behavior. What if you could do this instead?

await page.locator('angular=app-button[label="Submit"]').click();
await page.locator('angular=app-user-card[user.role="admin"]').textContent();
Enter fullscreen mode Exit fullscreen mode

That's exactly what @playwright-labs/selectors-angular provides.

What Is It?

A custom Playwright selector engine that queries Angular components by their actual component properties@Input() values, signals, and nested objects — rather than DOM attributes or CSS classes.

It hooks into Angular's DevTools API (window.ng) which is available in development builds.

Installation

npm install -D @playwright-labs/selectors-angular
Enter fullscreen mode Exit fullscreen mode

Setup

Register the engine in your playwright.config.ts:

import { defineConfig } from "@playwright/test";
import { AngularEngine } from "@playwright-labs/selectors-angular";

export default defineConfig({
  use: {
    /* ... */
  },
});

// Register once at module level
import { selectors } from "@playwright/test";
selectors.register("angular", AngularEngine);
Enter fullscreen mode Exit fullscreen mode

Or use the bundled test fixture which handles registration automatically:

import { test, expect } from "@playwright-labs/selectors-angular";

test("my angular test", async ({ page, $ng }) => {
  // $ng and engine are auto-registered
});
Enter fullscreen mode Exit fullscreen mode

The angular= Selector Syntax

The syntax mirrors CSS attribute selectors:

angular=<component-tag>[property="value"][another="value"]
Enter fullscreen mode Exit fullscreen mode

Operators

Operator Meaning Example
[prop] truthy check [disabled]
[prop="val"] exact match [label="Submit"]
[prop*="val"] contains [type*="ary"]
[prop^="val"] starts with [type^="prim"]
[prop$="val"] ends with [type$="ger"]
[prop~="val"] word in list [classes~="active"]
`[prop\ ="val"]` exact or hyphen-prefix

You can also use regex:

page.locator('angular=app-button[label=/^Sub/i]')
Enter fullscreen mode Exit fullscreen mode

And case-insensitive with i flag:

page.locator('angular=app-button[label="submit" i]')
Enter fullscreen mode Exit fullscreen mode

Nested Properties

Dot notation works for nested objects:

page.locator('angular=app-user-card[user.role="admin"]')
page.locator('angular=app-card[config.theme.color="dark"]')
Enter fullscreen mode Exit fullscreen mode

Multiple Conditions (AND)

Chain multiple attribute blocks — all must match:

page.locator('angular=app-button[type="danger"][disabled]')
Enter fullscreen mode Exit fullscreen mode

The $ng Fixture

The $ng fixture wraps standard Playwright locators with Angular-aware introspection methods:

test("inspects component state", async ({ $ng }) => {
  // Read @Input value
  const label = await $ng("app-button").first().input<string>("label");
  expect(label).toBe("Submit");

  // Read WritableSignal value
  const count = await $ng("app-counter").first().signal<number>("count");
  expect(count).toBe(0);

  // List all @Input names
  const inputs = await $ng("app-button").first().inputs();
  expect(inputs).toContain("label");
  expect(inputs).toContain("disabled");

  // Check directives
  const directives = await $ng("router-outlet").directives();
  expect(directives.some((d) => d.includes("RouterOutlet"))).toBe(true);
});
Enter fullscreen mode Exit fullscreen mode

Narrowing

const buttons = $ng("app-button");
const first = buttons.first();
const second = buttons.nth(1);
const last = buttons.last();
Enter fullscreen mode Exit fullscreen mode

Custom Matchers

Import the extended expect from the package:

import { expect } from "@playwright-labs/selectors-angular";

test("matchers", async ({ page }) => {
  const button = page.locator("app-button").first();

  // Is it an Angular component?
  await expect(button).toBeNgComponent();

  // Does it declare this @Input?
  await expect(button).toHaveNgInput("label");

  // Does the @Input hold this value?
  await expect(button).toBeNgInput("label", "Submit");

  // Does it declare this @Output?
  await expect(button).toHaveNgOutput("clicked");

  // Signal assertions
  const counter = page.locator("app-counter").first();
  await expect(counter).toHaveNgSignal("count");
  await expect(counter).toBeNgSignal("count", 0);
});
Enter fullscreen mode Exit fullscreen mode

Real-World Example

Here's a test that checks a disabled danger button exists and has the right label:

test("danger button is disabled and labeled Delete", async ({ page, $ng }) => {
  await page.goto("/");

  // Use the selector engine to find it
  const btn = page.locator('angular=app-button[type="danger"][disabled]');
  await expect(btn).toHaveCount(1);

  // Use the fixture to read its label
  const label = await $ng('angular=app-button[type="danger"][disabled]').input<string>("label");
  expect(label).toBe("Delete");
});
Enter fullscreen mode Exit fullscreen mode

Compare that to the equivalent DOM-based test:

// Before: fragile, depends on implementation details
const btn = page.locator('[data-testid="delete-btn"]');
await expect(btn).toBeDisabled();
Enter fullscreen mode Exit fullscreen mode

The Angular-aware test is self-documenting — it describes the component contract, not the DOM structure.

Requirements

  • Angular app running in development mode (ng serve or ng build --configuration=development)
  • Playwright 1.40+
  • Angular 14+ (DevTools API required)

Wrapping Up

@playwright-labs/selectors-angular closes the gap between how Angular apps are built (component trees with typed inputs) and how they're tested (brittle CSS selectors). By querying components by their properties, your tests:

  • Break less when the DOM changes
  • Read like documentation
  • Test the Angular contract, not the DOM implementation

Check out the playwright-labs repository for more packages in the ecosystem.

Top comments (0)