In Cypress, why does .should('be.visible').and('not.be.visible') on one cy.get() never pass?
answer
- one command, one subject reference
- chained checks share a single window
- a transition needs two statements
- a new cy.get() re-queries the page
- have.attr swaps the subject for a string
basics
~20 sEvery assertion chained onto one cy.get() runs against the same subject reference in the same retry window, so a single element cannot satisfy both conditions at once. Split the checks into two cy.get() statements so Cypress re-queries between them.
solid answer
~50 sAssertions chained onto one command all run against the subject that command produced and all share its retry window. So `cy.get('[data-cy=availability-spinner]').should('be.visible').and('not.be.visible')` asks one element reference to be visible and invisible simultaneously; the group retries until `defaultCommandTimeout` expires and then fails with a misleading visibility message. The fix is two statements — `cy.get('[data-cy=availability-spinner]').should('be.visible')` followed by `cy.get('[data-cy=availability-spinner]').should('not.exist')` — because a new `cy.get()` re-queries the page and sees the DOM after the availability call settles. The same rule explains subject-changing chainers: `have.attr` and `have.css` replace the subject with a string, so an `.and()` after either is asserting on a value, not an element. Order matters too: the assertions evaluate left to right inside each retry and the first to throw ends that attempt, so only that one is reported and everything after it in the chain is never reached.
go deeper
Recall that a spinner appearing and then disappearing needs two separate cy.get() statements, not one longer chain of assertions.
Explain that the subject reference and the retry budget belong to the command, so every chained assertion shares both and the group retries as a unit.
Diagnose this from the failure alone: a visibility timeout on a chain containing a positive and a negative check is a logic error, not a slow application.
Set the review expectation that a chain asserts one state of one subject, so state transitions are written as separate statements everyone can read at a glance.
## One command, one subject, one window Chaining assertions onto a Cypress query does not create several independent checks. `.should()` and `.and()` are **assertions owned by the command in front of them**, and everything about that group is shared: the subject the command produced, the retry window it was given, and the order the checks run in. ```javascript cy.get('[data-cy=availability-spinner]') .should('be.visible') .and('not.be.visible') ``` This asks one element reference to satisfy two mutually exclusive conditions at the same instant. There is no moment at which both hold, so the group retries until `defaultCommandTimeout` expires — 4000 ms by default as of Cypress 16 — and then fails with a message about visibility. That message reads like a slow spinner, which is why the bug survives so many code reviews: it looks like a timing problem and it is a logic problem. ## The fix is a new query, not a new assertion ```javascript cy.get('[data-cy=availability-spinner]').should('be.visible') cy.get('[data-cy=availability-spinner]').should('not.exist') ``` Each `cy.get()` starts a fresh chain, so the second statement looks at the page as it stands once the availability request has settled, rather than at the reference the first statement captured. Two statements are not two waits stacked end to end: the first resolves the moment the spinner appears, the second the moment it is gone. Splitting also gives you two independent failures, so a spinner that never appears and a spinner that never leaves are two different red lines rather than one ambiguous one. ## Which chainers hand a different subject to `.and()` Most chainers leave the subject alone, but a few deliberately replace it with a value. Reading a chain means tracking what each link yields. | Chainer | Yields to the next assertion | Booking example | | --- | --- | --- | | `be.visible`, `have.class`, `have.length` | the same jQuery subject | `.should('have.length', 8).and('be.visible')` | | `have.attr` | the attribute's value as a string | `.should('have.attr', 'href').and('include', '/rooms/')` | | `have.css` | the computed style value as a string | `.should('have.css', 'display').and('equal', 'grid')` | | a `.should(cb)` callback | the original subject, always | whatever the callback returns is discarded | The practical trap is a chain that reads sensibly in English but has quietly changed type mid-sentence: after `have.attr`, an `.and('be.visible')` is asking a string to be visible, and the failure message talks about the wrong thing entirely. ## The multi-element wrinkle The shared subject is often a *set*, because `cy.get('[data-cy=room-card]')` yields every match as one jQuery collection. The visibility chainers then quantify over that set in opposite ways: - `should('be.visible')` passes when **at least one** matched element is visible. - `should('not.be.visible')` passes only when **every** matched element is invisible. So over a set of more than one element the two are not opposites, and both can be false at once. A results page that renders eight room cards but paints only the first is green under `cy.get('[data-cy=room-card]').should('be.visible')` — the assertion is true, it is simply not the claim the author meant. `have.text` behaves the same way, comparing against the concatenated text of the whole collection rather than any single card. Pinning the size first with `should('have.length', 8)`, or narrowing with `.first()` or `.eq(n)`, turns a vague set into a stated one. ## Order, and the assertion nobody sees fail 1. Assertions evaluate left to right inside each retry. 2. The **first** one to throw ends that attempt, and the whole group is retried from the start. 3. When the window finally closes, only that first failing assertion is reported; anything after it in the chain was never reached. So a guest-form chain such as `cy.get('[data-cy=guest-email]').should('have.value', '[email protected]').and('not.have.class', 'invalid')` that fails on the value tells you nothing about the class. If both facts matter independently, two statements give you two independent failures. ## Where candidates go wrong - Reading `.and()` as "then wait for the next thing". It is the same command as `.should()` and adds no second wait; it adds a second condition to the existing one. - Splitting the chain with `.then()` to force a re-query. A new `cy.get()` statement is the plain way to say it, and the callback form brings its own rules. - Asserting a transition — appears then disappears — inside one chain, which is exactly the case that can never pass. - Assuming the retry window resets for each assertion. All the assertions in one group share the single budget the command was given. - Asserting over an unbounded set without stating its size, so the check can pass on one element out of eight.
- What does a failed chained assertion tell you about the later assertions in that chain?Nothing at all. Assertions in one group evaluate left to right inside each retry, and the first to throw ends that attempt, so the later ones are never reached. Only that first failure is reported, and the rest are simply absent from the result. When two facts matter independently, write two statements so each produces its own failure.
- Is .and() ever required, or is repeating .should() equivalent?They are interchangeable. Cypress registers `should` and `and` against the same implementation, so `.should('have.length', 8).should('be.visible')` behaves exactly like `.should('have.length', 8).and('be.visible')`. `.and()` exists only so a chain reads as a sentence. Neither form starts a new query, so neither escapes the shared subject that makes a visible-then-invisible chain impossible.
saying these in an interview costs you the question
- Thinks .and() re-queries the page for its check
- Blames the failure on a spinner that is too slow
- Believes each chained assertion gets its own timeout
- Expects .and() after have.attr to see the element
- Reads be.visible as all matched elements are visible