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
, notvisibility: 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
, orposition: 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.
- Visiting
- 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')
Leave a Reply