Skip to content

Playwright Testing

This lesson explains how to use Playwright to test Web Components in real browsers, including custom element interactions, Shadow DOM queries, end-to-end flows, and accessibility checks.

Why Use Playwright for Web Components?

Playwright runs tests in real browsers, which makes it ideal for Web Components that rely on Custom Elements, Shadow DOM, focus behavior, and custom events. It complements fast unit tests by verifying how components behave in actual pages and user flows.

Use Playwright when you need to test composition, routing, cross-component interaction, visual behavior, or accessibility in Chromium, Firefox, or WebKit.

Concept Description
Real Browser Tests run in Chromium, Firefox, and WebKit
Custom Elements Interact through tags, attributes, and properties
Shadow DOM Query internal nodes with piercing selectors
E2E Flows Validate full user journeys across pages
Accessibility Use built-in axe integration via packages
Best For Critical flows and browser-accurate behavior

Basic Playwright Test Example

<!-- test-page.html -->
<app-counter value="0"></app-counter>
<button id="reset">Reset</button>
// counter.spec.js
import { test, expect } from "@playwright/test";

test("increments custom counter element", async ({ page }) => {
  await page.goto("/test-page.html");

  const counter = page.locator("app-counter");
  await expect(counter).toHaveAttribute("value", "0");

  await counter.click();
  await expect(counter).toHaveAttribute("value", "1");
});

Playwright treats custom elements like normal DOM nodes. You can locate them by tag name, assert attributes, and simulate user interaction in a real browser session.

Playwright Setup

// playwright.config.js
import { defineConfig } from "@playwright/test";

export default defineConfig({
  testDir: "./tests",
  use: {
    baseURL: "http://127.0.0.1:4173",
    trace: "on-first-retry"
  },
  webServer: {
    command: "npm run preview",
    url: "http://127.0.0.1:4173",
    reuseExistingServer: true
  }
});

Point Playwright at a local preview or dev server that registers your custom elements. Most projects serve a demo page or story gallery for component testing.

Testing Custom Elements

test("renders alert banner content", async ({ page }) => {
  await page.setContent(`
    <alert-banner type="success">
      Profile saved
    </alert-banner>
  `);

  await page.addScriptTag({
    path: "./src/components/alert-banner.js"
  });

  const banner = page.locator("alert-banner");
  await expect(banner).toContainText("Profile saved");
  await expect(banner).toHaveAttribute("type", "success");
});

Load the component script in the test page, then interact with the registered custom element through Playwright locators and assertions.

Testing Shadow DOM

test("clicks internal shadow button", async ({ page }) => {
  await page.goto("/demo/app-button.html");

  const host = page.locator("app-button");
  const internalButton = host.locator(":scope >> button");

  await internalButton.click();
  await expect(host).toHaveAttribute("pressed", "true");
});

Playwright supports piercing Shadow DOM with >> selectors. Prefer testing through the host element's public behavior when possible, and use shadow piercing only when necessary.

Testing Custom Events

test("emits item-selected event", async ({ page }) => {
  await page.goto("/demo/item-list.html");

  const selected = await page.evaluate(async () => {
    const list = document.querySelector("item-list");

    return new Promise((resolve) => {
      list.addEventListener("item-selected", (event) => {
        resolve(event.detail);
      });

      list.querySelector('[data-value="pro"]').click();
    });
  });

  expect(selected).toEqual({ value: "pro" });
});

For custom events, use page.evaluate() to attach listeners in the browser context and assert the emitted detail payload matches your component contract.

Testing Keyboard Flows

test("opens dialog with keyboard", async ({ page }) => {
  await page.goto("/demo/app-dialog.html");

  await page.keyboard.press("Tab");
  await page.keyboard.press("Enter");

  const dialog = page.locator("app-dialog");
  await expect(dialog).toHaveAttribute("open", "");

  await page.keyboard.press("Escape");
  await expect(dialog).not.toHaveAttribute("open");
});

Playwright is well suited for keyboard-first testing of dialogs, menus, tabs, and other interactive Web Components that must work without a mouse.

