Playwright Component Testing
Framework Integration & Mounting Architecture
Playwright Component Testing operates by mounting isolated UI units into a real browser context, bypassing full-page navigation while preserving native rendering pipelines. The architecture relies on framework-specific adapters that bridge Playwright’s execution engine with your component tree. For React projects, initialize the experimental component testing adapter:
npm init playwright@latest -- --ct
npm install @playwright/experimental-ct-react --save-dev
The mount() function serves as the primary entry point. Unlike traditional DOM renderers, it executes within a Vite-powered sandbox that supports hot module replacement and context injection. To guarantee deterministic state propagation, wrap components with required providers at the mount boundary:
import { test, expect, mount } from '@playwright/experimental-ct-react';
import { ThemeProvider } from './theme';
import { UserCard } from './UserCard';
test('renders user card with injected context', async ({ mount }) => {
const component = await mount(
<ThemeProvider mode="dark">
<UserCard id="usr_992" />
</ThemeProvider>
);
await expect(component).toContainText('usr_992');
});
Component boundaries must align with established architectural guidelines for Component & Integration Testing Frameworks to prevent context leakage. Isolate side effects, defer network calls to the test harness, and ensure each mount() invocation receives a clean execution context.
Configuration Steps & Environment Setup
Deterministic execution requires explicit configuration in playwright.config.ts. The ctViteConfig property exposes the underlying Vite instance to the test runner, enabling path alias resolution, CSS module scoping, and static asset mapping. Configure the environment to mirror production bundling behavior while maintaining test isolation:
import { defineConfig, devices } from '@playwright/experimental-ct-react';
import path from 'path';
export default defineConfig({
testDir: './src',
testMatch: '**/*.spec.ts',
fullyParallel: true,
use: {
trace: 'on-first-retry',
ctViteConfig: {
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
},
},
css: {
modules: {
localsConvention: 'camelCase',
},
},
},
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
Static assets and CSS modules are automatically scoped to the isolated test context. If your project relies on complex asset pipelines or custom PostCSS plugins, align your Vite configuration with bundler optimization strategies documented in Vitest Configuration & Setup to ensure consistent parallel execution and cache invalidation across CI environments.
Reliability Tradeoffs & CI Pipeline Rules
Component testing introduces specific reliability constraints. The execution model prioritizes speed by skipping full browser navigation, which inherently limits access to certain browser APIs (e.g., window.location manipulation, cross-origin cookies). Additionally, network interception must be constrained to component boundaries to prevent test pollution.
Enforce strict CI pipeline rules to maintain deterministic outcomes:
# .github/workflows/component-tests.yml
name: Component Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test --project=chromium --grep-invert "e2e"
env:
CI: true
PLAYWRIGHT_TEST_TIMEOUT: 30000
Apply the following execution constraints:
- Test Isolation: Enforce
testMatch: '**/*.spec.ts'in CI to prevent E2E and component suites from competing for browser instances. - Network Mocking: Disable global
intercept()by default. Mock API responses at the component boundary usingpage.route()or dependency injection. - Query Stability: Adopt locator strategies from Testing Library Best Practices to anchor assertions to semantic roles and visible text, eliminating DOM structure flakiness.
Reliability Tradeoffs:
- Execution Speed vs Browser API Access: Component tests execute in ~200-400ms per spec but cannot simulate full navigation state or multi-tab interactions.
- Snapshot Diffs vs Behavior-Driven Assertions: Visual snapshots catch regression quickly but mask logical failures. Prioritize
expect(component).toHaveText(),toBeVisible(), and state-driven assertions over pixel-matching unless UI rendering is the explicit verification target.
Debugging Workflows & Trace Analysis
When component tests fail, deterministic debugging requires structured trace capture and interactive inspection. Enable trace generation selectively to avoid storage bloat while preserving failure diagnostics:
// playwright.config.ts (excerpt)
use: {
trace: 'on-first-retry', // Captures only on CI retry or local failure
screenshot: 'only-on-failure',
}
Execute interactive debugging sessions using the Playwright UI mode, which exposes a live component inspector and DOM tree:
npx playwright test --ui --project=chromium
Within the UI inspector, use page.pause() to halt execution at specific lifecycle points:
test('debug hydration mismatch', async ({ mount }) => {
const component = await mount(<DataGrid rows={mockRows} />);
await page.pause(); // Opens interactive inspector at this exact line
await expect(component.locator('tbody tr')).toHaveCount(5);
});
Analyze generated trace.zip artifacts to identify mount timing bottlenecks, unhandled promise rejections, or network request leaks. For React applications, validate state hydration by asserting against rendered output rather than internal state objects:
await expect(component).toHaveText(/Initial State Loaded/);
await expect(component.locator('[data-testid="loading-spinner"]')).toBeHidden();
Trace analysis should focus on the mount() duration, CSS injection order, and any unexpected console.warn/console.error emissions. Maintain strict separation between test setup and assertion phases to ensure reproducible execution across local and CI environments.