Cypress 14: Component and End-to-End Testing Tutorial
Cypress 14 has finally arrived with a polished experience for both component and end‑to‑end (E2E) testing. Whether you’re building a single‑page UI or a full‑stack web app, Cypress now lets you write, run, and debug tests from the same runner. In this tutorial we’ll walk through the setup, explore the new component testing workflow, dive into classic E2E scenarios, and show how to combine both approaches in a real‑world project.
Getting Started with Cypress 14
First things first: install Cypress via npm. The latest version ships with built‑in support for component testing, so you don’t need any extra plugins.
npm install cypress@latest --save-dev
After installation, open the Cypress Test Runner. It will auto‑detect whether you’re in a component or E2E context based on the folder structure.
npx cypress open
When the UI appears, you’ll see two tabs: Component Testing and E2E Testing. Click each tab once to let Cypress scaffold the necessary configuration files.
Configuring Component Testing
Component testing focuses on isolated UI pieces—think React, Vue, or Angular components. Cypress 14 uses the same cypress.config.js file for both modes, but you can override settings per testing type.
module.exports = {
component: {
devServer: {
framework: 'react',
bundler: 'webpack',
},
specPattern: 'cypress/component/**/*.cy.{js,jsx,ts,tsx}',
},
e2e: {
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
},
};
Notice the specPattern paths—keeping component specs in cypress/component and E2E specs in cypress/e2e helps maintain a clean separation.
Writing Your First Component Test
Let’s test a simple React button that toggles a label. Create ButtonToggle.jsx and its test file ButtonToggle.cy.jsx side by side.
import React, { useState } from 'react';
export default function ButtonToggle() {
const [on, setOn] = useState(false);
return (
<div>
<button onClick={() => setOn(!on)}>Toggle</button>
<span data-cy="status">{on ? 'ON' : 'OFF'}</span>
</div>
);
}
Now the Cypress test:
/// <reference types="cypress" />
import ButtonToggle from '../../src/ButtonToggle';
describe('ButtonToggle Component', () => {
it('toggles the label on click', () => {
cy.mount(<ButtonToggle />);
cy.get('[data-cy="status"]').should('contain', 'OFF');
cy.get('button').click();
cy.get('[data-cy="status"]').should('contain', 'ON');
});
});
The cy.mount command spins up a lightweight dev server behind the scenes, rendering the component in isolation. You can interact with it just like a regular DOM element, and Cypress automatically captures snapshots for visual debugging.
Pro tip: Use data-cy attributes instead of generic selectors. They’re resilient to UI changes and keep your tests readable.
Setting Up End‑to‑End Testing
E2E tests validate user journeys across the entire application. Cypress 14 continues to use its powerful network stubbing and time‑travel debugging features, now with a smoother UI for inspecting request logs.
Start by adding a baseUrl in the config (already shown) and spin up your dev server. For a typical React app, npm start will serve it on http://localhost:3000.
Basic Login Flow
Assume a login page at /login that posts credentials to /api/auth. We’ll write a test that mocks the API response, fills the form, and asserts a successful redirect.
/// <reference types="cypress" />
describe('Login Flow', () => {
beforeEach(() => {
cy.intercept('POST', '/api/auth', {
statusCode: 200,
body: { token: 'fake-jwt-token' },
}).as('loginRequest');
cy.visit('/login');
});
it('logs in and redirects to dashboard', () => {
cy.get('input[name="email"]').type('user@example.com');
cy.get('input[name="password"]').type('Password123');
cy.get('button[type="submit"]').click();
cy.wait('@loginRequest').its('response.statusCode').should('eq', 200);
cy.url().should('include', '/dashboard');
cy.contains('Welcome, user@example.com').should('be.visible');
});
});
Notice the cy.intercept call. It prevents hitting a real backend, making the test deterministic and fast. The @loginRequest alias lets us wait for the request to finish before asserting the URL.
Testing a Multi‑Page Flow
Let’s extend the scenario: after logging in, the user creates a new “Project” via a modal. This combines navigation, form handling, and UI state changes.
describe('Project Creation', () => {
beforeEach(() => {
// Re‑use the login stub
cy.loginAs('user@example.com'); // custom command defined later
cy.visit('/dashboard');
});
it('creates a project from the dashboard', () => {
cy.get('button[data-cy="new-project"]').click();
cy.get('input[name="title"]').type('Cypress Demo Project');
cy.get('textarea[name="description"]').type('A project created during a test.');
cy.intercept('POST', '/api/projects', {
statusCode: 201,
body: { id: 42, title: 'Cypress Demo Project' },
}).as('createProject');
cy.get('button[data-cy="save-project"]').click();
cy.wait('@createProject').its('response.statusCode').should('eq', 201);
cy.contains('Project created successfully').should('be.visible');
cy.get('a[data-cy="project-42"]').should('contain', 'Cypress Demo Project');
});
});
The test demonstrates how Cypress can handle modals, API stubs, and dynamic selectors—all within a single flow. By re‑using a custom cy.loginAs command, we keep the spec DRY.
Combining Component and E2E Tests
While component tests give you rapid feedback on isolated UI pieces, E2E tests verify that those pieces cooperate correctly in the full application. Cypress 14’s unified runner makes it easy to run both suites together or separately.
One practical pattern is to write component tests for all reusable UI widgets, then write a handful of high‑level E2E tests that cover critical user journeys. This approach reduces flakiness and speeds up CI pipelines.
Shared Test Utilities
Store common helpers in cypress/support. For example, a login helper works for both component and E2E specs because it uses the same API stub.
Cypress.Commands.add('loginAs', (email) => {
cy.intercept('POST', '/api/auth', {
statusCode: 200,
body: { token: 'fake-jwt-token', email },
}).as('login');
cy.visit('/login');
cy.get('input[name="email"]').type(email);
cy.get('input[name="password"]').type('anyPassword');
cy.get('button[type="submit"]').click();
cy.wait('@login');
});
Now any spec can simply call cy.loginAs('tester@cypress.io') without duplicating stub logic.
Running Tests in CI
In your CI configuration (GitHub Actions, GitLab CI, etc.), you can split the jobs:
jobs:
component-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npx cypress run --component
e2e-tests:
runs-on: ubuntu-latest
services:
web:
image: node:18
ports: ['3000:3000']
env:
NODE_ENV: test
options: >-
--health-cmd="curl -f http://localhost:3000 || exit 1"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npm start &
- run: npx cypress run --e2e
The --component flag tells Cypress to execute only component specs, while --e2e runs the full suite. Splitting jobs reduces overall runtime and isolates flaky UI tests from fast unit‑like component tests.
Pro tip: Enable video: false for component runs in CI to save storage; keep video on for E2E runs to capture failures.
Real‑World Example: A Todo Application
To cement the concepts, let’s build a tiny Todo app with React, then test it using both component and E2E strategies. The app consists of three parts: a TodoItem component, a TodoList container, and a page that persists data via a mock API.
Component Test for TodoItem
import React from 'react';
export default function TodoItem({ todo, onToggle }) {
return (
<li data-cy="todo-item">
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
data-cy="toggle-checkbox"
/>
<span data-cy="title">{todo.title}</span>
</li>
);
}
Test the checkbox interaction:
/// <reference types="cypress" />
import TodoItem from '../../src/TodoItem';
describe('TodoItem Component', () => {
const todo = { id: 1, title: 'Write Cypress tutorial', completed: false };
const onToggle = cy.stub().as('toggleStub');
it('renders title and respects completed state', () => {
cy.mount(<TodoItem todo={todo} onToggle={onToggle} />);
cy.get('[data-cy="title"]').should('contain', todo.title);
cy.get('[data-cy="toggle-checkbox"]').should('not.be.checked');
});
it('calls onToggle when checkbox is clicked', () => {
cy.mount(<TodoItem todo={todo} onToggle={onToggle} />);
cy.get('[data-cy="toggle-checkbox"]').click();
cy.get('@toggleStub').should('have.been.calledOnceWith', todo.id);
});
});
E2E Test for the Full Todo Flow
The E2E spec covers adding a new todo, toggling its status, and ensuring the list persists across page reloads (using local storage as a stand‑in for a backend).
/// <reference types="cypress" />
describe('Todo App End‑to‑End', () => {
beforeEach(() => {
// Clear any persisted state
cy.clearLocalStorage();
cy.visit('/');
});
it('adds a todo and toggles it', () => {
cy.get('input[data-cy="new-todo"]').type('Learn Cypress 14{enter}');
cy.get('ul[data-cy="todo-list"] li').should('have.length', 1);
cy.get('li[data-cy="todo-item"] span[data-cy="title"]')
.should('contain', 'Learn Cypress 14');
// Toggle completion
cy.get('li[data-cy="todo-item"] input[data-cy="toggle-checkbox"]')
.check()
.should('be.checked');
// Reload page and verify state persisted
cy.reload();
cy.get('li[data-cy="todo-item"] input[data-cy="toggle-checkbox"]')
.should('be.checked');
});
});
This test showcases Cypress’s ability to interact with the UI, manipulate the DOM, and verify persisted data—all without a real server.
Advanced Topics & Pro Tips
Testing with Multiple Browsers
Cypress 14 ships with Chromium, Firefox, and WebKit support out of the box. Use the --browser flag to run a suite in a specific engine, or configure parallel matrix builds in CI.
npx cypress run --e2e --browser firefox
Visual Regression with Snapshots
Component tests now include automatic snapshot diffing. After a successful run, Cypress stores a .snap file alongside the spec. When UI changes, the runner highlights pixel differences, letting you approve or reject updates.
Pro tip: Commit snapshot files to version control. They act as a visual contract for UI components and prevent accidental regressions.
Network Layer Testing
Beyond cy.intercept, Cypress 14 introduces cy.route2 aliases that support request/response transformations. This is handy for testing pagination, authentication token refresh, or GraphQL queries.
cy.intercept(
{ method: 'GET', url: '/api/items*' },
(req) => {
const page = req.query.page || 1;
req.reply({
statusCode: 200,
body: {
items: generateItems(page),
nextPage: page < 5 ? page + 1 : null,
},
});
}
).as('pagedItems');
Debugging with the Chrome DevTools Protocol
Cypress 14 exposes a cy.debug() command that pauses execution and opens the Chrome DevTools console. You can inspect variables, view the current DOM snapshot, or even modify the page on the fly.
cy.get('button[data-cy="danger"]').click();
cy.debug(); // execution stops here, DevTools opens
cy.get('div.alert').should('contain', 'Action completed');
Conclusion
Cypress 14 bridges the gap between fast, isolated component tests and comprehensive end‑to‑end scenarios. By configuring a single cypress.config.js, leveraging shared utilities, and running targeted CI jobs, you can achieve high confidence in both UI fidelity and user workflows. The examples above— a toggle button, a login flow, and a full Todo app—illustrate how the same testing framework can cover the entire spectrum of front‑end quality assurance. Dive in, experiment with the new snapshot features, and let Cypress become the single source of truth for your JavaScript testing strategy.