Assertions in Cypress — should, and, expect and Retry-Until-Pass

Assertions are where tests derive their value — they verify that the application’s actual behaviour matches the expected behaviour. Cypress assertions are unique because they retry automatically. When you write .should('have.length', 6), Cypress does not check once and fail — it re-queries the DOM and re-evaluates the assertion until it passes or the timeout expires. This retry-until-pass mechanism is what makes Cypress assertions both powerful and reliable without explicit waits.

Cypress Assertion Styles — should, and, expect

Cypress supports two assertion styles: .should() chains (implicit assertions that retry) and expect() statements (explicit BDD assertions inside .then() callbacks).

// ── STYLE 1: .should() — implicit, auto-retrying assertions ──

// Visibility
cy.get('.success-msg').should('be.visible');
cy.get('.spinner').should('not.exist');
cy.get('.modal').should('not.be.visible');

// Text content
cy.get('h1').should('contain.text', 'Dashboard');
cy.get('.error').should('have.text', 'Invalid email');  // exact match
cy.get('.greeting').should('include.text', 'Welcome');

// Element count
cy.get('.product-card').should('have.length', 6);
cy.get('.cart-item').should('have.length.greaterThan', 0);
cy.get('.error').should('have.length', 0);  // no errors exist

// CSS classes and attributes
cy.get('.tab-active').should('have.class', 'selected');
cy.get('a.docs').should('have.attr', 'href', '/documentation');
cy.get('input').should('have.attr', 'placeholder', 'Enter email');
cy.get('button').should('be.disabled');
cy.get('input').should('be.enabled');

// Input values
cy.get('#email').should('have.value', 'alice@test.com');
cy.get('#search').should('have.value', '');  // empty

// URL and page assertions
cy.url().should('include', '/dashboard');
cy.url().should('eq', 'https://app.com/dashboard');
cy.title().should('contain', 'Dashboard');

// Chaining multiple assertions with .and()
cy.get('[data-cy="alert"]')
  .should('be.visible')
  .and('have.class', 'alert-danger')
  .and('contain.text', 'Payment failed');


// ── STYLE 2: expect() — explicit BDD assertions in .then() ──

cy.get('.product-card').then(($cards) => {
  // $cards is a jQuery object — use expect() for complex logic
  expect($cards).to.have.length(6);
  expect($cards.first()).to.contain.text('Backpack');

  // Extract and verify data
  const prices = [...$cards].map(card =>
    parseFloat(card.querySelector('.price')?.textContent?.replace('$', '') || '0')
  );
  expect(prices).to.deep.equal([29.99, 9.99, 15.99, 49.99, 7.99, 15.99]);

  // Verify sorting (ascending)
  const sorted = [...prices].sort((a, b) => a - b);
  expect(prices).to.deep.equal(sorted);
});


// ── Negative assertions ──

// Element should NOT exist in the DOM
cy.get('.loading-spinner').should('not.exist');

// Element exists but should NOT be visible
cy.get('.tooltip').should('not.be.visible');

// Text should NOT contain
cy.get('.status').should('not.contain.text', 'Error');

// Element should NOT have class
cy.get('.tab').should('not.have.class', 'disabled');


// ── Custom assertion messages ──

// .should() with a callback for custom logic
cy.get('.price').should(($el) => {
  const price = parseFloat($el.text().replace('$', ''));
  expect(price, 'Price should be between $1 and $1000').to.be.within(1, 1000);
  expect(price, 'Price should have 2 decimal places')
    .to.equal(Math.round(price * 100) / 100);
});
Note: The critical difference between .should() and .then() + expect() is retry behaviour. .should() retries the entire preceding query chain until the assertion passes — if cy.get('.items').should('have.length', 5) finds 3 items, Cypress re-queries .items and checks again. Assertions inside .then() do NOT retry — the callback runs once with whatever the query returned. Use .should() for conditions that may take time to become true (element appearing, count changing). Use .then() + expect() for complex calculations on data that is already stable.
Tip: Use .should('not.exist') to wait for an element to disappear (like a loading spinner). This assertion retries: Cypress checks “does this element exist in the DOM?” repeatedly until it does not, or the timeout expires. It is the Cypress equivalent of Selenium’s EC.invisibility_of_element_located() but with zero boilerplate — one line versus four.
Warning: .should('have.text', 'Welcome') is an exact match including whitespace. If the element renders as Welcome with extra spaces, the assertion fails. Use .should('contain.text', 'Welcome') for substring matching or .invoke('text').invoke('trim').should('eq', 'Welcome') for trimmed exact matching. This whitespace sensitivity is a common source of “the assertion looks correct but fails” confusion.

Common Mistakes

Mistake 1 — Using expect() inside .then() for conditions that need retries

❌ Wrong: cy.get('.items').then(($el) => { expect($el).to.have.length(5); }) — if items are still loading, .then() runs once with 0 items and fails immediately.

✅ Correct: cy.get('.items').should('have.length', 5).should() retries until 5 items appear.

Mistake 2 — Confusing have.text (exact) with contain.text (substring)

❌ Wrong: .should('have.text', 'Welcome') on an element that renders “Welcome, Alice!” — fails because it is not an exact match.

✅ Correct: .should('contain.text', 'Welcome') — matches the substring within the full text.

🧠 Test Yourself

You need to verify that a product list displays exactly 6 items, but the items load asynchronously via an API call. Which Cypress assertion handles this correctly?