Skip to content
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
3bb1471
fix: (studio) remove `itGrep` lines from stack trace when determining…
astone123 Oct 10, 2025
b6bc0f5
Merge branch 'develop' into astone123/fix-itgrep-trace
astone123 Oct 28, 2025
926e1fc
handle .only and suites
astone123 Nov 3, 2025
49165cf
Merge branch 'develop' into astone123/fix-itgrep-trace
astone123 Nov 3, 2025
61311ee
Merge branch 'develop' into astone123/fix-itgrep-trace
astone123 Nov 3, 2025
0a8de00
Merge branch 'develop' into astone123/fix-itgrep-trace
astone123 Nov 6, 2025
a1480d8
for test block hooks, trim the stack to find the invocation details
astone123 Nov 6, 2025
36064a6
Merge branch 'develop' into astone123/fix-itgrep-trace
astone123 Nov 6, 2025
65d656c
fix stop only
astone123 Nov 6, 2025
c0fb620
fix cypress object access
astone123 Nov 7, 2025
96a9a10
Merge branch 'develop' into astone123/fix-itgrep-trace
astone123 Nov 7, 2025
d0dfb88
update comment to use generic host
astone123 Nov 11, 2025
5d5cc2d
Merge branch 'develop' into astone123/fix-itgrep-trace
astone123 Nov 11, 2025
79c51da
Merge branch 'develop' into astone123/fix-itgrep-trace
astone123 Nov 11, 2025
a7e5bb2
Update packages/driver/src/cypress/stack_utils.ts
astone123 Nov 12, 2025
7bb0f76
Update packages/driver/src/cypress/stack_utils.ts
astone123 Nov 12, 2025
42d6495
Update packages/driver/src/cypress/mocha.ts
astone123 Nov 12, 2025
1929cba
Update packages/driver/src/cypress/stack_utils.ts
astone123 Nov 12, 2025
ec9dad3
Update packages/driver/src/cypress/stack_utils.ts
astone123 Nov 12, 2025
85756f0
feedback
astone123 Nov 12, 2025
1bc1d68
Merge branch 'develop' into astone123/fix-itgrep-trace
astone123 Nov 12, 2025
4a53ed1
changelog entry
astone123 Nov 12, 2025
cde2e2c
remove unnecessary test, add driver integration tests
astone123 Nov 13, 2025
badf190
fix .only
astone123 Nov 13, 2025
d7f64f0
update test name
astone123 Nov 13, 2025
76e2319
Merge branch 'develop' into astone123/fix-itgrep-trace
astone123 Nov 13, 2025
31af0fb
fix types
astone123 Nov 13, 2025
dbe8c6f
add integration test
astone123 Nov 14, 2025
e9d72cf
logic and test updates
astone123 Nov 14, 2025
4df6616
Merge branch 'develop' into astone123/fix-itgrep-trace
astone123 Nov 14, 2025
1dfe50c
fix types
astone123 Nov 14, 2025
6fa202f
fix test
astone123 Nov 14, 2025
50418a8
only modify stacks for e2e tests
astone123 Nov 14, 2025
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 @@ -7,6 +7,7 @@ _Released 11/18/2025 (PENDING)_

- 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).
- Fixed an issue where stack traces that are used to determine a test's invocation details are sometimes incorrect. Addressed in [#32699](https://github.com/cypress-io/cypress/pull/32699)

**Misc:**

Expand Down
68 changes: 56 additions & 12 deletions packages/app/cypress/e2e/runner/reporter.hooks.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,25 +38,69 @@ describe('hooks', {
cy.contains('after all (2)').closest('.collapsible').should('contain', 'afterHook 1')
})

