Components (experimental)
Component testing mounts a React/Vue/Svelte component directly in a real browser and runs Playwright assertions against it — no JSDOM, real clicks, real layout. The key limitation: you can't pass live Node.js objects to the component, only plain data. I use it for testing complex UI components in isolation before wiring them into full e2e flows.
What component testing actually is
Standard Playwright tests open a full page in a browser. Component testing is different: I mount a single component (<OrderForm />) in isolation, pass it props, interact with it, and assert its output — without needing a running server or routing.
The component runs in a real browser (not JSDOM), so CSS, layout, hover states, and scroll behavior all work correctly. The test logic runs in Node.js, and Playwright bridges the two. This is different from unit testing with Vitest which uses JSDOM.
Getting started
I install the framework-specific package (not @playwright/test). For React:
The install creates a few files: playwright-ct.config.ts, playwright/index.html (the HTML scaffold where components mount), and playwright/index.ts (where I add global styles and theme setup). Tests go in src/ alongside component files with .spec.tsx extension.
The index files look like this:
The Node/browser boundary — the key limitation
The most important thing to understand: the test runs in Node.js, the component runs in the browser. Serializable data (strings, numbers, plain objects, arrays) crosses the boundary fine. Non-serializable things (live objects, functions as callbacks that need to return complex data) cannot.
The workaround for complex props: create a test wrapper component that accepts simple props and internally converts them to the complex format the real component needs. This keeps the test boundary clean.
beforeMount hooks — router, store, theme
For components that need a router or Pinia store, I configure them in playwright/index.ts using beforeMount. I can pass per-test configuration from the mount() call via hooksConfig.
Practical tips
Mount inside each test, not in `beforeEach`. It makes tests self-contained and easier to debug. When mount() is in beforeEach, I have to look at two places to understand what the test is working with.
Module mocks (`vi.mock()`, `jest.mock()`) don't affect the component. They run in Node.js but the component runs in the browser. To mock API calls, use router fixture or context.route() instead.
Don't access component instance or its methods. Test from the user's perspective — clicks and visibility checks. If a test breaks when seen from the user's angle, it's a real bug.