Q33 of 48 · Cypress
How do you handle conditional UI in Cypress (an element that may or may not appear)?
Short answer
Short answer: Don't use Cypress to branch on element existence — that fights retry-ability and produces flake. Either deterministically control whether the element appears (stub the network, seed state) or use `cy.get('body').then($body => $body.find(...).length)` only when the branching is genuinely required and you've waited for the deciding state to settle.
Detail
Conditional UI is the most common cause of "but it works locally" Cypress flake. The framework's retry model assumes assertions converge to one answer; if (element exists) ... else ... defeats that.
The first move is to remove the conditionality. Most apparent conditions are determined by something testable:
- "A modal sometimes pops up on first visit." → Stub the cookie that controls it (
cy.setCookie('seen-onboarding', 'true')) so the modal never appears, and write a separate test for the modal. - "A discount banner sometimes shows." → Stub the
/api/promotionsresponse to control whether it appears. - "A returning user sees X, a new user sees Y." → Two tests, each with seed state for one branch.
If you can pre-determine which branch the test is in, the test stops being conditional.
When the branch is genuinely runtime-determined, the documented pattern uses the underlying jQuery: query the body, check whether the element is in the DOM:
cy.get('body').then(($body) => {
if ($body.find('[data-test=cookie-banner]').length > 0) {
cy.get('[data-test=cookie-banner-accept]').click();
}
// Continue regardless of whether the banner was present
});
The crucial detail: cy.get('body').then runs once with no retry. So you must be sure the relevant elements have either rendered or definitively not rendered before this command. Wait for a deterministic event first:
// Wait for the page to settle — banner decision is made after this request
cy.intercept('GET', '/api/user-prefs').as('prefs');
cy.visit('/');
cy.wait('@prefs');
// Now check
cy.get('body').then(($body) => {
if ($body.find('[data-test=cookie-banner]').length > 0) {
cy.get('[data-test=cookie-banner-accept]').click();
}
});
The anti-pattern: cy.get('[data-test=banner]').then(...) — that retries the get, which never times out if the element exists, and always times out if it doesn't. cy.get('body').find(...) short-circuits the retry.
The senior signal: preferring deterministic setup over conditional logic, and knowing the get('body').then(...) pattern's retry-busting trade-off.
// EXAMPLE
cookie-banner.cy.ts
// ✅ Deterministic — control whether the banner appears
beforeEach(() => {
cy.setCookie('cookie-consent', 'accepted'); // banner won't show
});
it('proceeds without banner', () => {
cy.visit('/');
cy.get('[data-test=hero]').should('be.visible');
});
// ✅ Separate test for the banner path
it('shows and accepts the cookie banner on first visit', () => {
cy.clearCookie('cookie-consent');
cy.visit('/');
cy.get('[data-test=cookie-banner]').should('be.visible');
cy.get('[data-test=cookie-banner-accept]').click();
cy.get('[data-test=cookie-banner]').should('not.exist');
});
// ⚠️ Genuine conditional — only when branch is environmental
it('handles either banner state', () => {
cy.intercept('GET', '/api/user-prefs').as('prefs');
cy.visit('/');
cy.wait('@prefs');
cy.get('body').then(($body) => {
if ($body.find('[data-test=cookie-banner]').length > 0) {
cy.get('[data-test=cookie-banner-accept]').click();
}
});
cy.get('[data-test=hero]').should('be.visible');
});