Cypress tests follow a distinctive pattern that feels different from Selenium. Instead of imperative commands (driver.find_element().click()), Cypress uses a chainable, declarative syntax: cy.get('.btn').click(). Every cy command is automatically queued, retried, and executed in order. You never await promises or manage async callbacks — Cypress handles all of that internally. This lesson walks through the four-step pattern of every Cypress test: visit, query, act, assert.
The Cypress Command Chain — Visit, Query, Act, Assert
Every Cypress test follows the same flow: navigate to a page, find elements, interact with them, and verify the result. The magic is that each step automatically retries and waits.
// ── First Cypress test: login flow ──
// File: cypress/e2e/login.cy.ts
describe('Login Page', () => {
beforeEach(() => {
// VISIT — navigate to the page
cy.visit('/'); // Uses baseUrl from cypress.config.ts
});
it('should login with valid credentials and show inventory', () => {
// QUERY — find elements (auto-retries until found or timeout)
cy.get('[data-test="username"]').type('standard_user');
cy.get('[data-test="password"]').type('secret_sauce');
// ACT — interact with elements
cy.get('[data-test="login-button"]').click();
// ASSERT — verify the result
cy.url().should('include', '/inventory');
cy.get('.inventory_item').should('have.length', 6);
cy.get('.inventory_item_name').first().should('contain.text', 'Sauce Labs');
});
it('should show error for invalid credentials', () => {
cy.get('[data-test="username"]').type('invalid_user');
cy.get('[data-test="password"]').type('wrong_password');
cy.get('[data-test="login-button"]').click();
// Assert error message appears
cy.get('[data-test="error"]')
.should('be.visible')
.and('contain.text', 'do not match');
});
it('should show error when username is empty', () => {
cy.get('[data-test="login-button"]').click();
cy.get('[data-test="error"]')
.should('be.visible')
.and('contain.text', 'Username is required');
});
});
// ── Key Cypress commands ──
/*
NAVIGATION:
cy.visit('/path') Navigate to a URL (relative to baseUrl)
cy.reload() Reload the current page
cy.go('back') Browser back button
QUERYING (all auto-retry):
cy.get('selector') Find element(s) by CSS selector
cy.contains('text') Find element containing text
cy.get('form').find('input') Find within a parent element
ACTIONS:
.click() Click an element
.dblclick() Double-click
.type('text') Type into an input
.clear() Clear an input
.check() Check a checkbox
.uncheck() Uncheck a checkbox
.select('value') Select dropdown option
.scrollIntoView() Scroll element into viewport
ASSERTIONS (.should() auto-retries):
.should('be.visible') Element is visible
.should('not.exist') Element is not in DOM
.should('have.length', 6) Collection has 6 items
.should('contain.text', 'x') Contains text
.should('have.value', 'x') Input has value
.should('have.class', 'active') Has CSS class
.should('have.attr', 'href', '/path') Has attribute value
cy.url().should('include', '/dashboard') URL check
*/
// ── Automatic retry example ──
// This command retries for up to 4 seconds (defaultCommandTimeout):
// cy.get('.loading-spinner').should('not.exist');
// ↑ Keeps checking until the spinner disappears — no manual wait!
// cy.get('.results').should('have.length.greaterThan', 0);
// ↑ Keeps checking until at least one result appears
await them or store their results in variables like normal async JavaScript. Instead, Cypress commands are queued internally and executed in sequence. To work with a value, use .then(): cy.get('.price').invoke('text').then((text) => { expect(parseFloat(text)).to.be.greaterThan(0); }). This is the most common source of confusion for developers new to Cypress — treat commands as a queue, not as promises.data-test or data-cy attributes for selectors: cy.get('[data-test="login-button"]'). Cypress’s best practices documentation recommends this approach because data attributes are immune to CSS changes, clearly communicate their purpose, and can be stripped from production builds. Many teams adopt a convention where developers add data-cy attributes to every interactive element — the same principle as data-testid in Selenium.cy.get() to a variable and reuse it: const btn = cy.get('.btn'); btn.click(); btn.should('be.disabled'); — this does not work because Cypress commands are queued, not executed immediately. By the time btn.click() runs, the reference may be stale. Instead, chain commands or re-query: cy.get('.btn').click(); cy.get('.btn').should('be.disabled');.Common Mistakes
Mistake 1 — Adding cy.wait() with fixed milliseconds
❌ Wrong: cy.wait(3000); cy.get('.results').should('have.length', 5);
✅ Correct: cy.get('.results').should('have.length', 5); — the .should() assertion automatically retries until the condition is true or the timeout expires. No fixed wait needed.
Mistake 2 — Storing Cypress commands in variables
❌ Wrong: const el = cy.get('.btn'); el.click(); — commands are not assignable like Selenium WebElements.
✅ Correct: cy.get('.btn').click(); — chain directly, or use .as('alias') for reuse: cy.get('.btn').as('submitBtn'); cy.get('@submitBtn').click();