Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
## 15.8.2

_Released 12/30/2025 (PENDING)_

**Bugfixes:**

- Fixed a bug where `cy.wait()` with multiple aliases could crash with `TypeError: Cannot read properties of undefined` when a retry was canceled (e.g., via `Cypress.stop()`) or when retry logic executed between test runnables due to a Bluebird cancellation propagation bug, instead of properly failing the test. Fixed in [#33157](https://github.com/cypress-io/cypress/pull/33157).

## 15.8.1

_Released 12/18/2025_
Expand Down
12 changes: 10 additions & 2 deletions packages/driver/cypress/e2e/commands/waiting.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,15 @@ describe('src/cy/commands/waiting', () => {
it('does not retry after 1 alias times out', {
requestTimeout: 1000,
}, (done) => {
Promise.onPossiblyUnhandledRejection(done)
let doneCalled = false
const callDoneOnce = () => {
if (!doneCalled) {
doneCalled = true
done()
}
}

Promise.onPossiblyUnhandledRejection(callDoneOnce)

cy.on('command:retry', (options) => {
// force bar to time out before foo
Expand All @@ -475,7 +483,7 @@ describe('src/cy/commands/waiting', () => {
})

cy.on('fail', (err) => {
done()
callDoneOnce()
})

cy
Expand Down
35 changes: 22 additions & 13 deletions packages/driver/src/cy/retries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,19 +82,24 @@ export const create = (Cypress: ICypress, state: StateFunc, timeout: $Cy['timeou
return `${msg2}${msg1}`
})

if (error) {
const retryErr = mergeErrProps(error, retryErrProps)
// Always throw an error when timeout is exceeded, even if error is not set
// This handles edge cases where _runnableTimeout is manipulated (e.g., in tests)
// and ensures Promise.map correctly rejects instead of treating undefined as success
const retryErr = error
? mergeErrProps(error, retryErrProps)
: errByPath('miscellaneous.retry_timed_out', {
ms: options._runnableTimeout,
})

throwErr(retryErr, {
onFail: (err) => {
if (onFail) {
err = onFail(err)
}
throwErr(retryErr, {
onFail: (err) => {
if (onFail) {
err = onFail(err)
}

finishAssertions(err)
},
})
}
finishAssertions(err)
},
})
}

const runnableHasChanged = () => {
Expand Down Expand Up @@ -122,13 +127,17 @@ export const create = (Cypress: ICypress, state: StateFunc, timeout: $Cy['timeou
.delay(interval)
.then(() => {
if (ended()) {
return
// Reject the promise instead of returning undefined
// This prevents Promise.map from treating it as a successful resolution
// when one promise in the map should have failed
return Promise.reject(new Error('Retry ended: promise was canceled or runnable changed'))
}

Cypress.action('cy:command:retry', options)

if (ended()) {
return
// Reject the promise instead of returning undefined
return Promise.reject(new Error('Retry ended: promise was canceled or runnable changed'))
}

// if we are unstable then remove
Expand Down
Loading
Loading