Cypress is designed to make writing end-to-end tests feel more like building your application than writing brittle, complex test suites.

Let’s build a simple test for a hypothetical e-commerce site. Imagine we have a product page where users can select a size and add an item to their cart.

// cypress/e2e/product-add-to-cart.cy.js

describe('Product Page - Add to Cart', () => {
  beforeEach(() => {
    // Visit the product page before each test
    cy.visit('/products/widget-pro');
  });

  it('should allow a user to select a size and add the item to the cart', () => {
    // 1. Verify the initial state
    cy.get('[data-testid="selected-size"]').should('contain', 'No size selected');
    cy.get('[data-testid="add-to-cart-button"]').should('be.disabled');

    // 2. Select a size
    cy.get('[data-testid="size-selector"] option[value="M"]').click();

    // 3. Verify the size selection updated
    cy.get('[data-testid="selected-size"]').should('contain', 'Medium (M)');
    cy.get('[data-testid="add-to-cart-button"]').should('not.be.disabled');

    // 4. Add to cart
    cy.get('[data-testid="add-to-cart-button"]').click();

    // 5. Verify cart update (e.g., a success message or cart count)
    cy.get('[data-testid="cart-success-message"]').should('be.visible');
    cy.get('[data-testid="cart-item-count"]').should('contain', '1');
  });
});

This test does a few key things. It starts by visiting the specific product page. Then, it asserts that no size is selected and the "Add to Cart" button is disabled, confirming the initial state. Next, it simulates a user interaction by clicking a specific size option. The test then verifies that the UI updates to reflect the chosen size and that the "Add to Cart" button becomes enabled. Finally, it clicks the button and checks for a confirmation message and an updated cart item count.

The core problem Cypress solves is the flakiness and complexity of traditional end-to-end testing. It runs directly in the browser, giving it access to the DOM, network requests, and the application’s internal state. This direct access means it doesn’t rely on brittle selectors that might break with minor UI changes, and it can automatically handle waiting for elements to appear or become actionable. The cy.visit(), cy.get(), .click(), and .should() commands are fundamental building blocks. cy.visit() navigates to a URL, cy.get() finds elements using CSS selectors (or other strategies), .click() simulates a mouse click, and .should() makes assertions about the state of an element.

The beforeEach hook is crucial for setting up a consistent starting point for each test within a describe block, ensuring tests are independent. The data-testid attributes are a best practice for selecting elements, making tests less susceptible to breaking when CSS classes or HTML structure change. Cypress automatically retries commands like cy.get() and assertions like .should() for a default of 4 seconds, meaning you don’t need to write explicit waits for most asynchronous operations. This is a massive time saver and reduces test flakiness.

When you click cy.get('[data-testid="size-selector"] option[value="M"]').click();, Cypress doesn’t just look for the element and click. It waits for the element to be visible, enabled, and actionable before performing the click. If any of these conditions aren’t met within the retry timeout, the test will fail. This automatic waiting is one of Cypress’s most powerful features for ensuring reliable tests. Similarly, .should('be.disabled') or .should('not.be.disabled') checks the disabled attribute of the element, and .should('be.visible') confirms it’s not hidden by CSS.

You might notice that Cypress automatically adds a visual indicator on the element being interacted with during test execution, and it provides a time-traveling debugger that lets you step back and forth through your test commands, inspecting the DOM at each stage. This is invaluable for debugging.

The cy.intercept() command is how you can stub or mock network requests, allowing you to test specific scenarios without relying on your backend being available or returning specific data. For instance, you could intercept a POST /cart request and return a simulated error response to test how your UI handles that.

If you’re dealing with authenticated routes, you’ll often use cy.request() to log a user in via your API before visiting the application, bypassing the UI login flow for faster, more reliable test setup. This is generally preferred over simulating a UI login within the test itself.

The next logical step is often to test the complete checkout flow, which will involve navigating to the cart, proceeding to checkout, filling out form fields, and submitting the order.

Want structured learning?

Take the full Cypress course →