Interacting 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
Learn how UI Coverage highlights areas where interactive elements have not been tested. Understand coverage gaps, inform your testing strategy, and ship high-quality code with confidence.
Actionability
Some commands in Cypress are for interacting with the DOM such as:
.click()
.dblclick()
.rightclick()
.type()
.clear()
.check()
.uncheck()
.select()
.trigger()
.selectFile()
We call these "action commands." These actions simulate a user interacting with your application. Under the hood, Cypress fires the events a browser would fire thus causing your application's event bindings to fire.
Prior to issuing any of the commands, we check the current state of the DOM and take some actions to ensure the DOM element is "ready" to receive the action.
Cypress will watch the DOM - re-running the queries that yielded the current
subject - until an element passes all of these checks for the duration of the
defaultCommandTimeout
(described
in depth in the
Implicit Assertions
core concept guide).
Checks and Actions Performed
- Scroll the element into view.
- Ensure the element is not hidden.
- Ensure the element is not disabled.
- Ensure the element is not detached.
- Ensure the element is not readonly.
- Ensure the element is not animating.
- Ensure the element is not covered.
- Scroll the page if still covered by an element with fixed position.
- Fire the event at the desired coordinates.
Whenever Cypress cannot interact with an element, it could fail at any of the above steps. You will usually get an error explaining why the element was not found to be actionable.
Visibility
Cypress checks a lot of things to determine an element's visibility. The following calculations factor in CSS translations and transforms.
An element is considered hidden if:
- Its
width
orheight
is0
. - Its CSS property (or ancestors) is
visibility: hidden
. - Its CSS property (or ancestors) is
display: none
. - Its CSS property is
position: fixed
and it's offscreen or covered up. - Any of its ancestors hides overflow*
- AND that ancestor has a
width
orheight
of0
- AND an element between that ancestor and the element is
position: absolute
- AND that ancestor has a
- Any of its ancestors hides overflow*
- AND that ancestor or an ancestor between it and that ancestor is its offset parent
- AND it is positioned outside that ancestor's bounds
- Any of its ancestors hides overflow*
- AND the element is
position: relative
- AND it is positioned outside that ancestor's bounds
- AND the element is
*hides overflow means it has overflow: hidden
, overflow-x: hidden
,
overflow-y: hidden
, overflow: scroll
, or overflow: auto
Elements where the CSS property (or ancestors) is opacity: 0
are considered
hidden when
asserting on the element's visibility directly.
However elements where the CSS property (or ancestors) is opacity: 0
are
considered actionable and any commands used to interact with the hidden element
will perform the action.
Disability
Cypress checks whether the disabled
property is true
on a
form control element, such as button
or input
. Setting a disabled
attribute on other elements will
have no effect on a user's ability to interact with them,
and won't impact Cypress actionability checks.
Detached
Cypress checks whether an element you are making assertions on is still within
the document
of the application under test.
When many applications rerender the DOM, they actually remove the DOM element and insert a new DOM element in its place with the newly change attributes. This is why it's important not to chain action commands together - cypress can re-run queries to locate the fresh element, but it will never re-run commands.
Readonly
Cypress checks whether an element's readonly
property is set during
.type().
Animations
Cypress will automatically determine if an element is animating and wait until it stops.
To calculate whether an element is animating we take a sample of the last positions it was at and calculate the element's slope. You might remember this from 8th grade algebra. 😉
To calculate whether an element is animating we check the current and previous
positions of the element itself. If the distance exceeds the
animationDistanceThreshold
,
then we consider the element to be animating.
When coming up with this value, we did a few experiments to find a speed that "feels" too fast for a user to interact with. You can always increase or decrease this threshold.
You can also turn off our checks for animations with the configuration option
waitForAnimations
.
Covering
We also ensure that the element we're attempting to interact with isn't covered by a parent element.
For instance, an element could pass all of the previous checks, but a giant dialog could be covering the entire screen making interacting with the element impossible for any real user.
When checking to see if the element is covered we always check its center coordinates.
If a child of the element is covering it - that's okay. In fact we'll automatically issue the events we fire to that child.
Imagine you have a button:
<button>
<i class="fa fa-check">
<span>Submit</span>
</button>
Oftentimes either the <i>
or <span>
element is covering the exact coordinate
we're attempting to interact with. In those cases, the event fires on the child.
We even note this for you in the
Command Log.
Scrolling
Before interacting with an element, we will always scroll it into view (including any of its parent containers). Even if the element was visible without scrolling, we perform the scrolling algorithm in order to reproduce the same behavior every time the command is run.
This scrolling logic only applies to commands that are actionable above. We do not scroll elements into view when using DOM commands such as cy.get() or .find().
By default, the scrolling algorithm works by scrolling the top, leftmost point of the element we issued the command on to the top, leftmost scrollable point of its scrollable container.
After scrolling the element, if we determine that it is still being covered up,
we will continue to scroll and "nudge" the page until it becomes visible. This
most frequently happens when you have position: fixed
or position: sticky
navigation elements which are fixed to the top of the page.
Our algorithm should always be able to scroll until the element is not covered.
To change the position in the viewport to where we scroll an element, you can
use the scrollBehavior
configuration option. This can be useful if the element is covered up when
aligned to the top of the viewport, or if you just prefer the element to be
centered during scrolling of action commands. Accepted values are 'center'
,
'top'
, 'bottom'
, 'nearest'
, and false
, with false
disabling scrolling
altogether.
Coordinates
After we verify the element is actionable, Cypress will then fire all of the appropriate events and corresponding default actions. Usually these events' coordinates are fired at the center of the element, but most commands enable you to change the position it's fired to.
cy.get('button').click({ position: 'topLeft' })
The coordinates we fired the event at will generally be available when clicking the command in the Command Log.
Additionally we'll display a red "hitbox" - which is a dot indicating the coordinates of the event.
Debugging
It can be difficult to debug problems when elements are not considered actionable by Cypress.
Although you should see a nice error message, nothing beats visually inspecting and poking at the DOM yourself to understand the reason why.
When you use the Command Log to hover over a command, you'll notice that we will always scroll the element the command was applied to into view. Please note that this is NOT using the same algorithms that we described above.
In fact we only ever scroll elements into view when actionable commands are
running using the above algorithms. We do not scroll elements into view on
regular DOM queries like cy.get()
or
.find()
.
The reason we scroll an element into view when hovering over a snapshot is to help you to see which element(s) were found by that corresponding command. It's a purely visual feature and does not necessarily reflect what your page looked like when the command ran.
In other words, you cannot get a correct visual representation of what Cypress "saw" when looking at a previous snapshot.
The only way for you to "see" and debug why Cypress thought an element was not
visible is to use a debugger
statement.
We recommend placing debugger
or using the .debug()
command directly BEFORE the action.
Make sure your Developer Tools are open and you can get pretty close to "seeing" the calculations Cypress is performing.
You can also bind to Events that Cypress fires as it's working with your element. Using a debugger with these events will give you a much lower level view into how Cypress works.
// break on a debugger before the action command
cy.get('button').debug().click()
Forcing
While the above checks are super helpful at finding situations that would prevent your users from interacting with elements - sometimes they can get in the way!
Sometimes it's not worth trying to "act like a user" to get a robot to do the exact steps a user would to interact with an element.
Imagine you have a nested navigation structure where the user must hover over and move the mouse in a very specific pattern to reach the desired link.
Is this worth trying to replicate when you're testing?
Maybe not! For these scenarios, we give you an escape hatch to bypass all of the checks above and force events to happen!
You can pass { force: true }
to most action commands.
// force the click and all subsequent events
// to fire even if this element isn't considered 'actionable'
cy.get('button').click({ force: true })
When you force an event to happen we will:
- Continue to perform all default actions
- Forcibly fire the event at the element
We will NOT perform these:
- Scroll the element into view
- Ensure it is visible
- Ensure it is not disabled
- Ensure it is not detached
- Ensure it is not readonly
- Ensure it is not animating
- Ensure it is not covered
- Fire the event at a descendent
In summary, { force: true }
skips the checks, and it will always fire the
event at the desired element.
force .select()
disabled options
Passing { force: true }
to .select() will not override
the actionability checks for selecting a disabled <option>
or an option within
a disabled <optgroup>
. See
this issue for more detail.