Cypress Lesson 2 of 6 ⏱️ 4 min read

Selectors & Assertions

Stable data-cy selectors, chaining commands, should() assertions and how Cypress retries automatically.

Updated Oct 30, 2026

Choosing selectors that don't break

Most broken Cypress tests are caused by fragile selectors. Cypress's own best-practice ranking, from worst to best:

Selector Example Problem
Generic tag cy.get('button').click() Matches any button
CSS class cy.get('.btn.btn-primary') Classes change with styling
ID cy.get('#main') Better, but IDs change too
Text cy.contains('Submit') OK for user-facing text, breaks on copy changes or translation
Dedicated attribute cy.get('[data-cy=submit]') Exists only for tests — stable

Ask developers to add data-cy (or data-test) attributes to elements your tests interact with:

HTML
<form data-cy="signup-form">
  <input data-cy="email" type="email" name="email">
  <input data-cy="password" type="password" name="password">
  <button data-cy="submit" type="submit">Create account</button>
  <p data-cy="error" class="error"></p>
</form>
Javascript
cy.get('[data-cy=email]').type('ayesha@example.com');
cy.get('[data-cy=password]').type('Secret#2026');
cy.get('[data-cy=submit]').click();

Now designers can restyle the form freely without breaking a single test.

Finding elements

Javascript
cy.get('[data-cy=todo-list] li');                 // CSS selector
cy.get('[data-cy=todo-list]').find('li');         // search inside a previous subject
cy.get('[data-cy=todo-list] li').first();         // first of many
cy.get('[data-cy=todo-list] li').eq(2);           // third item (zero-based)
cy.contains('[data-cy=todo-list] li', 'Buy milk');// element containing text
cy.get('[data-cy=todo-list]').within(() => {      // scope several commands
  cy.get('li').should('have.length', 3);
});

Assertions with should

.should() uses Chai assertions and — crucially — retries until it passes or the timeout (4 seconds by default) expires.

Javascript
cy.get('[data-cy=error]').should('be.visible');
cy.get('[data-cy=error]').should('have.text', 'Email is already taken');
cy.get('[data-cy=error]').should('contain', 'already taken');
cy.get('[data-cy=submit]').should('be.disabled');
cy.get('[data-cy=email]').should('have.value', 'ayesha@example.com');
cy.get('[data-cy=todo-list] li').should('have.length', 3);
cy.get('[data-cy=banner]').should('have.class', 'success');
cy.get('[data-cy=link]').should('have.attr', 'href', '/dashboard');
cy.url().should('include', '/dashboard');

Chain multiple assertions on the same subject with .and():

Javascript
cy.get('[data-cy=error]')
  .should('be.visible')
  .and('contain', 'already taken')
  .and('have.css', 'color', 'rgb(220, 38, 38)');

Negative assertions also retry:

Javascript
cy.get('[data-cy=spinner]').should('not.exist');
cy.get('[data-cy=error]').should('not.be.visible');

How retry-ability works

Javascript
cy.get('[data-cy=todo-list] li').should('have.length', 3);

If the list renders 0 items, then 2, then 3 over the next second, Cypress keeps re-querying cy.get() and re-checking the assertion until it sees 3. That's why you almost never need cy.wait(2000).

Only queries (get, find, contains, its…) and assertions retry. Actions like .click() and .type() run once — so assert that the element is ready before acting if it might be disabled:

Javascript
cy.get('[data-cy=submit]').should('be.enabled').click();

A complete example on the demo app

Against Cypress's demo app (baseUrl: 'https://example.cypress.io'):

Javascript
// cypress/e2e/querying.cy.js
describe('Querying the kitchen sink', () => {
  beforeEach(() => cy.visit('/commands/querying'));

  it('finds elements by data attribute', () => {
    cy.get('[data-test-id="test-example"]').should('have.class', 'example');
  });

  it('finds elements by text', () => {
    cy.contains('ul', 'apples').should('have.class', 'query-list');
    cy.contains('.query-button', 'Save Form').should('have.class', 'query-button');
  });

  it('scopes queries with within()', () => {
    cy.get('.query-form').within(() => {
      cy.get('input:first').should('have.attr', 'placeholder', 'Email');
      cy.get('input:last').should('have.attr', 'placeholder', 'Password');
    });
  });
});

Explicit assertions with expect

Inside .then() you work with the real value, so use Chai's expect (which does not retry):

Javascript
cy.get('[data-cy=price]').invoke('text').then((text) => {
  const price = Number(text.replace('$', ''));
  expect(price).to.be.greaterThan(0);
  expect(price).to.be.lessThan(100);
});

Prefer .should() whenever possible — retrying assertions are what make Cypress tests stable. Next: controlling the network with cy.intercept.

Finished this lesson?
Tick it off to track your Cypress progress.
Next lesson