In Cypress, a background poll keeps satisfying your `cy.wait('@getForecast')` calls. Why, and what do you change?
answer
- Aliases do not know who called
- Every match advances the same counter
- Timers fire while the test is clicking
- Give the poll its own intercept
- cy.get with .all reads them all
basics
~20 sBecause a route alias records every request the route matches, whoever caused it. Each cy.wait consumes the next recorded request, so a timer's refresh takes a slot and your second wait resolves against it rather than against the fetch you triggered.
solid answer
~50 sA route alias counts **every** request that matches it, regardless of the caller. A dashboard that refreshes the forecast on a timer therefore feeds the same alias as the user's station change, and because each `cy.wait('@getForecast')` consumes the next recorded request in order, wait number two can resolve against a poll that fired while the test was clicking. Nothing errors; the test simply asserts on the wrong cycle, and it does so intermittently, because whether the poll lands first depends on machine speed. The fixes, in order of preference: give the poll its own `cy.intercept()` so two callers never share an alias; freeze the timer with `cy.clock()` installed before the app schedules it; or stop sequencing on waits and assert over `cy.get('@getForecast.all')`, which lets you check what was requested rather than how many waits you spent.
go deeper
Know that an intercept alias records every matching request, including ones a timer made. A wait cannot tell a user-triggered call apart from a background refresh.
Explain how the per-alias counter turns a background poll into an off-by-one, and show cy.get('@alias.all') as the read that does not depend on which slot a request landed in.
Show the diagnosis and the ranked fixes: count the route's real traffic first, then separate the callers into distinct intercepts, freeze the timer, or rewrite the assertions to be position-free.
Be ready to argue whether a suite should model background refresh traffic at all, and what the cost is of a convention that silences app timers in every test by default.
## What the alias is actually counting A `cy.intercept()` route matches requests, not intentions. When you name that route with `.as()`, the alias records **every** request the route matched, in the order they occurred, no matter what caused them. A weather dashboard that refreshes the forecast on a thirty-second interval feeds exactly the same alias as the user's station change does. Because each `cy.wait('@getForecast')` consumes the next recorded request, a poll that fires while the test is doing something else is not ignored — it takes a slot in the queue. The wait you wrote for the refetch resolves against the poll instead, and the wait you wrote for the poll's cycle now belongs to nobody. ## Why the symptom is confusing Nothing errors. The wait resolves, the test continues, and the assertions after it read a real interception — just not the one the author had in mind. Two consequences follow: - **It is intermittent by construction.** Whether the timer lands before or after the click depends on machine speed and on how long the previous command took, so the same spec behaves differently on a loaded machine. - **The failure surfaces late and elsewhere.** When an assertion finally does fail, it fails on a status code or a URL from the wrong cycle, pointing at a request the test never meant to inspect. A useful first move is simply to count. `cy.get('@getForecast.all').then((all) => cy.log(all.length))` prints how many requests the route really saw, which usually settles the question of whether a background caller exists at all. ## Three ways to take the poll out of the picture 1. **Give the poll its own intercept.** If the periodic refresh and the user-triggered fetch are distinguishable at all — a different path, a different method, a different query — register them as two routes with two aliases. Then an alias means one thing and the counter is trustworthy again. This is the fix that survives someone else editing the test later. 2. **Freeze the timer with `cy.clock()`.** Installing `cy.clock()` before the navigation that starts the app replaces the page's timer functions, so an interval scheduled afterwards never fires on its own. The only forecast requests are then the ones your test causes, plus any you release deliberately with `cy.tick()`. The ordering constraint matters: a clock installed after the app has already scheduled its interval controls nothing. 3. **Stop sequencing on the counter.** Where the point of the test is *what* was requested rather than *when*, drop the extra waits and assert over `cy.get('@getForecast.all')` instead. Because `cy.get()` is a query it re-reads the recording, so a chained assertion retries until it passes or `defaultCommandTimeout` expires. ## Asserting on content instead of position Position-free assertions are the ones that survive a background caller: - `cy.get('@getForecast.all').should('have.length.at.least', 2)` — the route was hit at least twice, without claiming which was which. - `cy.get('@getForecast.all').then((all) => { expect(all.some((i) => i.request.url.includes('KPDX'))).to.be.true })` — some request carried the new station, whichever slot it landed in. - `cy.get('@getForecast').its('response.statusCode').should('eq', 200)` — the most recent cycle succeeded, which is what the panel is showing. Chai's `have.length.at.least` and the plain `some()` check both express "this happened" without committing to an index, and that is the property you want when a timer shares the route. ## What does not help - **Widening `requestTimeout`.** The wait is resolving, not timing out. A bigger budget for a clock that never expires changes nothing. - **Adding another `cy.wait('@getForecast')`.** It goes green for the same reason the bug exists: the extra wait happens to absorb the stray request on the machine where it was tried. It will unbalance on a faster or slower one. - **Reordering the assertions.** The interception the wait handed back is a settled snapshot of the wrong cycle; nothing downstream can recover the right one. ## The takeaway An alias answers "did this route see traffic", not "did my action cause traffic". Once you internalise that, the presence of a background caller becomes a design input for the test rather than a mystery: either separate the callers, silence the timer, or write assertions that do not depend on which slot a request landed in.
- In Cypress, how does `cy.clock()` help when a polled route keeps advancing your alias?`cy.clock()` replaces the page's timer functions, so an interval the app schedules after the clock is installed never fires on its own. Install it before the navigation that starts the app and the only forecast requests are the ones your test causes, plus any you release deliberately with `cy.tick()`.
- If a polled route shares your Cypress alias, what can `cy.get('@getForecast.all')` assert?It yields every interception recorded for the alias, so you can assert on content rather than position: that some request carried the new station, or that the most recent cycle returned `200`. Those hold whether the poll fired once, twice, or not at all, which is exactly the property a timer-driven route needs.
saying these in an interview costs you the question
- Adds another cy.wait until the test goes green
- Raises requestTimeout for a wait that is resolving
- Assumes only user actions produce matching requests
- Thinks the alias counter tracks the triggering command
- Calls cy.clock after the app scheduled its interval