Selecting Elements — get, find, contains, within

9 min read

Every Cypress test boils down to find an element, do something with it, assert something about it. The "find an element" half is what this lesson is about. Cypress gives you four selection commands — cy.get, cy.find, cy.contains, and cy.within — and learning when each one is the right tool turns long, brittle locator chains into short, readable code that survives years of UI churn.

cy.get() — the workhorse

cy.get queries the entire document with a CSS selector and yields every match as a chainable. It's the command you'll type more than any other:

cy.get("[data-testid='submit-btn']");   // by data attribute (recommended)
cy.get(".product-card");                  // by class (fragile)
cy.get("#login-form");                    // by id (unique but rare)
cy.get("button");                         // by tag (too broad in real apps)
cy.get("input[type='email']");            // attribute selector
cy.get("nav > ul > li");                  // descendant combinator

cy.get returns all matching elements. When the selector matches several, you have to narrow to the one you want:

cy.get("[data-testid='product-card']").first();   // first match
cy.get("[data-testid='product-card']").last();    // last match
cy.get("[data-testid='product-card']").eq(2);     // third (zero-indexed)

cy.get is also where Cypress's auto-retry kicks in. If the element isn't there yet, Cypress keeps re-querying for up to four seconds (configurable via defaultCommandTimeout). You almost never need an explicit wait around it.

cy.contains() — find by visible text

cy.contains is the second-most useful command. It searches the DOM for an element containing a given text:

cy.contains("Add to cart");                // any element containing "Add to cart"
cy.contains("button", "Submit");           // a <button> containing "Submit"
cy.contains("[data-testid='alert']", "saved"); // a specific element + text
cy.contains(/^Total:\s+\$\d+/);            // regex match

The default is a partial match — cy.contains("Add") matches "Add to cart". Pass a regex when you need exact or anchored matching.

cy.contains shines when the element exists for the user as text, not as a stable test ID — flash messages, dynamic banners, the "12 results" counter on a search page. It also reads naturally to anyone who's ever read English: cy.contains("button", "Save").click() is what the test does.

cy.find() — descendant of a previous selection

cy.find is cy.get's nephew. It only searches inside the previously-yielded element, not the whole document:

cy.get("[data-testid='product-card']")
  .first()
  .find("button[aria-label='Add to cart']")
  .click();

Read it as: get all product cards, take the first one, find the add-to-cart button inside it, click. If the page has fifty product cards each with their own add-to-cart button, this targets the right one.

cy.find does not exist as a top-level command — cy.find("button") errors out. It's a child command, only chainable off something that already yielded an element. The mental model is: cy.get is document.querySelector; .find is element.querySelector.

cy.within() — scope all subsequent commands

cy.within takes a callback. Every Cypress command inside the callback is scoped to the previously-yielded element. This is the cleanest pattern for forms, dialogs, and any region with multiple interactive elements:

cy.get("[data-testid='login-form']").within(() => {
  cy.get("input[name='email']").type("alice@test.com");
  cy.get("input[name='password']").type("password123");
  cy.get("button[type='submit']").click();
});

Inside the within, plain cy.get only finds elements inside the login form — even though the page might have a search bar, navigation links, and other inputs. This is far more readable than chaining .find four times.

within shines for repeating components — table rows, list items, cards. Combine it with cy.contains to scope to that specific row:

cy.contains("tr", "alice@example.com").within(() => {
  cy.contains("button", "Edit").click();
});

Translate: find the row containing alice's email; inside that row, click the Edit button. No chained selectors, no parent().parent(). The pattern is so common that you'll see it in every real Cypress codebase.

Filtering and chaining

The selection commands compose. A few patterns you'll lean on:

// Take only the active product cards
cy.get("[data-testid='product-card']").filter(".active");
 
// Exclude the disabled inputs
cy.get("input").not("[disabled]");
 
// Get all anchors inside the nav, then keep the one with text "Products"
cy.get("nav").find("a").contains("Products").click();
 
// Walk up to a parent
cy.get("[data-testid='alert']").parent();
cy.get("input[name='email']").parents("form");
 
// Walk down or sideways
cy.get(".product-card").first().children();   // direct children
cy.get(".product-card").first().siblings();   // siblings at same level

.first(), .last(), .eq(n), .filter(selector), .not(selector), .children(), .parent(), .parents(), .siblings() — these are all chainables that operate on whatever the previous command yielded. The full list lives in the Cypress commands cheat sheet.

A real product-listing example

A typed test that exercises every selection command in a single spec:

