skip to content

Porting Specs Across Runners

What really transfers when a suite moves in or out: selectors mostly do, explicit waits mostly vanish, and a driver with its lifecycle becomes nothing. Asked as a planning question.

on this pageshow

explore

questions

5

Which of a ported suite's waits vanish into Cypress retry-ability, and which do not?

level: middleimportance: must knowfreq 74%

answer

  1. Ask which waits were about the DOM
  2. Queries retry, and assertions retry with them
  3. One timeout covers most DOM commands
  4. A network wait gets an alias, not a duration
  5. Nothing outside the browser retries itself

basics

~20 s

Waits for an element to appear, become visible or change text vanish: cy.get() and .should() retry inside defaultCommandTimeout. Network waits become cy.intercept() plus cy.wait('@alias'). Waits on state outside the browser do not vanish at all.

solid answer

~50 s

Most of them vanish, and that is the main payoff of the move. `cy.get('[data-cy=spec-row]')` retries until it matches, and a chained `.should()` retries the query until the assertion passes — both inside `defaultCommandTimeout`, **4000 ms** by default. So a wait for presence, visibility, enabled state, text or a URL change is not translated at all; it becomes the assertion you were going to write anyway, and a fixed sleep is deleted rather than turned into `cy.wait(3000)`. Three kinds survive in a new shape. A wait for one specific request becomes `cy.intercept()` plus `cy.wait('@alias')`, which is deterministic rather than timed. A page transition is already covered by `pageLoadTimeout` on `cy.visit()`. And a wait on something outside the browser — a queue draining, a file being written — has no DOM to retry against, so it needs `cy.request()` or `cy.task()`.

code

javascript · 13 lines
javascript
describe('run dashboard', () => {
  beforeEach(() => {
    cy.intercept('GET', '/api/runs*').as('runs')
    cy.visit('/runs')
    cy.wait('@runs')
  })

  it('queues the failed specs again', () => {
    cy.get('[data-cy=spec-row]').should('have.length', 12)
    cy.contains('button', 'Re-run failed').click()
    cy.get('[data-cy=run-status]', { timeout: 20000 }).should('have.text', 'Queued')
  })
})

go deeper

for a junior

Be ready to name the two commands that do the waiting for you — cy.get() retrying until it matches, and .should() retrying until the assertion passes — and to say that a fixed sleep is deleted rather than converted.

for a middle

Explain that retry-ability is bounded by defaultCommandTimeout at 4000 ms and exits as soon as the condition holds, and be able to sort a list of old waits into the ones that vanish and the ones that become an aliased cy.wait().

for a senior

Show that you know a faithful line-by-line port carries the old flake across, because assertions parked inside .then() never retry. Talk about how you would spot that pattern in a review of a large migration.

for a principal

Own the standard the migrated suite waits by: aliased requests over durations, per-command timeouts over a global raise, and a rule about what a test may poll outside the browser.

