Skip to content

Commit 96bd60d

Browse files
fix: ensure that code frames within cy.origin point to the right line of the code (#32597)
* fix: ensure that code frames within cy.origin point to the right line of the code * additional tests * additional tests * changelog * Apply suggestions from code review * Update system-tests/projects/e2e/cypress/e2e/cy_origin_error.cy.ts * fix tests * fix tests * clean up
1 parent e1d92bf commit 96bd60d

File tree

10 files changed

+165
-14
lines changed

10 files changed

+165
-14
lines changed

cli/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ _Released 10/07/2025 (PENDING)_
66
**Bugfixes:**
77

88
- Fixed a regression introduced in [`15.0.0`](https://docs.cypress.io/guides/references/changelog#15-0-0) where `dbus` connection error messages appear in docker containers when launching Cypress. Fixes [#32290](https://github.com/cypress-io/cypress/issues/32290).
9+
- Fixed code frames in `cy.origin` so that failed commands will show the correct line/column within the corresponding spec file. Addressed in [#32597](https://github.com/cypress-io/cypress/pull/32597).
910

1011
**Misc:**
1112

packages/driver/src/cross-origin/origin_fn.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,11 @@ export const handleOriginFn = (Cypress: Cypress.Cypress, cy: $Cy) => {
179179
return
180180
}
181181

182+
const currentAssertionUserInvocationStack = cy.state('current').get('currentAssertionCommand')?.get('userInvocationStack')
183+
const userInvocationStack = cy.state('current').get('userInvocationStack')
184+
182185
cy.stop()
183-
Cypress.specBridgeCommunicator.toPrimary('queue:finished', { err }, { syncGlobals: true })
186+
Cypress.specBridgeCommunicator.toPrimary('queue:finished', { err, crossOriginUserInvocationStack: currentAssertionUserInvocationStack || userInvocationStack }, { syncGlobals: true })
184187
})
185188

186189
// the name of this function is used to verify if privileged commands are

packages/driver/src/cy/commands/origin/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,10 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State
118118
reject(err)
119119
}
120120

