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.

A useful mental model: Cypress is primarily an application testing tool rather than a general browser automation or scraping framework. That focus explains many of its design decisions and strengths.

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 layerTypical Cypress useExamples
ComponentTest individual UI components in isolation.Button states, validation messages, component events.
APICall services directly or use APIs to prepare test state.Create user, seed order, validate response schema.
IntegrationValidate interactions between UI and backend behaviour.Form submission, error handling, feature flags.
End-to-endExercise critical user journeys in the deployed application.Login, checkout, account update, core business workflow.
Visual / accessibilityExtend 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
LocationPurpose
cypress/e2eEnd-to-end spec files.
cypress/fixturesStatic test data such as JSON payloads.
cypress/support/e2e.jsCode loaded before end-to-end specs, such as shared setup and imports.
cypress/support/commands.jsA common place for custom Cypress commands.
cypress.config.jsProject 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')
Environment rule: avoid hard-coding environment-specific hosts, credentials or secrets in spec files. Keep configuration deliberate and inject sensitive values through your CI or secret-management mechanism.

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')
SelectorStabilityUse when
[data-cy="save"]HighThe element has a dedicated automation contract.
Accessible role / label via Testing LibraryHigh and user-orientedYou want locators aligned with how users and assistive technology find controls.
cy.contains('Save')Context dependentVisible copy is part of the behaviour you intentionally want to test.
#save-buttonOften reasonableThe ID is stable and unique.
.btn.primary.mt-2Often brittleOnly when styling classes are intentionally stable.
:nth-child(...)LowAvoid when a semantic or dedicated selector is possible.
Practical rule: if a UI element is important enough to automate repeatedly, adding a dedicated 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:

  1. Find all list elements.
  2. Take the second list.
  3. Find cards inside that list.
  4. Find the card containing “Shampoo”.
  5. Verify that it is visible.
  6. Click the verified card.
Debugging tip: when a chain behaves unexpectedly, inspect each command in the Cypress Command Log and browser console. Understanding what each command yielded is often enough to reveal the mistake.

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.

ProblemWeak approachBetter approach
User must existCreate the user manually once.Create or seed the user through an API or test-data endpoint.
Cart must be emptyAssume previous tests cleaned it.Reset the cart in setup.
AuthenticationRepeat a long UI login before every scenario.Use programmatic authentication and cy.session() when appropriate.
Unique recordsReuse one hard-coded record forever.Create deterministic or uniquely named data per test.
CleanupDelete 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.
Do not over-stub: a suite where every backend response is mocked can give false confidence about real integration. Keep genuine end-to-end paths alongside focused stubbed tests.

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

NeedTypical extension
Accessible locatorsCypress Testing Library.
Accessibility scanningAccessibility tooling integrated into Cypress.
Visual regressionVisual testing service or screenshot comparison tooling.
Test filteringTags / grep-style tooling.
ReportsJUnit, Mochawesome or other compatible reporters.
CoverageCode-coverage integration where instrumentation is available.
Email / third-party flowsDedicated test-service integration rather than unsafe UI workarounds.

Open mode vs run mode

ModeCommandBest for
Open modenpx cypress openWriting tests, interactive debugging, inspecting commands and using browser DevTools.
Run modenpx cypress runCI, 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

MistakeWhy it hurtsBetter approach
Using long cy.wait(5000) callsSlow and still flaky on slower systems.Wait on a DOM condition or intercepted request.
Using CSS styling selectorsTests break during visual refactors.Prefer data-cy, roles or stable semantic selectors.
Tests depend on execution orderA single failure can corrupt later tests.Make specs independently executable.
Logging in through the UI in every testSlow repetition creates unnecessary failure points.Authenticate programmatically when login itself is not under test.
Forcing every clickCan hide visibility and usability defects.Fix the locator/state and use force only deliberately.
Huge end-to-end scenariosFailures are hard to diagnose and maintain.Keep focused scenarios around clear business behaviour.
Overusing custom commandsTests become difficult to understand and debug.Abstract repeated domain behaviour, not every line.
Mocking every API responseIntegration defects can escape.Combine focused stubs with genuine end-to-end flows.
Ignoring failed-test evidenceTeams repeatedly rerun failures instead of diagnosing them.Publish screenshots, reports and relevant application logs.
Blindly trusting recorded testsGenerated 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 baseUrl and 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: true is 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

NeedCypress command
Visit a pagecy.visit('/login')
Find by selectorcy.get('[data-cy="login"]')
Find by textcy.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 requestcy.intercept('GET', '/api/users').as('users')
Wait for requestcy.wait('@users')
Call APIcy.request('GET', '/api/health')
Pause testcy.pause()
Debug subject.debug()
Open Cypressnpx cypress open
Run headlesslynpx cypress run

Useful links