Cypress part 7

Cypress Actionability and Interaction with Elements

 What you’ll learn
  • How Cypress determines if an element is actionable
  • How to debug when elements are not actionable
  • How to ignore Cypress’ actionability checks
  • Common patterns for handling asynchronous code in Cypress
  • When to assign variables and when not to
  • How to use aliases to share objects between hooks and tests
  • Pitfalls with using this and how to avoid them
  • How to alias DOM elements, intercepts, and request
  • How Cypress ensures test isolation.
  • How to configure test isolation in end-to-end and component testing.
  • The trade-offs of enabling vs. disabling test isolation. 
  • The difference between commands, queries, and assertions in Cypress
  • How Cypress retries multiple assertions
  • How to increase the time Cypress retries commands
  • How Cypress retries queries with examples

     

Understanding Actionability

Cypress ensures that elements are “actionable” before interacting with them. This means Cypress verifies whether an element is visible, enabled, and ready for interaction before performing commands like .click(), .type(), or .select().

Checks Before Actions

Before executing an action command, Cypress checks:

  • If the element is visible (not hidden, not display: none, not visibility: hidden).
  • If the element is not disabled (for form elements).
  • If the element is not detached (still present in the DOM).
  • If the element is not readonly (for input fields).
  • If the element is not animating (by tracking movement).
  • If the element is not covered (by another element).

If any of these checks fail, Cypress throws an error.

Scrolling Behavior

  • Cypress automatically scrolls elements into view before interaction.
  • The scrolling behavior can be customized with the scrollBehavior option ('top', 'center', 'bottom', 'nearest', false).

Handling Visibility Issues

  • Cypress considers an element hidden if it has opacity: 0, display: none, overflow: hidden, or position: fixed but is offscreen.
  • If an element is covered by another element, Cypress will fire the event at the child element if applicable.

Debugging Actionability Issues

  • Use .debug() before the action to inspect the element:
    cy.get('button').debug().click()
    
  • Cypress also provides a Command Log and hitbox visualization to track interactions.

Forcing Interactions ({ force: true })

  • If an element is not actionable but you still want to interact with it, use { force: true }:
    cy.get('button').click({ force: true })
    
  • This bypasses Cypress’ default checks and forces the event to trigger.

Limitations of { force: true }

  • Cypress will not:
    • Scroll the element into view.
    • Ensure visibility.
    • Ensure the element is enabled or attached to the DOM.
    • Prevent interactions with disabled <option> elements inside <select>.

Cypress actionability checks help mimic real user interactions while preventing flaky tests. When debugging, use .debug() or { force: true } as necessary.

 

Variables and Aliases in Cypress

Cypress uses an asynchronous command queue, which can make managing variables tricky for beginners. Understanding closures, aliases, and Cypress commands will help you write efficient tests without unnecessary complexity.


Handling Asynchronous Code in Cypress

Key Concept: Cypress Commands Run Asynchronously

Unlike traditional synchronous code, Cypress commands do not return values immediately. Instead, they enqueue commands that execute in sequence.

// This does NOT work as expected
const button = cy.get('button');
const form = cy.get('form');
button.click();

Since commands are asynchronous, you need to use .then() to work with their yielded results.

Using Closures with .then()

Closures in Cypress allow you to access values from previous commands.

cy.get('button').then(($btn) => {
  const txt = $btn.text();
  cy.get('form').submit();
  cy.get('button').should(($btn2) => {
    expect($btn2.text()).not.to.eq(txt);
  });
});

Debugging Cypress Tests

Using .then() gives an opportunity to debug values at various stages of execution.

cy.get('button').then(($btn) => {
  debugger;
  cy.get('[data-testid="countries"]').select('USA').then(($select) => {
    debugger;
  });
});

When to Use Variables in Cypress

Cypress rarely requires const, let, or var since commands yield values automatically. However, when working with mutable objects, storing values is useful.

cy.get('[data-testid="num"]').then(($span) => {
  const num1 = parseFloat($span.text());
  cy.get('button').click().then(() => {
    const num2 = parseFloat($span.text());
    expect(num2).to.eq(num1 + 1);
  });
});