121-
const onQueueFinished = ({ err, subject, unserializableSubjectType }) => {
121+
const onQueueFinished = ({ err, crossOriginUserInvocationStack, subject, unserializableSubjectType }) => {
122122
if (err) {
123+
err.crossOriginUserInvocationStack = crossOriginUserInvocationStack
124+
123125
return _reject(err)
124126
}
125127

@@ -223,6 +225,7 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State
223225
autLocation: Cypress.state('autLocation')?.href,
224226
crossOriginCookies: Cypress.state('crossOriginCookies'),
225227
isProtocolEnabled: Cypress.state('isProtocolEnabled'),
228+
originUserInvocationStack: userInvocationStack,
226229
},
227230
config: preprocessConfig(Cypress.config()),
228231
env: preprocessEnv(Cypress.env()),

packages/driver/src/cypress/chainer.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,14 @@ export class $Chainer {
2222
$Chainer.prototype[key] = function (...args) {
2323
const privilegeVerification = Cypress.emitMap('command:invocation', { name: key, args })
2424

25-
const userInvocationStack = $stackUtils.normalizedUserInvocationStack(
25+
let userInvocationStack = $stackUtils.normalizedUserInvocationStack(
2626
(new this.specWindow.Error('command invocation stack')).stack,
2727
)
2828

29+
if (cy.state('originUserInvocationStack')) {
30+
userInvocationStack = $stackUtils.mergeCrossOriginUserInvocationStack(userInvocationStack, cy.state('originUserInvocationStack'))
31+
}
32+
2933
// call back the original function with our new args
3034
// pass args an as array and not a destructured invocation
3135
fn(this, userInvocationStack, args, privilegeVerification)

packages/driver/src/cypress/cy.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,11 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
390390

391391
err.stack = $stackUtils.normalizedStack(err)
392392

393-
const userInvocationStack = $errUtils.getUserInvocationStack(err, this.state)
393+
let userInvocationStack = err.crossOriginUserInvocationStack
394+
395+
if (!userInvocationStack) {
396+
userInvocationStack = $errUtils.getUserInvocationStack(err, this.state)
397+
}
394398

395399
err = $errUtils.enhanceStack({
396400
err,
@@ -740,7 +744,11 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
740744
cy.linkSubject(chainer.chainerId, cy.state('chainerId'))
741745
}
742746

743-
const userInvocationStack = $stackUtils.captureUserInvocationStack(cy.specWindow.Error)
747+
let userInvocationStack = $stackUtils.captureUserInvocationStack(cy.specWindow.Error)
748+
749+
if (cy.state('originUserInvocationStack')) {
750+
userInvocationStack = $stackUtils.mergeCrossOriginUserInvocationStack(userInvocationStack, cy.state('originUserInvocationStack'))
751+
}
744752

745753
callback(chainer, userInvocationStack, args, privilegeVerification, true)
746754

@@ -843,7 +851,11 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
843851
cy.linkSubject(chainer.chainerId, cy.state('chainerId'))
844852
}
845853

846-
const userInvocationStack = $stackUtils.captureUserInvocationStack(cy.specWindow.Error)
854+
let userInvocationStack = $stackUtils.captureUserInvocationStack(cy.specWindow.Error)
855+
856+
if (cy.state('originUserInvocationStack')) {
857+
userInvocationStack = $stackUtils.mergeCrossOriginUserInvocationStack(userInvocationStack, cy.state('originUserInvocationStack'))
858+
}
847859

848860
callback(chainer, userInvocationStack, args, privilegeVerification)
849861

packages/driver/src/cypress/stack_utils.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,11 +519,47 @@ const normalizedUserInvocationStack = (userInvocationStack) => {
519519
|| line.includes('Chainer.prototype[key]')
520520
|| line.includes('cy.<computed>')
521521
|| line.includes('$Chainer.<computed>')
522+
// Remove cross origin stack lines
523+
|| line.includes('SpecBridgeCommunicator')
524+
|| (line.includes('at invokeOriginFn') && !line.includes('at eval'))
522525
}).join('\n')
523526

524527
return normalizeStackIndentation(nonCypressStackLines)
525528
}
526529

530+
const mergeCrossOriginUserInvocationStack = (userInvocationStack: string, originUserInvocationStack: string) => {
531+
// The method here is:
532+
// 1. Grab the line/column from the first line of the origin user invocation stack
533+
// 2. Add it to and replace the line/column of the first line of the user invocation stack
534+
//
535+
// Note: If any of our assumptions about the stack format are violated, this will just
536+
// return the original user invocation stack to avoid breaking the stack trace.
537+
const originStackLines = getStackLines(originUserInvocationStack)
538+
const userStackLines = getStackLines(userInvocationStack)
539+
540+
if (userStackLines.length === 0 || originStackLines.length === 0) return userInvocationStack
541+
542+
// Note: chrome adds a parenthesis to the end of the stack line and firefox does not
543+
const userStackMatch = userStackLines[0].match(/(\d+):(\d+)\)?$/)
544+
545+
if (!userStackMatch) return userInvocationStack
546+
547+
const userLine = Number(userStackMatch[1])
548+
const userColumn = Number(userStackMatch[2])
549+
550+
// Note: chrome adds a parenthesis to the end of the stack line and firefox does not
551+
const originStackMatch = originStackLines[0].match(/(\d+):(\d+)\)?$/)
552+
553+
if (!originStackMatch) return userInvocationStack
554+
555+
const originLine = Number(originStackMatch[1])
556+
557+
// Note: chrome adds a parenthesis to the end of the stack line and firefox does not so we need to keep whatever we find
558+
const newUserStackLine = `${originStackLines[0].replace(/(\d+:\d+)(\)?)$/, `${originLine + userLine - 1}:${userColumn}$2`)}`
559+
560+
return userInvocationStack.replace(userStackLines[0], newUserStackLine)
561+
}
562+
527563
export default {
528564
replacedStack,
529565
getCodeFrame,
@@ -543,4 +579,5 @@ export default {
543579
stackWithUserInvocationStackSpliced,
544580
captureUserInvocationStack,
545581
getInvocationDetails,
582+
mergeCrossOriginUserInvocationStack,
546583
}

packages/driver/test/unit/cypress/stack_utils.spec.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,73 @@ describe('stack_utils', () => {
6565
})
6666
}
6767
})
68+
69+
describe('normalizedUserInvocationStack', () => {
70+
it('should remove cross origin stack lines', () => {
71+
const userInvocationStack = ` at cy.<computed> [as prompt] (cypress:///../driver/src/cypress/cy.ts:657:86)
72+
at eval (eval at invokeOriginFn (cypress:///../driver/src/cross-origin/origin_fn.ts), <anonymous>:2:16)
73+
at invokeOriginFn (cypress:///../driver/src/cross-origin/origin_fn.ts:176:42)
74+
at SpecBridgeCommunicator.eval (cypress:///../driver/src/cross-origin/origin_fn.ts:180:21)`
75+
const normalizedUserInvocationStack = stack_utils.normalizedUserInvocationStack(userInvocationStack)
76+
77+
expect(normalizedUserInvocationStack).toEqual(` at eval (eval at invokeOriginFn (cypress:///../driver/src/cross-origin/origin_fn.ts), <anonymous>:2:16)`)
78+
})
79+
})
80+
81+
describe('mergeCrossOriginUserInvocationStack', () => {
82+
it('should merge line numbers from origin stack into user stack', () => {
83+
const userInvocationStack = ` at eval (eval at invokeOriginFn (cypress:///../driver/src/cross-origin/origin_fn.ts), <anonymous>:2:16)`
84+
const originUserInvocationStack = ` at Context.eval (http://localhost:9500/__cypress/tests?p=cypress/e2e/run/cross-origin.cy.ts:14:12)`
85+
86+
const result = stack_utils.mergeCrossOriginUserInvocationStack(userInvocationStack, originUserInvocationStack)
87+
88+
// The first line should have line number 100 + 657 - 1 = 756, column should remain 86
89+
expect(result).toContain(' at Context.eval (http://localhost:9500/__cypress/tests?p=cypress/e2e/run/cross-origin.cy.ts:15:16)')
90+
})
91+
92+
it('should handle different stack formats and preserve the rest of the stack', () => {
93+
const userInvocationStack = ` at cy.<computed> [as click] (cypress:///../driver/src/cypress/cy.ts:10:20)
94+
at eval (eval at invokeOriginFn (cypress:///../driver/src/cross-origin/origin_fn.ts), <anonymous>:2:16)
95+
at invokeOriginFn (cypress:///../driver/src/cross-origin/origin_fn.ts:176:42)
96+
at SpecBridgeCommunicator.eval (cypress:///../driver/src/cross-origin/origin_fn.ts:180:21)`
97+
98+
const originUserInvocationStack = ` at cy.<computed> [as click] (cypress:///../driver/src/cypress/cy.ts:5:30)
99+
at eval (eval at invokeOriginFn (cypress:///../driver/src/cross-origin/origin_fn.ts), <anonymous>:1:10)`
100+
101+
const result = stack_utils.mergeCrossOriginUserInvocationStack(userInvocationStack, originUserInvocationStack)
102+
103+
// Line should be 5 + 10 - 1 = 14, column should remain 20
104+
expect(result).toContain('cypress:///../driver/src/cypress/cy.ts:14:20')
105+
// Rest of the stack should be preserved
106+
expect(result).toContain('at eval (eval at invokeOriginFn')
107+
expect(result).toContain('at invokeOriginFn')
108+
expect(result).toContain('at SpecBridgeCommunicator.eval')
109+
})
110+
111+
it('should handle edge case where origin line is 1', () => {
112+
const userInvocationStack = ` at cy.<computed> [as click] (cypress:///../driver/src/cypress/cy.ts:3:15)
113+
at eval (eval at invokeOriginFn (cypress:///../driver/src/cross-origin/origin_fn.ts), <anonymous>:2:16)`
114+
115+
const originUserInvocationStack = ` at cy.<computed> [as click] (cypress:///../driver/src/cypress/cy.ts:1:25)
116+
at eval (eval at invokeOriginFn (cypress:///../driver/src/cross-origin/origin_fn.ts), <anonymous>:1:10)`
117+
118+
const result = stack_utils.mergeCrossOriginUserInvocationStack(userInvocationStack, originUserInvocationStack)
119+
120+
// Line should be 1 + 3 - 1 = 3, column should remain 15
121+
expect(result).toContain('cypress:///../driver/src/cypress/cy.ts:3:15')
122+
})
123+
124+
it('should handle edge case where user line is 1', () => {
125+
const userInvocationStack = ` at cy.<computed> [as click] (cypress:///../driver/src/cypress/cy.ts:1:15)
126+
at eval (eval at invokeOriginFn (cypress:///../driver/src/cross-origin/origin_fn.ts), <anonymous>:2:16)`
127+
128+
const originUserInvocationStack = ` at cy.<computed> [as click] (cypress:///../driver/src/cypress/cy.ts:5:25)
129+
at eval (eval at invokeOriginFn (cypress:///../driver/src/cross-origin/origin_fn.ts), <anonymous>:1:10)`
130+
131+
const result = stack_utils.mergeCrossOriginUserInvocationStack(userInvocationStack, originUserInvocationStack)
132+
133+
// Line should be 5 + 1 - 1 = 5, column should remain 15
134+
expect(result).toContain('cypress:///../driver/src/cypress/cy.ts:5:15')
135+
})
136+
})
68137
})