describe("Product listing", () => {
  beforeEach(() => {
    cy.visit("/products");
  });
 
  it("shows the page header and search box", () => {
    cy.get("[data-testid='page-header']").within(() => {
      cy.get("h1").should("contain", "Products");
      cy.get("[data-testid='search-input']").should("be.visible");
    });
  });
 
  it("clicks Add to cart on the first product card", () => {
    cy.get("[data-testid='product-card']")
      .first()
      .find("[data-testid='add-to-cart-btn']")
      .click();
    cy.get("[data-testid='cart-count']").should("have.text", "1");
  });
 
  it("adds the Wireless Headphones product to the cart", () => {
    cy.contains("[data-testid='product-card']", "Wireless Headphones")
      .within(() => {
        cy.contains("button", "Add to cart").click();
      });
    cy.get("[data-testid='cart-count']").should("have.text", "1");
  });
 
  it("paginates through products", () => {
    cy.get("[data-testid='pagination']").within(() => {
      cy.contains("button", "Next").click();
    });
    cy.url().should("include", "page=2");
  });
});

Read each test from the outside in. The first uses within to scope to the header. The second composes cy.get → .first → .find → .click. The third uses cy.contains to find a card by product name, then cy.within to drill in and click. The fourth scopes pagination clicks to the pagination region. All of them avoid brittle CSS chains and stay under five lines.

The selection toolkit at a glance

Cypress selection
  • – Searches the whole document
  • – By data-testid (best)
  • – Returns all matches — narrow with .first / .eq
  • – Auto-retries while the DOM settles
  • – Searches inside previous element
  • – Child command — must follow cy.get
  • – Targets descendants of a known parent
  • – Selects by visible text
  • – Partial match by default
  • – Optional tag/selector first arg
  • – Great for buttons, links, banners
  • Scopes ALL nested commands –
  • Cleanest for forms and rows –
  • Combine with contains for table-row tests –

cy.get vs cy.find — the one to internalise

The single distinction that catches every new Cypress engineer:

// cy.get is unscoped — it searches the whole document
cy.get("[data-testid='login-form']").get("button");
// → returns ALL buttons on the page, not the ones inside the form
 
// cy.find IS scoped — it searches inside the parent
cy.get("[data-testid='login-form']").find("button");
// → returns ONLY buttons inside the login form

If you find yourself wondering "why did Cypress click the wrong button?", check whether you used .get where you meant .find (or wrap the inner work in cy.within).

⚠️ Common mistakes

  • Chaining .parent().parent().find(...) to get to a sibling. Brittle: any DOM restructure breaks it. Use cy.contains("tr", "alice@example.com").within(...) instead — the test reads as "find the alice row and operate inside it" and survives any wrapper-div the developer adds.
  • Forgetting that cy.get returns every match. A bare cy.get(".product-card").click() errors with "click can only be applied to a single element" when the page has more than one card. Use .first(), .last(), .eq(n), or scope with cy.contains or cy.within to narrow to exactly one.
  • Reaching for cy.contains when there's a stable data-testid. Text changes — copy gets edited, the i18n team translates the page, marketing wants "Add to basket" instead of "Add to cart." cy.get("[data-testid='add-to-cart']") survives all of those. Reserve cy.contains for cases where the text is the user-visible thing the test cares about (banners, error messages, totals).

🎯 Practice task

Wire up the selection toolkit on Sauce Demo. 20-30 minutes.

  1. With baseUrl: "https://www.saucedemo.com" set, log in to /inventory.html from a beforeEach.
  2. In cypress/e2e/selection.cy.ts, write five it blocks that each demonstrate one selection technique:
    • cy.get — assert there are exactly six product cards: cy.get("[data-test='inventory-item']").should("have.length", 6).
    • .first and .find — click the Add-to-cart button inside the first product: cy.get("[data-test='inventory-item']").first().find("button").click() and assert the cart badge shows "1".
    • cy.contains — click the Add-to-cart button on the Sauce Labs Backpack product card, regardless of position. Use cy.contains("[data-test='inventory-item']", "Sauce Labs Backpack").within(() => cy.contains("button", "Add to cart").click()).
    • cy.within — scope the page header (#header_container) and assert the cart icon is visible inside it.
    • Filtering — sort the inventory by price (high → low), then assert the first card contains "Sauce Labs Fleece Jacket" using cy.get("[data-test='inventory-item']").first().should("contain", "Fleece").
  3. Run the spec headlessly: npm run cy:run -- --spec "cypress/e2e/selection.cy.ts". All five should pass.
  4. Force a wrong-element bug. Replace find with get in the second test (cy.get("...").first().get("button")...). Run again. Cypress now finds every button on the page, not just the one in the first card — read the failure message carefully. This is the single highest-leverage debugging skill in Cypress.
  5. Stretch: write a sixth test that adds three items by name (Backpack, Fleece, T-Shirt) using cy.contains and cy.within, then asserts the cart badge shows "3". This is the pattern you'll repeat dozens of times in a real e-commerce suite.

Selection commands are the foundation. The next lesson tightens the screws on selector strategy — why data-testid is the gold standard, how to add it to your app, and what to do when you can't.

// tip to track lessons you complete and pick up where you left off across devices.