Using Aliases for Better Test Management

Aliases help share values between hooks and tests without using unnecessary global variables.

Creating an Alias

beforeEach(() => {
  cy.get('button').invoke('text').as('text');
});

it('has access to text', function () {
  this.text; // Available in test
});

Sharing Context with Aliases

Aliases work across different levels of hooks and test suites.

describe('parent', () => {
  beforeEach(() => {
    cy.wrap('one').as('a');
  });

  context('child', () => {
    beforeEach(() => {
      cy.wrap('two').as('b');
    });

    describe('grandchild', () => {
      beforeEach(() => {
        cy.wrap('three').as('c');
      });

      it('can access all aliases', function () {
        expect(this.a).to.eq('one');
        expect(this.b).to.eq('two');
        expect(this.c).to.eq('three');
      });
    });
  });
});

Accessing Fixtures with Aliases

Loading fixtures in beforeEach() and accessing them in tests is a common use case for aliases.

beforeEach(() => {
  cy.fixture('users.json').as('users');
});

it('uses fixture data', function () {
  const user = this.users[0];
  cy.get('header').should('contain', user.name);
});

Avoiding Async Issues

Since Cypress commands are asynchronous, avoid accessing aliases before they are assigned.

Incorrect:

it('incorrect alias usage', function () {
  cy.fixture('users.json').as('users');
  const user = this.users[0]; // Fails, users not available yet
});

Correct:

cy.fixture('users.json').then((users) => {
  const user = users[0];
  cy.get('header').should('contain', user.name);
});

Alternative to this.*: Using @ Notation

Instead of this.users, Cypress supports accessing aliases with cy.get('@alias').

beforeEach(() => {
  cy.fixture('users.json').as('users');
});

it('accesses users via @ alias', () => {
  cy.get('@users').then((users) => {
    const user = users[0];
    cy.get('header').should('contain', user.name);
  });
});

Understanding Cypress Aliases vs Variables

Using cy.get('@alias') fetches the latest value, while this.alias references the original value at alias creation.

const favorites = { color: 'blue' };
cy.wrap(favorites).its('color').as('favoriteColor');

cy.then(function () {
  favorites.color = 'red';
});

cy.get('@favoriteColor').then(function (aliasValue) {
  expect(aliasValue).to.eql('red'); // Fresh value
  expect(this.favoriteColor).to.eql('blue'); // Stale value
});

 Cypress Test Isolation

  • Best Practice

Tests should always be independent and pass whether run alone or in sequence.

How Cypress Ensures Test Isolation

  • Cypress clears the state before each test to prevent test dependencies.
  • This avoids nondeterministic failures and makes debugging easier.
  • It resets:
    • Aliases
    • Clock mocks
    • Intercepts
    • Spies & Stubs
    • Viewport changes

Test Isolation in End-to-End Testing

Test Isolation Enabled (testIsolation: true)

  • Cypress clears the browser context before each test by:
    • Visiting about:blank
    • Clearing cookies, localStorage, sessionStorage.
  • You must re-visit the app and rebuild the DOM for each test.
  • cy.session() clears the page and browser context.

Test Isolation Disabled (testIsolation: false)

  • Cypress does not alter the browser context before tests.
  • The page, cookies, localStorage, and sessionStorage persist between tests.
  • cy.session() only clears the browser context, not the page.

Comparison Table

testIsolation Before Each Test cy.session()
true (enabled) Clears page, cookies, localStorage, sessionStorage Clears page, cookies, localStorage, sessionStorage
false (disabled) Does not clear page, retains storage Clears cookies, localStorage, sessionStorage

Test Isolation in Component Testing

  • Always resets browser context before each test by:
    • Unmounting the component
    • Clearing cookies, localStorage, sessionStorage
  • Cannot be disabled.

Trade-offs of Test Isolation

Enabling isolation ensures reliable, independent tests but requires rebuilding the DOM for each test.
Disabling isolation improves test performance but risks state leakage, leading to flaky tests.

