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() {},
  },
});
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

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

cy.get('button')

Never

Too generic, no context.

cy.get('.btn.btn-large')

Never

Coupled to styling.

cy.get('#main')

Sparingly

Coupled to styling or to JavaScript event listeners.

cy.get('[name="submission"]')

Sparingly

Coupled to the name attribute, which has HTML semantics.

cy.contains('Submit')

Depends

Coupled to text content that may change.

cy.getByRole('button', { name: 'Submit' })

Depends

Checks the role and the accessible name, so it is coupled to both.

cy.get('[data-cy="submit"]')

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'],
});

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

log

true

Displays the command in the Command Log. Use { log: false } to hide noisy queries.

timeout

defaultCommandTimeout (4000 ms)

How long to wait for the command and the assertions chained to it. Must be a number.

withinSubject

the .within() subject, otherwise the document element

The element to search inside. Pass null to search from the document even inside .within().

includeShadowDom

the includeShadowDom config value (false)

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');
});

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');
});

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

matchCase

true

Case sensitivity. { matchCase: false } also makes a regular expression behave as if it had the i flag; combining the i flag with { matchCase: true } throws.

log

true

Displays the command in the Command Log.

timeout

defaultCommandTimeout

How long to wait for the element and the assertions chained to it.

includeShadowDom

the includeShadowDom config value

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, a and label when 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 an h3 inside a link, and the link is what cy.contains('Rivers of Static') yields, while cy.contains('h3', 'Rivers of Static') yields the heading.

  • Started from cy, the search covers the body, so text in <head>, such as the page title, never matches (use cy.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');
});

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

What it matches

Option Behaviour

role

A single role as a string: 'button', 'heading', 'navigation', 'tab'. An element has a role when its tag implies one (button, nav, input type="checkbox") or when a role attribute sets one; the attribute replaces the implied role. If the attribute lists several roles as fallbacks the first valid one counts (role="foo button" is a button). A regular expression or a role with a space throws.

name

Matches the accessible name, which comes from the content, a label, aria-label, aria-labelledby, an image’s alt or a title. A string must match the whole name, case-sensitively, after whitespace is collapsed and trimmed; a regular expression matches part of it; a function receives the name and the element and returns a boolean. Content hidden with aria-hidden="true" and CSS ::before and ::after content are left out of the name.

hidden

By default an element that assistive technology cannot reach is skipped: an element or ancestor with the hidden attribute, aria-hidden="true" or display: none, or a computed visibility: hidden. hidden: true includes them. An element that is only visually hidden (clipped to one pixel, opacity: 0) is still in the accessibility tree and is found without it. The option is about what a screen reader can reach, not about what Cypress considers visible.

native

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 <div role="button"> is skipped and the test fails if a native control is replaced by an imitation. For widget roles with no HTML element (tab, menuitem, switch) the role attribute is always honoured, and native: true throws. native: false also matches elements that have the role only through an attribute, which is needed for patterns such as an autocomplete <input role="combobox"> or an icon <svg role="img">.

log, timeout, includeShadowDom

