diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index 6e8c24fb126a..ebe2d205418e 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -33,7 +33,7 @@ mainBuildFilters: &mainBuildFilters - /^release\/\d+\.\d+\.\d+$/ # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - 'update-v8-snapshot-cache-on-develop' - - 'chore/browser_spike' + - 'cacie/30927/retry-error-codeframe' # usually we don't build Mac app - it takes a long time # but sometimes we want to really confirm we are doing the right thing @@ -44,6 +44,7 @@ macWorkflowFilters: &darwin-workflow-filters - equal: [ develop, << pipeline.git.branch >> ] # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] + - equal: [ 'cacie/30927/retry-error-codeframe', << pipeline.git.branch >> ] - equal: [ 'chore/browser_spike', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ @@ -154,7 +155,7 @@ commands: name: Set environment variable to determine whether or not to persist artifacts command: | echo "Setting SHOULD_PERSIST_ARTIFACTS variable" - echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "chore/browser_spike" ]]; then + echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "cacie/30927/retry-error-codeframe" ]]; then export SHOULD_PERSIST_ARTIFACTS=true fi' >> "$BASH_ENV" # You must run `setup_should_persist_artifacts` command and be using bash before running this command diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index e02e636db7c8..da7cc83cc9c8 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -5,6 +5,7 @@ _Released 2/11/2025 (PENDING)_ **Bugfixes:** +- Fixed a regression introduced in [`14.0.0`](https://docs.cypress.io/guides/references/changelog#14-0-0) where error codeframes in the runner UI were not populated with the correct data in failed retry attempts. Fixes [#30927](https://github.com/cypress-io/cypress/issues/30927). - All commands performed in `after` and `afterEach` hooks will now correctly retry when a test fails. Commands that are actions like `.click()` and `.type()` will now perform the action in this situation also. Fixes [#2831](https://github.com/cypress-io/cypress/issues/2831). - Fixed an issue in Cypress [`14.0.0`](https://docs.cypress.io/guides/references/changelog#14-0-0) where privileged commands did not run correctly when a spec file or support file contained characters that required encoding. Fixes [#30933](https://github.com/cypress-io/cypress/issues/30933). diff --git a/packages/driver/src/cypress/error_utils.ts b/packages/driver/src/cypress/error_utils.ts index 4512f0565772..dd455d9281be 100644 --- a/packages/driver/src/cypress/error_utils.ts +++ b/packages/driver/src/cypress/error_utils.ts @@ -1,5 +1,13 @@ // See: ./errorScenarios.md for details about error messages and stack traces +// NOTE: If you modify the logic relating to this file, ensure the +// UI for error code frames works as expected with the binary. This includes each +// browser, as well as e2e and CT testing types. Stack patterns differ in Chrome +// between the binary and dev mode, so Cy in Cy tests cannot catch them proactively. + +// Various stack patterns are saved as scenario fixtures in ./driver/test +// to prevent regressions. + import chai from 'chai' import _ from 'lodash' import $dom from '../dom' @@ -136,7 +144,7 @@ const getUserInvocationStack = (err, state) => { // command errors and command assertion errors (default assertion or cy.should) // have the invocation stack attached to the current command // prefer err.userInvocation stack if it's been set - let userInvocationStack = err.userInvocationStack || state('currentAssertionUserInvocationStack') + let userInvocationStack: string | undefined = err.userInvocationStack || state('currentAssertionUserInvocationStack') // if there is no user invocation stack from an assertion or it is the default // assertion, meaning it came from a command (e.g. cy.get), prefer the @@ -153,14 +161,29 @@ const getUserInvocationStack = (err, state) => { userInvocationStack = withInvocationStack?.get('userInvocationStack') } - if (!userInvocationStack) return + if (!userInvocationStack) return undefined - // In CT with vite, the user invocation stack includes internal cypress code, so clean it up + // In some environments, additional codepoints are included in the stack prior + // to the first userland codepoint. + const internalCodepointIdentifier = [ + '/__cypress/runner', // binary environments and most dev environments + 'cypress:///../driver', // webpack CT with a dev build + ].find((identifier) => { + return userInvocationStack?.includes(identifier) + }) - // remove lines that are included _prior_ to the first userland line - userInvocationStack = $stackUtils.stackWithLinesDroppedFromMarker(userInvocationStack, '/__cypress', true) + // removes lines in the invocation stack above the first userland line. If one + // of the cypress codepoint identifiers is not present in the stack trace, + // the first line will be a userland codepoint, so no dropping is necessary. + userInvocationStack = internalCodepointIdentifier ? _.dropWhile( + userInvocationStack.split('\n'), + (stackLine) => { + return stackLine.includes(internalCodepointIdentifier) + }, + ).join('\n') : userInvocationStack - // remove lines that are included _after and including_ the replacement marker + // remove lines that are included _after and including_ the replacement marker - + // these are also internal to cypress, and unimportant for the user invocation stack userInvocationStack = $stackUtils.stackPriorToReplacementMarker(userInvocationStack) if ( @@ -170,6 +193,8 @@ const getUserInvocationStack = (err, state) => { ) { return userInvocationStack } + + return undefined } const modifyErrMsg = (err, newErrMsg, cb) => { diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index 2dc557d370dc..3da82879709f 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -1,4 +1,13 @@ // See: ./errorScenarios.md for details about error messages and stack traces + +// NOTE: If you modify the logic relating to this file, ensure the +// UI for error code frames works as expected with the binary. This includes each +// browser, as well as e2e and CT testing types. Stack patterns differ in Chrome +// between the binary and dev mode, so Cy in Cy tests cannot catch them proactively. + +// Various stack patterns are saved as scenario fixtures in ./driver/test +// to prevent regressions. + import _ from 'lodash' import path from 'path' import errorStackParser from 'error-stack-parser' diff --git a/packages/driver/test/unit/cypress/__fixtures__/spec_stackframes.json b/packages/driver/test/unit/cypress/__fixtures__/getInvocationDetails_spec_stackframes.json similarity index 100% rename from packages/driver/test/unit/cypress/__fixtures__/spec_stackframes.json rename to packages/driver/test/unit/cypress/__fixtures__/getInvocationDetails_spec_stackframes.json diff --git a/packages/driver/test/unit/cypress/__fixtures__/getUserInvocationStack_stackFrames.json b/packages/driver/test/unit/cypress/__fixtures__/getUserInvocationStack_stackFrames.json new file mode 100644 index 000000000000..e64a5fbb2343 --- /dev/null +++ b/packages/driver/test/unit/cypress/__fixtures__/getUserInvocationStack_stackFrames.json @@ -0,0 +1,73 @@ +{ + "invocationFile": "cypress/spec.cy.js", + "line": "14", + "column": "16", + "scenarios": [ + { + "browser": "chrome", + "build": "binary", + "testingType": "e2e", + "stack": " at captureUserInvocationStack (http://localhost:54823/__cypress/runner/cypress_runner.js:138199:94)\n at Proxy. (http://localhost:54823/__cypress/runner/cypress_runner.js:138227:9)\n at Proxy.assertEqual (http://localhost:54823/__cypress/runner/cypress_runner.js:79824:12)\n at Proxy.methodWrapper (http://localhost:54823/__cypress/runner/cypress_runner.js:77670:25)\n at Context.eval (http://localhost:54823/__cypress/tests?p=cypress/spec.cy.js:14:16)\n at __stackReplacementMarker (http://localhost:54823/__cypress/runner/cypress_runner.js:136677:13)\n at runnable.fn (http://localhost:54823/__cypress/runner/cypress_runner.js:137464:19)\n at callFn (http://localhost:54823/__cypress/runner/cypress_runner.js:156061:21)\n at Runnable.run (http://localhost:54823/__cypress/runner/cypress_runner.js:156048:7)\n at http://localhost:54823/__cypress/runner/cypress_runner.js:161972:30" + }, + { + "browser": "chrome", + "build": "binary", + "testingType": "ct-webpack", + "stack": " at captureUserInvocationStack (http://localhost:8080/__cypress/runner/cypress_runner.js:138199:94)\n at Proxy. (http://localhost:8080/__cypress/runner/cypress_runner.js:138227:9)\n at Proxy.assertEqual (http://localhost:8080/__cypress/runner/cypress_runner.js:79824:12)\n at Proxy.methodWrapper (http://localhost:8080/__cypress/runner/cypress_runner.js:77670:25)\n at Context. (http://localhost:8080/__cypress/cypress/spec.cy.js:14:16)\n at __stackReplacementMarker (http://localhost:8080/__cypress/runner/cypress_runner.js:136677:13)\n at runnable.fn (http://localhost:8080/__cypress/runner/cypress_runner.js:137464:19)\n at callFn (http://localhost:8080/__cypress/runner/cypress_runner.js:156061:21)\n at Runnable.run (http://localhost:8080/__cypress/runner/cypress_runner.js:156048:7)\n at http://localhost:8080/__cypress/runner/cypress_runner.js:161972:30" + }, + { + "browser": "chrome", + "build": "binary", + "testingType": "ct-vite", + "stack": " at captureUserInvocationStack (http://localhost:3002/__cypress/runner/cypress_runner.js:138174:94)\n at Proxy. (http://localhost:3002/__cypress/runner/cypress_runner.js:138202:9)\n at Proxy.assertEqual (http://localhost:3002/__cypress/runner/cypress_runner.js:79824:12)\n at Proxy.methodWrapper (http://localhost:3002/__cypress/runner/cypress_runner.js:77670:25)\n at Context. (http://localhost:3002/__cypress/src/@fs/project/cypress/spec.cy.js:14:16)\n at __stackReplacementMarker (http://localhost:3002/__cypress/runner/cypress_runner.js:136652:13)\n at runnable.fn (http://localhost:3002/__cypress/runner/cypress_runner.js:137439:19)\n at callFn (http://localhost:3002/__cypress/runner/cypress_runner.js:156036:21)\n at Runnable.run (http://localhost:3002/__cypress/runner/cypress_runner.js:156023:7)\n at http://localhost:3002/__cypress/runner/cypress_runner.js:161947:30" + }, + { + "browser": "chrome", + "build": "dev", + "testingType": "e2e", + "stack": " at Context.eval (http://localhost:53021/__cypress/tests?p=cypress/spec.cy.js:14:16)\n at __stackReplacementMarker (cypress:///../driver/src/cypress/cy.ts:85:13)\n at runnable.fn (cypress:///../driver/src/cypress/cy.ts:872:19)\n at callFn (cypress:///../driver/node_modules/mocha/lib/runnable.js:394:21)\n at Runnable.run (cypress:///../driver/node_modules/mocha/lib/runnable.js:381:7)\n at eval (cypress:///../driver/src/cypress/runner.ts:1537:30)\n at PassThroughHandlerContext.finallyHandler (cypress:///../../node_modules/bluebird/js/release/finally.js:56:23)\n at PassThroughHandlerContext.tryCatcher (cypress:///../../node_modules/bluebird/js/release/util.js:17:23)\n at Promise._settlePromiseFromHandler (cypress:///../../node_modules/bluebird/js/release/promise.js:513:31)\n at Promise._settlePromise (cypress:///../../node_modules/bluebird/js/release/promise.js:570:18)\n at Promise._settlePromise0 (cypress:///../../node_modules/bluebird/js/release/promise.js:615:10)\n at Promise._settlePromises (cypress:///../../node_modules/bluebird/js/release/promise.js:695:18)\n at Promise._fulfill (cypress:///../../node_modules/bluebird/js/release/promise.js:639:18)\n at Promise._settlePromise (cypress:///../../node_modules/bluebird/js/release/promise.js:583:21)\n at Promise._settlePromise0 (cypress:///../../node_modules/bluebird/js/release/promise.js:615:10)\n at Promise._settlePromises (cypress:///../../node_modules/bluebird/js/release/promise.js:695:18)\n at Promise._fulfill (cypress:///../../node_modules/bluebird/js/release/promise.js:639:18)\n at Promise._resolveCallback (cypress:///../../node_modules/bluebird/js/release/promise.js:433:57)\n at Promise._settlePromiseFromHandler (cypress:///../../node_modules/bluebird/js/release/promise.js:525:17)\n at Promise._settlePromise (cypress:///../../node_modules/bluebird/js/release/promise.js:570:18)\n at Promise._settlePromise0 (cypress:///../../node_modules/bluebird/js/release/promise.js:615:10)\n at Promise._settlePromises (cypress:///../../node_modules/bluebird/js/release/promise.js:695:18)" + }, + { + "browser": "chrome", + "build": "dev", + "testingType": "ct-vite", + "stack": " at captureUserInvocationStack (cypress:///../driver/src/cy/chai.ts:381:94)\n at Proxy.eval (cypress:///../driver/src/cy/chai.ts:409:9)\n at Proxy.assertEqual (cypress:///../driver/node_modules/chai/lib/chai/core/assertions.js:1026:12)\n at Proxy.methodWrapper (cypress:///../driver/node_modules/chai/lib/chai/utils/addMethod.js:57:25)\n at Context. (http://localhost:4455/__cypress/src/@fs/project/cypress/spec.cy.js:14:16)\n at __stackReplacementMarker (cypress:///../driver/src/cypress/cy.ts:85:13)\n at runnable.fn (cypress:///../driver/src/cypress/cy.ts:872:19)\n at callFn (cypress:///../driver/node_modules/mocha/lib/runnable.js:394:21)\n at Runnable.run (cypress:///../driver/node_modules/mocha/lib/runnable.js:381:7)\n at eval (cypress:///../driver/src/cypress/runner.ts:1537:30)" + }, + { + "browser": "chrome", + "build": "dev", + "testingType": "ct-webpack", + "stack": " at captureUserInvocationStack (cypress:///../driver/src/cy/chai.ts:381:94)\n at Proxy.eval (cypress:///../driver/src/cy/chai.ts:409:9)\n at Proxy.assertEqual (cypress:///../driver/node_modules/chai/lib/chai/core/assertions.js:1026:12)\n at Proxy.methodWrapper (cypress:///../driver/node_modules/chai/lib/chai/utils/addMethod.js:57:25)\n at Context. (http://localhost:4455/__cypress/cypress/spec.cy.js:14:16)\n at __stackReplacementMarker (cypress:///../driver/src/cypress/cy.ts:85:13)\n at runnable.fn (cypress:///../driver/src/cypress/cy.ts:872:19)\n at callFn (cypress:///../driver/node_modules/mocha/lib/runnable.js:394:21)\n at Runnable.run (cypress:///../driver/node_modules/mocha/lib/runnable.js:381:7)\n at eval (cypress:///../driver/src/cypress/runner.ts:1537:30)" + }, + { + "browser": "firefox", + "build": "binary", + "testingType": "e2e", + "stack": "@http://localhost:54823/__cypress/tests?p=cypress/spec.cy.js:14:16\n__stackReplacementMarker@http://localhost:54823/__cypress/runner/cypress_runner.js:136677:13\n__webpack_modules__ { + return { + default: { + getSourcePosition: vi.fn(), + }, + } +}) + +describe('err_utils', () => { + beforeEach(() => { + // @ts-expect-error + global.Cypress = { + config: vi.fn(), + } + + vi.resetAllMocks() + }) + + describe('getUserInvocationStack', () => { + const { invocationFile, line, column, scenarios } = stackFrameFixture + + let stack: string + + class MockError { + name = 'CypressError' + get userInvocationStack () { + return stack + } + } + + const state = () => undefined + + for (const scenario of scenarios) { + const { browser, build, testingType, stack: scenarioStack } = scenario + + describe(`${browser}:${build}:${testingType}`, () => { + beforeEach(() => { + stack = scenarioStack + }) + + it('returns the userInvocationStack with no leading internal cypress codeframes', () => { + const invocationStack = errUtils.getUserInvocationStack(new MockError(), state) + + expect(invocationStack).not.toBeUndefined() + + const [first, second] = (invocationStack as string).split('\n') + + const invocationFrame = second ?? first + + expect(invocationFrame).toContain(`${invocationFile}:${line}:${column}`) + }) + }) + } + }) +}) diff --git a/packages/driver/test/unit/cypress/stack_utils.spec.ts b/packages/driver/test/unit/cypress/stack_utils.spec.ts index 5aadabde1206..f9d9970657ec 100644 --- a/packages/driver/test/unit/cypress/stack_utils.spec.ts +++ b/packages/driver/test/unit/cypress/stack_utils.spec.ts @@ -5,7 +5,7 @@ import { vi, describe, it, expect, beforeEach } from 'vitest' import source_map_utils from '../../../src/cypress/source_map_utils' import stack_utils from '../../../src/cypress/stack_utils' -import stackFrameFixture from './__fixtures__/spec_stackframes.json' +import stackFrameFixture from './__fixtures__/getInvocationDetails_spec_stackframes.json' vi.mock('../../../src/cypress/source_map_utils', () => { return { diff --git a/packages/extension/package.json b/packages/extension/package.json index ab1e41c55643..cd682db0a39a 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -44,5 +44,10 @@ "lib", "theme" ], - "nx": {} + "nx": { + "implicitDependencies": [ + "@packages/server", + "@packages/socket" + ] + } }