## What retry-ability absorbs In Cypress a query command such as `cy.get('[data-cy=spec-row]')` does not run once and give up. It re-queries the DOM until it matches or its budget runs out, and an assertion chained onto it — `.should('be.visible')`, `.should('have.length', 12)` — re-runs the query until the assertion passes. That budget is `defaultCommandTimeout`, **4000 ms** unless the project changes it, and retry-ability finishes the command the instant the condition holds, so the number is a ceiling rather than a duration. That one behaviour removes most of an old suite's synchronisation code: - A wait for an element to exist is just `cy.get(selector)`. - A wait for it to become visible is `cy.get(selector).should('be.visible')`. - A wait for it to become enabled is usually nothing at all, because Cypress checks actionability before `.click()` and retries until the element can be clicked. - A wait for text to settle is `.should('have.text', 'Queued')` or `.should('contain', 'Queued')`. - A wait for the address to change is `cy.url().should('include', '/runs/482')`. - A fixed sleep is **deleted**, not converted; the documentation is explicit that it should not reappear as `cy.wait(<number>)`. Name the shape of the change, because it is what the interviewer is listening for: the wait and the check stop being two lines and become one. You do not translate the wait — you write the condition, and the wait is implied by it. ## The waits that survive in a new shape Three kinds do not disappear, and a migration plan that assumes everything does will stall on them. | The old wait was for… | In Cypress it becomes | Governed by | |---|---|---| | an element, its visibility, text or count | a query plus `.should()` | `defaultCommandTimeout` (4000 ms) | | a page transition | `cy.visit()`, `cy.go()`, `cy.reload()` | `pageLoadTimeout` (60000 ms) | | one specific network call | `cy.intercept()` then `cy.wait('@alias')` | `requestTimeout` (5000 ms), `responseTimeout` (30000 ms) | | work happening outside the browser | `cy.request()` or `cy.task()` | `taskTimeout` (60000 ms) for tasks | The network row is where the port pays off most. A wait on a duration is a guess about the backend; an alias is a fact about it. `cy.intercept('GET', '/api/runs*').as('runs')` followed by `cy.wait('@runs')` resolves when that request actually completes, which is faster on a fast day and still correct on a slow one. The last row is the one teams forget. **Cypress retries the DOM, not the world.** If the old step waited for an import job to drain a queue, no assertion on the page retries that into existence: you poll the source, with a `cy.task()` that loops in Node or a small recursive helper around `cy.request()`. Chaining `.should()` onto `cy.request()` does not help, because only query commands retry — the request is issued once. ## Why a mechanical port keeps the old flake The failure mode of a careful, faithful translation is that it preserves the shape that made the old suite flaky in the first place: 1. **Assertions moved inside `.then()`.** A `.then(rows => expect(rows).to.have.length(12))` callback runs against whatever the DOM held at that moment. The retry lives in `.should()`, so a literal "find, then assert" port throws away the mechanism the team migrated for. 2. **Command results assigned to variables.** Reading a "result" out of a queued command is the same timing guess in new syntax. 3. **Sleeps translated instead of deleted.** A converted sleep is slower than the assertion it replaced on every good run and no more reliable on a bad one. The rule that keeps a port honest: the last thing in a chain should be an **assertion about the state you care about**, not a callback that inspects it. ## Sizing the budget while you translate - Leave `defaultCommandTimeout` at its default during the port, so genuinely broken steps fail fast and loudly instead of hiding inside a long budget. - Where one step is legitimately slow, pass `{ timeout: ms }` on that command rather than raising the global setting for everything. - Where a step is slow because it is waiting on the backend, alias the request instead of stretching a timeout. - Treat a step that needs a large timeout and has no request to alias as a question about the application, not about the test.

  • A ported step waited for a spinner to disappear. What is the Cypress form?
    `cy.get('[data-cy=spinner]').should('not.exist')`. The assertion retries until the element leaves the DOM, so the wait and the check are the same command. If the element stays in the DOM and only hides, `.should('not.be.visible')` is the honest assertion instead.
  • Where does an old sleep go when nothing on the page changes?
    It usually means the condition lives outside the DOM. Poll the source rather than guessing a duration: a `cy.task()` that loops in Node until the queue drains, or a small recursive helper around `cy.request()`. Chaining `.should()` onto `cy.request()` will not re-issue it, because only query commands retry.

The old suite scheduled the looking; Cypress keeps looking until the condition holds or the budget runs out. You stop writing waits and start writing conditions.

saying these in an interview costs you the question

  • Translates every explicit wait into cy.wait with a number
  • Replaces a sleep with cy.wait(3000) instead of asserting the state
  • Believes .then() retries the way .should() does
  • Assumes defaultCommandTimeout also governs cy.visit()
  • Claims Cypress waits automatically for backend work outside the browser
open as a page

Porting a suite to Cypress, what replaces its XPath locators?

level: juniorimportance: should knowfreq 58%

basics

~10 s

Cypress has no first-class XPath support: cy.get() takes a CSS selector. Text-based XPath becomes cy.contains(), structural XPath becomes a CSS selector, and XPath axes become traversal commands such as .find(), .parent(), .next() and .eq().

open as a page

A suite ported to Cypress kept its driver-shaped session helper. What does that cost?

level: seniorimportance: should knowfreq 46%

basics

~20 s

In Cypress there is no browser session object to wrap: Cypress launches and disposes of the browser itself. The wrapper's start and quit calls do nothing, anything it caches dies at the spec boundary, and its methods cannot return values.

open as a page

How far should a Cypress port raise defaultCommandTimeout to absorb old waits?

level: principalimportance: should knowfreq 36%

basics

~20 s

Barely at all. The 4000 ms default costs nothing on a passing run but is paid in full on every failure, so a global 30-second budget buys slow red builds and hidden regressions. Raise it per command instead.

open as a page

In Cypress, what replaces a ported suite's soft assertions that collected every failure?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Nothing does. Cypress bundles Chai and the test ends at the first failed assertion. Port a batch either into separate it blocks, one per behaviour, or into a single .should() callback whose expect calls are retried together.

open as a page