Best Practice

Use .only() to verify that each test runs successfully on its own without depending on previous tests.

Here’s the bolded version of your text emphasizing key points:


Retry-ability

 

If you are looking to retry tests a configured number of times when the test fails, check out our guide on Test Retries.


Commands, Queries, and Assertions

While all methods you chain off of cy in your Cypress tests are commands, it’s important to understand the different rules by which they operate.

  • Queries link up, retrying the entire chain together.
  • Assertions are a type of query that’s specially displayed in the command log.
  • Non-queries only execute once.

For example, in the test below, there are 5 queries, an action, and 2 assertions:

it('creates an item', () => {
  // Non-query commands only execute once.
  cy.visit('/')

  // The .focused() query and .should() assertion link together,
  // rerunning until the currently focused element has the 'new-todo' class
  cy.focused().should('have.class', 'new-todo')

  // The queries .get() and .find() link together,
  // forming the subject for the non-query `.type()`.
  cy.get('.header').find('.new-todo').type('todo A{enter}')

  // Two queries and an assertion chained together
  cy.get('.todoapp').find('.todo-list li').should('have.length', 1)
})

The Command Log shows all commands regardless of types, with passing assertions shown in green.


How Cypress Handles Assertions and Queries

Because modern web applications are not synchronous, Cypress must be able to retry queries and assertions.

Why queries must retry:

  • The application might not have updated the DOM yet
  • The application could be waiting for a backend response
  • The application might be processing data before updating the DOM

Thus, cy.get() and cy.find() must be smart enough to expect changes and keep retrying.

If the assertion passes, then .should() finishes successfully.
If the assertion fails, Cypress will requery the application’s DOM and retry the entire query chain until the timeout is reached.

Retry-ability allows the test to complete each command as soon as the assertion passes, without hard-coding waits.


Multiple Assertions

Cypress always retries from the top of the chain until all assertions pass.

For example, in the test below, Cypress will retry until both assertions pass:

it('creates two items', () => {
  cy.visit('/')

  cy.get('.new-todo').type('todo A{enter}')
  cy.get('.new-todo').type('todo B{enter}')

  cy.get('.todo-list li') // query
    .should('have.length', 2) // assertion
    .and(($li) => {
      expect($li.get(0).textContent, 'first item').to.equal('todo a')
      expect($li.get(1).textContent, 'second item').to.equal('todo B')
    })
})

If the first assertion fails, Cypress never executes the second one.


Implicit Assertions

Cypress has built-in implicit assertions that will retry queries automatically.

For example:

cy.get('.todo-list li') // query
  .should('have.length', 2) // assertion
  .eq(3) // query

Only queries are retried, but action commands (like .click()) have built-in waiting mechanisms.


Timeouts

By default, Cypress retries commands for up to 4 seconds (defaultCommandTimeout).

To increase the timeout for a command:

cypress run --config defaultCommandTimeout=10000

To set a timeout for a specific command:

cy.get('[data-testid="mobile-nav"]', { timeout: 10000 })
  .should('be.visible')
  .and('contain', 'Home')

Avoid changing the global timeout unless necessary.

To disable retries:

cy.get('[data-testid="ssr-error"]', { timeout: 0 }).should('not.exist')

Only Queries Are Retried

  • Action commands like .click() execute only once.
  • Cypress does not retry commands that could change the application’s state.

Incorrect chaining (prone to failure):

cy.get('.new-todo')
  .type('todo A{enter}') // action
  .type('todo B{enter}') // action after another action - bad
  .should('have.class', 'active') // assertion after an action - bad

Correct approach (avoid re-rendering issues):

cy.get('.new-todo').type('todo A{enter}')
cy.get('.new-todo').type('todo B{enter}')
cy.get('.new-todo').should('have.class', 'active')

Using cy.as() can help make the pattern cleaner:

cy.get('.new-todo').as('new')

cy.get('@new').type('todo A{enter}')
cy.get('@new').type('todo B{enter}')
cy.get('@new').should('have.class', 'active')

 


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *