-
Notifications
You must be signed in to change notification settings - Fork 3.4k
fix: normalize test body invocationDetails from stack traces
#32699
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 23 commits
3bb1471
b6bc0f5
926e1fc
49165cf
61311ee
0a8de00
a1480d8
36064a6
65d656c
c0fb620
96a9a10
d0dfb88
5d5cc2d
79c51da
a7e5bb2
7bb0f76
42d6495
1929cba
ec9dad3
85756f0
1bc1d68
4a53ed1
cde2e2c
badf190
d7f64f0
76e2319
31af0fb
dbe8c6f
e9d72cf
4df6616
1dfe50c
6fa202f
50418a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
| 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') | ||
| }) | ||
| }) | ||
| }) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||||
|
||||||||||||
| // 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
Outdated
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
cursor[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
astone123 marked this conversation as resolved.
Show resolved
Hide resolved
astone123 marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment.
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.
Uh oh!
There was an error while loading. Please reload this page.