As for cy.get(). Any other option throws.

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');
});

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);
});

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);
});

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');
});

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 }`.

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

.find(selector)

Descendants of the subject that match the selector.

.children([selector]), .parent([selector]), .parents([selector]), .parentsUntil(selector)

Direct children, the direct parent, all ancestors, ancestors up to a selector.

.closest(selector)

The subject itself or its nearest ancestor that matches the selector.

.siblings([selector]), .next(), .nextAll(), .nextUntil(), .prev(), .prevAll(), .prevUntil()

Siblings, following siblings, preceding siblings.

.first(), .last(), .eq(index)

One element of a collection by position; .eq() accepts a negative index.

.filter(selector or fn), .not(selector)

Keep or remove elements that match.

.shadow(), .root()

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$/);
});

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

not, deep, nested, ordered, any, all

.should('not.equal', 'Jane'), .should('deep.equal', { a: 1 }), .should('have.nested.property', 'a.b'), .should('have.ordered.members', [1, 2])

a(type) / an

.should('be.a', 'string')

include(value) / contain

.should('include', 2), .should('include.members', [3, 2])

ok, true, false, null, undefined, exist, empty

.should('be.true'), .should('be.empty'), .should('exist')

equal(value) / eq, eql(value)

.should('equal', 42); eql is a deep comparison

greaterThan / gt / above, least / gte, lessThan / lt / below, most / lte, within(a, b)

.should('be.greaterThan', 5), .should('be.within', 5, 10)

instanceOf, property(name, value), ownProperty, lengthOf(n)

.should('be.instanceOf', Array), .should('have.property', 'name'), .should('have.lengthOf', 4)

match(RegExp), string(text), keys(...), members(set), oneOf(list)

.should('match', /^test/), .should('have.string', 'test'), .should('be.oneOf', [1, 2, 3])

throw, respondTo, satisfy(fn), closeTo(n, delta)

.should('throw', Error), .should('satisfy', (n) => n > 0), .should('be.closeTo', 1, 0.5)

change, increase, decrease

.should('change', obj, 'val')

Chai-jQuery chainers apply to DOM elements yielded by cy.get(), cy.contains() and the other element queries:

Chainer Example

attr(name, value), prop(name, value), css(name, value), data(name, value)

.should('have.attr', 'name', 'q'), .should('have.prop', 'disabled', false), .should('have.css', 'background-color', 'rgb(0, 0, 0)')

class(name), id(id)

.should('have.class', 'foo'), .should('have.id', 'foo')

text(text), html(html), value(value)

.should('have.text', 'I love testing'), .should('have.value', 'a@b.c'). have.text is an exact match; include.text is a substring.

visible, hidden, selected, checked, focus / focused, enabled, disabled, empty, exist

.should('be.visible'), .should('be.checked'), .should('have.focus'), .should('be.disabled')

match(selector), contain(text), descendants(selector)

.should('match', ':empty'), .should('contain', 'text'), .should('have.descendants', 'div')

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.text for the exact text, include.text or contain for part of it, and .invoke('text').should('match', /^Hello/) or cy.contains(selector, /^Hello/) for a pattern. Normalise whitespace before comparing when the markup is indented.

  • Visibility: with several elements, be.visible passes when some are visible while not.be.visible requires every element to be invisible. be.visible follows Cypress’s visibility rules, which differ from the accessibility tree that getByRole uses.

  • 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 separate cy.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');
});

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'

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 });
});

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

false (default)

Parent

Always starts a new chain and ignores any previous subject. cy.visit(), cy.request() and cy.intercept() are parents.

true

Child

Requires a previous subject and receives it as the first argument, followed by the command’s own arguments. .click() and .trigger() are children.

'element', 'document', 'window' or an array of these

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.

'optional'

Dual

May start a chain or continue one; the subject is undefined when there is none. cy.screenshot(), cy.scrollTo() and cy.wait() are duals.

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 texts wraps its array with cy.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: signIn reads the password with cy.env(['READER_PASSWORD']), the Cypress 16 replacement for Cypress.env(), and caches the session with cy.session() and a validate callback that checks the server still recognises it, because Bookshelf’s /api/test/reset clears every server session. The value comes from the CYPRESS_READER_PASSWORD environment 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 a cy.request() error rather than silently succeeding. cy.env() and cy.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 the declare global block in a file that is a module (it has an import or export), as export {} 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.json sets sideEffects: 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');
});

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');
});

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 .then() and asserting once

End the chain with .should() so the whole chain retries. .then() runs once.

Chaining cy.contains() off an action

Start a new chain with cy, or use .within().

Expecting cy.get('@alias') to remember an old value

Aliases re-run their queries; use { type: 'static' } for a snapshot.

.should('not.exist') for something that has not appeared yet

It passes immediately. Assert the appearance first, or assert on the end state.

Using .within() to return an inner element

.within() always yields its subject. Query the inner element after it, with .find().

Calling a Cypress command inside a .should() callback

It throws since Cypress 11. Run the command outside, or use .then().

cy.getByRole('button') finds nothing for a div role="button"

It skips non-native elements by default. Fix the markup or pass native: false.

cy.getByRole('textbox', { name: 'Email' }) finds nothing

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.

be.visible passes for a hidden element in an accessibility check

Cypress’s visibility rules differ from the accessibility tree. Use getByRole (which respects aria-hidden) when the question is whether assistive technology can reach it.

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 function () {} so Cypress can set the timeout through this.

Same getByRole command defined in your project

getByRole is built in since 16.2.0; rename or delete your own.

What changed recently

  • 16.2.0 (13 October 2026): cy.getByRole() added, with the name, hidden, native, log, timeout and includeShadowDom options. A project that defines its own getByRole command 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 use cy.env() for secrets and Cypress.expose() for public values. cy.exec() and cy.end() were also removed in 16. Native network interception affects cy.intercept(), not the queries and assertions on this page.

  • 15.17.0: aliases that start with @ are typed as Chainable<S> rather than Chainable<JQuery<HTMLElement>> in cy.get().

  • 12.17.4: the Webpack 5 upgrade can tree-shake support files that only have side effects when sideEffects: false is set; wrap command registration in a function.

  • 12.6.0: Cypress.Commands.overwriteQuery() added; .find() is overwritten with it, no longer with Cypress.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: includeShadowDom option added to cy.get(), .find() and .contains(). 4.0.0: matchCase option added to .contains().

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.