Cypress Queries, Assertions & Custom Commands
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Every Cypress test is a sequence of the same three moves: find an element (a query), do something to it (an action) and
check the result (an assertion). This page covers the first and the last, plus the machinery for packaging your own
finders and helpers: cy.get(), cy.contains() and, new in Cypress 16.2, cy.getByRole(); scoping with .within() and
the traversal commands; the assertions bundled with Cypress and how .should() and .and() retry them; and custom
commands, custom queries and the two overwrite APIs. It is written for Cypress 16.2.0 and was checked on 25 October 2026.
Every example on this page is a spec that was run against the Bookshelf
application: 30 tests in five spec files, all passing in headless Electron (Chromium 146) and in headless Google Chrome 155
on macOS. The failure messages quoted below were captured from deliberately failing tests in a separate spec. For the command queue, the retry loop
and the difference between queries, actions and assertions see
Cypress Core Concepts. The ideas about what to assert on and how to choose
selectors are shared with the other tools and are discussed in
Test Design & Page Objects; the Playwright counterpart of this page is
Locators, Actions & Auto-waiting together with
Playwright Assertions.
The project used for the examples
The examples live in a Cypress project that has the cypress package (16.2.0) as its only dependency and no plugins. The
configuration reads the base URL from the environment; the specs are TypeScript and are loaded after a support file that
registers the custom commands.
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: process.env.BASE_URL ?? 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.cy.ts',
supportFile: 'cypress/support/e2e.ts',
screenshotsFolder: 'cypress/screenshots',
video: false,
setupNodeEvents() {},
},
});
Source: Cypress: Configuration
import './commands';
The specs were run with the commands below. CYPRESS_READER_PASSWORD supplies the secret that cy.env() reads (see
Custom commands); it is copied from the variable that Bookshelf itself reads. Set it to a local test password; never commit a real one.
BASE_URL=http://localhost:3923 CYPRESS_READER_PASSWORD="$BOOKSHELF_READER_PASSWORD" npx cypress run --browser electron
BASE_URL=http://localhost:3923 CYPRESS_READER_PASSWORD="$BOOKSHELF_READER_PASSWORD" npx cypress run --browser chrome
Source: Cypress: Command line
Both runs ended with All specs passed!: selecting.cy.ts 7 tests, get-by-role.cy.ts 7, assertions.cy.ts 9,
custom-commands.cy.ts 4 and overwrite.cy.ts 3. The project also type-checks with tsc --noEmit, so the type
declarations shown below are exactly what a TypeScript project needs. The cypress run commands are headless; cypress open
runs the same specs in a headed browser with the Command Log, which is the better place to watch a query retry. Nothing on
this page behaves differently between headless and headed runs, between Electron and Chrome, or between a laptop and a CI
job; the only difference to watch is that a headed run lets you click a command in the log and inspect what a query yielded.
Each spec file wraps its tests in a describe block with a beforeEach hook that restores the seeded data through
Bookshelf’s reset endpoint. selecting.cy.ts and assertions.cy.ts also open the catalogue in the hook, while the other
specs visit the pages they need inside the tests. The hook of the first spec, which the tests that are quoted from it assume:
describe('Selecting elements', () => {
beforeEach(() => {
cy.request('POST', '/api/test/reset');
cy.visit('/books');
});
// ... the tests quoted below
});
Queries, actions and assertions in one paragraph
Cypress commands come in kinds, and the kind decides what happens when something is not ready yet. A query (cy.get(),
cy.contains(), cy.getByRole(), .find(), .filter(), .first(), .parent(), .its(), .invoke(), .url() and the
rest of the list in the Cypress command table) reads the state of the page, is synchronous inside, has no side effects and is
retried: Cypress runs it again, from the start of the chain, until it yields something that satisfies the assertions
attached to it or the timeout (4 seconds by default, the defaultCommandTimeout setting) expires. An action
(.click(), .type(), .select()) waits for the element to be actionable, performs one effect and is never repeated. An
assertion (.should(), .and()) is attached to the previous command and is what makes the retry loop meaningful. Commands
that are neither, such as cy.request(), cy.visit() or .then(), run once. The practical rule that follows is to end
every chain of queries with an assertion that states what you expect, rather than reading a value and checking it in
.then(), which runs only once.
Choosing what to select
Every test needs a way to point at an element. The Cypress best-practices page ranks the options by how well they survive changes to the page:
| Selector | Recommended | Why |
|---|---|---|
|
Never |
Too generic, no context. |
|
Never |
Coupled to styling. |
|
Sparingly |
Coupled to styling or to JavaScript event listeners. |
|
Sparingly |
Coupled to the |
|
Depends |
Coupled to text content that may change. |
|
Depends |
Checks the role and the accessible name, so it is coupled to both. |
|
Always |
Isolated from every change to markup, styling and copy. |
The ranking is the one in the Cypress documentation; it is a recommendation, not a measurement. Two judgements are worth
making explicit. First, a dedicated attribute such as data-cy is the most stable handle because nothing but the test
uses it, but it says nothing about whether a real user could find the element. A test that clicks
[data-cy="submit"] still passes when the button is hidden from assistive technology. Second, cy.getByRole() and
cy.contains() are coupled to what the user perceives, which makes them better evidence that the page works and a little
more likely to break during a copy change. Most suites use a mix: roles and names for the controls a user operates, and test
ids for containers, list items and values that have no accessible name. The Cypress documentation also notes that none of
these approaches is a complete accessibility test.
Cypress does not care which attribute you pick; the documentation uses data-cy, other projects use data-test,
data-testid or data-qa. Bookshelf follows the data-testid convention that Testing Library and Playwright
popularised (see the example application page), so the specs on
this page select [data-testid=...]; replace the attribute name to use data-cy. Unlike Playwright, Cypress has no setting
that changes what a "test id" is, so wrapping the attribute in a custom query, as Custom queries does with
cy.getByTestId(), is the usual way to avoid repeating the attribute name in every spec. The eslint-plugin-cypress rule
cypress/require-data-selectors can enforce data-* selectors at lint time.
Selector priority for generated selectors
Cypress Studio and cy.prompt() generate selectors for you. The Cypress.ElementSelector.defaults() API controls which
attributes they prefer. The default priority is, in order, data-cy, data-test, data-testid, data-qa, name, id,
class, tag, attributes and nth-child. The accepted values are attribute:<name> (for example attribute:aria-label),
attributes, class, data-<name>, id, name, nth-child and tag. Cypress still guarantees that the selector it
generates is unique in the document, so it may skip a preferred attribute that is not unique or combine several. The API is
marked as under active development in the documentation, so treat the accepted values as subject to change.
// cypress/support/e2e.ts
// Prefer semantic and accessibility attributes before styling details.
Cypress.ElementSelector.defaults({
selectorPriority: ['data-testid', 'attribute:role', 'attribute:aria-label', 'name', 'class', 'attributes'],
});
Source: Cypress: Cypress.ElementSelector
This snippet was written from the documentation and was not run, because Studio and cy.prompt() need an interactive
session (and, for cy.prompt(), a Cypress Cloud account).
cy.get
cy.get(selector) queries the document for every element that matches a CSS selector (with the jQuery extensions Cypress
bundles, such as :first and :contains()), and cy.get('@name') reads an alias. It yields the elements it finds, which may
be one, many or, with a negative assertion, none. Because it is a query it retries until the element exists and every
assertion chained to it passes; without an explicit assertion, the implicit assertion is that the element exists.
| Option | Default | Description |
|---|---|---|
|
|
Displays the command in the Command Log. Use |
|
|
How long to wait for the command and the assertions chained to it. Must be a number. |
|
the |
The element to search inside. Pass |
|
the |
Whether to traverse shadow roots. |
The first spec shows the everyday uses: counting, picking one element, selector lists and attribute selectors.
it('cy.get finds elements by selector and retries until they exist', () => {
cy.get('[data-testid=book-card]').should('have.length', 6);
cy.get('[data-testid=book-card]').first().should('contain', 'The Quiet Compiler');
cy.get('[data-testid=book-card]').eq(1).should('contain', 'Rivers of Static');
cy.get('[data-testid=book-card]').last().should('contain', 'Small Systems, Big Days');
// A comma list yields the elements that match any selector.
cy.get('h1, h3[data-testid=book-title]').should('have.length', 7);
// Attribute selectors keep working when the markup changes around them.
cy.get('a[href^="/books/"]').should('have.length', 6);
});
Source: Cypress: get
.first(), .last() and .eq() read better than the jQuery :first and :eq() selectors, and they behave differently in
a way that matters: :first is a jQuery extension that yields one element for the whole page, so ul li:first is the first
li of the first list only, while the CSS :first-child yields the first li of every list. When an id contains
characters that CSS treats as syntax, such as . or :, escape them (cy.get('#id\\.\\.\\.1234')) or build the selector at
runtime with CSS.escape().
Aliases
.as('name') stores the chain that produced a subject, and cy.get('@name') re-runs it. Since Cypress 12 an alias for a DOM
element re-runs the queries leading up to it on every use, so the alias always reflects the current page and survives a
re-render. This is why aliasing a list and using it after pagination works in the next example. An alias created with
.as('name', { type: 'static' }) keeps the value captured at creation time instead. cy.get() also reads aliases of
primitives, objects, fixtures, spies, stubs and intercepts; for an intercept alias cy.get('@alias.all') yields every
matching interception and cy.get('@alias.1') the first one (indices start at 1). Those are covered with
cy.intercept() in Network, Stubs, Spies & Clocks.
it('aliases an element and re-runs the query each time it is used', () => {
cy.get('[data-testid=book-card]').as('cards');
cy.get('@cards').should('have.length', 6);
cy.get('[data-testid=page-next]').click();
cy.url().should('include', 'page=2');
// The alias re-queries the new page.
cy.get('@cards').first().should('contain', 'Orchard of Echoes').and('not.contain', 'Quiet');
});
Source: Cypress: Variables and aliases
get versus find
cy.get() always starts at cy.root(), which is the document element unless you are inside .within(). .find() starts at
the subject of the previous command, and ignoring the subject in cy.get() is a common source of confusion when a chain
like cy.get('#a').get('div') is read as meaning the divs inside #a.
it('cy.get starts at the document, .find starts at the subject', () => {
cy.get('[data-testid=book-list]')
.get('[data-testid=book-title]') // ignores the subject: the whole page
.should('have.length', 6);
cy.get('[data-testid=book-list]')
.find('[data-testid=book-author]')
.should('have.length', 6);
cy.get('[data-testid=pagination]').find('[data-testid=book-title]').should('not.exist');
});
Source: Cypress: get, Cypress: find
What cy.get does not do
cy.get() does not descend into iframes, so a same-origin iframe needs the custom command shown later (Custom commands)
or the pattern in the Cypress FAQ. It does not search shadow roots unless you pass includeShadowDom: true or set the
includeShadowDom configuration option; to search one shadow root only, chain .shadow() and then the query. A
timeout error names the selector and the time that elapsed:
AssertionError: Timed out retrying after 4000ms: Expected to find element: `[data-testid=nope]`, but never found it.
Source: Cypress: get
A typed element can be requested with a type argument, cy.get<HTMLInputElement>('[data-cy=email]'), which makes the
yielded jQuery object JQuery<HTMLInputElement>. A chain that starts with cy.get('@alias') can be typed the same way.
cy.contains
cy.contains(content) and cy.contains(selector, content) find elements by their text. The content can be a string, a number
or a regular expression, the selector restricts which elements are candidates, and the command yields at most one element.
| Option | Default | Description |
|---|---|---|
|
|
Case sensitivity. |
|
|
Displays the command in the Command Log. |
|
|
How long to wait for the element and the assertions chained to it. |
|
the |
Whether to search shadow roots. |
The matching rules explain most surprises:
-
A string matches a substring, so
cy.contains('button', 'Save')also matches "Save draft". Anchor a regular expression (/^Save$/) to match the whole text. Cypress collapses runs of whitespace in the element’s text to one space but does not trim it, so markup such as<button> Save </button>has the text " Save "; allow for it with/^\s*Save\s*$/. The text you pass is not collapsed, and a<pre>element is matched as written. -
Without a selector, Cypress yields the deepest element that contains the text, except that it prefers, in this order,
input[type='submit'],button,aandlabelwhen one of them contains the text. The preference applies only if the selector you pass matches one of those elements. The next spec shows it: the title of a Bookshelf card is anh3inside a link, and the link is whatcy.contains('Rivers of Static')yields, whilecy.contains('h3', 'Rivers of Static')yields the heading. -
Started from
cy, the search covers thebody, so text in<head>, such as the page title, never matches (usecy.title()). Chained off a subject it searches the subject’s descendants and, if none contains the text, the subject itself. -
.contains()yields hidden elements too. If the user must see the text, add.should('be.visible'). -
A non-breaking space in the page can be matched with an ordinary space in the content.
it('cy.contains matches text, prefers links and buttons, and can take a selector', () => {
// The heading is the deepest element holding the text, but a link, button or
// label that contains it is preferred, so the yielded element is the <a>.
cy.contains('Rivers of Static').should('match', 'a');
// With a selector, only matching elements are candidates.
cy.contains('h3', 'Rivers of Static').should('match', 'h3');
// A selector restricts the candidates and can pick a higher element.
cy.contains('[data-testid=book-card]', 'Salt and Signal').should('match', 'li');
// A regular expression anchors the match to the whole text.
cy.contains('button', /^Search$/).should('have.attr', 'type', 'submit');
// Case-insensitive match without writing the i flag.
cy.contains('catalogue', { matchCase: false }).should('be.visible');
// Text that is not there makes the command fail after the timeout, so use not.exist.
cy.contains('No such book').should('not.exist');
});
Source: Cypress: contains
Because there is no built-in negation, "elements that do not contain this text" is written with .not() and the jQuery
:contains() selector (cy.get('li').not(':contains("(")')). The text must also be the whole story for waiting:
cy.contains('Saving').should('not.exist') passes immediately when "Saving" has not rendered yet, so first assert that it
appeared, or assert on the end state (cy.contains('Saved').should('be.visible')).
Chaining two contains calls
cy.contains('Delete User').click().contains('Yes, Delete!').click() does not do what it appears to. The second
.contains() is chained off the button, so Cypress looks for "Yes, Delete!" inside the button. Start a new chain with cy
for the confirmation, or scope with .within() (Scoping with within).
cy.getByRole
Cypress 16.2.0 (13 October 2026) added cy.getByRole(role, options), a built-in query that finds elements the way assistive
technology does: by the role the browser exposes (button, link, heading, navigation, textbox, …) and, optionally, by the
accessible name. It does what the findByRole() and findAllByRole() queries of Cypress Testing Library do, without a plugin.
The signature, as shipped in 16.2.0 and confirmed by running it:
cy.getByRole(role: string, options?: {
name?: string | number | RegExp | ((name: string, element: Element) => boolean);
hidden?: boolean; // default false
native?: boolean; // default: true for roles HTML has an element for, otherwise false
log?: boolean; // default true
timeout?: number; // default defaultCommandTimeout
includeShadowDom?: boolean; // default: the includeShadowDom config value
})
cy.get('nav').getByRole('link') // also usable as a child query
cy.getByRole<HTMLInputElement>('textbox', { name: 'Email' }) // typed element
Source: Cypress: getByRole
What it matches
| Option | Behaviour |
|---|---|
|
A single role as a string: |
|
Matches the accessible name, which comes from the content, a |
|
By default an element that assistive technology cannot reach is skipped: an element or ancestor with the |
|
For a role that HTML has an element for (button, link, heading, dialog, checkbox, …) only elements whose tag gives them the
role match, so a |
|
As for |
Two behaviours differ from what you may expect. cy.getByRole() yields every match in document order, like cy.get(), rather
than failing when more than one element matches, so narrow the name or chain .first() or .eq() to act on one. And it
matches on role and name only: to narrow by a state such as checked, expanded, pressed or the heading level, chain
.filter() (cy.getByRole('tab').filter('[aria-selected="true"]'), cy.getByRole('checkbox').filter(':checked')), or .not()
to leave matches out. The role of an element is computed from the aria-query mapping, so an <a> needs an href to be a
link, a <section> needs an aria-label or aria-labelledby to be a region, and an <input> with a missing or unknown type is
a textbox; the mapping ignores context, so every <header> is a banner even inside an <article>.
Examples on Bookshelf
Bookshelf’s markup is plain, accessible HTML, which is what getByRole rewards: a labelled search box, a named navigation,
buttons, lists and images with alt text. The first test finds elements by role and accessible name, including the negative
cases that show how strictly a string name is matched.
it('finds elements by role and accessible name', () => {
cy.visit('/books');
cy.getByRole('heading', { name: 'Catalogue' }).should('have.length', 1);
cy.getByRole('navigation', { name: 'Main' }).should('exist');
// A label gives the search box its accessible name.
cy.getByRole('searchbox', { name: 'Search books' }).type('quiet{enter}');
cy.getByRole('link', { name: /Quiet Compiler/ }).should('have.attr', 'href', '/books/1');
// A string name must match the whole accessible name, case-sensitively.
cy.getByRole('button', { name: 'Search' }).should('exist');
cy.getByRole('button', { name: 'Sear' }).should('not.exist');
cy.getByRole('button', { name: 'search' }).should('not.exist');
});
Source: Cypress: getByRole
The pager’s disabled "Previous" link is an <a> without an href, so it is not a link to the accessibility tree, and the
img role finds the six covers by their alt text. Chaining getByRole off another query searches that element’s
descendants, and inside .within() it searches only the subject. A link takes its name from its content, which for a
Bookshelf card is the image alt text followed by the title, so a pattern on the name matches the whole link.
it('yields every match and scopes with chaining and within', () => {
cy.visit('/books');
// The <a> without an href (the disabled "Previous" pager link) has no link role.
cy.getByRole('navigation', { name: 'Pagination' }).getByRole('link').should('have.length', 1);
cy.getByRole('navigation', { name: 'Pagination' }).within(() => {
cy.getByRole('link', { name: 'Next' }).click();
});
cy.url().should('include', 'page=2');
// Images take their accessible name from alt text.
cy.getByRole('img', { name: 'Cover of Orchard of Echoes' }).should('be.visible');
cy.getByRole('img').should('have.length', 6);
});
it('matches names with a regular expression or a function', () => {
cy.visit('/books');
// A link takes its name from its content, here the image alt text plus the title.
cy.getByRole('link', { name: /^cover of the quiet/i }).should('have.length', 1);
cy.getByRole('img', { name: /^Cover of The/ }).should('have.length', 2);
cy.getByRole('img', {
name: (name) => name.length > 31,
}).should('have.length', 2);
cy.getByRole('img', {
name: (name) => name.includes('Field Guide'),
}).should('have.length', 1);
});
Source: Cypress: getByRole
Forms are where the accessible name earns its keep. The review form has a label for each control, so the test never needs a test id or a CSS selector, and the type argument gives the typed element in the callback.
it('works with forms and with state through .filter', () => {
cy.signIn('reader');
cy.visit('/books/1');
cy.getByRole('combobox', { name: 'Rating' }).select('5');
cy.getByRole('textbox', { name: 'Your review' }).type('Short and sweet but fine.');
// A type argument types the yielded element.
cy.getByRole<HTMLTextAreaElement>('textbox', { name: 'Your review' }).then(($textarea) => {
expect($textarea[0].value).to.have.length.greaterThan(10);
});
cy.getByRole('button', { name: 'Submit review' }).click();
cy.getByRole('list').getByRole('listitem').should('have.length', 3);
// The state options of Testing Library became .filter().
cy.getByRole('combobox').filter(':enabled').should('have.length', 1);
cy.getByRole('button').not(':disabled').should('have.length.greaterThan', 1);
});
Source: Cypress: getByRole
The last three tests cover the options that trip people up: elements hidden from the accessibility tree, the native default,
and invalid arguments. Bookshelf’s empty sign-in error is role="alert" with the hidden attribute, so it exists in the DOM
but not in the accessibility tree. The div role="button" and the tab list are inserted with cy.document() because
Bookshelf has no such markup.
it('skips elements hidden from the accessibility tree unless hidden is true', () => {
cy.visit('/signin');
cy.getByRole('alert').should('not.exist');
cy.getByRole('alert', { hidden: true }).should('exist').and('not.be.visible');
});
it('matches only native elements for roles HTML has an element for', () => {
cy.visit('/');
cy.document().then((doc) => {
doc.body.insertAdjacentHTML(
'beforeend',
'<div role="button" tabindex="0" id="fake">Cancel</div>' +
'<div role="tablist"><button role="tab">Profile</button></div>',
);
});
cy.getByRole('button', { name: 'Cancel' }).should('not.exist');
cy.getByRole('button', { name: 'Cancel', native: false }).should('have.id', 'fake');
// HTML has no element for "tab", so the role attribute is always honoured.
cy.getByRole('tab', { name: 'Profile' }).should('exist');
});
it('rejects unsupported arguments', () => {
cy.visit('/');
cy.on('fail', (err) => {
expect(err.message).to.include('role');
return false;
});
cy.getByRole('switch checkbox');
});
Source: Cypress: getByRole
What the errors look like
cy.getByRole() reports what it looked for and what it found, which turns a misspelled name into a one-line fix. These
messages were captured on 16.2.0 from tests that were meant to fail, on the Bookshelf sign-in page.
AssertionError: Timed out retrying after 4000ms: Expected to find an accessible native element with the role "button" and name "Sing in", but never did.
Here are the accessible roles that were found, with the accessible name of each element:
- link: "Skip to content", "Bookshelf", "Catalogue", "Sign in"
- banner: (no name)
- navigation: "Main"
- button: "Toggle theme", "Sign in"
- main: (no name)
- heading: "Sign in"
- textbox: "Username"
- contentinfo: (no name)
CypressError: `cy.getByRole()` was passed the role `switch checkbox`, but a role is a single word with no spaces, such as `'button'`. Query one role at a time.
CypressError: `cy.getByRole()` does not accept the `level` option. To narrow the results by `level`, chain `.filter('h2, [aria-level=2]')` instead. It accepts: `name`, `hidden`, `native`, `timeout`, `log`, `includeShadowDom`.
AssertionError: Timed out retrying after 4000ms: Expected to find an accessible native element with the role "button" and name "Cancel", but never did.
Some elements have the role "button" only through a `role` attribute, so they were skipped. The native elements for this role are: `<input>`, `<button>`. To include elements with a `role` attribute, pass `{ native: false }`.
Source: Cypress: getByRole
The password input is absent from the role list because a password field has no role in the accessibility tree; reach it by
label with a different query. That is a real limit of getByRole: it cannot select every control, so keep cy.get() with a test
id (or a label query from a plugin) for the cases it does not cover.
Changing the defaults and migrating from Testing Library
If a component library renders its buttons as <div role="button">, make native: false the default with
Cypress.Commands.overwriteQuery() (see Overwriting commands and queries); a test can still pass native: true to require
a native element. Migration from @testing-library/cypress is mostly a rename of cy.findByRole() to cy.getByRole() and
cy.findAllByRole() to cy.getByRole(), with these differences: native elements only by default, every match instead of
one, and the checked, selected, pressed, expanded, current, busy, level and value options replaced by a chained
.filter(). Do not add native: false to every call; list the elements that fail and decide whether the application should
use the native element. Also, if your project already defines its own getByRole with Cypress.Commands.add() or
Cypress.Commands.addQuery(), that call now throws, because getByRole is a built-in query: rename yours. The migration
guidance was taken from the documentation and was not run against a project that uses Testing Library.
// Before (Testing Library) // After (Cypress 16.2)
cy.findByRole('tab', { selected: true }); cy.getByRole('tab').filter('[aria-selected="true"]');
cy.findByRole('checkbox', { checked: true }); cy.getByRole('checkbox').filter(':checked');
cy.findAllByRole('listitem'); cy.getByRole('listitem');
Scoping with within
.within(callback) makes cy.get(), cy.contains() and cy.getByRole() inside the callback search only inside the subject,
and cy.root() yields the subject. It requires a single DOM element as the subject (since Cypress 12 it throws for several).
It yields the same subject it was given, and anything returned from the callback, including a cy.wrap(), is ignored, so
never chain commands that rely on an inner element after .within(): query the inner element with .find() instead.
it('scopes commands with .within', () => {
cy.get('[data-testid=pagination]').within(() => {
cy.get('[data-testid=page-indicator]').should('have.text', 'Page 1 of 4');
cy.contains('a', 'Next').should('have.attr', 'href', '/books?page=2');
// cy.root() is the subject of within().
cy.root().should('have.attr', 'aria-label', 'Pagination');
});
// Outside the callback the scope is the page again.
cy.get('[data-testid=book-card]').should('have.length', 6);
});
Source: Cypress: within
To escape the scope temporarily, start a new chain with cy.root() and walk up to an ancestor outside the subject, for
example cy.root().closest('.example').find('#name'). Starting at cy.root() inside .within() is the documented way,
because cy.get() is always scoped.
Traversal
Traversal commands are queries, so they retry like cy.get(). They start from the subject, yield jQuery collections and
are the answer to "find the thing next to the thing I can identify".
| Command | Yields |
|---|---|
|
Descendants of the subject that match the selector. |
|
Direct children, the direct parent, all ancestors, ancestors up to a selector. |
|
The subject itself or its nearest ancestor that matches the selector. |
|
Siblings, following siblings, preceding siblings. |
|
One element of a collection by position; |
|
Keep or remove elements that match. |
|
Traverse into a shadow root; the root element of the current scope. |
The next spec uses them to start from a recognisable title and reach the card, its neighbours and its author, and to turn a
list into plain data with .then(), .invoke() and .its(). A query chain such as .closest().children() is retried as a
whole, which is why this style survives re-renders.
it('walks the DOM with traversal commands', () => {
cy.contains('[data-testid=book-title]', 'Rivers of Static')
.closest('[data-testid=book-card]')
.should('have.attr', 'data-book-id', '2')
.children()
.should('have.length', 3);
cy.contains('[data-testid=book-card]', 'Rivers of Static')
.next()
.should('have.attr', 'data-book-id', '3');
cy.contains('[data-testid=book-card]', 'Rivers of Static')
.prev()
.should('have.attr', 'data-book-id', '1');
cy.contains('[data-testid=book-card]', 'Rivers of Static')
.siblings()
.should('have.length', 5);
cy.get('[data-testid=book-card]')
.filter('[data-book-id="4"]')
.find('[data-testid=book-author]')
.should('have.text', 'Ines Okafor');
cy.get('[data-testid=book-card]').not('[data-book-id="4"]').should('have.length', 5);
cy.get('[data-testid=book-title]').parent().should('match', 'a');
cy.get('[data-testid=page-indicator]').parents('nav').should('have.attr', 'aria-label', 'Pagination');
});
it('turns a list of elements into data with .then, .invoke and .its', () => {
cy.get('[data-testid=book-title]')
.then(($titles) => $titles.toArray().map((el) => el.textContent))
.should('have.length', 6)
.and('include', 'Salt and Signal');
cy.get('[data-testid=result-count]').invoke('text').should('match', /^24 books found$/);
cy.get('[data-testid=book-card]').its('length').should('eq', 6);
});
A common idiom is to find a table row or list item by its text and then act inside it:
cy.contains('tr', 'Jane').contains('button', 'Edit').click(). Both calls are queries, so the whole chain retries until the
button exists, and only .click() runs once.
Assertions
An assertion states what must be true of the subject. Cypress bundles Chai (the BDD expect and should styles and the TDD
assert style), Chai-jQuery for DOM elements and Sinon-Chai for spies and stubs, and exposes them through .should(),
.and() and the globals expect and assert. .and() is an alias of .should() that reads better in a chain.
should and and
.should(chainers), .should(chainers, value) and .should(chainers, method, value) take a Chai chainer as a dot-separated
string. Arguments follow the string, so .should('have.attr', 'href', '/users') is expect($el).to.have.attr('href', '/users').
Aliases work (eq for equal, contain for include). .should() is itself retried together with the query before it,
until the assertion passes or the command times out, and the error message says "Timed out retrying after 4000ms". Most
chainers yield the subject unchanged, so more assertions can follow, but some change it. have.attr with one argument yields
the attribute value, and have.css with one argument yields the computed value, which is why
.should('have.attr', 'href').and('match', /page=2$/) works. The chainer exist is also the implicit assertion of every
query: a query without an explicit assertion fails if nothing is found.
it('uses Chai-jQuery chainers on DOM elements', () => {
cy.get('[data-testid=search-input]')
.should('be.visible')
.and('be.enabled')
.and('have.attr', 'name', 'q')
.and('have.value', '')
.and('have.id', 'q');
cy.get('[data-testid=result-count]')
.should('have.text', '24 books found')
.and('include.text', 'books')
.and('have.attr', 'aria-live', 'polite');
cy.get('[data-testid=page-prev]').should('have.attr', 'aria-disabled', 'true');
cy.get('[data-testid=book-list]').should('have.descendants', 'li.card');
cy.get('img').first().should('match', 'img').and('have.prop', 'loading', 'lazy');
cy.get('[data-testid=main-nav]').should('contain', 'Catalogue').and('not.contain', 'Account');
cy.get('body').should('have.css', 'margin-top');
});
it('chains with .and and lets a chainer change the subject', () => {
cy.get('[data-testid=page-next]')
.should('have.attr', 'href') // the subject is now the attribute value
.and('match', /page=2$/)
.and('include', '/books');
cy.get('[data-testid=page-next]').should('have.attr', 'rel', 'next').and('have.text', 'Next');
cy.get('h1').should('have.css', 'font-size').and('match', /px$/);
});
Source: Cypress: should, Cypress: Assertions
The chainers
The reference page splits the chainers into three groups. The tables below list them with an example each; the complete lists, including every alias, are on the Assertions reference.
Chai chainers are used on any value (a number, a string, an array, an object, a function):
| Chainer | Example |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Chai-jQuery chainers apply to DOM elements yielded by cy.get(), cy.contains() and the other element queries:
| Chainer | Example |
|---|---|
|
|
|
|
|
|
|
|
|
|
Sinon-Chai chainers apply to cy.spy() and cy.stub() objects: called, callCount, calledOnce, calledTwice,
calledThrice, calledBefore, calledAfter, calledWithNew, calledOn, calledWith, calledOnceWith, calledWithExactly,
calledOnceWithExactly, calledWithMatch, the always... variants, returned, alwaysReturned, threw and alwaysThrew,
written as .should('have.been.calledWith', 'x'), .should('have.callCount', 3) and .should('have.always.been.calledOn', ctx).
The example in Spies and stubs with Sinon-Chai runs several of them.
You can also extend Chai: npm install any Chai plugin and import it in a spec or the support file, or write your own with
chai.use(); Cypress "just works" with the new assertions.
Common patterns
-
Count:
cy.get('li.selected').should('have.length', 3)retries until three match. Prefer this to.then(($i) => expect($i).to.have.length(3)), which checks once and fails if the list is still filling in. -
Text:
have.textfor the exact text,include.textorcontainfor part of it, and.invoke('text').should('match', /^Hello/)orcy.contains(selector, /^Hello/)for a pattern. Normalise whitespace before comparing when the markup is indented. -
Visibility: with several elements,
be.visiblepasses when some are visible whilenot.be.visiblerequires every element to be invisible.be.visiblefollows Cypress’s visibility rules, which differ from the accessibility tree thatgetByRoleuses. -
Existence:
.should('not.exist')retries until the element is gone, and is the right way to wait for a spinner to disappear, provided the test knows the spinner appeared (see cy.contains). -
Several assertions on one command: chain
.and(). All of them use the same reference to the original subject, so a single element cannot be both visible and not visible in one chain; re-query it with two separatecy.get()calls.
it('retries .should until the assertion passes', () => {
cy.get('[data-testid=search-input]').type('quiet{enter}');
// The page reloads: the retried query follows the new document.
cy.get('[data-testid=book-card]').should('have.length', 1);
cy.get('[data-testid=result-count]').should('have.text', '1 book found');
});
it('waits for the optimistic review row to settle', () => {
cy.signIn('reader');
cy.visit('/books/2');
cy.get('[data-testid=review-rating-input]').select('4');
cy.get('[data-testid=review-text-input]').type('Worth reading twice, honestly.');
cy.get('[data-testid=review-submit]').click();
// The row appears with data-state="pending" and becomes "saved" once the server answers.
// .should retries the query and the assertion together until both hold.
cy.contains('[data-testid=review]', 'Worth reading twice')
.should('have.attr', 'data-state', 'saved')
.find('[data-testid=review-rating]')
.should('have.text', '★★★★');
});
it('asserts negatively and for existence', () => {
cy.get('[data-testid=empty-state]').should('not.exist');
cy.get('[data-testid=search-input]').type('zzzz{enter}');
cy.get('[data-testid=empty-state]').should('exist').and('be.visible');
cy.get('[data-testid=book-card]').should('not.exist');
cy.get('[data-testid=result-count]').should('have.text', '0 books found');
});
The optimistic review test is the typical case for retrying: Bookshelf adds the review row immediately with
data-state="pending" and flips it to saved when the server answers (100 ms here, set with BOOKSHELF_REVIEW_DELAY_MS).
.should('have.attr', 'data-state', 'saved') retries the whole cy.contains() plus the assertion until both hold, so there is
no cy.wait(100).
Negative assertions can pass for the wrong reason
cy.get('[data-testid=todos]').should('not.have.length', 2) passes when the list has three items, but also when the
application deleted the whole list or added a blank item. State what you expect (have.length, have.text) rather than what
you do not expect, and use a negative assertion where the absence itself is the behaviour: not.exist for a removed element,
not.be.visible for a collapsed one, not.have.class for a state flag.
Callbacks and plain values
Pass a function to .should() for assertions the chainers cannot express. The function is retried until none of the
assertions in it throws, receives the subject, may run any number of Chai expect calls and can throw its own errors. Its
return value is ignored and the original subject is yielded. Because it runs repeatedly, it must have no side effects and,
since Cypress 11, must not call Cypress commands (it throws); run commands before or after .should(), or use .then(), which
does not retry. A string as the second argument of expect names the assertion in the Command Log.
it('accepts a callback with explicit Chai assertions', () => {
cy.get('[data-testid=book-card]').should(($cards) => {
expect($cards, 'six cards on page one').to.have.length(6);
const ids = $cards.toArray().map((el) => Number(el.dataset.bookId));
expect(ids).to.deep.equal([1, 2, 3, 4, 5, 6]);
expect($cards.first()).to.contain('The Quiet Compiler');
expect(ids).to.satisfy((list: number[]) => list.every((id) => id > 0));
});
});
it('uses BDD and TDD Chai on plain values', () => {
cy.get('[data-testid=book-title]')
.then(($titles) => $titles.toArray().map((el) => el.textContent as string))
.should('have.length', 6)
.and('include.members', ['Salt and Signal', 'Rivers of Static'])
.and('have.ordered.members', [
'The Quiet Compiler',
'Rivers of Static',
'A Field Guide to Clouds',
'Salt and Signal',
'The Lantern Index',
'Small Systems, Big Days',
]);
cy.wrap({ page: 1, pages: 4 }).should('deep.equal', { page: 1, pages: 4 });
cy.wrap(24).should('be.within', 20, 30).and('be.a', 'number').and('be.greaterThan', 23);
cy.wrap('Page 1 of 4').should('match', /^Page \d of \d$/).and('have.string', 'of');
cy.wrap({ a: { b: 'x' } }).should('have.nested.property', 'a.b', 'x');
cy.wrap([]).should('be.empty');
cy.wrap(null).should('be.null');
// The assert interface is Chai's too.
assert.isTrue(Cypress.$('[data-testid=book-card]').length === 6, 'six cards');
expect(Cypress.isBrowser('electron') || Cypress.isBrowser('chrome')).to.be.true;
});
Source: Cypress: should
cy.wrap(value) turns a plain value into a subject so the chainers can run on it, and .then() followed by .should()
gives you an assertion on data you computed from the DOM. The globals expect and assert are the Chai interfaces: they run
once, immediately, and are not retried, which is correct inside a .then() callback or on a value that cannot change.
Spies and stubs with Sinon-Chai
cy.spy() records calls to an existing method and cy.stub() replaces it. Both can be aliased and asserted on with the
Sinon-Chai chainers. They are covered in more depth in
Network, Stubs, Spies & Clocks; here they show the
assertion side.
it('asserts on spies and stubs with Sinon-Chai', () => {
cy.window().then((win) => {
cy.spy(win.document, 'addEventListener').as('addListener');
cy.stub(win.console, 'error').as('consoleError');
win.document.addEventListener('keyup', () => {});
win.console.error('boom', 42);
});
cy.get('@addListener').should('have.been.calledOnce').and('have.been.calledWith', 'keyup');
cy.get('@consoleError')
.should('have.been.calledWithExactly', 'boom', 42)
.and('have.callCount', 1);
cy.get('@consoleError').should('have.been.calledWithMatch', 'boo');
cy.get('@consoleError').should('not.have.been.calledWith', 'other');
});
Source: Cypress: Assertions, Sinon-Chai
Timeouts and failures
An assertion that does not hold fails the test after the timeout. The timeout is the command’s timeout option or the
defaultCommandTimeout, and it covers the query and the assertions together. Cypress reports a failing implicit assertion as
"Expected to find element: …, but never found it", and a failing .should('have.text', ...) as the expected and actual
values:
AssertionError: Timed out retrying after 4000ms: Expected to find content: 'Nothing here' but never did.
Timed out retrying after 4000ms
+ expected - actual
-'Sign in'
+'Log in'
Source: Cypress: Retry-ability
The last test of the assertion spec shows how to assert on a failure itself with cy.on('fail', ...): return false from the
handler so that the test does not fail, and assert on the message. Use this sparingly; it is meant for checking the behaviour
of your own custom commands and queries, not application behaviour.
it('shows what a failed implicit assertion does: .get fails after the timeout', () => {
cy.on('fail', (err) => {
expect(err.message).to.include('Expected to find element');
return false;
});
cy.get('[data-testid=nothing]', { timeout: 500 });
});
Source: Cypress: Catalog of events
Custom commands
A custom command packages a sequence of Cypress commands under one name. Add it with Cypress.Commands.add(name, callbackFn) or
Cypress.Commands.add(name, options, callbackFn), or add several at once with Cypress.Commands.addAll(callbackObj). The
prevSubject option decides the kind of command:
prevSubject |
Kind | Behaviour |
|---|---|---|
|
Parent |
Always starts a new chain and ignores any previous subject. |
|
Child |
Requires a previous subject and receives it as the first argument, followed by the command’s own arguments. |
|
Child with validation |
As a child, but Cypress also checks the type of the subject and throws a clear error if it does not conform. |
|
Dual |
May start a chain or continue one; the subject is |
The support file, cypress/support/e2e.ts, runs before every spec, so commands defined in a file it imports (conventionally
cypress/support/commands.ts) are available in all of them. The file below defines one command of each kind plus a query, and
declares their types once in the global Cypress.Chainable interface so that IntelliSense and tsc know them.
// Custom commands and queries used by the specs on this page.
// Types are declared once, in the global Cypress namespace.
declare global {
namespace Cypress {
interface Chainable {
/** Query: yields the elements whose data-testid attribute equals `id`. */
getByTestId(
id: string,
options?: Partial<Cypress.Loggable & Cypress.Timeoutable>,
): Chainable<JQuery<HTMLElement>>;
/** Parent command: signs in through the API and caches the session. */
signIn(username: 'reader' | 'admin'): Chainable<void>;
/** Child command: yields the trimmed text of every element in the subject. */
texts(): Chainable<string[]>;
/** Dual command: fills the catalogue search form, in the subject if there is one. */
searchFor(term: string): Chainable<JQuery<HTMLElement>>;
}
}
}
// A custom query: synchronous, retried, and free of side effects.
Cypress.Commands.addQuery('getByTestId', function getByTestId(id, options = {}) {
const getFn = cy.now('get', `[data-testid="${id}"]`, options) as (
subject: unknown,
) => JQuery<HTMLElement>;
return (subject) => getFn(subject);
});
// A parent command: it may be asynchronous and it runs once.
Cypress.Commands.add('signIn', (username) => {
const passwordName = username === 'admin' ? 'ADMIN_PASSWORD' : 'READER_PASSWORD';
cy.env([passwordName]).then((secrets) => {
cy.session(
username,
() => {
cy.request('POST', '/api/session', { username, password: secrets[passwordName] });
},
{
validate: () => {
cy.request('/api/session').its('body.authenticated').should('eq', true);
},
},
);
});
});
// A child command: the previous subject is required and must be an element.
Cypress.Commands.add('texts', { prevSubject: ['element'] }, (subject) => {
return cy.wrap(
subject.toArray().map((el) => (el.textContent ?? '').trim()),
{ log: false },
);
});
// A dual command: the subject is optional.
Cypress.Commands.add('searchFor', { prevSubject: 'optional' }, (subject, term) => {
const scope = subject ? cy.wrap(subject) : cy.get('form[role=search]');
scope.find('[data-testid=search-input]').type(`${term}{enter}`);
return cy.get('[data-testid=result-count]');
});
export {};
Points worth noting in that file:
-
Return types: a command’s callback returns nothing, a chainable or a promise-like; it must not return a plain value. The child command
textswraps its array withcy.wrap(..., { log: false })because returning an array directly is a type error, and it is not retried (it is a command, not a query). -
Secrets:
signInreads the password withcy.env(['READER_PASSWORD']), the Cypress 16 replacement forCypress.env(), and caches the session withcy.session()and avalidatecallback that checks the server still recognises it, because Bookshelf’s/api/test/resetclears every server session. The value comes from theCYPRESS_READER_PASSWORDenvironment variable; when it was not set, the sign-in request was sent without a usable password and Bookshelf answered 401, so the test failed with acy.request()error rather than silently succeeding.cy.env()andcy.session()are explained in Sessions, Authentication & Cross-Origin. -
Declarations:
Chainable<void>for a command that yields nothing,Chainable<JQuery<HTMLElement>>when it yields elements,Chainable<string[]>for data. Put thedeclare globalblock in a file that is a module (it has animportorexport), asexport {}at the end of the support file does. -
Registration: since Cypress 12.17.4 (Webpack 5) a support file that has only side effects can be tree-shaken if your
package.jsonsetssideEffects: false. Then wrap the registrations in an exported function and call it from the support file (registerCommands()).
The specs that use them:
it('texts is a child command: it needs an element subject and yields data', () => {
cy.visit('/books');
cy.getByTestId('book-author').texts().should('deep.equal', [
'Mara Velde',
'Ilan Brooks',
'Nora Quill',
'Ines Okafor',
'Tomas Reyes',
'Priya Nandakumar',
]);
});
it('searchFor is a dual command: with or without a subject', () => {
cy.visit('/books');
cy.searchFor('compiler').should('have.text', '1 book found');
cy.visit('/books');
cy.get('form[role=search]').searchFor('rivers').should('have.text', '1 book found');
});
it('signIn is a parent command built on cy.session and cy.env', () => {
cy.signIn('reader');
cy.visit('/account');
cy.getByTestId('account-role').should('contain', 'reader');
});
Source: Cypress: Custom commands
When not to write a command
The Cypress documentation is blunt about overuse. Do not wrap cy.get(selector).click() in a command: you would end up with a
command for every combination of element interaction, and the reader has to open a second file to learn that "clickButton"
clicks a button. Add zero or as few assertions as possible to a command, so the calling test decides how and when to assert.
Skip the UI for set-up (cy.request(), cy.session(), seeding endpoints), which is why signIn uses the API. For code that is
used by only one spec, a plain function is simpler than a command, and a function that returns a chain (return cy.get(...))
can be chained from. The one case where a command beats a function is when you want the step to show up in the Command Log with
a readable name; Cypress.log() gives you control over that. A command that finds elements for others to act on should be a
query, so that it retries.
Custom queries
Cypress 12 added Cypress.Commands.addQuery(name, callbackFn), the same API that the built-in queries use. A query obeys three
rules: it is synchronous (no promises, no await), it is retriable (Cypress calls the function you return repeatedly) and
it is idempotent (calling it many times must not change the application). Use a query when you return DOM elements or data
that other commands act or assert on, and a command when the work is asynchronous, has side effects or runs only once.
The callback has two layers. The outer function runs once per use and receives the arguments the test passed; it does set-up,
such as validating arguments, creating the log entry and this.set('timeout', options.timeout). It must return the inner
function, which receives the previous subject and returns the new subject, and which Cypress may call many times: first
repeatedly until it passes or times out, then again to resolve the subject for later commands or an alias. If the inner function
throws, Cypress retries it until the timeout, which is the whole of retry-ability. Use function () {} rather than an arrow
function for the outer function, because Cypress sets the timeout through this. Cypress.ensure provides the subject checks
the built-in queries use, such as Cypress.ensure.isType(subject, ['optional', 'element', 'window', 'document'], 'contains', cy).
getByTestId in the support file above is a query. It builds on the built-in get through cy.now(), which returns the
inner function of a query. cy.now is an internal Cypress function, not part of the documented API, but it worked on 16.2.0 and it
passes the timeout option through: a failing cy.getByTestId('missing', { timeout: 700 }) reported "Timed out retrying after
700ms" and the default one 4000 ms. If you prefer to stay on documented ground, cy.get() with Cypress.Commands.overwriteQuery()
or a plain helper function that returns cy.get(...) achieves the same without a new query.
it('getByTestId is a query: it retries and can be chained', () => {
cy.visit('/books');
cy.getByTestId('book-card').should('have.length', 6);
cy.getByTestId('book-list').getByTestId('book-title').first().should('have.text', 'The Quiet Compiler');
cy.getByTestId('search-input').type('signal{enter}');
// The query re-runs after the page reload until the assertion holds.
cy.getByTestId('book-card').should('have.length', 1).and('contain', 'Salt and Signal');
cy.getByTestId('missing', { timeout: 300 }).should('not.exist');
});
Source: Cypress: Custom queries
As with commands, the documentation warns against abstractions that merely rename cy.get() (cy.getButton()); good candidates
are queries specific to your application that are used everywhere, such as cy.findBreadcrumbs() or cy.getLoginForm().
Overwriting commands and queries
Cypress.Commands.overwrite(name, callbackFn) replaces an existing command; the callback receives the original function first,
then (for a child or dual command) the subject, then the arguments, and must call and return the original. Use it sparingly,
because it changes the behaviour for Cypress’s own use too. Cypress.Commands.overwriteQuery(name, callbackFn) does the same for a
query and is the only way to change a query: overwrite cannot overwrite queries, and overwriteQuery cannot overwrite
commands (the API was added in 12.6.0). The callback of overwriteQuery must be a function, and the original must be invoked with
.call(this, ...) or .apply(this, ...) because query functions rely on this.
// Cypress.Commands.overwrite replaces a command (not a query); overwriteQuery replaces a query.
// The overwrites live in this spec so they do not leak into the others.
interface SensitiveTypeOptions extends Cypress.TypeOptions {
sensitive: boolean;
}
declare global {
namespace Cypress {
interface Chainable {
visit(url: string, options?: Partial<Cypress.VisitOptions>): Chainable<Cypress.AUTWindow>;
type(text: string, options?: Partial<SensitiveTypeOptions>): Chainable<JQuery<HTMLElement>>;
}
}
}
// Mask the typed text in the Command Log when { sensitive: true } is passed.
Cypress.Commands.overwrite<'type', 'element'>(
'type',
(originalFn, element, text, options?: Partial<SensitiveTypeOptions>) => {
if (options?.sensitive) {
options.log = false;
Cypress.log({
$el: element,
name: 'type',
message: '*'.repeat(text.length),
});
}
return originalFn(element, text, options);
},
);
// Record every URL the spec visits, then call the original command.
const visited: string[] = [];
Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
visited.push(url);
return originalFn(url, options);
});
// Make native: false the default for getByRole in this spec.
Cypress.Commands.overwriteQuery('getByRole', function (originalFn, role, options = {}) {
return originalFn.call(this, role, { native: false, ...options });
});
describe('Overwriting commands and queries', () => {
it('masks a sensitive value in the Command Log', () => {
cy.visit('/signin');
cy.get('[data-testid=username]').type('reader');
cy.env(['READER_PASSWORD']).then(({ READER_PASSWORD }) => {
cy.get('[data-testid=password]').type(READER_PASSWORD, { sensitive: true });
});
cy.get('[data-testid=password]').should('not.have.value', '');
cy.get('[data-testid=signin-submit]').click();
cy.location('pathname').should('eq', '/account');
});
it('keeps a record of the visited URLs', () => {
cy.visit('/books').then(() => {
expect(visited).to.include('/books');
});
});
it('defaults getByRole to native: false and lets a test opt back in', () => {
cy.visit('/');
cy.document().then((doc) => {
doc.body.insertAdjacentHTML('beforeend', '<div role="button" tabindex="0">Cancel</div>');
});
cy.getByRole('button', { name: 'Cancel' }).should('exist');
cy.getByRole('button', { name: 'Cancel', native: true }).should('not.exist');
});
});
export {};
The three overwrites in that spec do different things: type masks the characters in the Command Log (and in screenshots and videos)
when { sensitive: true } is passed, so a password typed in a test is not recorded; visit records every URL; and the getByRole
overwrite makes native: false the default for that spec. The overwrites are declared in the spec so that they do not leak into
the other specs; put them in the support file when they should apply everywhere. The type augmentation for visit is needed
because the built-in typings have overloads that make an overwrite with explicit arguments fail tsc. The overwritten type accepts the
extra option in TypeScript only because it is declared in the same file.
Overwrites compose with baseUrl: the Cypress documentation points out that many projects write their own "visit the app"
command only to swap base URLs between environments. That is unnecessary, because baseUrl is used by cy.visit() and
cy.request() and can be overridden with the CYPRESS_BASE_URL environment variable; this project reads BASE_URL in the
configuration for the same reason.
Pitfalls
| Mistake | Fix |
|---|---|
Reading a value in |
End the chain with |
Chaining |
Start a new chain with |
Expecting |
Aliases re-run their queries; use |
|
It passes immediately. Assert the appearance first, or assert on the end state. |
Using |
|
Calling a Cypress command inside a |
It throws since Cypress 11. Run the command outside, or use |
|
It skips non-native elements by default. Fix the markup or pass |
|
The name must match the whole accessible name, case-sensitively; use a regular expression for part of it. Read the error: it lists the roles and names that were found. |
|
Cypress’s visibility rules differ from the accessibility tree. Use |
A custom query with side effects |
Queries may run many times. Use a command for anything that changes the application. |
Arrow function as a query callback |
Use |
Same |
|
What changed recently
-
16.2.0 (13 October 2026):
cy.getByRole()added, with thename,hidden,native,log,timeoutandincludeShadowDomoptions. A project that defines its owngetByRolecommand or query now gets an error and must rename it. -
16.0.0 (1 September 2026):
Cypress.env()was removed; the documentation and the examples here usecy.env()for secrets andCypress.expose()for public values.cy.exec()andcy.end()were also removed in 16. Native network interception affectscy.intercept(), not the queries and assertions on this page. -
15.17.0: aliases that start with
@are typed asChainable<S>rather thanChainable<JQuery<HTMLElement>>incy.get(). -
12.17.4: the Webpack 5 upgrade can tree-shake support files that only have side effects when
sideEffects: falseis set; wrap command registration in a function. -
12.6.0:
Cypress.Commands.overwriteQuery()added;.find()is overwritten with it, no longer withCypress.Commands.overwrite(). -
12.0.0: queries introduced (
Cypress.Commands.addQuery()),.find()and the other query commands became retriable queries, aliases of DOM elements re-run their queries by default, and.within()throws if the subject is several elements. -
11.0.0:
.should()throws when a Cypress command is used inside its callback. -
5.4.0:
.within()always yields its original subject. 5.2.0:includeShadowDomoption added tocy.get(),.find()and.contains(). 4.0.0:matchCaseoption added to.contains().
Source: Cypress: Changelog
The versions in this list come from the History tables of the command pages and from the changelog and migration guide; they were checked on 25 October 2026.