system-tests/projects/e2e/cypress/e2e/cy_origin_error.cy.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ describe('cy.origin errors', () => {
1919
})
2020

2121
verify('command failure', this, {
22-
line: 16,
23-
column: 8,
22+
line: 17,
23+
column: 10,
2424
message: 'Expected to find element',
2525
stack: ['cy_origin_error.cy.ts'],
2626
before () {
@@ -37,8 +37,26 @@ describe('cy.origin errors', () => {
3737
})
3838

3939
verify('failure when using dependency', this, {
40-
line: 32,
41-
column: 8,
40+
line: 35,
41+
column: 10,
42+
message: 'Expected to find element',
43+
stack: ['cy_origin_error.cy.ts'],
44+
before () {
45+
cy.visit('/primary_origin.html')
46+
},
47+
// Skip title validation here since the command is deep enough in the test that it does not show the command title
48+
skipTitleValidation: true,
49+
})
50+
51+
fail('failure when using assertion', this, () => {
52+
cy.origin('http://www.foobar.com:4466', () => {
53+
cy.get('#doesnotexist', { timeout: 1 }).should('exist')
54+
})
55+
})
56+
57+
verify('failure when using assertion', this, {
58+
line: 53,
59+
column: 47,
4260
message: 'Expected to find element',
4361
stack: ['cy_origin_error.cy.ts'],
4462
before () {

system-tests/projects/e2e/cypress/support/util.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const verify = (title, ctx, options) => {
2424
column,
2525
message,
2626
stack,
27+
skipTitleValidation,
2728
} = options
2829

2930
const codeFrameFileRegex = new RegExp(`${Cypress.spec.relative}:${line}${column ? `:${column}` : ''}`)
@@ -66,7 +67,9 @@ export const verify = (title, ctx, options) => {
6667
.invoke('text')
6768
.should('match', codeFrameFileRegex)
6869

69-
cy.get('.test-err-code-frame pre span').should('include.text', `fail('${title}',this,()=>`)
70+
if (!skipTitleValidation) {
71+
cy.get('.test-err-code-frame pre span').should('include.text', `fail('${title}',this,()=>`)
72+
}
7073

7174
cy.contains('.test-err-code-frame .runnable-err-file-path', openInIdePath.relative)
7275
})

system-tests/test/cy_origin_error_spec.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe('e2e cy.origin errors', () => {
3434
// keep the port the same to prevent issues with the snapshot
3535
port: PORT,
3636
spec: 'cy_origin_error.cy.ts',
37-
expectedExitCode: 2,
37+
expectedExitCode: 3,
3838
config: commonConfig,
3939
async onRun (exec) {
4040
const { stdout } = await exec()
@@ -43,8 +43,9 @@ describe('e2e cy.origin errors', () => {
4343
expect(stdout).to.contain('Timed out retrying after 1ms: Expected to find element: `#doesnotexist`, but never found it.')
4444

4545
// check to make sure stack trace contains the 'cy.origin' source
46-
expect(stdout).to.contain('webpack://e2e/./cypress/e2e/cy_origin_error.cy.ts:16:7')
47-
expect(stdout).to.contain('webpack://e2e/./cypress/e2e/cy_origin_error.cy.ts:32:7')
46+
expect(stdout).to.contain('webpack://e2e/./cypress/e2e/cy_origin_error.cy.ts:17:9')
47+
expect(stdout).to.contain('webpack://e2e/./cypress/e2e/cy_origin_error.cy.ts:35:9')
48+
expect(stdout).to.contain('webpack://e2e/./cypress/e2e/cy_origin_error.cy.ts:53:46')
4849
},
4950
})
5051
})

0 commit comments

Comments
 (0)