Lesson 3 of 4 · Playwright Deep-Dive

Component Testing with Playwright

Playwright’s component testing lets you mount and test React, Vue, and Svelte components in a real browser — without a full app. Faster than E2E. More realistic than JSDOM. The sweet spot for complex UI components.

Playwright Deep-Dive CTAL-TAE — Lesson 3 of 4 ~25 min read · ~50 min with exercises

Senior engineer insight

The moment that changed how I think about component testing was discovering that Playwright CT gives you a real Chromium instance, not a simulated DOM. That distinction matters more than most teams realise: CSS custom properties, focus management, scroll behaviour, and ResizeObserver all work exactly as they do in production. When our Vue design system team in Wellington migrated complex dropdown and modal components from JSDOM-based tests, they found three genuine rendering bugs that jsdom had silently swallowed for months — bugs that only surfaced on specific viewport widths.

The most common mistake: teams treat the mounted component handle like a page and immediately reach for page.locator() instead of component.locator(). Tests silently pass because the element exists somewhere in the minimal HTML shell, not because the component renders it correctly.

From the field

A Christchurch fintech team had 60 React component tests running beautifully in Jest/JSDOM — so when the tech lead proposed moving to Playwright CT, there was genuine resistance. "We already have fast unit tests, why add another tool?" They made the switch for their payment summary component after a production incident: a CSS Grid layout bug caused the GST line item to render off-screen on Samsung Galaxy S-series phones, but JSDOM never caught it because it doesn't compute layout at all. The first Playwright CT run on that component, targeting a mobile viewport, caught the regression in 900ms. After that, the team's rule became: use JSDOM for pure logic, use Playwright CT for anything that touches the DOM or layout.

1 The Hook

A Wellington SaaS team has a complex date range picker component used in 12 different places across their app. E2E tests for date scenarios take 45 seconds each: full app load, auth, navigation to the page that contains the picker, then the interaction itself.

They have 8 edge cases to cover: timezone crossing, daylight saving, date order validation, min/max bounds, and four more. That’s 6 minutes of E2E test time for one component.

With Playwright component testing, each scenario runs in 800ms. The component mounts directly in the browser without the full app around it. The full 8 edge cases complete in 6.4 seconds.

45× faster. Same browser. Same assertions. The only difference is isolation.

2 The Rule

Test UI components in isolation when the component has complex logic. E2E tests verify integration; component tests verify the component. Both have their place. Do not replace one with the other.

3 The Analogy

Analogy

Component testing is like testing a car engine on a test bench, not in a car.

You can run the engine at different speeds, check all sensors, simulate every load condition, and inject faulty inputs — without the weight, cost, and complexity of the rest of the vehicle. When the engine passes the bench tests, you put it in the car and run the integration test. Two different things. Both necessary.

Component testing on the bench does not tell you whether the car drives. Integration testing in the car does not easily let you isolate an engine problem. You need both, at different times, for different purposes.

4 Watch Me Do It

Setup — initialise Playwright CT in an existing project:

npm init playwright@latest -- --ct
# Follow the prompts: choose React, Vue, or Svelte

This adds a playwright/index.html entry point and a separate CT config. Your existing E2E tests are unaffected.

The component — a KiwiSaver contribution calculator:

// KiwiSaverCalculator.tsx
interface Props {
  grossSalary: number;
  contributionRate: 3 | 4 | 6 | 8 | 10;
}

export function KiwiSaverCalculator({ grossSalary, contributionRate }: Props) {
  const annual = grossSalary * (contributionRate / 100);
  const employerMatch = annual * 3 / contributionRate;
  return (
    <div data-testid="calculator">
      <p>Annual contribution: <strong>${annual.toFixed(2)}</strong></p>
      <p>Employer match: <strong>${employerMatch.toFixed(2)}</strong></p>
    </div>
  );
}

The component tests:

// KiwiSaverCalculator.spec.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import { KiwiSaverCalculator } from './KiwiSaverCalculator';

test('calculates 3% contribution correctly', async ({ mount }) => {
  const component = await mount(
    <KiwiSaverCalculator grossSalary={80000} contributionRate={3} />
  );
  await expect(component.getByText('$2400.00')).toBeVisible();
});

test('calculates 10% contribution correctly', async ({ mount }) => {
  const component = await mount(
    <KiwiSaverCalculator grossSalary={80000} contributionRate={10} />
  );
  await expect(component.getByText('$8000.00')).toBeVisible();
});

test('all valid contribution rates render without error', async ({ mount }) => {
  for (const rate of [3, 4, 6, 8, 10] as const) {
    const component = await mount(
      <KiwiSaverCalculator grossSalary={60000} contributionRate={rate} />
    );
    await expect(component.getByTestId('calculator')).toBeVisible();
  }
});

Run component tests separately from E2E tests:

npx playwright test --config=playwright-ct.config.ts
Pro tip: Playwright CT is marked experimental. The API is stable enough for production use, but pin your Playwright version and review the changelog on each upgrade. Breaking changes are flagged clearly in the release notes.

5 When to Use It

Good candidates for component testing:

  • Components with complex computation or state logic (calculators, formatters, validators)
  • Date pickers, multi-step forms, currency inputs
  • Components used in many places — high ROI on isolation tests
  • Components where an E2E test takes more than 20 seconds to reach the interaction point

