From 3bb147145f9a54dcda60471cd09d07985e1c1577 Mon Sep 17 00:00:00 2001 From: astone123 Date: Fri, 10 Oct 2025 15:30:20 -0400 Subject: [PATCH 01/22] fix: (studio) remove `itGrep` lines from stack trace when determining invocationDetails --- packages/driver/src/cypress/stack_utils.ts | 12 +++++++++ .../test/unit/cypress/stack_utils.spec.ts | 26 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index 7cc04c11e12..778d957927c 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -63,6 +63,12 @@ const stackWithLinesRemoved = (stack, cb) => { return unsplitStack(messageLines, remainingStackLines) } +const stackWithGrepLinesRemoved = (stack) => { + return stackWithLinesRemoved(stack, (lines) => { + return _.reject(lines, (line) => line.includes('itGrep')) + }) +} + const stackWithLinesDroppedFromMarker = (stack, marker, includeLast = false) => { return stackWithLinesRemoved(stack, (lines) => { // drop lines above the marker @@ -149,6 +155,12 @@ const getInvocationDetails = (specWindow, config): InvocationDetails | undefined } } + // if the stack includes the 'itGrep' function, remove any lines that include it + // so that the first line in the stack is the spec invocation + if (stack.includes('itGrep')) { + stack = stackWithGrepLinesRemoved(stack) + } + const details: Omit = getSourceDetailsForFirstLine(stack, config('projectRoot')) || {}; (details as any).stack = stack diff --git a/packages/driver/test/unit/cypress/stack_utils.spec.ts b/packages/driver/test/unit/cypress/stack_utils.spec.ts index 1a912e88908..bd53889a6b9 100644 --- a/packages/driver/test/unit/cypress/stack_utils.spec.ts +++ b/packages/driver/test/unit/cypress/stack_utils.spec.ts @@ -64,6 +64,32 @@ describe('stack_utils', () => { }) }) } + + it('returns the correct invocation details for a grep stack trace', () => { + const stack = `Error\n +at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14)\n +at eval (http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js:14:1)\n +at eval (http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js:18:12)\n +at eval ()\n +at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` + + class GrepError { + get stack () { + return stack + } + } + + stack_utils.getInvocationDetails( + { Error: GrepError, Cypress: {} }, + config, + ) + + expect(source_map_utils.getSourcePosition).toHaveBeenCalledWith('http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js', expect.objectContaining({ + column: 1, + line: 14, + file: 'http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js', + })) + }) }) describe('normalizedUserInvocationStack', () => { From 926e1fc106d609215dcbc4896a5f1eb53627054d Mon Sep 17 00:00:00 2001 From: astone123 Date: Mon, 3 Nov 2025 11:12:09 -0500 Subject: [PATCH 02/22] handle .only and suites --- packages/driver/src/cypress/stack_utils.ts | 22 ++++++-- .../test/unit/cypress/stack_utils.spec.ts | 52 +++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index 778d957927c..1e1e908229b 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -65,7 +65,22 @@ const stackWithLinesRemoved = (stack, cb) => { const stackWithGrepLinesRemoved = (stack) => { return stackWithLinesRemoved(stack, (lines) => { - return _.reject(lines, (line) => line.includes('itGrep')) + // Remove any lines containing 'itGrep' first + let cleanedLines = _.reject(lines, (line) => line.includes('itGrep')) + + // There are cases where there are other lines in the stack trace before the invocation (eg. `context.it.only`, `createRunnable`, etc) + // Remove lines from the start until the top line starts with 'at eval' or 'at Suite.eval' so that we only keep the actual invocation line. + while ( + cleanedLines.length > 0 && + !( + cleanedLines[0].trim().startsWith('at eval') || + cleanedLines[0].trim().startsWith('at Suite.eval') + ) + ) { + cleanedLines.shift() + } + + return cleanedLines }) } @@ -155,8 +170,9 @@ const getInvocationDetails = (specWindow, config): InvocationDetails | undefined } } - // if the stack includes the 'itGrep' function, remove any lines that include it - // so that the first line in the stack is the spec invocation + // if the stack includes the 'itGrep' function, this suggests that the user has registered + // the @cypress/grep plugin. In this case, we need to remove the lines that include 'itGrep' + // so that the first line in the stack is the spec invocation. if (stack.includes('itGrep')) { stack = stackWithGrepLinesRemoved(stack) } diff --git a/packages/driver/test/unit/cypress/stack_utils.spec.ts b/packages/driver/test/unit/cypress/stack_utils.spec.ts index bd53889a6b9..1d50369171f 100644 --- a/packages/driver/test/unit/cypress/stack_utils.spec.ts +++ b/packages/driver/test/unit/cypress/stack_utils.spec.ts @@ -90,6 +90,58 @@ at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` file: 'http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js', })) }) + + it('returns the correct invocation details for a grep stack trace for suites', () => { + const stack = `Error + at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14) + at context.it.only (cypress:///../driver/node_modules/mocha/lib/interfaces/bdd.js:98:46) + at createRunnable (cypress:///../driver/src/cypress/mocha.ts:126:31) + at itGrep.eval [as only] (cypress:///../driver/src/cypress/mocha.ts:187:14) + at Suite.eval (http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js:12:6)` + + class GrepError { + get stack () { + return stack + } + } + + stack_utils.getInvocationDetails( + { Error: GrepError, Cypress: {} }, + config, + ) + + expect(source_map_utils.getSourcePosition).toHaveBeenCalledWith('http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js', expect.objectContaining({ + column: 6, + line: 12, + file: 'http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js', + })) + }) + + it('returns the correct invocation details for a grep stack trace for tests with only', () => { + const stack = `Error + at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14) + at context.it.only (cypress:///../driver/node_modules/mocha/lib/interfaces/bdd.js:98:46) + at createRunnable (cypress:///../driver/src/cypress/mocha.ts:126:31) + at itGrep.eval [as only] (cypress:///../driver/src/cypress/mocha.ts:187:14) + at eval (http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js:11:4)` + + class GrepError { + get stack () { + return stack + } + } + + stack_utils.getInvocationDetails( + { Error: GrepError, Cypress: {} }, + config, + ) + + expect(source_map_utils.getSourcePosition).toHaveBeenCalledWith('http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js', expect.objectContaining({ + column: 4, + line: 11, + file: 'http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js', + })) + }) }) describe('normalizedUserInvocationStack', () => { From a1480d868c737458348fff4b88a16473bc167a2c Mon Sep 17 00:00:00 2001 From: astone123 Date: Thu, 6 Nov 2025 17:23:31 -0500 Subject: [PATCH 03/22] for test block hooks, trim the stack to find the invocation details --- .../cypress/e2e/runner/reporter.hooks.cy.ts | 90 ++++++++++++++++--- packages/driver/src/cypress/mocha.ts | 2 +- packages/driver/src/cypress/stack_utils.ts | 60 +++++++++---- .../test/unit/cypress/stack_utils.spec.ts | 29 +++--- .../cypress/e2e/hooks/wrapped-it.cy.js | 11 +++ 5 files changed, 145 insertions(+), 47 deletions(-) create mode 100644 system-tests/project-fixtures/runner-specs/cypress/e2e/hooks/wrapped-it.cy.js diff --git a/packages/app/cypress/e2e/runner/reporter.hooks.cy.ts b/packages/app/cypress/e2e/runner/reporter.hooks.cy.ts index 328dc093481..1da05049e65 100644 --- a/packages/app/cypress/e2e/runner/reporter.hooks.cy.ts +++ b/packages/app/cypress/e2e/runner/reporter.hooks.cy.ts @@ -38,25 +38,91 @@ 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.get('.hook-open-in-ide').should('have.length', 4) + cy.withCtx((ctx, o) => { + o.sinon.stub(ctx.actions.file, 'openFile') + }) - 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$`)), 10, 2) + }) }) - cy.get('.hook-open-in-ide').first().invoke('show').click() + it('sends the correct invocation details for .only test body', () => { + loadSpec({ + filePath: 'hooks/only.cy.js', + passCount: 2, + hasPreferredIde: true, + }) + + cy.contains('test 2').click() + + cy.get('.hook-open-in-ide').should('have.length', 2) + + 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/only\.cy\.js$`)), 13, 7) + }) + }) + + it('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) + }) }) }) diff --git a/packages/driver/src/cypress/mocha.ts b/packages/driver/src/cypress/mocha.ts index 4950f3732c5..ae42277fbdb 100644 --- a/packages/driver/src/cypress/mocha.ts +++ b/packages/driver/src/cypress/mocha.ts @@ -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-body') } const ret = suiteAddTest.apply(this, args) diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index 2ce1efadbaa..68905cea5b8 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -60,25 +60,51 @@ const stackWithLinesRemoved = (stack, cb) => { return unsplitStack(messageLines, remainingStackLines) } -const stackWithGrepLinesRemoved = (stack) => { - return stackWithLinesRemoved(stack, (lines) => { - // Remove any lines containing 'itGrep' first - let cleanedLines = _.reject(lines, (line) => line.includes('itGrep')) - +const stackWithWrappingLinesRemoved = (stack) => { + const modifiedStack = stackWithLinesRemoved(stack, (lines) => { + if (Cypress.isBrowser('chrome')) { // There are cases where there are other lines in the stack trace before the invocation (eg. `context.it.only`, `createRunnable`, etc) // Remove lines from the start until the top line starts with 'at eval' or 'at Suite.eval' so that we only keep the actual invocation line. - while ( - cleanedLines.length > 0 && + while ( + lines.length > 0 && !( - cleanedLines[0].trim().startsWith('at eval') || - cleanedLines[0].trim().startsWith('at Suite.eval') + lines[0].trim().startsWith('at eval') || + lines[0].trim().startsWith('at Suite.eval') ) - ) { - cleanedLines.shift() + ) { + lines.shift() + } + } else if (Cypress.isBrowser('firefox')) { + const isTestInvocationLine = (line: string) => { + const splitAtAt = line.split('@') + + // firefox stacks traces look like: + // functionName@https://aicotravel.com/__cypress/tests?p=cypress/support/e2e.js:444:14 + // @https://aicotravel.com/__cypress/tests?p=cypress/e2e/spec.cy.js:43:3 + // @https://aicotravel.com/__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 + } + + while ( + lines.length > 0 && + !isTestInvocationLine(lines[0]) + ) { + lines.shift() + } } - return cleanedLines + return lines }) + + // if we removed all the lines then something went wrong. return the original stack instead + if (modifiedStack.length === 0) { + return stack + } + + return modifiedStack } const stackWithLinesDroppedFromMarker = (stack, marker, includeLast = false) => { @@ -145,7 +171,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-body'): InvocationDetails | undefined => { if (specWindow.Error) { let stack = (new specWindow.Error()).stack @@ -167,11 +193,9 @@ const getInvocationDetails = (specWindow, sourceMapProjectRoot: string): Invocat } } - // if the stack includes the 'itGrep' function, this suggests that the user has registered - // the @cypress/grep plugin. In this case, we need to remove the lines that include 'itGrep' - // so that the first line in the stack is the spec invocation. - if (stack.includes('itGrep')) { - stack = stackWithGrepLinesRemoved(stack) + // if the hook is the test body, we will try to remove the lines that are not the actual invocation of the test + if (type === 'test-body') { + stack = stackWithWrappingLinesRemoved(stack) } const details: Omit = getSourceDetailsForFirstLine(stack, sourceMapProjectRoot) || {} diff --git a/packages/driver/test/unit/cypress/stack_utils.spec.ts b/packages/driver/test/unit/cypress/stack_utils.spec.ts index 1d50369171f..328d0887d4f 100644 --- a/packages/driver/test/unit/cypress/stack_utils.spec.ts +++ b/packages/driver/test/unit/cypress/stack_utils.spec.ts @@ -20,6 +20,7 @@ describe('stack_utils', () => { // @ts-expect-error global.Cypress = { config: vi.fn(), + isBrowser: vi.fn(() => true), } vi.resetAllMocks() @@ -37,7 +38,7 @@ describe('stack_utils', () => { return stack } } - const config = () => projectRoot + const config = projectRoot for (const scenario of scenarios) { const { browser, build, specFrame, stack: scenarioStack } = scenario @@ -66,8 +67,7 @@ describe('stack_utils', () => { } it('returns the correct invocation details for a grep stack trace', () => { - const stack = `Error\n -at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14)\n + const stack = `Error\n at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14)\n at eval (http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js:14:1)\n at eval (http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js:18:12)\n at eval ()\n @@ -82,6 +82,7 @@ at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` stack_utils.getInvocationDetails( { Error: GrepError, Cypress: {} }, config, + 'test-body', ) expect(source_map_utils.getSourcePosition).toHaveBeenCalledWith('http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js', expect.objectContaining({ @@ -91,9 +92,8 @@ at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` })) }) - it('returns the correct invocation details for a grep stack trace for suites', () => { - const stack = `Error - at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14) + it('returns the correct invocation details for a grep stack trace for a test body', () => { + const stack = `Error at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14) at context.it.only (cypress:///../driver/node_modules/mocha/lib/interfaces/bdd.js:98:46) at createRunnable (cypress:///../driver/src/cypress/mocha.ts:126:31) at itGrep.eval [as only] (cypress:///../driver/src/cypress/mocha.ts:187:14) @@ -108,6 +108,7 @@ at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` stack_utils.getInvocationDetails( { Error: GrepError, Cypress: {} }, config, + 'test-body', ) expect(source_map_utils.getSourcePosition).toHaveBeenCalledWith('http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js', expect.objectContaining({ @@ -117,13 +118,12 @@ at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` })) }) - it('returns the correct invocation details for a grep stack trace for tests with only', () => { - const stack = `Error - at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14) + it('returns the original stack if it cannot be normalized for a test body', () => { + const stack = `Error at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14) at context.it.only (cypress:///../driver/node_modules/mocha/lib/interfaces/bdd.js:98:46) at createRunnable (cypress:///../driver/src/cypress/mocha.ts:126:31) at itGrep.eval [as only] (cypress:///../driver/src/cypress/mocha.ts:187:14) - at eval (http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js:11:4)` + at somethingElse (http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js:12:6)` class GrepError { get stack () { @@ -131,16 +131,13 @@ at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` } } - stack_utils.getInvocationDetails( + const result = stack_utils.getInvocationDetails( { Error: GrepError, Cypress: {} }, config, + 'test-body', ) - expect(source_map_utils.getSourcePosition).toHaveBeenCalledWith('http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js', expect.objectContaining({ - column: 4, - line: 11, - file: 'http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js', - })) + expect(result.stack).toEqual(stack) }) }) diff --git a/system-tests/project-fixtures/runner-specs/cypress/e2e/hooks/wrapped-it.cy.js b/system-tests/project-fixtures/runner-specs/cypress/e2e/hooks/wrapped-it.cy.js new file mode 100644 index 00000000000..c049b3e51bd --- /dev/null +++ b/system-tests/project-fixtures/runner-specs/cypress/e2e/hooks/wrapped-it.cy.js @@ -0,0 +1,11 @@ +function myIt (name, fn) { + it(name, fn) +} + +myIt('test 1', () => { + cy.log('testBody 1') +}) + +myIt('test 2', () => { + cy.log('testBody 2') +}) From 65d656c7149d559b92a6364d03e5eb339b55924c Mon Sep 17 00:00:00 2001 From: astone123 Date: Thu, 6 Nov 2025 17:30:32 -0500 Subject: [PATCH 04/22] fix stop only --- packages/driver/test/unit/cypress/stack_utils.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/driver/test/unit/cypress/stack_utils.spec.ts b/packages/driver/test/unit/cypress/stack_utils.spec.ts index 328d0887d4f..ce670e85935 100644 --- a/packages/driver/test/unit/cypress/stack_utils.spec.ts +++ b/packages/driver/test/unit/cypress/stack_utils.spec.ts @@ -94,7 +94,7 @@ at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` it('returns the correct invocation details for a grep stack trace for a test body', () => { const stack = `Error at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14) - at context.it.only (cypress:///../driver/node_modules/mocha/lib/interfaces/bdd.js:98:46) + at context.notIt.only (cypress:///../driver/node_modules/mocha/lib/interfaces/bdd.js:98:46) at createRunnable (cypress:///../driver/src/cypress/mocha.ts:126:31) at itGrep.eval [as only] (cypress:///../driver/src/cypress/mocha.ts:187:14) at Suite.eval (http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js:12:6)` @@ -120,7 +120,7 @@ at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` it('returns the original stack if it cannot be normalized for a test body', () => { const stack = `Error at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14) - at context.it.only (cypress:///../driver/node_modules/mocha/lib/interfaces/bdd.js:98:46) + at context.notIt.only (cypress:///../driver/node_modules/mocha/lib/interfaces/bdd.js:98:46) at createRunnable (cypress:///../driver/src/cypress/mocha.ts:126:31) at itGrep.eval [as only] (cypress:///../driver/src/cypress/mocha.ts:187:14) at somethingElse (http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js:12:6)` From c0fb6202827982c834031e950401e04697541b4a Mon Sep 17 00:00:00 2001 From: astone123 Date: Fri, 7 Nov 2025 09:42:02 -0500 Subject: [PATCH 05/22] fix cypress object access --- packages/driver/src/cypress/stack_utils.ts | 13 +++++++++---- .../driver/test/unit/cypress/stack_utils.spec.ts | 6 +++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index 68905cea5b8..6b3e08ae30e 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -60,9 +60,14 @@ const stackWithLinesRemoved = (stack, cb) => { return unsplitStack(messageLines, remainingStackLines) } -const stackWithWrappingLinesRemoved = (stack) => { +const stackWithWrappingLinesRemoved = (stack, specWindow) => { const modifiedStack = stackWithLinesRemoved(stack, (lines) => { - if (Cypress.isBrowser('chrome')) { + // Guard against Cypress being undefined/null (can happen when users quickly reload tests) + if (!specWindow?.Cypress) { + return lines + } + + if (specWindow.Cypress.isBrowser('chrome')) { // There are cases where there are other lines in the stack trace before the invocation (eg. `context.it.only`, `createRunnable`, etc) // Remove lines from the start until the top line starts with 'at eval' or 'at Suite.eval' so that we only keep the actual invocation line. while ( @@ -74,7 +79,7 @@ const stackWithWrappingLinesRemoved = (stack) => { ) { lines.shift() } - } else if (Cypress.isBrowser('firefox')) { + } else if (specWindow.Cypress.isBrowser('firefox')) { const isTestInvocationLine = (line: string) => { const splitAtAt = line.split('@') @@ -195,7 +200,7 @@ const getInvocationDetails = (specWindow, sourceMapProjectRoot: string, type?: ' // if the hook is the test body, we will try to remove the lines that are not the actual invocation of the test if (type === 'test-body') { - stack = stackWithWrappingLinesRemoved(stack) + stack = stackWithWrappingLinesRemoved(stack, specWindow) } const details: Omit = getSourceDetailsForFirstLine(stack, sourceMapProjectRoot) || {} diff --git a/packages/driver/test/unit/cypress/stack_utils.spec.ts b/packages/driver/test/unit/cypress/stack_utils.spec.ts index ce670e85935..6884db9271e 100644 --- a/packages/driver/test/unit/cypress/stack_utils.spec.ts +++ b/packages/driver/test/unit/cypress/stack_utils.spec.ts @@ -80,7 +80,7 @@ at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` } stack_utils.getInvocationDetails( - { Error: GrepError, Cypress: {} }, + { Error: GrepError, Cypress: { isBrowser: vi.fn(() => true) } }, config, 'test-body', ) @@ -106,7 +106,7 @@ at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` } stack_utils.getInvocationDetails( - { Error: GrepError, Cypress: {} }, + { Error: GrepError, Cypress: { isBrowser: vi.fn(() => true) } }, config, 'test-body', ) @@ -132,7 +132,7 @@ at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` } const result = stack_utils.getInvocationDetails( - { Error: GrepError, Cypress: {} }, + { Error: GrepError, Cypress: { isBrowser: vi.fn(() => true) } }, config, 'test-body', ) From d0dfb883dce4de9304de460e3639dc9bd317e529 Mon Sep 17 00:00:00 2001 From: astone123 Date: Tue, 11 Nov 2025 10:39:54 -0500 Subject: [PATCH 06/22] update comment to use generic host --- packages/driver/src/cypress/stack_utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index 6b3e08ae30e..fc0c91fee4c 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -84,9 +84,9 @@ const stackWithWrappingLinesRemoved = (stack, specWindow) => { const splitAtAt = line.split('@') // firefox stacks traces look like: - // functionName@https://aicotravel.com/__cypress/tests?p=cypress/support/e2e.js:444:14 - // @https://aicotravel.com/__cypress/tests?p=cypress/e2e/spec.cy.js:43:3 - // @https://aicotravel.com/__cypress/tests?p=cypress/e2e/spec.cy.js:45:12 + // 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 From a7e5bb2e27ddac0fc4cf8f5e9aa5b20410129068 Mon Sep 17 00:00:00 2001 From: Adam Stone-Lord Date: Wed, 12 Nov 2025 14:46:52 -0500 Subject: [PATCH 07/22] Update packages/driver/src/cypress/stack_utils.ts Co-authored-by: Matt Schile --- packages/driver/src/cypress/stack_utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index fc0c91fee4c..85df4368f81 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -67,7 +67,7 @@ const stackWithWrappingLinesRemoved = (stack, specWindow) => { return lines } - if (specWindow.Cypress.isBrowser('chrome')) { + 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) // Remove lines from the start until the top line starts with 'at eval' or 'at Suite.eval' so that we only keep the actual invocation line. while ( From 7bb0f76c47b7bd73e3357bbd8c45568b8624b41d Mon Sep 17 00:00:00 2001 From: Adam Stone-Lord Date: Wed, 12 Nov 2025 14:47:14 -0500 Subject: [PATCH 08/22] Update packages/driver/src/cypress/stack_utils.ts Co-authored-by: Matt Schile --- packages/driver/src/cypress/stack_utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index 85df4368f81..a851df149c7 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -79,7 +79,7 @@ const stackWithWrappingLinesRemoved = (stack, specWindow) => { ) { lines.shift() } - } else if (specWindow.Cypress.isBrowser('firefox')) { + } else if (specWindow.Cypress.isBrowser({ family: 'firefox' })) { const isTestInvocationLine = (line: string) => { const splitAtAt = line.split('@') From 42d64950f37fb8e174d8fbccf33aef0b1549fad7 Mon Sep 17 00:00:00 2001 From: Adam Stone-Lord Date: Wed, 12 Nov 2025 14:48:05 -0500 Subject: [PATCH 09/22] Update packages/driver/src/cypress/mocha.ts Co-authored-by: Matt Schile --- packages/driver/src/cypress/mocha.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/driver/src/cypress/mocha.ts b/packages/driver/src/cypress/mocha.ts index ae42277fbdb..50501bf41a9 100644 --- a/packages/driver/src/cypress/mocha.ts +++ b/packages/driver/src/cypress/mocha.ts @@ -535,7 +535,7 @@ const patchSuiteAddTest = (specWindow) => { const test = args[0] if (!test.invocationDetails) { - test.invocationDetails = $stackUtils.getInvocationDetails(specWindow, $sourceMapUtils.getSourceMapProjectRoot(), 'test-body') + test.invocationDetails = $stackUtils.getInvocationDetails(specWindow, $sourceMapUtils.getSourceMapProjectRoot(), 'test') } const ret = suiteAddTest.apply(this, args) From 1929cba7ab3e67737fea5b3c6acd54de6e01b104 Mon Sep 17 00:00:00 2001 From: Adam Stone-Lord Date: Wed, 12 Nov 2025 14:48:16 -0500 Subject: [PATCH 10/22] Update packages/driver/src/cypress/stack_utils.ts Co-authored-by: Matt Schile --- packages/driver/src/cypress/stack_utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index a851df149c7..5a84c5af894 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -60,7 +60,7 @@ const stackWithLinesRemoved = (stack, cb) => { return unsplitStack(messageLines, remainingStackLines) } -const stackWithWrappingLinesRemoved = (stack, specWindow) => { +const stackTrimmedToTestInvocation = (stack, specWindow) => { const modifiedStack = stackWithLinesRemoved(stack, (lines) => { // Guard against Cypress being undefined/null (can happen when users quickly reload tests) if (!specWindow?.Cypress) { From ec9dad3dc21270ea9b7969467b648dbcda91e303 Mon Sep 17 00:00:00 2001 From: Adam Stone-Lord Date: Wed, 12 Nov 2025 14:49:47 -0500 Subject: [PATCH 11/22] Update packages/driver/src/cypress/stack_utils.ts Co-authored-by: Matt Schile --- packages/driver/src/cypress/stack_utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index 5a84c5af894..30c4bc11369 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -69,7 +69,8 @@ const stackTrimmedToTestInvocation = (stack, specWindow) => { 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) - // Remove lines from the start until the top line starts with 'at eval' or 'at Suite.eval' so that we only keep the actual 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 while ( lines.length > 0 && !( From 85756f0da719fe5a464e44c472318b8980247bce Mon Sep 17 00:00:00 2001 From: astone123 Date: Wed, 12 Nov 2025 15:16:05 -0500 Subject: [PATCH 12/22] feedback --- packages/driver/src/cypress/stack_utils.ts | 42 +++++++++---------- .../test/unit/cypress/stack_utils.spec.ts | 16 +++---- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index 30c4bc11369..88f03542929 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -60,8 +60,8 @@ const stackWithLinesRemoved = (stack, cb) => { return unsplitStack(messageLines, remainingStackLines) } -const stackTrimmedToTestInvocation = (stack, specWindow) => { - const modifiedStack = stackWithLinesRemoved(stack, (lines) => { +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 @@ -71,16 +71,15 @@ const stackTrimmedToTestInvocation = (stack, specWindow) => { // 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 - while ( - lines.length > 0 && - !( - lines[0].trim().startsWith('at eval') || - lines[0].trim().startsWith('at Suite.eval') - ) - ) { - lines.shift() - } - } else if (specWindow.Cypress.isBrowser({ family: 'firefox' })) { + return _.dropWhile(lines, (line) => { + return !( + line.trim().startsWith('at eval') || + line.trim().startsWith('at Suite.eval') + ) + }) + } + + if (specWindow.Cypress.isBrowser({ family: 'firefox' })) { const isTestInvocationLine = (line: string) => { const splitAtAt = line.split('@') @@ -94,19 +93,16 @@ const stackTrimmedToTestInvocation = (stack, specWindow) => { return splitAtAt.length > 1 && splitAtAt[0].trim().length === 0 } - while ( - lines.length > 0 && - !isTestInvocationLine(lines[0]) - ) { - lines.shift() - } + 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.length === 0) { + if (modifiedStack.trim() === 'Error') { return stack } @@ -177,7 +173,7 @@ type InvocationDetails = { } // used to determine codeframes for hook/test/etc definitions rather than command invocations -const getInvocationDetails = (specWindow, sourceMapProjectRoot: string, type?: 'test-body'): InvocationDetails | undefined => { +const getInvocationDetails = (specWindow, sourceMapProjectRoot: string, type?: 'test'): InvocationDetails | undefined => { if (specWindow.Error) { let stack = (new specWindow.Error()).stack @@ -199,9 +195,9 @@ const getInvocationDetails = (specWindow, sourceMapProjectRoot: string, type?: ' } } - // if the hook is the test body, we will try to remove the lines that are not the actual invocation of the test - if (type === 'test-body') { - stack = stackWithWrappingLinesRemoved(stack, specWindow) + // 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) } const details: Omit = getSourceDetailsForFirstLine(stack, sourceMapProjectRoot) || {} diff --git a/packages/driver/test/unit/cypress/stack_utils.spec.ts b/packages/driver/test/unit/cypress/stack_utils.spec.ts index 6884db9271e..26b87b48d6b 100644 --- a/packages/driver/test/unit/cypress/stack_utils.spec.ts +++ b/packages/driver/test/unit/cypress/stack_utils.spec.ts @@ -82,7 +82,7 @@ at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` stack_utils.getInvocationDetails( { Error: GrepError, Cypress: { isBrowser: vi.fn(() => true) } }, config, - 'test-body', + 'test', ) expect(source_map_utils.getSourcePosition).toHaveBeenCalledWith('http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js', expect.objectContaining({ @@ -92,8 +92,9 @@ at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` })) }) - it('returns the correct invocation details for a grep stack trace for a test body', () => { - const stack = `Error at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14) + it('returns the correct invocation details for a test with a stack that needs to be trimmed', () => { + const stack = `Error + at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14) at context.notIt.only (cypress:///../driver/node_modules/mocha/lib/interfaces/bdd.js:98:46) at createRunnable (cypress:///../driver/src/cypress/mocha.ts:126:31) at itGrep.eval [as only] (cypress:///../driver/src/cypress/mocha.ts:187:14) @@ -108,7 +109,7 @@ at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` stack_utils.getInvocationDetails( { Error: GrepError, Cypress: { isBrowser: vi.fn(() => true) } }, config, - 'test-body', + 'test', ) expect(source_map_utils.getSourcePosition).toHaveBeenCalledWith('http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js', expect.objectContaining({ @@ -118,8 +119,9 @@ at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` })) }) - it('returns the original stack if it cannot be normalized for a test body', () => { - const stack = `Error at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14) + it('returns the original stack if it cannot be normalized for a test', () => { + const stack = `Error + at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14) at context.notIt.only (cypress:///../driver/node_modules/mocha/lib/interfaces/bdd.js:98:46) at createRunnable (cypress:///../driver/src/cypress/mocha.ts:126:31) at itGrep.eval [as only] (cypress:///../driver/src/cypress/mocha.ts:187:14) @@ -134,7 +136,7 @@ at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` const result = stack_utils.getInvocationDetails( { Error: GrepError, Cypress: { isBrowser: vi.fn(() => true) } }, config, - 'test-body', + 'test', ) expect(result.stack).toEqual(stack) From 4a53ed1626e05f95daf17d66474c64493df5cb13 Mon Sep 17 00:00:00 2001 From: astone123 Date: Wed, 12 Nov 2025 15:25:40 -0500 Subject: [PATCH 13/22] changelog entry --- cli/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 6e9465c8124..d3a72d9d903 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -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:** From cde2e2cabd35fb75bc7fd03cef33c026cbb202b6 Mon Sep 17 00:00:00 2001 From: astone123 Date: Thu, 13 Nov 2025 09:22:22 -0500 Subject: [PATCH 14/22] remove unnecessary test, add driver integration tests --- .../cypress/e2e/runner/reporter.hooks.cy.ts | 24 +-- .../e2e/cypress/invocationDetails.cy.ts | 157 ++++++++++++++++++ 2 files changed, 158 insertions(+), 23 deletions(-) create mode 100644 packages/driver/cypress/e2e/cypress/invocationDetails.cy.ts diff --git a/packages/app/cypress/e2e/runner/reporter.hooks.cy.ts b/packages/app/cypress/e2e/runner/reporter.hooks.cy.ts index 1da05049e65..30baa45f596 100644 --- a/packages/app/cypress/e2e/runner/reporter.hooks.cy.ts +++ b/packages/app/cypress/e2e/runner/reporter.hooks.cy.ts @@ -83,29 +83,7 @@ describe('hooks', { }) }) - it('sends the correct invocation details for .only test body', () => { - loadSpec({ - filePath: 'hooks/only.cy.js', - passCount: 2, - hasPreferredIde: true, - }) - - cy.contains('test 2').click() - - cy.get('.hook-open-in-ide').should('have.length', 2) - - 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/only\.cy\.js$`)), 13, 7) - }) - }) - - it('sends the correct invocation details for wrapped it', () => { + it.only('sends the correct invocation details for wrapped it', () => { loadSpec({ filePath: 'hooks/wrapped-it.cy.js', passCount: 2, diff --git a/packages/driver/cypress/e2e/cypress/invocationDetails.cy.ts b/packages/driver/cypress/e2e/cypress/invocationDetails.cy.ts new file mode 100644 index 00000000000..f88bf0cd739 --- /dev/null +++ b/packages/driver/cypress/e2e/cypress/invocationDetails.cy.ts @@ -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') + }) + }) + }) + }) +}) From badf190d4c7103a3483c6bbca3b732e182001a93 Mon Sep 17 00:00:00 2001 From: astone123 Date: Thu, 13 Nov 2025 09:24:48 -0500 Subject: [PATCH 15/22] fix .only --- packages/app/cypress/e2e/runner/reporter.hooks.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/cypress/e2e/runner/reporter.hooks.cy.ts b/packages/app/cypress/e2e/runner/reporter.hooks.cy.ts index 30baa45f596..26ecb2cf5d5 100644 --- a/packages/app/cypress/e2e/runner/reporter.hooks.cy.ts +++ b/packages/app/cypress/e2e/runner/reporter.hooks.cy.ts @@ -83,7 +83,7 @@ describe('hooks', { }) }) - it.only('sends the correct invocation details for wrapped it', () => { + it('sends the correct invocation details for wrapped it', () => { loadSpec({ filePath: 'hooks/wrapped-it.cy.js', passCount: 2, From d7f64f06218a491de54b74045106610e042cd200 Mon Sep 17 00:00:00 2001 From: astone123 Date: Thu, 13 Nov 2025 09:34:06 -0500 Subject: [PATCH 16/22] update test name --- packages/driver/test/unit/cypress/stack_utils.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/driver/test/unit/cypress/stack_utils.spec.ts b/packages/driver/test/unit/cypress/stack_utils.spec.ts index 26b87b48d6b..86ccf85393b 100644 --- a/packages/driver/test/unit/cypress/stack_utils.spec.ts +++ b/packages/driver/test/unit/cypress/stack_utils.spec.ts @@ -66,7 +66,7 @@ describe('stack_utils', () => { }) } - it('returns the correct invocation details for a grep stack trace', () => { + it('returns the correct invocation details for a test stack trace that needs to be trimmed', () => { const stack = `Error\n at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14)\n at eval (http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js:14:1)\n at eval (http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js:18:12)\n @@ -92,7 +92,7 @@ at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` })) }) - it('returns the correct invocation details for a test with a stack that needs to be trimmed', () => { + it('returns the correct invocation details for a .only test with a stack that needs to be trimmed', () => { const stack = `Error at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14) at context.notIt.only (cypress:///../driver/node_modules/mocha/lib/interfaces/bdd.js:98:46) From 31af0fb037957566f0597ca79975e6d8821792ca Mon Sep 17 00:00:00 2001 From: astone123 Date: Thu, 13 Nov 2025 10:20:48 -0500 Subject: [PATCH 17/22] fix types --- .../e2e/cypress/invocationDetails.cy.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/driver/cypress/e2e/cypress/invocationDetails.cy.ts b/packages/driver/cypress/e2e/cypress/invocationDetails.cy.ts index f88bf0cd739..79435def8a1 100644 --- a/packages/driver/cypress/e2e/cypress/invocationDetails.cy.ts +++ b/packages/driver/cypress/e2e/cypress/invocationDetails.cy.ts @@ -8,6 +8,8 @@ describe('stack_utils getInvocationDetails', () => { const details = $stackUtils.getInvocationDetails(window, $sourceMapUtils.getSourceMapProjectRoot(), 'test') expect(details).to.exist + if (!details) return + expect(details.line).to.be.a('number') expect(details.column).to.be.a('number') expect(details.stack).to.be.a('string') @@ -18,6 +20,8 @@ describe('stack_utils getInvocationDetails', () => { const firstStackLine = stackLines.find((line) => line.trim().startsWith('at')) expect(firstStackLine).to.exist + if (!firstStackLine) return + 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"') @@ -41,6 +45,8 @@ describe('stack_utils getInvocationDetails', () => { const details = $stackUtils.getInvocationDetails(window, $sourceMapUtils.getSourceMapProjectRoot(), 'test') expect(details).to.exist + if (!details) return + expect(details.line).to.be.a('number') expect(details.column).to.be.a('number') expect(details.stack).to.be.a('string') @@ -51,6 +57,8 @@ describe('stack_utils getInvocationDetails', () => { const firstStackLine = stackLines.find((line) => line.includes('@')) expect(firstStackLine).to.exist + if (!firstStackLine) return + const splitAtAt = firstStackLine.split('@') expect(splitAtAt.length).to.be.greaterThan(1) @@ -82,6 +90,8 @@ describe('stack_utils getInvocationDetails', () => { const details = $stackUtils.getInvocationDetails(window, $sourceMapUtils.getSourceMapProjectRoot(), 'test') expect(details).to.exist + if (!details) return + expect(details.line).to.be.a('number') expect(details.column).to.be.a('number') @@ -91,6 +101,8 @@ describe('stack_utils getInvocationDetails', () => { const firstStackLine = stackLines.find((line) => line.trim().startsWith('at')) expect(firstStackLine).to.exist + if (!firstStackLine) return + 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') @@ -100,6 +112,8 @@ describe('stack_utils getInvocationDetails', () => { const details = $stackUtils.getInvocationDetails(window, $sourceMapUtils.getSourceMapProjectRoot(), 'test') expect(details).to.exist + if (!details) return + expect(details.line).to.be.a('number') expect(details.column).to.be.a('number') @@ -108,6 +122,8 @@ describe('stack_utils getInvocationDetails', () => { const firstStackLine = stackLines.find((line) => line.includes('@')) expect(firstStackLine).to.exist + if (!firstStackLine) return + const splitAtAt = firstStackLine.split('@') expect(splitAtAt.length).to.be.greaterThan(1) @@ -122,6 +138,8 @@ describe('stack_utils getInvocationDetails', () => { const details = $stackUtils.getInvocationDetails(window, $sourceMapUtils.getSourceMapProjectRoot(), 'test') expect(details).to.exist + if (!details) return + expect(details.line).to.be.a('number') expect(details.column).to.be.a('number') @@ -129,6 +147,8 @@ describe('stack_utils getInvocationDetails', () => { const firstStackLine = details.stack.split('\n').find((line) => line.trim().startsWith('at')) expect(firstStackLine).to.exist + if (!firstStackLine) return + 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') @@ -138,6 +158,8 @@ describe('stack_utils getInvocationDetails', () => { const details = $stackUtils.getInvocationDetails(window, $sourceMapUtils.getSourceMapProjectRoot(), 'test') expect(details).to.exist + if (!details) return + expect(details.line).to.be.a('number') expect(details.column).to.be.a('number') @@ -146,6 +168,8 @@ describe('stack_utils getInvocationDetails', () => { const firstStackLine = stackLines.find((line) => line.includes('@')) expect(firstStackLine).to.exist + if (!firstStackLine) return + const splitAtAt = firstStackLine.split('@') expect(splitAtAt.length).to.be.greaterThan(1) From dbe8c6f479de83444de13c661a00ec48ffc768bc Mon Sep 17 00:00:00 2001 From: astone123 Date: Fri, 14 Nov 2025 10:33:55 -0500 Subject: [PATCH 18/22] add integration test --- .../e2e/cypress/invocationDetails.cy.ts | 181 ------------------ .../stack_utils-invocationDetails.cy.ts | 98 ++++++++++ packages/driver/src/cypress/stack_utils.ts | 4 +- 3 files changed, 101 insertions(+), 182 deletions(-) delete mode 100644 packages/driver/cypress/e2e/cypress/invocationDetails.cy.ts create mode 100644 packages/driver/cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts diff --git a/packages/driver/cypress/e2e/cypress/invocationDetails.cy.ts b/packages/driver/cypress/e2e/cypress/invocationDetails.cy.ts deleted file mode 100644 index 79435def8a1..00000000000 --- a/packages/driver/cypress/e2e/cypress/invocationDetails.cy.ts +++ /dev/null @@ -1,181 +0,0 @@ -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 - if (!details) return - - 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 - if (!firstStackLine) return - - 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 - if (!details) return - - 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 - if (!firstStackLine) return - - 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 - if (!details) return - - 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 - if (!firstStackLine) return - - 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 - if (!details) return - - 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 - if (!firstStackLine) return - - 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 - if (!details) return - - 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 - if (!firstStackLine) return - - 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 - if (!details) return - - 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 - if (!firstStackLine) return - - 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') - }) - }) - }) - }) -}) diff --git a/packages/driver/cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts b/packages/driver/cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts new file mode 100644 index 00000000000..d6a4c5b56a4 --- /dev/null +++ b/packages/driver/cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts @@ -0,0 +1,98 @@ +import { InvocationDetails } from '../../../src/cypress/stack_utils' + +// Note: the tests in this spec assert against their own invocation details. So if any of the line numbers change in this file, the assertions will need to be updated. +describe('stack_utils getInvocationDetails', () => { + context('basic test invocation', () => { + it('correctly extracts invocation details for Chrome', { browser: 'chrome' }, function () { + // Get invocation details from Cypress object + const details = Cypress.state('test').invocationDetails as InvocationDetails + + expect(details.function).to.equal('Suite.eval') + expect(details.fileUrl).to.equal('http://localhost:3500/__cypress/tests?p=cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts') + expect(details.originalFile).to.equal('webpack://@packages/driver/./cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts') + expect(details.relativeFile).to.equal('cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts') + expect(details.line).to.equal(6) // the line number should be the line number of the invocation of this test + expect(details.column).to.equal(4) + expect(details.absoluteFile).to.satisfy((file: string) => { + return file.endsWith('cypress/packages/driver/cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts') + }) + + expect(details.stack).to.equal(`Error + at Suite.eval (http://localhost:3500/__cypress/tests?p=cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts:9:5) + at Object.create (cypress:///../driver/node_modules/mocha/lib/interfaces/common.js:141:19) + at context.describe.context.context (cypress:///../driver/node_modules/mocha/lib/interfaces/bdd.js:42:27) + at createRunnable (cypress:///../driver/src/cypress/mocha.ts:128:31) + at eval (cypress:///../driver/src/cypress/mocha.ts:189:14)`) + }) + + it('correctly extracts invocation details for Firefox', { browser: 'firefox' }, function () { + const details = Cypress.state('test').invocationDetails as InvocationDetails + + expect(details.absoluteFile).to.satisfy((file: string) => { + return file.endsWith('cypress/packages/driver/cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts') + }) + + expect(details.fileUrl).to.equal('http://localhost:3500/__cypress/tests?p=cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts') + expect(details.originalFile).to.equal('webpack://@packages/driver/./cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts') + expect(details.relativeFile).to.equal('cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts') + expect(details.line).to.equal(28) // the line number should be the line number of the invocation of this test + expect(details.column).to.equal(7) + + // the firefox traces are really long, so just validate the first line + const firstLine = details.stack.split('\n')[0] + + expect(firstLine).to.equal('@http://localhost:3500/__cypress/tests?p=cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts:30:7') + }) + }) + + 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 = Cypress.state('test').invocationDetails as InvocationDetails + + expect(details.absoluteFile).to.satisfy((file: string) => { + return file.endsWith('cypress/packages/driver/cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts') + }) + + expect(details.fileUrl).to.equal('http://localhost:3500/__cypress/tests?p=cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts') + expect(details.function).to.equal('Suite.eval') + expect(details.originalFile).to.equal('webpack://@packages/driver/./cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts') + expect(details.relativeFile).to.equal('cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts') + expect(details.line).to.equal(59) // the line number should be the line number of the invocation of this test + expect(details.column).to.equal(4) + expect(details.stack).to.equal(`Error + at Suite.eval (http://localhost:3500/__cypress/tests?p=cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts:58:5) + at Object.create (cypress:///../driver/node_modules/mocha/lib/interfaces/common.js:141:19) + at context.describe.context.context (cypress:///../driver/node_modules/mocha/lib/interfaces/bdd.js:42:27) + at createRunnable (cypress:///../driver/src/cypress/mocha.ts:128:31)`) + }) + + myIt('correctly extracts invocation details for wrapped it in Firefox', { browser: 'firefox' }, function () { + const details = Cypress.state('test').invocationDetails as InvocationDetails + + expect(details.absoluteFile).to.satisfy((file: string) => { + return file.endsWith('cypress/packages/driver/cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts') + }) + + expect(details.fileUrl).to.equal('http://localhost:3500/__cypress/tests?p=cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts') + expect(details.originalFile).to.equal('webpack://@packages/driver/./cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts') + expect(details.relativeFile).to.equal('cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts') + expect(details.line).to.equal(79) // the line number should be the line number of the invocation of this test + expect(details.column).to.equal(9) + + // the firefox traces are really long, so just validate the first line + const firstLine = details.stack.split('\n')[0] + + expect(firstLine).to.equal('@http://localhost:3500/__cypress/tests?p=cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts:77:9') + }) + }) +}) diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index 88f03542929..e1dc4847065 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -163,7 +163,9 @@ const stackWithUserInvocationStackSpliced = (err, userInvocationStack): StackAnd } } -type InvocationDetails = { +export type InvocationDetails = { + function?: string + fileUrl?: string absoluteFile?: string column?: number line?: number From e9d72cf9c7b2e9273d0c109e581beda00491aa6d Mon Sep 17 00:00:00 2001 From: astone123 Date: Fri, 14 Nov 2025 10:47:40 -0500 Subject: [PATCH 19/22] logic and test updates --- packages/driver/src/cypress/stack_utils.ts | 34 ++++++++++--------- .../test/unit/cypress/stack_utils.spec.ts | 11 +++--- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index e1dc4847065..e2e7140dd5a 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -67,19 +67,19 @@ const stackTrimmedToTestInvocation = (stack: string, specWindow) => { return lines } + const originalLines = lines + let processedLines: string[] + 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 - return _.dropWhile(lines, (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 + processedLines = _.dropWhile(lines, (line) => { return !( - line.trim().startsWith('at eval') || - line.trim().startsWith('at Suite.eval') + line.trim().startsWith('at eval ') || + line.trim().startsWith('at Suite.eval ') ) }) - } - - if (specWindow.Cypress.isBrowser({ family: 'firefox' })) { + } else if (specWindow.Cypress.isBrowser({ family: 'firefox' })) { const isTestInvocationLine = (line: string) => { const splitAtAt = line.split('@') @@ -93,18 +93,20 @@ const stackTrimmedToTestInvocation = (stack: string, specWindow) => { return splitAtAt.length > 1 && splitAtAt[0].trim().length === 0 } - return _.dropWhile(lines, (line) => { + processedLines = _.dropWhile(lines, (line) => { return !isTestInvocationLine(line) }) + } else { + processedLines = lines } - return lines - }) + // if we removed all the lines then something went wrong with parsing. Return the original lines instead + if (processedLines.length === 0) { + return originalLines + } - // if we removed all the lines then something went wrong. return the original stack instead - if (modifiedStack.trim() === 'Error') { - return stack - } + return processedLines + }) return modifiedStack } diff --git a/packages/driver/test/unit/cypress/stack_utils.spec.ts b/packages/driver/test/unit/cypress/stack_utils.spec.ts index 86ccf85393b..af562ef0135 100644 --- a/packages/driver/test/unit/cypress/stack_utils.spec.ts +++ b/packages/driver/test/unit/cypress/stack_utils.spec.ts @@ -67,11 +67,12 @@ describe('stack_utils', () => { } it('returns the correct invocation details for a test stack trace that needs to be trimmed', () => { - const stack = `Error\n at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14)\n -at eval (http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js:14:1)\n -at eval (http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js:18:12)\n -at eval ()\n -at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` + const stack = `Error + at itGrep (http://localhost:3000/__cypress/tests?p=cypress/support/e2e.js:444:14) + at eval (http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js:14:1) + at eval (http://localhost:3000/__cypress/tests?p=cypress/e2e/spec.cy.js:18:12) + at eval () + at eval (cypress:///../driver/src/cypress/script_utils.ts:38:23)` class GrepError { get stack () { From 1dfe50ce3280d9f9edf7e44ad4ee1c3e013c695f Mon Sep 17 00:00:00 2001 From: astone123 Date: Fri, 14 Nov 2025 11:27:10 -0500 Subject: [PATCH 20/22] fix types --- .../cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/driver/cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts b/packages/driver/cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts index d6a4c5b56a4..ec2c794c56d 100644 --- a/packages/driver/cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts +++ b/packages/driver/cypress/e2e/cypress/stack_utils-invocationDetails.cy.ts @@ -1,4 +1,4 @@ -import { InvocationDetails } from '../../../src/cypress/stack_utils' +import type { InvocationDetails } from '../../../src/cypress/stack_utils' // Note: the tests in this spec assert against their own invocation details. So if any of the line numbers change in this file, the assertions will need to be updated. describe('stack_utils getInvocationDetails', () => { From 6fa202f02e5f1efee3bf32f20c6b9a63f17c3137 Mon Sep 17 00:00:00 2001 From: astone123 Date: Fri, 14 Nov 2025 14:07:14 -0500 Subject: [PATCH 21/22] fix test --- packages/driver/src/cypress/stack_utils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index e2e7140dd5a..2fa00c35ac9 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -76,7 +76,9 @@ const stackTrimmedToTestInvocation = (stack: string, specWindow) => { processedLines = _.dropWhile(lines, (line) => { return !( line.trim().startsWith('at eval ') || - line.trim().startsWith('at Suite.eval ') + line.trim().startsWith('at Suite.eval ') || + // component tests have the invocation details at the Suite. line + line.trim().startsWith('at Suite. ') ) }) } else if (specWindow.Cypress.isBrowser({ family: 'firefox' })) { From 50418a845ce93e696602c69251e471179b03fe3b Mon Sep 17 00:00:00 2001 From: astone123 Date: Fri, 14 Nov 2025 15:06:01 -0500 Subject: [PATCH 22/22] only modify stacks for e2e tests --- packages/driver/src/cypress/stack_utils.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index 2fa00c35ac9..aee79ed9cf4 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -76,9 +76,7 @@ const stackTrimmedToTestInvocation = (stack: string, specWindow) => { processedLines = _.dropWhile(lines, (line) => { return !( line.trim().startsWith('at eval ') || - line.trim().startsWith('at Suite.eval ') || - // component tests have the invocation details at the Suite. line - line.trim().startsWith('at Suite. ') + line.trim().startsWith('at Suite.eval ') ) }) } else if (specWindow.Cypress.isBrowser({ family: 'firefox' })) { @@ -201,8 +199,8 @@ const getInvocationDetails = (specWindow, sourceMapProjectRoot: string, type?: ' } } - // 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') { + // if the hook is the test, and it's an e2e test,, we will try to remove the lines that are not the actual invocation of the test + if (type === 'test' && specWindow.Cypress.testingType !== 'component') { stack = stackTrimmedToTestInvocation(stack, specWindow) }