-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Open
Description
Current behavior
When running hours of tests, in some environments (e.g. our Gitlab CI) cy.session restore sometimes fails with:
CypressError: `cy.then()` timed out after waiting `20000ms`.
Your callback function returned a promise that never resolved.
The callback function was:
async () => {
setSessionLogStatus(_utils__WEBPACK_IMPORTED_MODULE_6__.statusMap.inProgress(_utils__WEBPACK_IMPORTED_MODULE_6__.SESSION_STEPS.restore));
await (0,_utils__WEBPACK_IMPORTED_MODULE_6__.navigateAboutBlank)();
await sessions.clearCurrentSessionData();
return restoreSession(existingSession);
}
https://on.cypress.io/then
at <unknown> (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:122830:75)
at tryCatcher (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:1777:23)
at <unknown> (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:4156:41)
at tryCatcher (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:1777:23)
at Promise._settlePromiseFromHandler (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:1489:31)
at Promise._settlePromise (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:1546:18)
at Promise._settlePromise0 (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:1591:10)
at Promise._settlePromises (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:1667:18)
at _drainQueueStep (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:2377:12)
at _drainQueue (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:2370:9)
at Async._drainQueues (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:2386:5)
at Async.drainQueues (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:2256:14)
Desired behavior
No response
Test code to reproduce
Please run this in Gitlab CI:
/*
* This test is to replicate a problem seen in Gitlab CI.
* https://github.com/cypress-io/cypress/issues/33419
*/
Cypress._.times(10, (o) => {
it("tries to trigger the session restore bug, o == " + o, () => {
Cypress._.times(100, () => {
cy.session("foo", () => {
cy.visit("https://example.com/")
},
{
/*
* a validate function seems to be
* needed to replicate the problem
*/
validate() {
cy.log("validate")
},
})
})
})
})
Cypress Version
15.11.0
Debug Logs
[13:37:37] $ yarn cypress run --browser chrome --headless --spec cypress/e2e/auto/$TEST_SUITE
[13:37:38] Verifying Cypress can run /root/.cache/Cypress/15.11.0/Cypress [started]
[13:37:40] Verifying Cypress can run /root/.cache/Cypress/15.11.0/Cypress [COMPLETED]
DevTools listening on ws://127.0.0.1:40217/devtools/browser/83c5ed40-9b9f-4a9d-b56a-022b75f88cbd
<<<CYPRESS.STDERR.START>>>npm warn exec The following package was not found and will be installed: tsx@4.21.0
====================================================================================================
(Run Starting)
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Cypress: 15.11.0 │
│ Browser: Chrome 144 (headless) │
│ Node Version: v24.13.0 (/usr/local/bin/node) │
│ Specs: 1 found (cypress-bug.cy.ts) │
│ Searched: cypress/e2e/cypress-bug.cy.ts │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
────────────────────────────────────────────────────────────────────────────────────────────────────
Running: cypress-bug.cy.ts (1 of 1)
✓ tries to trigger the session restore bug + 0 (2077804ms)
2 passing (39m)
1 failing
CypressError: `cy.then()` timed out after waiting `20000ms`.
Your callback function returned a promise that never resolved.
The callback function was:
async () => {
setSessionLogStatus(_utils__WEBPACK_IMPORTED_MODULE_6__.statusMap.inProgress(step));
await (0,_utils__WEBPACK_IMPORTED_MODULE_6__.navigateAboutBlank)();
await sessions.clearCurrentSessionData();
return cy.whenStable(() => createSession(existingSession, step));
}
https://on.cypress.io/then
Because this error occurred during a `before all` hook we are skipping all of the remaining tests.
at <unknown> (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:122830:75)
at tryCatcher (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:1777:23)
at <unknown> (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:4156:41)
at tryCatcher (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:1777:23)
at Promise._settlePromiseFromHandler (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:1489:31)
at Promise._settlePromise (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:1546:18)
at Promise._settlePromise0 (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:1591:10)
at Promise._settlePromises (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:1667:18)
at _drainQueueStep (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:2377:12)
at _drainQueue (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:2370:9)
at Async._drainQueues (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:2386:5)
at <unknown> (http://edc6-autotest.os.iomedico.local/__cypress/runner/cypress_runner.js:2256:14)Workaround
I have this workaround in my commands.ts file where I have custom commands etc.
Another good place might be cypress/support/e2e.ts, I guess.
And if you're not using TypeScript, you'll obviously need to remove the types.
/**
* Workaround for a known Cypress bug where the internal navigateAboutBlank()
* during cy.session restore hangs forever because the "window:load" event
* never fires after navigating to about:blank.
* https://github.com/cypress-io/cypress/issues/29496
* https://github.com/cypress-io/cypress/issues/31988
* https://github.com/cypress-io/cypress/issues/33419
*
* Root cause: navigateAboutBlank() does:
* 1. cy.once('window:load', resolve)
* 2. Cypress.action('cy:visit:blank', ...)
* If about:blank loads before the listener receives the event (race condition),
* the Promise never resolves and the test times out after 20s.
*
* Fix strategy:
* - We intercept cy.once('window:load', ...) and check cy.state('current').
* - navigateAboutBlank() is called from an async Promise callback, so there is
* no active Cypress command (current is not 'visit'/'reload'/'go').
* - cy.visit/cy.reload/cy.go always have their command name as cy.state('current')
* when they register the window:load listener.
* - If the caller is navigateAboutBlank():
* Case 1: AUT already on about:blank → call the listener (resolve()) directly,
* then install a stability guard that restores isStable(true) if
* visitBlankPage's subsequent src="about:blank" causes isStable(false)
* without a matching load event.
* Case 2: AUT on a real page → pass through; the race condition does not
* apply here because about:blank hasn't loaded yet.
* - If the caller is cy.visit/cy.reload/cy.go → pass through unchanged.
*/
function getAutHref(): string | null {
try {
const aut = window.top?.document.querySelector("iframe.aut-iframe") as HTMLIFrameElement | null
return aut?.contentWindow?.location.href ?? ""
} catch {
return null // SecurityError: cross-origin
}
}
const originalCyOnce = (cy as any).once.bind(cy)
;(cy as any).once = function(event: string, listener: (...args: any[]) => void, ...rest: any[]) {
if (event === "window:load") {
const href = getAutHref()
const isAboutBlank = href === "" || href === "about:blank"
/*
* navigateAboutBlank() is called from an async Promise callback, outside
* of the Cypress command queue. cy.state('current') is therefore null or
* the previously completed command — not 'visit'/'reload'/'go'.
* cy.visit/cy.reload/cy.go always have cy.state('current')?.get('name')
* equal to their command name when they register cy.once('window:load').
*/
const currentCmd: string | undefined = (cy as any).state?.("current")?.get?.("name")
const isFromNavigateAboutBlank = currentCmd !== "visit" && currentCmd !== "reload" && currentCmd !== "go"
Cypress.log({
name: "cy.once workaround",
message: `href="${
href ?? "(cross-origin)"
}", isAboutBlank=${isAboutBlank}, currentCmd=${currentCmd}, isFromNavigateAboutBlank=${isFromNavigateAboutBlank}`,
})
if (isAboutBlank && isFromNavigateAboutBlank) {
/*
* Case 1: The frame is already on about:blank when the listener is registered.
*
* Root cause: after this cy.once() call, navigateAboutBlank() fires
* Cypress.action('cy:visit:blank'), which calls visitBlankPage() in the
* runner UI. visitBlankPage() sets $iframe[0].src = "about:blank" and
* waits for a "load" event. But setting src to the same value does NOT
* fire a new load event → visitBlankPage()'s Promise never resolves.
*
* Worse: the src="about:blank" attempt fires beforeunload on the
* contentWindow → cy.isStable(false). No matching load follows, so
* isStable stays false. The CommandQueue calls whenStable() before each
* command → hangs → outer cy.then() times out after 20s.
*
* Fix: Call the listener (= resolve()) directly to unblock
* navigateAboutBlank(). Then install a one-shot stability guard that
* restores isStable(true) if visitBlankPage's side effect sets it to
* false without a subsequent load event.
*/
const aut = window.top?.document.querySelector("iframe.aut-iframe") as HTMLIFrameElement | null
listener(aut?.contentWindow)
Cypress.log({ name: "cy.once workaround", message: "Case 1: called listener directly (already on about:blank)" })
// Guard: visitBlankPage() will set src="about:blank" which may fire
// beforeunload → isStable(false) without a subsequent load → isStable
// stuck at false. If that happens, force stability back after a short delay.
const restoreStability = () => {
setTimeout(() => {
if (!(cy as any).state?.("isStable")) {
;(cy as any).isStable?.(true, "workaround")
Cypress.log({ name: "cy.once workaround", message: "Case 1: forced isStable(true) after visitBlankPage side effect" })
}
}, 200)
}
Cypress.once("stability:changed" as any, (stable: boolean) => {
if (!stable) restoreStability()
})
return this
}
}
return originalCyOnce(event, listener, ...rest)
}```
Reactions are currently unavailable