- Home
- Cypress Tutorial
Cypress
Automation Testing Tutorial
Master end-to-end web automation with Cypress — from installation to writing real tests, selectors, assertions, network interception, reports, and professional best practices.
Key facts about Cypress:
What Cypress can test:
- →End-to-End (E2E) Tests Full user flows — login, checkout, form submission — testing the entire application from browser to server.
- →Integration Tests How multiple modules or components work together — e.g. does the login module correctly update the header component?
- →Component Tests Test individual UI components in isolation (React, Angular, Vue) without running the full application.
- →API Tests Using cy.request() to test REST APIs directly — verify response status codes, body content, and headers.
Cypress key features that make it special:
- 1Time Travel Cypress takes a snapshot at every test step. You can hover over any command in the Command Log and see exactly what the page looked like at that moment — like rewinding a video.
- 2Automatic Waiting Cypress automatically waits for elements to appear, for animations to finish, and for network requests to complete before moving to the next step. No need to add manual waits or sleeps.
- 3Real-time Reloads When you save a test file, Cypress instantly re-runs the tests. You see results live as you code — great for Test-Driven Development (TDD).
- 4Network Control (cy.intercept) Intercept, modify, and stub any network request your app makes. Test error scenarios by faking API responses without touching the real server.
- 5Built-in Debuggability Run tests with the browser's DevTools open. Use cy.pause() or debugger to step through tests interactively.
| Feature | 🌲 Cypress | 🎭 Playwright | 🔵 Selenium |
|---|---|---|---|
| Language | JS / TypeScript only | JS/TS, Python, Java, .NET | Java, Python, C#, Ruby, JS, PHP |
| Architecture | Inside browser (same event loop) | Outside browser (direct protocol) | Outside browser (WebDriver) |
| Auto-wait | ✅ Built-in | ✅ Built-in | ❌ Manual waits needed |
| Safari / WebKit | ❌ Not supported | ✅ Supported | ✅ Supported |
| Multi-tab | ⚠️ Limited support | ✅ Native support | ⚠️ Via window handles |
| Network stubbing | ✅ cy.intercept() built-in | ✅ Built-in | ❌ No built-in |
| Time Travel debug | ✅ Snapshots in Test Runner | ✅ Trace Viewer | Screenshots only |
| Component testing | ✅ Supported | ✅ Experimental | ❌ Not supported |
| Best use case | JS/TS projects, React/Vue/Angular front-end | Multi-browser, multi-language, CI/CD | Legacy enterprise, broader language needs |
Prerequisites:
- 1Node.js (v18 or higher) Download from nodejs.org. After installing, verify with: node --version
- 2npm (comes with Node.js) Verify with: npm --version
- 3VS Code (recommended) Best editor for Cypress. Install the Cypress extension for autocompletion and IntelliSense.
Step 1 — Create project folder and initialize npm:
Step 2 — Install Cypress as a dev dependency:
Step 3 — Open Cypress for the first time:
2. You choose E2E Testing (most common) or Component Testing
3. Cypress automatically creates all needed configuration files and folder structure
4. You choose your browser (Chrome, Firefox, or Electron)
5. Cypress creates a cypress.config.js file in your project root
6. The interactive Test Runner opens — you can now write and run tests!
Add scripts to package.json for easier running:
{
"scripts": {
"cy:open": "cypress open", // opens interactive Test Runner
"cy:run": "cypress run" // runs all tests headlessly (for CI)
}
}npx cypress open — Opens the interactive Test Runner GUI. You can see the browser window, Command Log, and Time Travel snapshots. Best for local development and debugging.
npx cypress run — Runs all tests headlessly in the terminal without a visible browser. Faster. Best for CI/CD pipelines (GitHub Actions, Jenkins, etc.).
Default Cypress folder structure:
The cypress.config.js file — key settings explained:
const { defineConfig } = require('cypress'); module.exports = defineConfig({ e2e: { baseUrl: 'http://localhost:3000', // Root URL — use cy.visit('/login') instead of full URL specPattern: 'cypress/e2e/**/*.cy.{js,ts}', // Where Cypress looks for test files viewportWidth: 1280, // Browser window width for tests viewportHeight: 720, // Browser window height for tests defaultCommandTimeout: 10000, // Max wait time for each command (10 seconds) pageLoadTimeout: 60000, // Max wait time for page to load (60 seconds) video: true, // Record video of test runs (headless mode) screenshotOnRunFailure: true, // Auto-capture screenshot when test fails retries: { runMode: 2, // Retry failed tests 2 times in CI (cypress run) openMode: 0 // No retries in interactive mode (cypress open) } } });
Anatomy of a Cypress test file:
// describe() groups related tests — like a folder describe('Login Page', () => { // beforeEach() runs before every it() in this describe beforeEach(() => { cy.visit('/login'); // uses baseUrl from cypress.config.js }); // it() declares one test case it('should login with valid credentials', () => { // Step 1: Type email into the email field cy.get('[data-testid="email"]').type('[email protected]'); // Step 2: Type password cy.get('[data-testid="password"]').type('Test@123'); // Step 3: Click the Login button cy.get('[data-testid="login-btn"]').click(); // Step 4: Verify we landed on the dashboard cy.url().should('include', '/dashboard'); // Step 5: Verify welcome message is visible cy.contains('Welcome, Priya').should('be.visible'); }); it('should show error for wrong password', () => { cy.get('[data-testid="email"]').type('[email protected]'); cy.get('[data-testid="password"]').type('WrongPass'); cy.get('[data-testid="login-btn"]').click(); cy.contains('Invalid credentials').should('be.visible'); }); });
In Playwright you write: await page.click('button')
In Cypress you write: cy.get('button').click()
Cypress commands are not Promises — they are enqueued in an internal command queue and executed one by one automatically. Cypress handles all the async timing for you behind the scenes. Never add async/await to standard Cypress commands — it causes unexpected behavior.
it('test', async () => { await cy.get('button').click(); }). What is wrong with this?Cypress selector strategies — best to worst:
// HTML: <input data-testid="email-input" /> cy.get('[data-testid="email-input"]').type('[email protected]'); // HTML: <button data-cy="login-btn">Login</button> cy.get('[data-cy="login-btn"]').click(); // HTML: <div data-testid="error-msg">Invalid credentials</div> cy.get('[data-testid="error-msg"]').should('be.visible');
// Click a button with text "Submit" cy.contains('Submit').click(); // Assert error message is visible cy.contains('Invalid credentials').should('be.visible'); // Find a specific element type containing text cy.contains('button', 'Login').click(); // only find <button> with text "Login" // Using regex for partial/case-insensitive match cy.contains(/welcome/i).should('be.visible');
// By stable ID (ok if ID never changes) cy.get('#submit-button').click(); // By input type (fairly stable) cy.get('input[type="email"]').type('[email protected]'); // By CSS class — FRAGILE, avoid if possible cy.get('.btn-primary').click(); // breaks if class name changes // Chaining: find within a parent element cy.get('form').find('input[name="email"]').type('[email protected]');
// ── NAVIGATION ────────────────────────────────── cy.visit('/login'); // Navigate to a page (relative path) cy.visit('https://example.com'); // Absolute URL cy.go('back'); // Browser back button cy.reload(); // Refresh the page // ── CLICK ─────────────────────────────────────── cy.get('[data-testid="login-btn"]').click(); cy.contains('Submit').click(); cy.get('[data-testid="menu"]').dblclick(); // double click cy.get('[data-testid="item"]').rightclick(); // right click // ── TYPE (into input fields) ──────────────────── cy.get('[data-testid="email"]').type('[email protected]'); cy.get('[data-testid="search"]').type('laptop{enter}'); // type & press Enter // ── CLEAR ─────────────────────────────────────── cy.get('[data-testid="email"]').clear(); cy.get('[data-testid="email"]').clear().type('[email protected]'); // clear then retype // ── SELECT DROPDOWN ───────────────────────────── cy.get('select[name="country"]').select('India'); // by visible text cy.get('select[name="country"]').select('IN'); // by option value // ── CHECKBOX / RADIO ──────────────────────────── cy.get('[data-testid="terms"]').check(); // check a checkbox cy.get('[data-testid="terms"]').uncheck(); // uncheck a checkbox cy.get('input[value="male"]').check(); // check a radio button // ── HOVER ─────────────────────────────────────── cy.get('[data-testid="account-menu"]').trigger('mouseover'); // ── SCROLL ────────────────────────────────────── cy.get('[data-testid="lazy-section"]').scrollIntoView(); // ── SCREENSHOT ────────────────────────────────── cy.screenshot('login-page'); // saves to cypress/screenshots/
{enter} — press Enter key
{tab} — press Tab key
{backspace} — press Backspace
{selectall} — select all text
{del} — press Delete
{esc} — press Escape
Example: cy.get('[data-testid="search"]').type('laptop{enter}')
// ── VISIBILITY ────────────────────────────────── cy.get('[data-testid="welcome-msg"]').should('be.visible'); cy.get('[data-testid="error"]').should('not.be.visible'); // ── URL ───────────────────────────────────────── cy.url().should('include', '/dashboard'); cy.url().should('eq', 'http://localhost:3000/dashboard'); // ── TEXT CONTENT ──────────────────────────────── cy.get('h1').should('have.text', 'Dashboard'); // exact match cy.get('[data-testid="cart"]').should('include.text', '3'); // partial match cy.get('[data-testid="msg"]').should('contain', 'Success'); // ── INPUT VALUE ───────────────────────────────── cy.get('[data-testid="email"]').should('have.value', '[email protected]'); // ── CSS CLASS ─────────────────────────────────── cy.get('[data-testid="tab"]').should('have.class', 'active'); cy.get('[data-testid="btn"]').should('not.have.class', 'disabled'); // ── ELEMENT STATE ─────────────────────────────── cy.get('[data-testid="submit"]').should('be.disabled'); cy.get('[data-testid="submit"]').should('be.enabled'); cy.get('[data-testid="terms"]').should('be.checked'); cy.get('[data-testid="terms"]').should('not.be.checked'); cy.get('[data-testid="field"]').should('be.empty'); // ── COUNT ─────────────────────────────────────── cy.get('li.product-item').should('have.length', 10); // ── EXISTENCE IN DOM ──────────────────────────── cy.get('[data-testid="modal"]').should('exist'); cy.get('[data-testid="modal"]').should('not.exist'); // ── CHAIN MULTIPLE ASSERTIONS with .and() ─────── cy.get('[data-testid="link"]') .should('be.visible') .and('have.attr', 'href') .and('include', '/profile');
'have.text', 'Dashboard' — exact full text match. Element text must be exactly "Dashboard".
'include.text', '3' — partial text match. Element text just needs to contain "3".
'contain', 'Success' — also partial match. Identical to include.text for most use cases.
Use exact match when you know the full text; use partial match when the text may have surrounding content (e.g., "Cart (3 items)").
describe('Login Feature', () => { before(() => { // Runs ONCE before all tests in this describe block cy.log('Setting up test data...'); // e.g. create a test user via API }); beforeEach(() => { // Runs before EACH test — navigate to login page cy.visit('/login'); cy.clearCookies(); // ensure fresh state }); it('valid login redirects to dashboard', () => { cy.get('[data-testid="email"]').type('[email protected]'); cy.get('[data-testid="password"]').type('Test@123'); cy.get('[data-testid="login-btn"]').click(); cy.url().should('include', '/dashboard'); }); it('empty fields show validation errors', () => { cy.get('[data-testid="login-btn"]').click(); cy.contains('Email is required').should('be.visible'); }); afterEach(() => { // Runs after EACH test — clear session data cy.clearLocalStorage(); }); after(() => { // Runs ONCE after all tests — cleanup cy.log('All login tests complete. Cleaning up...'); }); });
// ── SPY — observe a real request without changing it ── cy.intercept('GET', '/api/products').as('getProducts'); cy.visit('/products'); cy.wait('@getProducts'); // wait for request to finish cy.get('[data-testid="product-list"]').should('be.visible'); // ── STUB — replace real API with fake response ──────── cy.intercept('GET', '/api/products', { statusCode: 200, body: [ { id: 1, name: 'Laptop', price: 50000 }, { id: 2, name: 'Phone', price: 30000 } ] }).as('stubbedProducts'); cy.visit('/products'); cy.wait('@stubbedProducts'); cy.get('li.product').should('have.length', 2); // ── STUB ERROR — test how app handles API failure ──── cy.intercept('GET', '/api/products', { statusCode: 500, body: { error: 'Internal Server Error' } }).as('productError'); cy.visit('/products'); cy.wait('@productError'); cy.contains('Something went wrong').should('be.visible'); // ── INTERCEPT POST (login API) ─────────────────────── cy.intercept('POST', '/api/login').as('loginRequest'); cy.get('[data-testid="login-btn"]').click(); cy.wait('@loginRequest').its('response.statusCode').should('eq', 200);
1. Test error handling without breaking the real API
2. Make tests faster — no real network calls
3. Test edge cases that are hard to trigger in real APIs (500 errors, empty responses, slow responses)
4. Keep tests deterministic — same fake data every run, no dependency on external services
Essential CLI commands:
Left panel — Command Log: Every Cypress command listed in order — cy.visit, cy.get, cy.click, .should, etc. Click any command to see the Time Travel snapshot of the page at that moment.
Right panel — App Preview: The actual browser rendering your application as the test runs live. You can see every click, every form fill, every page navigation happening in real time.
🕰️ Time Travel: Hover over any command in the Command Log → the app preview jumps back to show exactly what the page looked like at that step. This is Cypress's most powerful debugging feature.
cypress/videos/ — MP4 video recording of the entire test run for each spec file. Watch the video to see exactly what happened step by step during a CI failure.
These files are generated by default. To disable: set video: false and screenshotOnRunFailure: false in cypress.config.js.
These are official Cypress recommendations and industry-standard practices followed by professional QA automation engineers. Apply them consistently to build a stable, maintainable test suite.
- 1Use data-testid attributes as selectors — avoid CSS classes and IDs Ask developers to add data-testid attributes to all testable elements. These never break due to styling changes. Never use .btn-primary or #submit as selectors if they change with redesigns.
- 2Never use cy.wait(number) — it is an anti-pattern cy.wait(2000) is a hardcoded sleep. It makes tests slow when things are fast and still fails when they are slow. Instead use cy.wait('@alias') with route aliases, or rely on Cypress auto-waiting via assertions.
- 3Tests must be independent — no test should depend on another test's state Each test must set up and tear down its own state. Tests that only pass when run in a specific order are fragile and misleading. Use beforeEach() for fresh setup before every test.
- 4Use beforeEach() for setup — not before() State from before() can persist between tests in unintended ways. beforeEach() guarantees every test starts clean. This is Cypress's official recommendation.
- 5Write descriptive test names in it() Name tests like sentences: 'should show error message when email field is empty'. When a test fails, the name tells you exactly what broke without reading the code. Avoid names like 'test1' or 'login test'.
- 6Use cy.intercept() to control network requests in tests Don't rely on a live backend for all test scenarios. Use cy.intercept() to stub error responses (404, 500), test loading states, and keep tests fast and reliable without hitting real APIs.
- 7Use fixtures for test data — not hardcoded values Store test data (credentials, form data, API mock responses) in JSON files inside cypress/fixtures/. Load with cy.fixture('users.json'). This separates data from test logic and makes data changes easy.
- 8Create custom commands for repeated actions If you write the same 5-line login flow in every test, extract it to a custom command in cypress/support/commands.js: Cypress.Commands.add('login', (email, password) => {...}). Then every test can use cy.login('[email protected]', 'pass').
- 9Test both positive and negative paths For every form or feature, write tests for valid input (happy path) AND invalid input (error handling). Tests that only check the happy path miss the most common bugs in validation logic.
- 10Configure retries in CI to handle flakiness Set retries: { runMode: 2 } in cypress.config.js so that tests that fail due to minor flakiness are retried twice in CI before marking as failed. This reduces false failures without hiding real bugs.
"I start by creating a file cypress/e2e/login.cy.js. I use describe() to group all login tests and beforeEach() to navigate to the login page before each test. I use cy.get('[data-testid]') selectors for stability. I write one positive test — valid credentials redirect to dashboard — verified with cy.url().should('include', '/dashboard'). I write negative tests for wrong password, empty fields, and locked account. For the wrong password test I use cy.intercept() to stub a 401 response and verify the error message appears. I use cy.screenshot() for any tricky assertions. I run the suite with npx cypress run in CI and check the video on any failure."
Ready to Master Cypress in Real Projects?
STAD Solution's QA Automation course covers Cypress end-to-end with hands-on practice, CI/CD integration, real app testing, and 100% placement support.
Explore Courses at STAD Solution →OUR ACCREDITATIONS
For Training Inquiry
For Business Inquiry
© 2025 STAD Solution. All rights reserved. Made by Dikshtech | SEO & Digital Marketing by ShoutnHike.
Our Accreditations
For Training Inquiry
For Business Inquiry
Location
Ahmede
© 2026 STAD Solution. All rights reserved. SEO by ShoutnHike. | llm Info