In Cypress, what does `const rows = cy.get('[data-cy=book-row]')` put into `rows`?
answer
- The line records, it does not run
- What type comes back from a command
- Cypress commands on it, no jQuery methods
- Reading length gives you undefined
- Chain an assertion or a then callback
basics
~20 sA Cypress chainer object: a handle you use to append more commands to that chain. It holds no element and no jQuery data, because at the moment the assignment runs cy.get() has only been queued, not executed.
solid answer
~40 s`cy.get('[data-cy=book-row]')` hands back a Cypress chainer, and that is what lands in `rows`. Its prototype carries the Cypress commands — `.click()`, `.should()`, `.then()`, `.as()` — and nothing you can read a value from. So `rows.length` is `undefined`, `rows.text()` throws because `text()` is a jQuery method, and `if (rows)` is always true because a plain object is truthy. `rows.click()` does not click at that line either; it appends a click command to the same chain. The reason is timing: the spec body runs to the end before Cypress executes anything, so when the assignment evaluates, the catalogue page may not even have loaded. To reach the twelve real rows you either chain an assertion — `.should('have.length', 12)` — or chain `.then(($rows) => …)` and let Cypress hand the collection in.
code
javascript · 19 linesit('lists the borrowable copies', () => {
cy.visit('/catalogue?q=dune')
const rows = cy.get('[data-cy=book-row]')
// None of these read the page:
// rows.length -> undefined
// rows.text() -> TypeError, text() is a jQuery method
// if (rows) -> always true, it is a plain object
// This works only because it appends another command:
rows.should('have.length', 12)
// This is how you actually get the elements:
cy.get('[data-cy=book-row]').then(($rows) => {
expect($rows).to.have.length(12)
expect($rows.eq(0).text()).to.contain('Dune')
})
})go deeper
Be ready to say in one sentence what a Cypress command returns: a chainer you append more commands to, never the element. Interviewers open with this and listen for whether you have actually run a spec yourself.
Explain the mechanics, not just the rule. The chainer is a plain object whose prototype holds the Cypress commands, so jQuery methods and length are simply absent, and the query has not run when the assignment evaluates.
Show how you spot this in someone else's spec — a stored command result, a length read, a truthiness guard — and how you steer the fix toward a chained assertion rather than yet another callback.
Own the guardrail. Decide whether the suite tolerates stored command results at all, and wire the eslint-plugin-cypress no-assigning-return-values rule into the pipeline so the pattern cannot spread through a large spec base.
Cypress's API looks synchronous and is not, and the first place that bites everybody is a `const`. Understanding exactly what the variable receives explains almost every "why is this `undefined`" question people ask about the tool. ## What `rows` actually contains Every Cypress chain is built out of an internal chainer object. When you call `cy.get('[data-cy=book-row]')`, Cypress creates a chainer, records a `get` command on its queue, and returns the chainer. **That chainer is what lands in `rows`.** Its prototype carries the Cypress commands and Cypress bookkeeping — nothing else. There is no element in it, no jQuery collection, no `length`, no `text()`, and no promise machinery you could `await`. The consequences are easy to reproduce in a spec: - `typeof rows` is `'object'`, so `if (rows)` is **always** true — the guard tells you nothing about the catalogue. - `rows.length` is `undefined` — not `0`, and not `12`. - `rows.text()` throws a `TypeError`, because `text()` is a **jQuery** method and a chainer is not a jQuery object. - `rows.click()` clicks nothing at that line: it appends a `click` command to the same chain and returns the chainer. - Passing `rows` into a helper that expects an element quietly hands that helper the chainer, so the failure surfaces far away from the assignment. ## Why the assignment could not hold an element even in principle The spec body runs top to bottom first, recording commands rather than executing them. At the instant `const rows = …` evaluates, a `cy.visit('/catalogue?q=dune')` written above it has not navigated anywhere. There is no results page, no `<tr>`, and no count to capture. A runner that returned live elements here would be returning them from the wrong moment in time. There is a second reason, and it is the one Cypress's design leans on: **the DOM is mutable**. A catalogue row that exists at assignment time can be thrown away when the search results re-render. A variable is a snapshot; a chain is a description Cypress can re-evaluate as the page settles. ## The mistakes, side by side | What you write | What you expect | What actually happens | |---|---|---| | `const rows = cy.get('[data-cy=book-row]')` | twelve `<tr>` elements | a chainer object for that chain | | `rows.length` | `12` | `undefined` | | `rows.text()` | the row text | `TypeError: rows.text is not a function` | | `if (rows.length > 0)` | a presence check | the comparison is false, so the `else` branch always runs | | `rows.click()` | an immediate click | a click command appended to the chain | ## What the TypeScript signature is telling you In TypeScript, `cy.get()` is declared as returning `Chainable<JQuery<HTMLElement>>`. Read that carefully: the **outer** type is the object you hold, and the **type parameter** describes the subject the chain will eventually yield. `JQuery<HTMLElement>` never reaches your variable; it reaches the parameter of a `.then()` callback or an assertion callback. That is why `rows.text()` fails to compile as well as fail to run — the type system is describing the same split the runtime enforces. ## The two supported ways to reach the real value 1. **Chain an assertion.** `cy.get('[data-cy=book-row]').should('have.length', 12)` deliberately never puts the collection in your hands, which lets Cypress re-run the query until the assertion holds. 2. **Chain `.then()`.** `cy.get('[data-cy=book-row]').then(($rows) => { … })` runs your callback once the query has resolved, with the real jQuery collection passed in as `$rows`. Both keep the value inside the chain, where Cypress controls the moment it is read. ## Catching it before review does The community `eslint-plugin-cypress` package ships a `cypress/no-assigning-return-values` rule that flags assigning a command's return value to `const`, `let` or `var` at lint time, and Cypress's own best-practices guide labels the assignment an anti-pattern outright. Turning the rule on is cheaper than catching it in review, because the symptom — an `undefined`, a branch that always takes the same path, a helper handed the wrong object — never points back at the assignment line that caused it. One narrow exception is worth stating plainly, because candidates over-correct on it: assigning a value you read **inside** a callback is completely normal. `const count = Number($rows.text())` within a `.then()` is a plain JavaScript variable holding a plain JavaScript value, and Cypress has no opinion about it at all. The rule is about a command's return value, not about the `const` keyword.
- Does `rows.click()` on that stored chainer click anything at the line where you write it?No. It appends a `click` command to the same chain and returns the chainer, so the click happens later, when Cypress runs the queue. The line only records intent; nothing touches the catalogue page while the spec body is still executing.
- Is it ever legitimate to use `const` in a Cypress spec?Yes — inside a callback. `cy.get('[data-cy=copies]').then(($el) => { const before = Number($el.text()) })` stores a plain JavaScript number that has already been read from the page. The anti-pattern is assigning the command's return value, not the keyword itself.
The chainer is the order slip, not the meal. Holding the slip tells you nothing about what will arrive, because the kitchen has not started cooking yet.
saying these in an interview costs you the question
- Says cy.get returns a jQuery object you can read
- Thinks the variable holds a promise you can await
- Reads .length off a stored Cypress chainer
- Assumes an if on a chainer tests element presence
- Believes assigning cy.get makes the query run early