QA How-To
Cypress component testing: Examples
Build a runnable cypress component testing example with React, forms, intercepts, routing, timers, modal focus, deterministic fixtures, and CI for 2026.
25 min read | 3,029 words
TL;DR
A strong Cypress component testing example mounts a real component, drives its public UI, controls only external dependencies, and asserts a meaningful outcome. The included React recipes cover props, callbacks, forms, HTTP, routing, timers, focus, and CI with public Cypress APIs.
Key Takeaways
- Register `cy.mount` with TypeScript and import the global styles needed by production components.
- Use callbacks and visible output to test controlled components without reaching into React internals.
- Drive forms through labels and controls, then assert accessible errors and exact callback payloads.
- Use `cy.intercept` to cover success, empty, and failure UI states while keeping provider correctness in API tests.
- Supply fresh memory routing and provider instances instead of importing application singletons.
- Control deliberate debounce timers with `cy.clock` and `cy.tick`, but keep real browser interaction for focus behavior.
- Make the example reproducible in CI and document which guarantees belong at other test layers.
A useful cypress component testing example should be small enough to run from a clean clone and realistic enough to teach state, interaction, requests, providers, and failure diagnosis. This guide builds a React and TypeScript example suite from configuration through CI, then explains why each assertion belongs at the component layer.
The examples use public Cypress APIs: defineConfig, cy.mount, cy.get, cy.contains, cy.intercept, cy.wait, cy.stub, cy.clock, and cy.tick. No proprietary visual command or imaginary helper is assumed. Where a helper appears, its complete implementation is included.
You can copy individual examples into a React plus Vite repository, but treat the tests as patterns rather than a universal architecture. Your components, framework adapter, domain contracts, and accessibility requirements remain the source of truth.
TL;DR
| Example | Boundary under test | Cypress control | Main assertion |
|---|---|---|---|
| Counter | Props and callback | Stubbed callback | Correct next value |
| Signup form | Local validation | User input | Accessible error and submit state |
| Customer search | Browser HTTP | Intercepted responses | Request contract and rendered states |
| Routed link | Router context | Memory history | Correct destination behavior |
| Debounced input | Browser timer | Clock and tick | One delayed callback |
| Modal | Keyboard and focus | Real browser events | Focus entry, Escape, restoration |
1. Create the Cypress Component Testing Example Project
Begin with an existing React, TypeScript, and Vite application. Run the Cypress Launchpad so it detects the framework and bundler and creates the correct support files. This avoids hand-selecting an adapter that does not match the project.
npm install --save-dev cypress
npx cypress open
In the Launchpad, select Component Testing, confirm React and Vite, and review the proposed files. A minimal configuration looks like this:
// cypress.config.ts
import { defineConfig } from 'cypress'
export default defineConfig({
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
specPattern: 'src/**/*.cy.{ts,tsx}',
viewportWidth: 1000,
viewportHeight: 660,
},
})
Keep the generated configuration when it differs for a supported framework version. The important properties are the component block, compatible dev server, and a spec pattern that does not accidentally collect production files or end-to-end tests.
Register the React mount function and import the same global stylesheet the application needs:
// cypress/support/component.ts
import { mount } from 'cypress/react'
import '../../src/index.css'
Cypress.Commands.add('mount', mount)
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount
}
}
}
export {}
Run npx cypress open --component during development. The interactive runner displays the mounted component, command log, and failure location. Use npx cypress run --component in CI. If setup fails, first verify framework detection, Vite aliases, CSS imports, TypeScript inclusion, and lockfile installation.
For the conceptual model behind this setup, read how Cypress component testing works.
2. Example One: Test Props, a Disabled State, and a Callback
Start with a quantity stepper. It receives the current value and boundaries from its parent and reports requested changes through a callback. The component does not own quantity state, so the test should not expect the displayed value to change unless it remounts with new props.
// src/components/QuantityStepper.tsx
type QuantityStepperProps = {
value: number
min: number
max: number
onChange: (next: number) => void
}
export function QuantityStepper({
value,
min,
max,
onChange,
}: QuantityStepperProps) {
return (
<div aria-label="Quantity selector">
<button
type="button"
aria-label="Decrease quantity"
disabled={value <= min}
onClick={() => onChange(value - 1)}
>
-
</button>
<output aria-label="Current quantity">{value}</output>
<button
type="button"
aria-label="Increase quantity"
disabled={value >= max}
onClick={() => onChange(value + 1)}
>
+
</button>
</div>
)
}
// src/components/QuantityStepper.cy.tsx
import { QuantityStepper } from './QuantityStepper'
describe('QuantityStepper', () => {
it('reports an increment within the allowed range', () => {
const onChange = cy.stub().as('onChange')
cy.mount(
<QuantityStepper value={2} min={1} max={4} onChange={onChange} />,
)
cy.get('button[aria-label="Increase quantity"]').click()
cy.get('@onChange').should('have.been.calledOnceWith', 3)
cy.get('output[aria-label="Current quantity"]').should('have.text', '2')
})
it('protects both boundaries', () => {
cy.mount(
<QuantityStepper value={1} min={1} max={1} onChange={cy.stub()} />,
)
cy.get('button[aria-label="Decrease quantity"]').should('be.disabled')
cy.get('button[aria-label="Increase quantity"]').should('be.disabled')
})
})
The first test deliberately confirms that the controlled component keeps displaying 2. That assertion documents its contract: the parent must supply the new value. Avoid reaching into React state or invoking a component function. Real clicks plus a stub make the public boundary visible.
3. Example Two: Exercise Form Validation as a User
Form tests should cover meaningful validation rules, accessibility, and submission behavior without asserting every implementation detail. This example requires a work email and a role. Native labels give the tests stable selectors and users accessible controls.
// src/components/InviteForm.tsx
import { useState } from 'react'
import type { FormEvent } from 'react'
type Invite = { email: string; role: 'viewer' | 'editor' }
export function InviteForm({ onInvite }: { onInvite: (invite: Invite) => void }) {
const [email, setEmail] = useState('')
const [role, setRole] = useState<Invite['role']>('viewer')
const [error, setError] = useState('')
function submit(event: FormEvent) {
event.preventDefault()
if (!email.endsWith('@example.com')) {
setError('Use your example.com work email')
return
}
setError('')
onInvite({ email, role })
}
return (
<form onSubmit={submit}>
<label htmlFor="invite-email">Work email</label>
<input
id="invite-email"
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
<label htmlFor="invite-role">Role</label>
<select
id="invite-role"
value={role}
onChange={(event) => setRole(event.target.value as Invite['role'])}
>
<option value="viewer">Viewer</option>
<option value="editor">Editor</option>
</select>
{error && <p role="alert">{error}</p>}
<button type="submit">Send invite</button>
</form>
)
}
// src/components/InviteForm.cy.tsx
import { InviteForm } from './InviteForm'
describe('InviteForm', () => {
it('explains an invalid email without submitting', () => {
cy.mount(<InviteForm onInvite={cy.stub().as('onInvite')} />)
cy.get('#invite-email').type('person@other.test')
cy.contains('button', 'Send invite').click()
cy.get('[role="alert"]')
.should('be.visible')
.and('have.text', 'Use your example.com work email')
cy.get('@onInvite').should('not.have.been.called')
})
it('submits the normalized user selections', () => {
cy.mount(<InviteForm onInvite={cy.stub().as('onInvite')} />)
cy.get('#invite-email').type('qa@example.com')
cy.get('#invite-role').select('editor')
cy.contains('button', 'Send invite').click()
cy.get('@onInvite').should('have.been.calledOnceWith', {
email: 'qa@example.com',
role: 'editor',
})
cy.get('[role="alert"]').should('not.exist')
})
})
Because labels are associated through htmlFor, users and accessibility APIs can connect them to the controls. That association does not create an aria-label HTML attribute, so the base Cypress examples select the stable control IDs. In a project with Cypress Testing Library, cy.findByLabelText('Work email') is more expressive, but it requires installing and importing that integration.
The rule shown is intentionally illustrative. Production email policies should come from actual requirements, and service-side validation still needs API tests.
4. Example Three: Stub Success, Empty, and Failure Responses
The most valuable cypress component testing example often covers a request-driven component. The UI owns request construction and rendering states, while the service owns authorization, persistence, and search semantics. cy.intercept() lets the component spec control the browser HTTP boundary.
// src/components/CustomerSearch.tsx
import { useState } from 'react'
import type { FormEvent } from 'react'
type Customer = { id: string; name: string }
export function CustomerSearch() {
const [query, setQuery] = useState('')
const [customers, setCustomers] = useState<Customer[]>([])
const [message, setMessage] = useState('')
async function search(event: FormEvent) {
event.preventDefault()
setMessage('Searching')
try {
const response = await fetch(`/api/customers?q=${encodeURIComponent(query)}`)
if (!response.ok) throw new Error('request failed')
const body = (await response.json()) as { items: Customer[] }
setCustomers(body.items)
setMessage(body.items.length === 0 ? 'No customers found' : '')
} catch {
setCustomers([])
setMessage('Customer search is unavailable')
}
}
return (
<section>
<h2>Find customers</h2>
<form onSubmit={search}>
<label htmlFor="customer-query">Name</label>
<input
id="customer-query"
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
<button type="submit">Search</button>
</form>
{message && <p role="status">{message}</p>}
<ul>{customers.map((item) => <li key={item.id}>{item.name}</li>)}</ul>
</section>
)
}
Register the intercept before the action that sends the request:
// src/components/CustomerSearch.cy.tsx
import { CustomerSearch } from './CustomerSearch'
describe('CustomerSearch', () => {
it('encodes the query and renders matching customers', () => {
cy.intercept('GET', '/api/customers?q=Ana%20Lee', {
statusCode: 200,
body: { items: [{ id: 'c-7', name: 'Ana Lee' }] },
}).as('customerSearch')
cy.mount(<CustomerSearch />)
cy.get('#customer-query').type('Ana Lee')
cy.contains('button', 'Search').click()
cy.wait('@customerSearch').its('request.method').should('equal', 'GET')
cy.contains('li', 'Ana Lee').should('be.visible')
cy.get('[role="status"]').should('not.exist')
})
it('renders an explicit empty state', () => {
cy.intercept('GET', '/api/customers*', {
statusCode: 200,
body: { items: [] },
}).as('emptySearch')
cy.mount(<CustomerSearch />)
cy.get('#customer-query').type('Nobody')
cy.contains('button', 'Search').click()
cy.wait('@emptySearch')
cy.get('[role="status"]').should('have.text', 'No customers found')
})
it('does not display stale results after a server error', () => {
cy.intercept('GET', '/api/customers*', {
statusCode: 503,
body: { code: 'UNAVAILABLE' },
}).as('failedSearch')
cy.mount(<CustomerSearch />)
cy.get('#customer-query').type('Ana')
cy.contains('button', 'Search').click()
cy.wait('@failedSearch')
cy.get('[role="status"]')
.should('have.text', 'Customer search is unavailable')
cy.get('li').should('not.exist')
})
})
These tests prove UI request construction and response handling. They do not prove that the deployed API searches correctly. Cover authorization, schema, filtering, and database state with API automation testing practices.
5. Example Four: Supply Router Context Without a Deployed App
A component that renders links or reads route parameters needs router context. For React Router, a MemoryRouter supplies controlled history without a server. This is a component-level contract, not a test of production rewrite rules.
// src/components/OrderLink.tsx
import { Link, useLocation } from 'react-router-dom'
export function OrderLink({ orderId }: { orderId: string }) {
const location = useLocation()
return (
<Link to={`/orders/${orderId}`} state={{ from: location.pathname }}>
View order
</Link>
)
}
// src/components/OrderLink.cy.tsx
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom'
import { OrderLink } from './OrderLink'
function Destination() {
const location = useLocation()
const from = (location.state as { from?: string } | null)?.from
return <p>{`Path: ${location.pathname}; From: ${from ?? 'unknown'}`}</p>
}
describe('OrderLink', () => {
it('navigates to the order and preserves the source path', () => {
cy.mount(
<MemoryRouter initialEntries={['/search']}>
<OrderLink orderId="A-12" />
<Routes>
<Route path="/orders/:id" element={<Destination />} />
</Routes>
</MemoryRouter>,
)
cy.contains('a', 'View order').click()
cy.contains('Path: /orders/A-12; From: /search').should('be.visible')
})
})
This test uses the actual router library and browser click. It keeps server deployment, authentication redirects, and browser refresh out of scope. An end-to-end route test should cover those integration behaviors.
Create a fresh memory router per test. Do not import the production singleton if it carries state between cases. When route construction is pure and complicated, extract and unit-test the URL function separately, then keep one component test for the link behavior.
6. Example Five: Test Debouncing With Controlled Time
Real waits make a debounce test slow and can create timing noise. Cypress can replace browser timers with a controllable clock. Install the clock before mounting because the component may schedule timers during initialization.
// src/components/DebouncedSearchBox.tsx
import { useEffect, useState } from 'react'
export function DebouncedSearchBox({
delayMs,
onSearch,
}: {
delayMs: number
onSearch: (query: string) => void
}) {
const [query, setQuery] = useState('')
useEffect(() => {
if (!query) return
const timer = window.setTimeout(() => onSearch(query), delayMs)
return () => window.clearTimeout(timer)
}, [delayMs, onSearch, query])
return (
<label>
Search
<input value={query} onChange={(event) => setQuery(event.target.value)} />
</label>
)
}
// src/components/DebouncedSearchBox.cy.tsx
import { DebouncedSearchBox } from './DebouncedSearchBox'
describe('DebouncedSearchBox', () => {
it('emits only the final query after the delay', () => {
cy.clock()
cy.mount(
<DebouncedSearchBox delayMs={300} onSearch={cy.stub().as('search')} />,
)
cy.contains('label', 'Search').find('input').type('qa')
cy.tick(299)
cy.get('@search').should('not.have.been.called')
cy.tick(1)
cy.get('@search').should('have.been.calledOnceWith', 'qa')
})
})
cy.tick() advances the installed clock, it does not invent a sleep. Use it only when browser time is a boundary the component intentionally depends on. If the behavior under test involves CSS transition rendering, request scheduling, or an external clock, controlling timers may hide the real issue.
Also test cancellation when unmounting or replacing a query if that behavior is important. Avoid hard-coding the production delay in multiple specs. Pass it as a prop or import a single documented constant, then assert behavior around the boundary.
7. Example Six: Verify Modal Keyboard and Focus Behavior
A modal is a strong component-test candidate because it combines rendering, events, focus, and a callback. The detailed focus-trap algorithm may belong to a well-tested library, but your component still owns entry focus, accessible naming, dismissal policy, and focus restoration in its parent workflow.
Assume ConfirmDialog renders a native <dialog open> with a heading, Cancel button, Delete button, and an onClose callback. A focused spec can drive the public interface:
// src/components/ConfirmDialog.cy.tsx
import { ConfirmDialog } from './ConfirmDialog'
describe('ConfirmDialog', () => {
it('labels the dialog and gives focus to the safe action', () => {
cy.mount(
<ConfirmDialog
title="Delete report?"
onConfirm={cy.stub()}
onClose={cy.stub()}
/>,
)
cy.get('dialog')
.should('be.visible')
.and('have.attr', 'aria-labelledby')
cy.contains('button', 'Cancel').should('be.focused')
})
it('reports dismissal when Escape is pressed', () => {
cy.mount(
<ConfirmDialog
title="Delete report?"
onConfirm={cy.stub()}
onClose={cy.stub().as('close')}
/>,
)
cy.contains('button', 'Cancel').type('{esc}')
cy.get('@close').should('have.been.calledOnceWith', 'escape')
})
})
The example assumes the component implements the stated public callback contract. It does not rely on a private field or framework internals. Whether Escape should close a destructive confirmation is a product decision, so document the expected policy instead of copying it blindly.
Add a parent harness when focus restoration is in scope. Render an Open button that toggles the dialog, open it, close it, then assert the Open button is focused. This verifies a workflow the dialog alone cannot own. For accessibility fundamentals, see accessibility testing for web applications.
8. Organize Fixtures, Selectors, and Test Data
Keep a component spec near the component when that matches repository ownership, or use a dedicated component test tree if the project standard requires it. Consistency matters more than a universal layout. A colocated spec makes review and deletion easier when a component changes.
Inline small response objects when they explain the scenario. Use fixture files for substantial, reused, versioned representations. Large fixtures should name the state they model, not a ticket number. Remove unrelated fields so reviewers can see which contract matters, but keep enough shape to remain realistic.
Use this selector priority:
- Native role or semantic element with accessible name.
- Associated label or visible text.
- Stable domain attribute.
- Explicit
data-cytest contract. - CSS structure only when structure is the requirement.
Base Cypress does not include Testing Library commands. Install @testing-library/cypress and import its command registration if your team wants findByRole and findByLabelText. Do not mix unsupported commands into examples.
Builders are useful for complex props. A buildCustomer({ tier: 'gold' }) function can provide valid defaults and make the relevant override obvious. Keep it deterministic. Random data without a recorded seed creates failures that are difficult to reproduce. Unique data is important for real services, but most isolated component fixtures do not need randomness.
Never put credentials or production customer data in fixtures. Intercepted requests and screenshots can enter CI artifacts. Use synthetic values and review what reporters retain.
9. Diagnose Failures in a Cypress Component Testing Example
When a mount fails before the component renders, inspect bundling first: missing alias, unsupported syntax, wrong framework adapter, CSS processor, environment variable, or import with server-only code. Reproduce with the same component command and dependency lock used in CI.
When an element is absent, inspect the mounted DOM and the state that should produce it. Confirm the request was intercepted, provider values are valid, and selector describes the accessible output. Do not add a wait simply because the command failed. Cypress queries already retry within their timeout when the subject is produced asynchronously.
When a request escapes, check registration order and route matching. The intercept must exist before mount if an effect fetches immediately. Match pathname and query intentionally, alias the request, and inspect the yielded interception. An overly broad wildcard can let an incorrect URL pass.
When a test passes alone but fails in a suite, look for module singletons, reused stores, query caches, clocks that were not restored, global event listeners, and mutable fixtures. Cypress test isolation resets browser state according to configuration, but application state held outside the browser page or in imported modules can still surprise poorly designed harnesses.
When the component looks different from production, compare global CSS, theme, fonts, viewport, feature flags, locale, and container size. Do not compensate with assertions that accept both versions. Make the harness represent one documented state.
Retain screenshots, command logs, browser console output where safe, and the exact spec and environment revision. Debugging flaky Cypress tests provides a deeper failure classification method.
10. Run the Example Suite in CI and Decide What to Keep
Add explicit scripts so contributors do not have to remember command options:
{
"scripts": {
"cy:component:open": "cypress open --component",
"cy:component:run": "cypress run --component"
}
}
A GitHub Actions job can install the locked dependency graph and invoke the same script:
name: component-checks
on:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run cy:component:run -- --browser chrome
The -- forwards the browser option through the npm script. If browser choice is not part of the required matrix, the default Electron browser can be sufficient for fast pull-request feedback. Use additional browsers based on supported-product risk, not ceremony.
Upload failure artifacts using your CI platform's supported artifact action and retention policy. Sanitize request details and screenshots. If you split specs, ensure every worker has independent ports, generated files, and external resources.
Review the suite as the product evolves. Keep component cases that protect meaningful rendering and interaction states. Move exhaustive pure calculations to unit tests. Promote only a few integration-critical scenarios to end-to-end tests. Delete a spec when its behavior is removed instead of preserving it as historical clutter.
Useful health measures are first-attempt reliability, median and tail runtime, failure classification, diagnostic time, and defects caught before deployment. The number of test cases does not reveal whether the suite deserves trust.
11. Review This Cypress Component Testing Example as a Portfolio
A portfolio example should prove that another engineer can install, run, understand, and diagnose the suite. A folder full of green screenshots is not enough. Include the locked dependencies, explicit component scripts, a short architecture note, and the supported Node and browser assumptions. A reviewer should be able to clone the repository, run npm ci, execute the headless command, and see the same behavior.
Choose components that demonstrate different boundaries without becoming a demo catalog. The six patterns in this guide make a coherent set: controlled props and callback, local validation, browser HTTP, router context, deterministic time, and keyboard focus. Each teaches a different testing decision. Adding ten cosmetic components would increase file count without adding engineering depth.
For every spec, be ready to answer five questions:
- What user or parent-component guarantee does this protect?
- Why is a real browser relevant to the behavior?
- Which dependencies remain real and which are controlled?
- What lower and higher test layers complement this case?
- What evidence would help if the test failed only in CI?
Add a README table that maps each example to those answers. Keep setup commands current and avoid claiming support for frameworks or browsers you did not run. If a service response is intercepted, state that the fixture proves UI handling, not provider correctness. Link to a contract or API test if the portfolio includes one.
Review accessibility as part of code quality. Labels should be genuinely associated, dialogs should have names, status messages should have appropriate semantics, and keyboard behavior should be explicit. Do not add ARIA only to make selectors pass. The production component contract comes first, and the test should reward usable markup.
Run the portfolio in CI on every pull request. Configure linting and TypeScript checks before Cypress so basic failures are cheap. Retain failure artifacts for a short documented period, and make sure the synthetic responses contain no copied customer information. A public repository should never include environment credentials, internal hostnames, or screenshots of private systems.
Finally, include one deliberate design note about what you did not test. Explain that pure email-policy combinations belong at a unit or service layer, real customer search semantics belong at the API layer, and server route rewrites belong in end-to-end coverage. This demonstrates risk-based placement. Interviewers are more likely to trust a candidate who can limit a tool than one who says every UI behavior belongs in Cypress.
When extending the example, add a component only if it introduces a new boundary such as drag-and-drop, file input, localization, or a complex provider. Write the contract first, choose deterministic setup, and check the official adapter documentation. A maintained small portfolio is stronger evidence than a large stale repository created for one interview.
Add one failure drill before calling the portfolio finished. Break an intercept path, remove a provider, and make a focus assertion fail. Confirm that the command log and artifact identify the cause without exposing unrelated data. Then restore each defect and verify a clean run. This exercise tests the diagnosability of the harness, not just its happy path.
Code review should examine component production code and tests together. A test that requires a complicated selector may reveal missing semantics. Repeated fixture translation may reveal an unstable service contract. A provider that cannot be constructed independently may reveal tight coupling. Component testing creates value when those design signals improve the application, not only when another check turns green.
Interview Questions and Answers
Q1: Why would you mount a component instead of visiting a page?
Mounting constructs the relevant UI state directly and avoids unrelated deployment, login, and navigation dependencies. It gives faster, more localized feedback for rendering and interaction. I still visit the deployed page for a selected integrated journey.
Q2: Why does the quantity example assert that the displayed value stays unchanged?
The stepper is controlled by its parent. Clicking requests a new value through the callback, but only new props can change the display. The assertion documents that contract and prevents accidental hidden state.
Q3: Why must an intercept be registered before mount sometimes?
Effects can send a request as soon as the component mounts. If the intercept is registered later, the request may already have reached the network. Registration order is therefore part of deterministic setup.
Q4: What does an intercepted component test not prove?
It does not prove that the real service returns the fixture, enforces authorization, persists data, or implements search correctly. It proves the UI request and documented response handling. Contract and API tests cover the provider.
Q5: When should you use cy.clock()?
Use it when browser timers are an intentional boundary, such as debounce, interval, or countdown behavior. Install it before scheduling occurs and advance with cy.tick(). Do not fake time when real scheduling or animation is the risk.
Q6: How do you choose selectors for these examples?
I prefer native semantics, accessible labels, and visible text. Stable domain or data-cy attributes are useful when user-facing text is ambiguous. I avoid layout selectors unless layout structure itself is the contract.
Q7: How do you test provider-backed components?
Create a fresh valid provider instance per test and wrap the component with only required context. Make important provider options visible in the spec or a small named harness. Avoid shared stores and hidden universal setup.
Q8: What makes this suite CI-ready?
It uses locked dependencies, explicit scripts, no production data, deterministic state, and headless component execution. CI should retain safe first-failure artifacts and identify browser, revision, and shard. Every worker must own its resources.
Common Mistakes
- Copying code without installing the matching Cypress React adapter.
- Expecting a controlled component to update without new props.
- Registering an intercept after an initialization request starts.
- Using a wildcard so broad that a wrong endpoint still matches.
- Claiming a fixture proves the real backend contract.
- Testing private hook state instead of visible behavior and callbacks.
- Sharing router, store, or query-client singletons across cases.
- Using real time for a deterministic debounce rule.
- Faking time for behavior that actually depends on browser rendering.
- Referencing Testing Library commands without adding the integration.
- Selecting elements by nested CSS position.
- Keeping real customer data or tokens in fixtures and artifacts.
- Turning every component state into an end-to-end journey as well.
- Measuring suite success by file count rather than feedback quality.
Conclusion
The best cypress component testing example makes boundaries obvious. Mount a realistic component, drive it through public browser behavior, control only the dependencies outside its responsibility, and assert an outcome that matters to a user or parent component. The counter, form, request, router, timer, and modal patterns in this guide cover the core decisions without relying on invented APIs.
Begin with one difficult UI state from your current end-to-end suite. Recreate it through props, a provider, or cy.intercept, add the success and most important failure path, and run it in pull requests. Keep lower-level logic tests and selected deployed journeys around it, and you will gain speed without trading away confidence.
Interview Questions and Answers
Why does the stepper example use a stub?
The controlled stepper reports change through a callback and waits for its parent to supply new props. A stub verifies the exact requested value. The test therefore observes the public contract rather than component internals.
Why should the displayed value remain unchanged after the click?
The example is a controlled component and does not own the value. Clicking requests a parent update but cannot mutate the prop. The assertion documents that important responsibility boundary.
Why test success, empty, and error responses separately?
They are distinct user states with different content and recovery needs. Direct response control makes each state deterministic. One happy response cannot prove empty and failure behavior.
What does MemoryRouter add to a component spec?
It supplies route context and controlled history without a deployed server. The test can exercise links and route-aware rendering. Server rewrite and real authentication behavior remain outside its scope.
When is cy.clock appropriate?
It is appropriate when browser timers are an intentional dependency, such as debounce or countdown behavior. It makes the boundary fast and deterministic. It is inappropriate when real animation or scheduling is the risk.
How would you test focus restoration for a modal?
Mount a parent harness with an opener and the modal. Open the modal, verify its initial focus, dismiss it through the supported action, and assert focus returns to the opener. This tests the complete local workflow.
Why avoid large reusable fixtures?
Unrelated fields hide the state relevant to the scenario and become expensive to maintain. Small representative fixtures make the contract visible. Large versioned fixtures are justified only when their complete shape matters.
How do you know the example is CI-ready?
A clean clone can install locked dependencies and run the documented headless script. Tests own their state and retain safe failure artifacts with browser and revision context. No local singleton or secret is required.
Frequently Asked Questions
What is a simple Cypress component testing example?
Mount a controlled button or stepper, click it, and assert the callback receives the correct next value. This proves browser interaction and the parent-component contract without a deployed application.
How do I test a React form with Cypress component testing?
Mount the form with a stubbed submit callback, interact through associated labels and controls, and assert errors or the submitted value. Keep service-side validation in API tests.
How do I mock an API response in a component example?
Register `cy.intercept()` before the request occurs, provide a representative status and body, and alias the route. Wait for the alias and assert both request details and rendered behavior when relevant.
How do I test React Router components in Cypress?
Wrap the component with a fresh `MemoryRouter` and provide controlled initial entries. Keep server rewrites, real redirects, and refresh behavior in end-to-end tests.
How do I test a debounce without waiting in real time?
Call `cy.clock()` before mount, type the input, then use `cy.tick()` around the delay boundary. Assert the callback is absent before the boundary and called once afterward.
What should a component test portfolio include?
Include locked setup, clear scripts, several distinct boundaries, CI execution, and safe failure artifacts. Document why each case belongs at the component layer and what other layers complement it.
Do Cypress labels automatically become aria-label attributes?
No. An associated HTML label contributes to the accessible name but does not create an `aria-label` attribute. Use the control ID, a supported accessibility query, or an explicit attribute as appropriate.