Cypress is a modern testing framework for web applications that gives QA engineers an interactive runner, automatic retryability, strong browser debugging tools and a concise JavaScript or TypeScript API. It is particularly effective for end-to-end, component and API-assisted testing where fast local feedback and reliable interaction with the application are important.
What Cypress is
Cypress is designed for testing applications that run in a browser. Unlike traditional WebDriver-based approaches, Cypress has a close relationship with the browser and the application under test, which gives it rich access to the DOM, network activity, browser developer tools and application state.
End-to-end testing
Exercise complete user journeys through the real application UI.
Component testing
Mount supported frontend components directly and test them without booting the complete application.
API-assisted testing
Use HTTP requests to validate APIs or prepare and clean test state faster than navigating through the UI.
Interactive debugging
Inspect the command log, DOM snapshots, browser console and network panel while the test runs.
Automatic retryability
Queries and assertions retry while the application reaches the expected state, reducing arbitrary waits.
CI execution
Run the same test suite headlessly in continuous integration with screenshots, reports and optional video.
Where Cypress fits in a QA strategy
Cypress can cover several layers of a modern web test strategy, but it should not mean putting every scenario through the UI. The fastest and most maintainable suites normally combine lower-level checks with a focused set of browser journeys.
| Test layer | Typical Cypress use | Examples |
|---|---|---|
| Component | Test individual UI components in isolation. | Button states, validation messages, component events. |
| API | Call services directly or use APIs to prepare test state. | Create user, seed order, validate response schema. |
| Integration | Validate interactions between UI and backend behaviour. | Form submission, error handling, feature flags. |
| End-to-end | Exercise critical user journeys in the deployed application. | Login, checkout, account update, core business workflow. |
| Visual / accessibility | Extend Cypress using dedicated services, plugins or platform features. | Visual regressions, accessibility scans, UI coverage. |
For a large project, treat Cypress as one part of the wider quality strategy alongside unit tests, contract tests, exploratory testing, performance testing, security testing and production monitoring.
Create a Cypress project
Cypress is distributed as an npm package, so a typical project starts with Node.js and a package manager. Use a Node version supported by the Cypress version your team has selected.
1. Initialise a Node project
mkdir cypress-demo
cd cypress-demo
npm init -y
2. Install Cypress as a development dependency
npm install cypress --save-dev
3. Open Cypress
npx cypress open
The Cypress Launchpad guides you through choosing a testing type, selecting a browser and creating the initial project structure.
Useful npm scripts
{
"scripts": {
"cy:open": "cypress open",
"cy:run": "cypress run",
"test:e2e": "cypress run --e2e"
}
}
Once these scripts exist, team members can use the same commands everywhere:
npm run cy:open
npm run cy:run
Understand the project structure
A JavaScript end-to-end project commonly contains the following files and folders.
cypress-demo/
├── cypress/
│ ├── e2e/
│ │ └── login.cy.js
│ ├── fixtures/
│ │ └── users.json
│ └── support/
│ ├── commands.js
│ └── e2e.js
├── cypress.config.js
├── package.json
└── package-lock.json
| Location | Purpose |
|---|---|
cypress/e2e | End-to-end spec files. |
cypress/fixtures | Static test data such as JSON payloads. |
cypress/support/e2e.js | Code loaded before end-to-end specs, such as shared setup and imports. |
cypress/support/commands.js | A common place for custom Cypress commands. |
cypress.config.js | Project configuration including base URL, viewport, retries and Node event hooks. |
Configure the project
A central baseUrl keeps URLs out of test code and makes environment changes easier.
const { defineConfig } = require('cypress')
module.exports = defineConfig({
viewportWidth: 1440,
viewportHeight: 900,
e2e: {
baseUrl: 'http://localhost:3000',
setupNodeEvents(on, config) {
return config
},
},
})
Your tests can now visit relative routes rather than repeating the full host:
cy.visit('/')
cy.visit('/login')
cy.visit('/orders/123')
Write your first test
Cypress uses Mocha-style test structure. A simple spec can visit a page, find an element, perform an action and verify the result.
describe('Login', () => {
it('logs in with valid credentials', () => {
cy.visit('/login')
cy.get('[data-cy="email"]')
.type('qa.user@example.com')
cy.get('[data-cy="password"]')
.type('correct-password')
cy.get('[data-cy="login-submit"]')
.click()
cy.url()
.should('include', '/dashboard')
cy.contains('Welcome')
.should('be.visible')
})
})
When running in open mode, saving the file normally causes Cypress to re-run the relevant spec automatically. This fast edit-run-debug loop is one of Cypress's strongest development-time features.
Use the Cypress runner as a debugging tool
The interactive runner provides much more than a pass/fail result. Treat it as part of your test development workflow.
- Command Log: inspect each query, action and assertion executed by the test.
- Time-travel snapshots: hover over commands to inspect the DOM around that moment in execution.
- Browser DevTools: use Elements, Console, Network, Application and other browser panels while the test runs.
- Console details: click commands in the Cypress log to inspect what they yielded and how they behaved.
- Selector tools: use Cypress-assisted selector workflows as a starting point, then review the generated locator for stability.
- Pause/debug: use
.pause(),.debug()or browser debugging when a chain is not behaving as expected.
Selectors that survive application changes
Selector quality is one of the biggest factors in UI test maintenance. Cypress supports normal CSS selectors through cy.get() and text-based selection through cy.contains().
Common options
// Dedicated test attribute
cy.get('[data-cy="checkout"]')
// ID
cy.get('#checkout')
// Class
cy.get('.checkout-button')
// Visible text
cy.contains('Checkout')
// Element type + text
cy.contains('button', 'Checkout')
| Selector | Stability | Use when |
|---|---|---|
[data-cy="save"] | High | The element has a dedicated automation contract. |
| Accessible role / label via Testing Library | High and user-oriented | You want locators aligned with how users and assistive technology find controls. |
cy.contains('Save') | Context dependent | Visible copy is part of the behaviour you intentionally want to test. |
#save-button | Often reasonable | The ID is stable and unique. |
.btn.primary.mt-2 | Often brittle | Only when styling classes are intentionally stable. |
:nth-child(...) | Low | Avoid when a semantic or dedicated selector is possible. |
data-cy or equivalent test attribute is usually cheaper than maintaining a fragile selector.Traverse an existing DOM selection
cy.get('[data-cy="card"]')
.first()
cy.get('[data-cy="card"]')
.last()
cy.get('[data-cy="card"]')
.eq(2)
cy.get('[data-cy="list"]')
.eq(1)
.find('[data-cy="card"]')
.contains('Shampoo')
Cypress also provides commands such as parent(), children(), next(), prev(), find() and within(). Prefer readable DOM relationships over unnecessarily complex XPath expressions.
Interactions and actionability
Most UI tests are built from queries followed by actions. Cypress performs actionability checks before interacting with an element, which helps expose situations where a real user could not perform the same action.
cy.get('[data-cy="name"]')
.type('Ricardo')
cy.get('[data-cy="country"]')
.select('Portugal')
cy.get('[data-cy="terms"]')
.check()
cy.get('[data-cy="submit"]')
.click()
Special keys
cy.get('[data-cy="search"]')
.type('cypress{enter}')
Use state-setting commands where possible
If the test needs a checkbox to be checked, prefer .check() over a blind .click(). The intent is clearer and the test does not accidentally toggle the state in the wrong direction.
cy.get('[data-cy="newsletter"]')
.check()
.should('be.checked')
force: true is an escape hatch
cy.get('[data-cy="hidden-action"]')
.click({ force: true })
Forcing an action bypasses some actionability checks. Use it only when the application behaviour justifies it. If a user genuinely cannot interact with the element, forcing the click can hide a real product defect.
Assertions
Assertions describe what must be true for the scenario to pass. Cypress integrates with Chai-style assertions through .should() and .and().
// Visibility
cy.get('[data-cy="success"]')
.should('be.visible')
// Count
cy.get('[data-cy="card"]')
.should('have.length', 2)
// Text inside normal elements
cy.get('[data-cy="status"]')
.should('have.text', 'Ready')
// Value of an input
cy.get('[data-cy="name"]')
.should('have.value', 'Ricardo')
// Class
cy.get('[data-cy="task"]')
.should('have.class', 'completed')
// URL
cy.url()
.should('include', '/dashboard')
// Attribute
cy.get('[data-cy="download"]')
.should('have.attr', 'href')
Text vs value
One common beginner mistake is asserting have.text against an <input>. Inputs usually expose their current content through the element's value, so use have.value.
// Normal text element
cy.get('[data-cy="card-title"]')
.should('have.text', 'Groceries')
// Input element
cy.get('[data-cy="list-name"]')
.should('have.value', 'Groceries')
Chaining and yielded subjects
Cypress commands form chains. Queries yield a subject and later commands operate on that subject.
cy.get('[data-cy="list"]')
.eq(1)
.find('[data-cy="card"]')
.contains('Shampoo')
.should('be.visible')
.click()
Read the chain from top to bottom:
- Find all list elements.
- Take the second list.
- Find cards inside that list.
- Find the card containing “Shampoo”.
- Verify that it is visible.
- Click the verified card.
Retryability: one of Cypress’s most important concepts
Modern web applications are asynchronous. Elements appear after network calls, components re-render and content changes over time. Cypress handles much of this through built-in retryability.
Queries and assertions can be retried until they pass or reach their timeout. Actions such as click() are not repeatedly executed in the same way because repeating an action could change application state multiple times.
Stable pattern
cy.get('[data-cy="card"]')
.last()
.should('contain.text', 'Shampoo')
.click()
The assertion acts as a guard. Cypress keeps retrying the linked query chain until the selected last card actually contains the expected text, then performs the click.
Avoid fixed waits
// Avoid this when the real requirement is state-based
cy.wait(5000)
// Prefer waiting through a meaningful assertion
cy.get('[data-cy="results"]')
.should('be.visible')
// Or wait for a specific network request
cy.intercept('GET', '/api/results*').as('getResults')
cy.get('[data-cy="search"]').type('cypress{enter}')
cy.wait('@getResults')
.its('response.statusCode')
.should('eq', 200)
Timeouts are upper bounds, not sleeps
cy.get('[data-cy="slow-widget"]', { timeout: 10000 })
.should('be.visible')
If the element appears in 800 ms, Cypress continues immediately. The ten seconds only defines how long the command is allowed to keep trying before failing.
Manage test state deliberately
Test-data management is often harder than writing the UI commands themselves. A repeatable test should control the state it depends on rather than relying on whatever another test or a previous manual session left behind.
| Problem | Weak approach | Better approach |
|---|---|---|
| User must exist | Create the user manually once. | Create or seed the user through an API or test-data endpoint. |
| Cart must be empty | Assume previous tests cleaned it. | Reset the cart in setup. |
| Authentication | Repeat a long UI login before every scenario. | Use programmatic authentication and cy.session() when appropriate. |
| Unique records | Reuse one hard-coded record forever. | Create deterministic or uniquely named data per test. |
| Cleanup | Delete data manually after failures. | Automate cleanup or use disposable test environments. |
Programmatic setup with cy.request()
beforeEach(() => {
cy.request('POST', '/api/test/reset')
cy.request('POST', '/api/users', {
email: 'qa.user@example.com',
role: 'customer',
})
})
Using the UI to test the UI is appropriate for the behaviour under test. Using the UI merely to prepare every prerequisite often makes suites slower and more fragile.
Fixtures and reusable data
The fixtures directory is convenient for static payloads, expected data and reusable test inputs.
// cypress/fixtures/user.json
{
"name": "QA User",
"email": "qa.user@example.com"
}
cy.fixture('user').then((user) => {
cy.get('[data-cy="name"]').type(user.name)
cy.get('[data-cy="email"]').type(user.email)
})
Do not turn fixtures into a global dumping ground. Use them when the data is genuinely reusable and static. Dynamic test data is often clearer when created in code or through an API.
Network control with cy.intercept
cy.intercept() is extremely useful for observing, waiting for, modifying or stubbing network traffic.
Wait for a real request
cy.intercept('POST', '/api/orders').as('createOrder')
cy.get('[data-cy="submit-order"]').click()
cy.wait('@createOrder')
.its('response.statusCode')
.should('eq', 201)
Stub an error response
cy.intercept('GET', '/api/profile', {
statusCode: 500,
body: { message: 'Internal Server Error' },
}).as('profileFailure')
cy.visit('/profile')
cy.contains('Unable to load your profile')
.should('be.visible')
Why this matters for QA
- Test rare backend failure states without forcing the real service to fail.
- Make a test deterministic when third-party data is outside your control.
- Verify that the frontend sends the expected request.
- Synchronise tests with meaningful application activity rather than arbitrary sleeps.
- Build focused UI tests for loading, empty, error and edge states.
Cypress Studio
Cypress Studio can help generate or extend end-to-end tests by recording real interactions in the application. It is useful for learning commands, quickly sketching flows and accelerating the first version of a test.
Recorded automation should still be reviewed like manually written code. Generated selectors may be fragile, recorded actions may be redundant and the tool cannot automatically solve test-data design or decide whether the scenario is strategically valuable.
A sensible record-and-refine workflow
1. Record the basic user flow
│
2. Save generated commands
│
3. Replace fragile selectors
│
4. Remove redundant actions
│
5. Add meaningful assertions
│
6. Control test data
│
7. Re-run repeatedly
│
8. Add to CI
Custom commands and plugins
Cypress can be extended with custom commands, Node event handlers and community or commercial plugins. Extension is useful, but every dependency increases maintenance, so add one when it solves a real project problem.
Simple custom command
// cypress/support/commands.js
Cypress.Commands.add('loginByApi', (email, password) => {
cy.request('POST', '/api/login', {
email,
password,
}).then(({ body }) => {
window.localStorage.setItem('token', body.token)
})
})
beforeEach(() => {
cy.loginByApi('qa.user@example.com', 'password')
})
Common extension areas
| Need | Typical extension |
|---|---|
| Accessible locators | Cypress Testing Library. |
| Accessibility scanning | Accessibility tooling integrated into Cypress. |
| Visual regression | Visual testing service or screenshot comparison tooling. |
| Test filtering | Tags / grep-style tooling. |
| Reports | JUnit, Mochawesome or other compatible reporters. |
| Coverage | Code-coverage integration where instrumentation is available. |
| Email / third-party flows | Dedicated test-service integration rather than unsafe UI workarounds. |
Open mode vs run mode
| Mode | Command | Best for |
|---|---|---|
| Open mode | npx cypress open | Writing tests, interactive debugging, inspecting commands and using browser DevTools. |
| Run mode | npx cypress run | CI, full-suite execution, repeatable command-line runs and machine-readable reporting. |
Useful run commands
# Run all configured specs
npx cypress run
# Run in Chrome
npx cypress run --browser chrome
# Run in Firefox
npx cypress run --browser firefox
# Watch the browser during run mode
npx cypress run --browser chrome --headed
# Run one spec
npx cypress run --spec "cypress/e2e/login.cy.js"
# Use JUnit reporting
npx cypress run \
--reporter junit \
--reporter-options "mochaFile=results/junit-[hash].xml"
Failure artefacts
During cypress run, Cypress automatically captures screenshots for test failures unless that behaviour is disabled. Video recording is available but is not enabled by default, so enable it deliberately if your debugging workflow needs it.
const { defineConfig } = require('cypress')
module.exports = defineConfig({
video: true,
screenshotOnRunFailure: true,
})
Cross-browser testing
Cypress supports Chrome-family browsers and Firefox, with experimental WebKit support available for Safari-engine coverage. Do not assume that passing once in the default browser is sufficient for a product that officially supports several browser families.
Example CI browser matrix
Smoke suite
├── Chrome → every pull request
├── Firefox → every pull request / nightly
└── WebKit → scheduled or compatibility run
Full regression
├── Chrome → main browser
├── Edge → supported enterprise browser
├── Firefox → supported browser
└── WebKit → if required by product support
The exact matrix should follow customer usage, product commitments, risk and execution cost rather than attempting to run every test in every browser without reason.
Cypress in CI/CD
The same command used locally can be run in Jenkins, GitHub Actions, GitLab CI, Azure DevOps or another CI platform. A useful pipeline separates fast feedback from broader regression coverage.
Commit / Pull Request
│
▼
Install dependencies
│
▼
Lint + unit/component tests
│
▼
Start / deploy test application
│
▼
Cypress smoke suite
│
├── FAIL → screenshot + logs + report
│
▼
Merge / deploy to QA
│
▼
Broader Cypress regression
│
▼
Release decision
Simple GitLab CI example
cypress_e2e:
image: cypress/included:latest
stage: test
script:
- npm ci
- npm run cy:run
artifacts:
when: always
paths:
- cypress/screenshots/
- cypress/videos/
- results/
reports:
junit:
- results/*.xml
For real projects, pin an intentional Cypress or container image version rather than relying indefinitely on a moving latest tag.
What CI should preserve on failure
- Test report with the failing test name and stack trace.
- Failure screenshots.
- Video when enabled and useful.
- Application/backend logs when your pipeline controls the environment.
- Browser and Cypress version.
- Environment and build identifier.
- Network or API evidence where it materially helps diagnosis.
A practical test example
This example combines controlled state, meaningful selectors, a network assertion and user-visible verification.
describe('Create order', () => {
beforeEach(() => {
cy.request('POST', '/api/test/reset')
cy.request('POST', '/api/test/login', {
user: 'qa-customer',
})
})
it('creates an order from the basket', () => {
cy.intercept('POST', '/api/orders').as('createOrder')
cy.visit('/basket')
cy.get('[data-cy="basket-item"]')
.should('have.length', 1)
cy.get('[data-cy="checkout"]')
.should('be.enabled')
.click()
cy.get('[data-cy="confirm-order"]')
.click()
cy.wait('@createOrder')
.its('response.statusCode')
.should('eq', 201)
cy.get('[data-cy="order-success"]')
.should('be.visible')
.and('contain.text', 'Order confirmed')
})
})
The important part is not the number of Cypress commands. The value comes from the test being deterministic, readable and tied to a business outcome.
Common Cypress mistakes
| Mistake | Why it hurts | Better approach |
|---|---|---|
Using long cy.wait(5000) calls | Slow and still flaky on slower systems. | Wait on a DOM condition or intercepted request. |
| Using CSS styling selectors | Tests break during visual refactors. | Prefer data-cy, roles or stable semantic selectors. |
| Tests depend on execution order | A single failure can corrupt later tests. | Make specs independently executable. |
| Logging in through the UI in every test | Slow repetition creates unnecessary failure points. | Authenticate programmatically when login itself is not under test. |
| Forcing every click | Can hide visibility and usability defects. | Fix the locator/state and use force only deliberately. |
| Huge end-to-end scenarios | Failures are hard to diagnose and maintain. | Keep focused scenarios around clear business behaviour. |
| Overusing custom commands | Tests become difficult to understand and debug. | Abstract repeated domain behaviour, not every line. |
| Mocking every API response | Integration defects can escape. | Combine focused stubs with genuine end-to-end flows. |
| Ignoring failed-test evidence | Teams repeatedly rerun failures instead of diagnosing them. | Publish screenshots, reports and relevant application logs. |
| Blindly trusting recorded tests | Generated flows can contain weak selectors and redundant steps. | Record, review, refactor and add purposeful assertions. |
Test design guidelines
Prefer behaviour over implementation details
// Brittle implementation-oriented check
cy.get('.react-component-wrapper > div:nth-child(2)')
.should('have.class', 'green')
// Clear behaviour-oriented check
cy.get('[data-cy="payment-status"]')
.should('contain.text', 'Payment approved')
Keep tests understandable
A test should tell a reviewer what behaviour is being verified. Use descriptive test names and avoid hiding the entire scenario behind opaque helper methods.
describe('Password reset', () => {
it('sends a reset email for an existing account', () => {
// ...
})
it('does not reveal whether an unknown account exists', () => {
// ...
})
})
Assert before irreversible actions when timing matters
cy.get('[data-cy="delete-account"]')
.should('be.visible')
.and('be.enabled')
.click()
Test what the user cares about
Do not turn UI automation into a sequence of clicks with no verification. Every scenario should have a clear reason to exist and one or more observable outcomes that prove the behaviour.
Suggested Cypress suite structure
cypress/e2e/
├── smoke/
│ ├── login.cy.js
│ ├── navigation.cy.js
│ └── checkout.cy.js
├── account/
│ ├── profile.cy.js
│ └── password-reset.cy.js
├── orders/
│ ├── create-order.cy.js
│ ├── cancel-order.cy.js
│ └── order-history.cy.js
└── admin/
├── users.cy.js
└── permissions.cy.js
There is no universal folder structure. Organise by business capability or product area when it makes ownership and failures easier to understand.
A QA engineer’s Cypress checklist
- Cypress and Node versions are intentionally managed.
- The project has a shared
baseUrland environment strategy. - Tests use stable selectors.
- Important elements have automation-friendly attributes where needed.
- Each test controls the data and state it depends on.
- Tests do not depend on execution order.
- Fixed waits are avoided unless time itself is part of the behaviour.
- Network calls are intercepted when synchronisation or controlled responses are useful.
- Assertions validate business outcomes, not just successful clicks.
- UI login/setup is bypassed when it is not the behaviour under test.
force: trueis exceptional rather than routine.- Open mode is used to inspect and debug commands locally.
- Run mode executes the suite in CI.
- Failure screenshots and reports are retained as CI artefacts.
- Video is enabled only where its diagnostic value justifies the execution/storage cost.
- Cross-browser coverage matches product support and customer risk.
- Flaky tests are investigated rather than permanently hidden behind retries.
- The suite has a fast smoke layer and appropriately scoped broader regression coverage.
Quick command cheat sheet
| Need | Cypress command |
|---|---|
| Visit a page | cy.visit('/login') |
| Find by selector | cy.get('[data-cy="login"]') |
| Find by text | cy.contains('Login') |
| Click | .click() |
| Type | .type('hello') |
| Press Enter | .type('{enter}') |
| Clear | .clear() |
| Check checkbox | .check() |
| Uncheck | .uncheck() |
| Select option | .select('Portugal') |
| Assert visible | .should('be.visible') |
| Assert text | .should('contain.text', 'Success') |
| Assert input value | .should('have.value', 'Ricardo') |
| Assert count | .should('have.length', 3) |
| Intercept request | cy.intercept('GET', '/api/users').as('users') |
| Wait for request | cy.wait('@users') |
| Call API | cy.request('GET', '/api/health') |
| Pause test | cy.pause() |
| Debug subject | .debug() |
| Open Cypress | npx cypress open |
| Run headlessly | npx cypress run |
Useful links
- Why Cypress ↗ — overview of Cypress and the testing workflow.
- Install Cypress ↗ — current installation and environment requirements.
- Open the Cypress App ↗ — interactive project setup and execution.
- Cypress best practices ↗ — selectors, test state, login and suite organisation.
- Retryability ↗ — queries, assertions and timeout behaviour.
- Interacting with elements ↗ — Cypress actionability checks.
- Assertions ↗ — common assertion syntax and examples.
- cy.intercept() ↗ — observe and control network traffic.
- cy.request() ↗ — make HTTP requests from tests.
- Cypress Studio ↗ — generate and extend end-to-end tests through recorded interactions.
- Cross-browser testing ↗ — Chrome-family, Firefox and WebKit coverage.
- Screenshots and videos ↗ — failure artefacts and video configuration.
- Continuous Integration ↗ — running Cypress in CI.
- Cypress plugins ↗ — community and official extensions.