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 combinatorcy.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 matchThe 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
- – 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 formIf 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. Usecy.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.getreturns every match. A barecy.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 withcy.containsorcy.withinto narrow to exactly one. - Reaching for
cy.containswhen there's a stabledata-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. Reservecy.containsfor 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.
- With
baseUrl: "https://www.saucedemo.com"set, log in to/inventory.htmlfrom abeforeEach. - In
cypress/e2e/selection.cy.ts, write fiveitblocks 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)..firstand.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. Usecy.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").
- Run the spec headlessly:
npm run cy:run -- --spec "cypress/e2e/selection.cy.ts". All five should pass. - Force a wrong-element bug. Replace
findwithgetin 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. - Stretch: write a sixth test that adds three items by name (Backpack, Fleece, T-Shirt) using
cy.containsandcy.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.