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 |
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.