Not suited for component testing:

  • Simple presentational components with no logic (a styled button, a static card)
  • Integration scenarios that require real API calls or routing
  • User journeys that span multiple pages — that is what E2E is for

6 Common Mistakes

✗ I used to think: component tests replace E2E tests.

Actually: they test at a different level. A component test proves the KiwiSaver calculator computes correctly in isolation. An E2E test proves the calculator appears on the right step of the enrolment form, receives the right data, and its output feeds correctly into the next step. You need both. Replacing one with the other leaves a gap.

✗ I used to think: Playwright CT is production-ready stable.

Actually: it is marked experimental. The core API — mount, locators, assertions — is stable enough for real projects, but the CT-specific internals can change between Playwright major versions. Pin your version in package.json and treat CT upgrades with the same care as a framework upgrade.

✗ I used to think: I can use the full page object from my E2E tests in component tests.

Actually: component tests do not have a full page — they have a mounted component. page.goto() does not exist in a CT test. Your locators must target within the mounted component using the component handle returned by mount(). E2E page objects cannot be reused directly; they must be adapted or replaced with component-level helpers.

7 Now You Try

✎ Prompt Lab — AI Exercise

A NZ government form has a GST calculator component that takes a pre-tax amount and returns the amount plus 15% GST, formatted in NZD. Write 4 Playwright component tests covering: correct calculation, boundary value at $0, a very large amount ($1,000,000), and display of the correct currency symbol.

Why teams fail here

  • Running CT tests with the E2E config — the playwright-ct.config.ts is a separate file; mixing the two causes silent test skips and confusing "no tests found" errors that waste hours of CI debugging time.
  • Not providing a realistic mounting context — components that expect a Router, Vuex store, or React Context provider will throw cryptic errors at mount time; stub providers must be set up in playwright/index.tsx or passed as wrappers to mount().
  • Writing CT tests for simple presentational components — a styled button with no logic does not need CT overhead; over-testing at this level bloats the suite without adding signal, and teams then blame CT for being slow.
  • Pinning a stale Playwright version across both the CT and E2E config — the experimental CT internals can diverge from the E2E runtime if you update one config's dependency without the other, producing version mismatch errors that only appear in CI.

Key takeaway

Component testing in Playwright is not a shortcut to avoid E2E tests — it is a precision instrument for the one scenario where E2E is genuinely the wrong tool: verifying complex component logic in a real browser without paying the cost of running an entire application to reach it.

Interview Questions

What NZ hiring managers ask about Playwright component testing.

Q1. What is Playwright component testing and how does it differ from E2E testing?

Strong answer: Playwright component testing mounts individual UI components in isolation inside a real browser — without a full application server. Faster than E2E (no routing, auth, database), but with real browser rendering unlike jsdom. Use it for: components with complex interactions (date pickers, drag-and-drop), components with difficult-to-reach visual states (loading, error, empty), and accessibility testing at the component level before integration.

Q2. How do you mock dependencies in a Playwright component test?

Strong answer: Mount the component with controlled props instead of real data. For components that fetch data, mock network calls using page.route() — it works the same as in E2E tests since the component runs in a real browser. For context providers (React Context, Vuex), wrap the component in the provider with test-specific values. The key principle: component tests must be deterministic — the same props always produce the same rendered output.

Q3. When would you choose Playwright component testing over jsdom unit tests?

Strong answer: Choose Playwright component testing when correctness depends on real browser rendering: CSS layout, overflow behaviour, Web APIs (ResizeObserver, IntersectionObserver), touch events, or SVG. jsdom does not render CSS and fakes many browser APIs — tests pass while the component is broken in real browsers. Component tests are typically 10-50x faster than E2E tests for the same assertion while providing the real-rendering guarantee that jsdom cannot.

8 Self-Check

Click each question to reveal the answer.

What is the key difference between a Playwright E2E test and a component test?

An E2E test loads the full application in a browser and navigates to a page to interact with a component in its real context. A component test mounts a single component directly in the browser, without the full application around it. E2E tests verify integration; component tests verify the component in isolation. The speed difference is significant: component tests run in under a second, E2E tests often take 20–60 seconds to reach the interaction point.

When does component testing provide the most value over E2E?

When a component has complex internal logic (calculation, validation, state machines) that has many test cases, and when the E2E setup cost is high (login, navigation, data setup). If each E2E test takes 45 seconds to reach a component and you have 20 test cases for that component, you spend 15 minutes on E2E setup for one component. Component testing makes each case take under a second.

Can you use page.goto() in a component test?

No. Component tests do not have a navigable page. The component is mounted directly into a minimal HTML shell — there is no routing, no full app, and no URL to navigate to. Use the component handle returned by mount() to locate and interact with elements. If you need page.goto(), you need an E2E test, not a component test.

9 ISTQB Mapping

CTAL-TAE Section 3.3 — Test isolation: component-level testing strategies. Component testing is an explicit level in the test automation pyramid, sitting between unit tests and integration tests. The standard recognises that testing components in isolation, before testing their integration, finds defects earlier and at lower cost. Playwright CT is a browser-native implementation of this principle — isolation without sacrificing the fidelity of a real browser environment.