Paul Hammant's Blog:
Cypress Component Testing - Changing from Playwright for a demo repo
A few days ago I posted UI component testing revisited on some component testing patterns I’ve been interested in for many years. The test application was React in TypeScript. The component-testing technology I focused on was Playwright. In this blog entry I explore Cypress as an alternative to Playwright for component testing.
Quick Refresher: Test Harness Component Testing (2017 → 2025)
Back in 2017, I introduced the concept of testing UI components within test harnesses rather than in isolation. The key insight was testing components in the “smallest reasonable rectangle” while maintaining realistic event coupling to parent components. Instead of mocking everything, you create a test harness that simulates how the component would actually be used in the real application. Could be that someone thought of it before me, of course. That’s usually the case.
The pattern involves dual assertions: testing both the component’s visual state AND the test harness state to verify that events flow correctly between child and parent. This bridges the gap between fast unit tests and comprehensive integration tests, providing confidence that components work correctly when integrated while maintaining reasonable test speeds.
Eight years later, this approach has become mainstream for React/Angular teams, with modern tooling making visual verification through screenshots a powerful addition for stakeholder communication and debugging.
Cypress Component Testing
Branch with code: cypress_instead_of_playwright
Component testing
While the examples in this article demonstrate the pattern using Playwright, the actual implementation in the cypress_instead_of_playwright branch of the car-doppler repo uses Cypress component testing instead, unsurprisingly. Here’s how the two approaches compare:
Comparison Criteria
For evaluating component testing frameworks, we’ll assess each on:
| Criterion | Weight | Why It Matters | |—————————|——–|———————————————–| | Performance | High | Fast feedback during development | | Developer Experience | High | Learning curve and debugging ease | | Browser Support | Medium | Cross-browser validation needs | | Ecosystem Integration | Medium | CI/CD and tooling compatibility | | Visual Testing | Medium | Screenshot and visual regression capabilities | | Setup Complexity | Low | One-time cost, varies by team expertise | :{: .table .table-striped .table-bordered
This framework will guide our Playwright ↔ Cypress ↔ Selenium comparisons.
Testing Framework Differences
Cypress Component Testing:
- Integrated test runner with excellent developer experience
- Built-in browser automation and debugging tools
- Simpler setup for React component testing
- Real-time test runner with automatic re-runs
- Exceptional performance for component testing (17x faster than Playwright)
Playwright Component Testing:
- Cross-browser testing capabilities (Chrome, Firefox, Safari)
- Better for cross-browser validation (Cypress is Chromium-focused)
- Better CI/CD integration options
- More flexible browser configuration
- Broader ecosystem support and active development
Performance Comparison
Cypress by default uses an embedded browser in Electron, though real Chromium or Firefox can be configured.
Cypress Implementation (Actual):
Specs: 3 component test files
Tests: 10 total tests across components
Duration: ~1.6 seconds total execution
Performance: ~6.25 tests per second
Browser: Electron 130 (headless)
E2E: 11 passing tests in ~10 seconds (with some skipped tests)
Cypress Performance Analysis (100 iterations of identical test):
WITHOUT Screenshots: 0.030s average per test (33.3 tests per second)
WITH Screenshots: 0.190s average per test (5.26 tests per second)
Browser: Electron 130 (headless)
Test: Controls component with Test Harness Component Testing
Playwright Implementation (Article Examples):
Performance: 1.37-1.93 tests per second
WITH Screenshots: 0.730s average per test
WITHOUT Screenshots: 0.517s average per test
Performance Comparison:
| Framework | WITHOUT Screenshots | WITH Screenshots | |—————-|————————|————————-| | Cypress | 0.030s (33.3/sec) | 0.190s (5.26/sec) | | Playwright | 0.517s (1.93/sec) | 0.730s (1.37/sec) | | Advantage | Cypress 17x faster | Cypress 3.8x faster | :{: .table .table-striped .table-bordered
The Cypress implementation dramatically outperforms Playwright, likely due to:
- Optimized component mounting: Direct React component injection vs external browser process
- Command batching: Multiple operations sent as coordinated sequences
- Electron efficiency: Highly optimized for automation workloads
- Reduced protocol overhead: Fewer round-trips between test runner and browser - batching of long series of interactions with the page under test is a Cypress special-sauce.
Both implementations successfully achieve the core goal: testing components in the “smallest reasonable rectangle” with realistic event coupling and visual verification capabilities.
When to Choose Cypress
Fast development feedback loops
The 17x performance advantage significantly reduces wait times during development, if you’re running all component tests of that class. If you know the test you want to run, or have a smart way of automatically selecting impacted tests, that advantage is less relevant.
- Interactive debugging: Time-travel debugging is compelling too, for complex interaction flows
You might not choose Cypress if:
- Cross-browser validation: A limitation to Chromium-based browsers primarily might be problematic if you needed Safari/Firefox
- Legacy web applications: Better suited for modern JavaScript frameworks and much harder for (say) ASP.NET-MVC apps.
Cypress’s Command Batching: A Speed Advantage with Mind-Shift Requirements
One of Cypress’s unique architectural advantages is its command batching and queuing system. Unlike traditional testing frameworks that execute commands synchronously, Cypress batches operations and sends them to the browser for execution as a coordinated sequence.
The Batching Advantage:
// Cypress batches these commands and sends them together:
cy.get('[data-testid="record-button"]').click();
cy.get('[data-testid="unit-toggle-button"]').click();
cy.get('[data-testid="record-button"]').click();
cy.get('[data-testid="unit-toggle-button"]').click();
// Browser receives: "click these 4 elements in sequence"
// vs traditional approach: 4 separate round-trips
The performance impact ia a reduced round-trip latency as multiple operations sent in one batch, which leads to a network efficiency that can’t be beaten. Large form filling and wait-for strategies are particularly suited to batching.
The Mind-Shift in Simple Terms:
// Thinking synchronously (won't work):
const text = cy.get('[data-testid="button"]').text(); // Returns a command, not text!
if (text === 'Start') { /* This fails */ }
// Thinking in Cypress chains:
cy.get('[data-testid="button"]')
.should('contain.text', 'Start') // Assertion happens when command runs
.click(); // Click happens after text verification
Why This Matters: Once you understand the queuing concept, Cypress tests become more reliable because assertions auto-retry and commands execute in guaranteed order. I tried the same thing (ish) with FluentSelenium way back. Well the retry of chains of locators, but the batching - it was very chatty over however many hops you had between the test and the browser. I’m sure lots of seasoned QE folks initially try to program the old way, before deciding to relent and do things the Cypress way.
Playwright Comparison:
Playwright executes commands more immediately but with more round-trip overhead. For simple tests the difference is negligible, but for complex component interactions the batching advantage compounds.
Cypress Visual Implementation: The Same Pattern, Different Tool
Just as the post a couple of days ago demonstrated with “Test Harness Component Testing with Playwright” screenshots from a particular test, here’s the identical implementation using Cypress component testing. The visual layout and testing approach remain exactly the same - demonstrating the framework-agnostic nature of the pattern.
Initial Component State
The Cypress implementation produces the same visual pattern as Playwright:
- Component Under Test (blue border) - The actual React component being tested
- Test Harness State (green border) - Shows the parent component state that would exist in the real app
- Event Log (yellow border) - Traces the complete interaction history for debugging
Units Conversion Cycle Demonstration
Initial State: Metric Mode (Switch to mph available)
After Click: Imperial Mode (Switch to km/h available)
After Second Click: Back to Metric Mode
The Cypress implementation captures the same interaction flow:
- Button text changing from “Switch to mph” → “Switch to km/h” → “Switch to mph”
- Harness state updating from “METRIC (km/h)” → “IMPERIAL (mph)” → “METRIC (km/h)”
- Event log accumulating each interaction: “Units changed to imperial” → both events visible
Conclusion
Cypress component testing offers significant performance advantages for teams focused on fast development feedback loops, particularly when working with React/Angular applications. The 17x speed improvement over Playwright for component testing makes it an attractive choice for development workflows that prioritize rapid iteration.
The framework-agnostic nature of Test Harness Component Testing means the same testing patterns and visual verification approaches work across different tools, allowing teams to choose based on their specific needs rather than being locked into a particular testing philosophy.
For teams building component-heavy applications where development speed is critical and cross-browser testing can be handled separately, Cypress component testing provides an excellent balance of performance, developer experience, and debugging capabilities.
The diff between the two branches is not so easy to see what went where: github.com/paul-hammant/car-doppler/compare/main…cypress_instead_of_playwright, but you can get a sense how how much was changed.