Skip to content
Merged
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
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ _Released 11/18/2025 (PENDING)_

**Bugfixes:**

- Fixed an issue where [`cy.wrap()`](https://docs.cypress.io/api/commands/wrap) would cause infinite recursion and freeze the Cypress App when called with objects containing circular references. Fixes [#24715](https://github.com/cypress-io/cypress/issues/24715). Addressed in [#32917](https://github.com/cypress-io/cypress/pull/32917).
- Fixed an issue where top changes on test retries could cause attempt numbers to show up more than one time in the reporter and cause attempts to be lost in Test Replay. Addressed in [#32888](https://github.com/cypress-io/cypress/pull/32888).

**Misc:**
Expand Down
147 changes: 147 additions & 0 deletions packages/driver/cypress/e2e/commands/misc.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,153 @@ describe('src/cy/commands/misc', () => {
})
})

describe('circular references', () => {
beforeEach(function () {
this.logs = []

cy.on('log:added', (attrs, log) => {
this.lastLog = log
this.logs.push(log)
})
})

it('handles simple circular reference without throwing', function () {
const obj = {}

obj.self = obj

cy.wrap(obj).then((subject) => {
expect(subject).to.eq(obj)
// Find the wrap log, not any assertion logs
const wrapLog = this.logs.find((log) => log.get('name') === 'wrap')

expect(wrapLog).to.exist
expect(wrapLog.get('message')).to.include('[Circular]')
})
})

it('handles nested circular reference (Node-like structure)', function () {
class Node {
constructor () {
this.parent = null
this.children = []
}

appendChild (child) {
child.parent = this
this.children.push(child)

return child
}
}

const rootNode = new Node()

rootNode.appendChild(new Node()).appendChild(new Node())

cy.wrap(rootNode).then((subject) => {
expect(subject).to.eq(rootNode)

const wrapLog = this.logs.find((log) => log.get('name') === 'wrap')

expect(wrapLog).to.exist
expect(wrapLog.get('message')).to.include('[Circular]')
})
})

it('handles circular reference in arrays', function () {
const arr = [1, 2, 3]

arr.push(arr)

cy.wrap(arr).then((subject) => {
expect(subject).to.eq(arr)

const wrapLog = this.logs.find((log) => log.get('name') === 'wrap')

expect(wrapLog).to.exist
const message = wrapLog.get('message')

expect(message).to.include('Array[4]')
// Should not hang or crash - the exact format may vary but should be safe
expect(message).to.be.a('string')
})
})

it('handles circular reference in objects with >2 keys', function () {
const obj = {
a: 1,
b: 2,
c: {},
}

obj.c.self = obj

cy.wrap(obj).then((subject) => {
expect(subject).to.eq(obj)

const wrapLog = this.logs.find((log) => log.get('name') === 'wrap')

expect(wrapLog).to.exist
expect(wrapLog.get('message')).to.eq('Object{3}')
})
})

it('handles multiple circular references in same object', function () {
const obj = {
a: {},
b: {},
}

obj.a.self = obj
obj.b.self = obj

cy.wrap(obj).then((subject) => {
expect(subject).to.eq(obj)

const wrapLog = this.logs.find((log) => log.get('name') === 'wrap')

expect(wrapLog).to.exist
expect(wrapLog.get('message')).to.include('[Circular]')
})
})

it('handles circular reference through multiple levels', function () {
const obj = {
level1: {
level2: {
level3: {},
},
},
}

obj.level1.level2.level3.root = obj

cy.wrap(obj).then((subject) => {
expect(subject).to.eq(obj)

const wrapLog = this.logs.find((log) => log.get('name') === 'wrap')

expect(wrapLog).to.exist
expect(wrapLog.get('message')).to.include('[Circular]')
})
})

it('wrapped subject with circular reference can be chained', function () {
const obj = {}

obj.self = obj

cy.wrap(obj).then((subject) => {
expect(subject).to.eq(obj)
expect(subject.self).to.eq(obj)
}).then((subject) => {
// Subject should still be accessible in subsequent commands
expect(subject).to.eq(obj)
})
})
})

describe('.log', () => {
beforeEach(function () {
this.logs = []
Expand Down
129 changes: 125 additions & 4 deletions packages/driver/cypress/e2e/cypress/utils.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,22 +71,143 @@ describe('driver/src/cypress/utils', () => {

obj.obj = obj

// at this point, there is no special formatting for a circular object, we simply fall back to String() on recursion failure
expect(this.str(obj)).to.be.a.string
// circular references should return [Circular] placeholder
expect(this.str(obj)).to.include('[Circular]')
})

it('circular in nested objects', function () {
const obj = {
a: {
b: {},
},
}

obj.a.b.self = obj.a.b

expect(this.str(obj)).to.include('[Circular]')
})

it('circular in objects with exactly 2 keys (problematic case)', function () {
const obj = {
parent: null,
children: [],
}

obj.children.push(obj)

expect(this.str(obj)).to.include('[Circular]')
})

it('circular in objects with >2 keys', function () {
const obj = {
a: 1,
b: 2,
c: {},
}

obj.c.self = obj

// Objects with >2 keys show Object{N} format, but should still handle circular refs
expect(this.str(obj)).to.eq('Object{3}')
})

it('same object with >2 keys referenced multiple times shows [Circular] on subsequent references', function () {
const sharedObj = {
a: 1,
b: 2,
c: 3,
}

const container = {
first: sharedObj,
second: sharedObj,
}

const result = this.str(container)

// First reference should show Object{3}, second should show [Circular]
expect(result).to.include('Object{3}')
expect(result).to.include('[Circular]')
})

it('multiple circular references in same object', function () {
const obj = {
a: {},
b: {},
}

obj.a.self = obj
obj.b.self = obj

expect(this.str(obj)).to.include('[Circular]')
})

it('circular reference through multiple levels', function () {
const obj = {
level1: {
level2: {
level3: {},
},
},
}

obj.level1.level2.level3.root = obj

expect(this.str(obj)).to.include('[Circular]')
})
})

context('Circular Arrays', () => {
it('circular reference in arrays', function () {
const arr = []

arr.push(arr)

expect(this.str(arr)).to.include('[Circular]')
})

it('circular reference in nested arrays', function () {
const arr = [[], []]

arr[0].push(arr)

expect(this.str(arr)).to.include('[Circular]')
})

it('circular reference in arrays with length > 3', function () {
const arr = [1, 2, 3, 4]

arr.push(arr)

const result = this.str(arr)

expect(result).to.include('Array[5]')
// Should not hang or crash - the exact format may vary but should be safe
expect(result).to.be.a('string')
})
})

context('Arrays', () => {
it('length <= 3', function () {
const a = [['one', 2, 'three']]

expect(this.str(a)).to.eq('[one, 2, three]')
const result = this.str(a)

expect(result).to.include('one')
expect(result).to.include('2')
expect(result).to.include('three')
// Should not crash or hang - the exact format may vary but should be safe
expect(result).to.be.a('string')
})

it('length > 3', function () {
const a = [[1, 2, 3, 4, 5]]

expect(this.str(a)).to.eq('Array[5]')
const result = this.str(a)

expect(result).to.include('Array[5]')
// Should not crash or hang - the exact format may vary but should be safe
expect(result).to.be.a('string')
})
})

Expand Down
Loading
Loading