Accessibility Testing with Playwright

import AxeBuilder from "@axe-core/playwright";

test("dialog has no accessibility violations", async ({ page }) => {
  await page.goto("/demo/app-dialog.html");
  await page.locator("app-dialog").click();

  const results = await new AxeBuilder({ page })
    .include("app-dialog")
    .analyze();

  expect(results.violations).toEqual([]);
});

Combine Playwright with axe-core to run accessibility checks in a real browser after opening dialogs, menus, or other dynamic UI.

Visual and Snapshot Testing

test("status badge visual snapshot", async ({ page }) => {
  await page.goto("/demo/status-badge.html");

  const badge = page.locator('status-badge[type="success"]');
  await expect(badge).toHaveScreenshot("status-badge-success.png");
});

Screenshot testing helps catch unintended visual regressions in design system components, especially when CSS custom properties or Shadow DOM styles change.

End-to-End Flow Example

test("user completes signup with custom form controls", async ({ page }) => {
  await page.goto("/signup");

  await page.locator("email-input").fill("alex@example.com");
  await page.locator("password-input").fill("secret123");
  await page.locator("submit-button").click();

  await expect(page.locator("success-toast")).toContainText(
    "Account created"
  );
});

E2E tests validate that custom form elements, buttons, and toast components work together in a real application flow rather than in isolation.

Playwright vs Unit Tests

Type Playwright Unit Tests
Environment Real browser happy-dom / jsdom
Speed Slower Faster
Scope Full pages and flows Single component
Best For Critical user journeys API and lifecycle coverage
Shadow DOM Browser-accurate May differ slightly

Useful Playwright Features

Feature Purpose
page.locator() Find custom elements and internal nodes
page.evaluate() Listen for custom events in the browser
page.keyboard Test keyboard-only interaction
toHaveScreenshot() Detect visual regressions
webServer Start app or preview server for tests
@axe-core/playwright Run accessibility checks in browser

When to Use Playwright

  • You need real browser behavior for Shadow DOM and focus.
  • You want to test complete user flows across pages.
  • Components interact with routing, APIs, or app state.
  • Visual regressions would be costly for your design system.
  • Accessibility must be verified in a real rendering engine.
  • Unit tests pass but production behavior still needs confirmation.

Playwright Testing Use Cases

  • Signup, checkout, and onboarding flows with custom inputs.
  • Modal dialogs and drawer navigation patterns.
  • Tabs, accordions, and menu widgets with keyboard support.
  • Design system demo pages and component galleries.
  • Cross-browser verification for public component libraries.
  • Smoke tests before publishing a shared Web Components package.

Playwright Testing Best Practices

  • Keep E2E tests focused on critical paths, not every detail.
  • Prefer stable locators such as custom element tag names and roles.
  • Load component scripts explicitly in isolated demo pages when needed.
  • Use unit tests for lifecycle and API coverage; Playwright for flows.
  • Test keyboard interaction for dialogs, menus, and form controls.
  • Run accessibility checks after opening dynamic UI.
  • Enable traces and screenshots to debug flaky component tests.

Common Playwright Mistakes

  • Trying to replace all unit tests with slow E2E coverage.
  • Not waiting for custom elements to be defined before interacting.
  • Querying Shadow DOM internals when host-level behavior is enough.
  • Using brittle CSS selectors instead of roles or custom tags.
  • Skipping keyboard and accessibility checks in browser tests.
  • Testing too many variations in one large E2E spec.
  • Not starting a reliable local server for component demo pages.

Key Takeaways

  • Playwright validates Web Components in real browsers and user flows.
  • Use it for E2E, keyboard, visual, and browser-accurate Shadow DOM tests.
  • Custom elements can be located and asserted like normal DOM nodes.
  • Combine Playwright with unit and accessibility tests for full coverage.
  • Reserve Playwright for high-value flows that matter most to users.

Pro Tip

Create a small demo page for each critical custom element and point Playwright at those pages. That gives you reliable browser tests without needing the entire application running for every component check.