it('creates open in IDE button', () => {
loadSpec({
filePath: 'hooks/basic.cy.js',
passCount: 2,
hasPreferredIde: true,
describe('open in IDE button', () => {
it('sends the correct invocation details for before hook', () => {
loadSpec({
filePath: 'hooks/basic.cy.js',
passCount: 2,
hasPreferredIde: true,
})

cy.contains('tests 1').click()

cy.get('.hook-open-in-ide').should('have.length', 4)

cy.withCtx((ctx, o) => {
o.sinon.stub(ctx.actions.file, 'openFile')
})

cy.contains('before all').closest('.hook-header').find('.hook-open-in-ide').invoke('show').click()

cy.withCtx((ctx, o) => {
expect(ctx.actions.file.openFile).to.have.been.calledWith(o.sinon.match(new RegExp(`hooks/basic\.cy\.js$`)), 2, 2)
})
})

cy.contains('tests 1').click()
it('sends the correct invocation details for basic test body', () => {
loadSpec({
filePath: 'hooks/basic.cy.js',
passCount: 2,
hasPreferredIde: true,
})

cy.contains('tests 1').click()

cy.get('.hook-open-in-ide').should('have.length', 4)

cy.withCtx((ctx, o) => {
o.sinon.stub(ctx.actions.file, 'openFile')
})

cy.get('.hook-open-in-ide').should('have.length', 4)
cy.contains('test body').closest('.hook-header').find('.hook-open-in-ide').invoke('show').click()

cy.withCtx((ctx, o) => {
o.sinon.stub(ctx.actions.file, 'openFile')
cy.withCtx((ctx, o) => {
expect(ctx.actions.file.openFile).to.have.been.calledWith(o.sinon.match(new RegExp(`hooks/basic\.cy\.js$`)), 10, 2)
})
})

cy.get('.hook-open-in-ide').first().invoke('show').click()
it.only('sends the correct invocation details for wrapped it', () => {
loadSpec({
filePath: 'hooks/wrapped-it.cy.js',
passCount: 2,
hasPreferredIde: true,
})

cy.contains('test 1').click()

cy.withCtx((ctx, o) => {
o.sinon.stub(ctx.actions.file, 'openFile')
})

cy.contains('test body').closest('.hook-header').find('.hook-open-in-ide').invoke('show').click()

cy.withCtx((ctx, o) => {
expect(ctx.actions.file.openFile).to.have.been.calledWith(o.sinon.match(new RegExp(`hooks/basic\.cy\.js$`)), 2, 2)
cy.withCtx((ctx, o) => {
expect(ctx.actions.file.openFile).to.have.been.calledWith(o.sinon.match(new RegExp(`hooks/wrapped-it\.cy\.js$`)), 5, 1)
})
})
})

Expand Down
157 changes: 157 additions & 0 deletions packages/driver/cypress/e2e/cypress/invocationDetails.cy.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this spec is correct (possibly AI generated nonsense?).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this spec in favor of packages/driver/cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also have an existing stack_utils.cy.js file that we can add to for any getInvocationDetails tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ended up making a separate spec for these instead of adding them to stack_utils.cy.js

Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import $stackUtils from '../../../src/cypress/stack_utils'
import $sourceMapUtils from '../../../src/cypress/source_map_utils'

describe('stack_utils getInvocationDetails', () => {
context('basic test invocation', () => {
it('correctly extracts invocation details for Chrome', { browser: 'chrome' }, function () {
// Chrome stack traces for test invocations start with 'at eval' or 'at Suite.eval'
const details = $stackUtils.getInvocationDetails(window, $sourceMapUtils.getSourceMapProjectRoot(), 'test')

expect(details).to.exist
expect(details.line).to.be.a('number')
expect(details.column).to.be.a('number')
expect(details.stack).to.be.a('string')

// Verify the stack is trimmed to start with the test invocation
// Chrome format: "at eval" or "at Suite.eval"
const stackLines = details.stack.split('\n')
const firstStackLine = stackLines.find((line) => line.trim().startsWith('at'))

expect(firstStackLine).to.exist
expect(firstStackLine.trim()).to.satisfy((line: string) => {
return line.startsWith('at eval') || line.startsWith('at Suite.eval')
}, 'Chrome stack should start with "at eval" or "at Suite.eval"')

// Verify that the stack was actually trimmed (should not include Cypress internals before the test invocation)
// The trimmed stack should start with the test invocation pattern, not internal Cypress code
const hasCypressInternalBeforeInvocation = stackLines.some((line, index) => {
const trimmedLine = line.trim()

return index < stackLines.indexOf(firstStackLine) &&
(trimmedLine.includes('cypress:///../driver/src/cypress/runner.ts') ||
trimmedLine.includes('cypress:///../driver/src/cypress/mocha.ts'))
})

expect(hasCypressInternalBeforeInvocation).to.be.false
})

it('correctly extracts invocation details for Firefox', { browser: 'firefox' }, function () {
// Firefox stack traces for test invocations have no function name before '@'
// Format: "@http://localhost:3000/__cypress/tests?p=..."
const details = $stackUtils.getInvocationDetails(window, $sourceMapUtils.getSourceMapProjectRoot(), 'test')

expect(details).to.exist
expect(details.line).to.be.a('number')
expect(details.column).to.be.a('number')
expect(details.stack).to.be.a('string')

// Verify the stack is trimmed to start with the test invocation
// Firefox format: "@" with empty function name before it
const stackLines = details.stack.split('\n')
const firstStackLine = stackLines.find((line) => line.includes('@'))

expect(firstStackLine).to.exist
const splitAtAt = firstStackLine.split('@')

expect(splitAtAt.length).to.be.greaterThan(1)
expect(splitAtAt[0].trim()).to.equal('', 'Firefox stack should have empty function name before @')

// Verify that the stack was actually trimmed (should not include Cypress internals before the test invocation)
const hasCypressInternalBeforeInvocation = stackLines.some((line, index) => {
return index < stackLines.indexOf(firstStackLine) &&
(line.includes('cypress:///../driver/src/cypress/runner.ts') ||
line.includes('cypress:///../driver/src/cypress/mocha.ts'))
})

expect(hasCypressInternalBeforeInvocation).to.be.false
})
})

context('wrapped it function', () => {
// Test case for when users re-define Mocha's it function
// This creates additional stack frames that need to be trimmed correctly
function myIt (name: string, optionsOrFn: any, fn?: () => void) {
if (fn) {
it(name, optionsOrFn, fn)
} else {
it(name, optionsOrFn)
}
}

myIt('correctly extracts invocation details for wrapped it in Chrome', { browser: 'chrome' }, function () {
const details = $stackUtils.getInvocationDetails(window, $sourceMapUtils.getSourceMapProjectRoot(), 'test')

expect(details).to.exist
expect(details.line).to.be.a('number')
expect(details.column).to.be.a('number')

// The stack should be trimmed to the actual test invocation (myIt call)
// not the wrapper function call
const stackLines = details.stack.split('\n')
const firstStackLine = stackLines.find((line) => line.trim().startsWith('at'))

expect(firstStackLine).to.exist
expect(firstStackLine.trim()).to.satisfy((line: string) => {
return line.startsWith('at eval') || line.startsWith('at Suite.eval')
}, 'Chrome stack should start with "at eval" or "at Suite.eval" even with wrapped it')
})

myIt('correctly extracts invocation details for wrapped it in Firefox', { browser: 'firefox' }, function () {
const details = $stackUtils.getInvocationDetails(window, $sourceMapUtils.getSourceMapProjectRoot(), 'test')

expect(details).to.exist
expect(details.line).to.be.a('number')
expect(details.column).to.be.a('number')

// The stack should be trimmed to the actual test invocation
const stackLines = details.stack.split('\n')
const firstStackLine = stackLines.find((line) => line.includes('@'))

expect(firstStackLine).to.exist
const splitAtAt = firstStackLine.split('@')

expect(splitAtAt.length).to.be.greaterThan(1)
expect(splitAtAt[0].trim()).to.equal('', 'Firefox stack should have empty function name before @ even with wrapped it')
})
})

context('nested describes', () => {
describe('outer describe', () => {
describe('inner describe', () => {
it('correctly extracts invocation details in nested describe for Chrome', { browser: 'chrome' }, function () {
const details = $stackUtils.getInvocationDetails(window, $sourceMapUtils.getSourceMapProjectRoot(), 'test')

expect(details).to.exist
expect(details.line).to.be.a('number')
expect(details.column).to.be.a('number')

// Stack should still be trimmed correctly even in nested describes
const firstStackLine = details.stack.split('\n').find((line) => line.trim().startsWith('at'))

expect(firstStackLine).to.exist
expect(firstStackLine.trim()).to.satisfy((line: string) => {
return line.startsWith('at eval') || line.startsWith('at Suite.eval')
}, 'Chrome stack should start with "at eval" or "at Suite.eval" in nested describes')
})

it('correctly extracts invocation details in nested describe for Firefox', { browser: 'firefox' }, function () {
const details = $stackUtils.getInvocationDetails(window, $sourceMapUtils.getSourceMapProjectRoot(), 'test')

expect(details).to.exist
expect(details.line).to.be.a('number')
expect(details.column).to.be.a('number')

// Stack should still be trimmed correctly even in nested describes
const stackLines = details.stack.split('\n')
const firstStackLine = stackLines.find((line) => line.includes('@'))

expect(firstStackLine).to.exist
const splitAtAt = firstStackLine.split('@')

expect(splitAtAt.length).to.be.greaterThan(1)
expect(splitAtAt[0].trim()).to.equal('', 'Firefox stack should have empty function name before @ in nested describes')
})
})
})
})
})
2 changes: 1 addition & 1 deletion packages/driver/src/cypress/mocha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ const patchSuiteAddTest = (specWindow) => {
const test = args[0]

if (!test.invocationDetails) {
test.invocationDetails = $stackUtils.getInvocationDetails(specWindow, $sourceMapUtils.getSourceMapProjectRoot())
test.invocationDetails = $stackUtils.getInvocationDetails(specWindow, $sourceMapUtils.getSourceMapProjectRoot(), 'test')
}

const ret = suiteAddTest.apply(this, args)
Expand Down
56 changes: 55 additions & 1 deletion packages/driver/src/cypress/stack_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,55 @@ const stackWithLinesRemoved = (stack, cb) => {
return unsplitStack(messageLines, remainingStackLines)
}

const stackTrimmedToTestInvocation = (stack: string, specWindow) => {
const modifiedStack = stackWithLinesRemoved(stack, (lines: string[]) => {
// Guard against Cypress being undefined/null (can happen when users quickly reload tests)
if (!specWindow?.Cypress) {
return lines
}

if (specWindow.Cypress.isBrowser({ family: 'chromium' })) {
// There are cases where there are other lines in the stack trace before the invocation (eg. `context.it.only`, `createRunnable`, etc)
// The actual test invocation line starts with either 'at eval' or 'at Suite.eval',
// so remove all lines until we reach the test invocation line
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// There are cases where there are other lines in the stack trace before the invocation (eg. `context.it.only`, `createRunnable`, etc)
// The actual test invocation line starts with either 'at eval' or 'at Suite.eval',
// so remove all lines until we reach the test invocation line
// The actual test invocation line starts with either 'at eval' or 'at Suite.eval',
// so remove all lines until we reach the test invocation line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

return _.dropWhile(lines, (line) => {
return !(
line.trim().startsWith('at eval') ||
line.trim().startsWith('at Suite.eval')
)
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Flawed Stack Trimming Breaks Test Debugging

The Chromium stack trimming logic uses startsWith('at eval') and startsWith('at Suite.eval'), which incorrectly matches function names like evalScripts, evaluate, or Suite.evalSomething. This causes the stack to be trimmed at the wrong location, breaking invocation details for tests when such function names appear in the stack trace.

Fix in Cursor Fix in Web

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can probably add a space to at eval and at Suit.eval

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, added a space to the end of those

}

if (specWindow.Cypress.isBrowser({ family: 'firefox' })) {
const isTestInvocationLine = (line: string) => {
const splitAtAt = line.split('@')

// firefox stacks traces look like:
// functionName@http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14
// @http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js:43:3
// @http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js:45:12
// evalScripts/<@cypress:///../driver/src/cypress/script_utils.ts:38:23
//
// the actual invocation details will be at the first line with no function name
return splitAtAt.length > 1 && splitAtAt[0].trim().length === 0
}

return _.dropWhile(lines, (line) => {
return !isTestInvocationLine(line)
})
}

return lines
})

// if we removed all the lines then something went wrong. return the original stack instead
if (modifiedStack.trim() === 'Error') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems brittle, I think in the stackWithLinesRemoved callback above, you can create a copy of the lines and then do your processing on that copy. Before returning lines, you can check if all of the lines were removed and if they were, you can return the original lines, otherwise return the copy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated this to create a copy of the lines and check to make sure there are lines in the copy before returning it

return stack
}

return modifiedStack
}

const stackWithLinesDroppedFromMarker = (stack, marker, includeLast = false) => {
return stackWithLinesRemoved(stack, (lines) => {
// drop lines above the marker
Expand Down Expand Up @@ -124,7 +173,7 @@ type InvocationDetails = {
}

// used to determine codeframes for hook/test/etc definitions rather than command invocations
const getInvocationDetails = (specWindow, sourceMapProjectRoot: string): InvocationDetails | undefined => {
const getInvocationDetails = (specWindow, sourceMapProjectRoot: string, type?: 'test'): InvocationDetails | undefined => {
if (specWindow.Error) {
let stack = (new specWindow.Error()).stack

Expand All @@ -146,6 +195,11 @@ const getInvocationDetails = (specWindow, sourceMapProjectRoot: string): Invocat
}
}

// if the hook is the test, we will try to remove the lines that are not the actual invocation of the test
if (type === 'test') {
stack = stackTrimmedToTestInvocation(stack, specWindow)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Prevent errors from missing Cypress object.

Accessing specWindow.Cypress.testingType without checking if specWindow.Cypress exists will throw an error when users quickly reload tests, since specWindow.Cypress can be null or undefined in those cases. The check should be inside the existing if (specWindow.Cypress) block or include its own null check.

Fix in Cursor Fix in Web


const details: Omit<InvocationDetails, 'stack'> = getSourceDetailsForFirstLine(stack, sourceMapProjectRoot) || {}

;(details as any).stack = stack
Expand Down
Loading
Loading