From 2245d8719969d7d3a77725fc7b619c9f280e0c30 Mon Sep 17 00:00:00 2001 From: estrada9166 Date: Wed, 24 Sep 2025 17:09:06 -0500 Subject: [PATCH 1/5] chore: display custom link title --- packages/driver/src/cy/commands/files.ts | 3 ++- .../src/cy/commands/querying/querying.ts | 6 +++-- packages/driver/src/cypress/error_utils.ts | 25 ++++++++++++++++++- packages/reporter/src/errors/err-model.ts | 4 +++ packages/reporter/src/errors/test-error.tsx | 7 +++--- packages/server/lib/socket-base.ts | 4 ++- 6 files changed, 41 insertions(+), 8 deletions(-) diff --git a/packages/driver/src/cy/commands/files.ts b/packages/driver/src/cy/commands/files.ts index 874c8b546b0..1cde5d22b22 100644 --- a/packages/driver/src/cy/commands/files.ts +++ b/packages/driver/src/cy/commands/files.ts @@ -122,12 +122,13 @@ export default (Commands, Cypress, cy, state) => { if (err.type === 'existence') { // file exists but it shouldn't - or - file doesn't exist but it should const errPath = fileResult.contents ? 'files.existent' : 'files.nonexistent' - const { message, docsUrl } = $errUtils.cypressErrByPath(errPath, { + const { message, docsUrl, docsUrlTitle } = $errUtils.cypressErrByPath(errPath, { args: { cmd: 'readFile', file, filePath: fileResult.filePath }, }) err.message = message err.docsUrl = docsUrl + err.docsUrlTitle = docsUrlTitle } createFilePromise() diff --git a/packages/driver/src/cy/commands/querying/querying.ts b/packages/driver/src/cy/commands/querying/querying.ts index c43f60a12af..d1a118305f2 100644 --- a/packages/driver/src/cy/commands/querying/querying.ts +++ b/packages/driver/src/cy/commands/querying/querying.ts @@ -306,10 +306,11 @@ export default (Commands, Cypress, cy, state) => { switch (err.type) { case 'length': if (err.expected > 1) { - const { message, docsUrl } = $errUtils.cypressErrByPath('contains.length_option') + const { message, docsUrl, docsUrlTitle } = $errUtils.cypressErrByPath('contains.length_option') err.message = message err.docsUrl = docsUrl + err.docsUrlTitle = docsUrlTitle err.retry = false } @@ -381,10 +382,11 @@ export default (Commands, Cypress, cy, state) => { this.set('onFail', (err) => { switch (err.type) { case 'existence': { - const { message, docsUrl } = $errUtils.cypressErrByPath('shadow.no_shadow_root') + const { message, docsUrl, docsUrlTitle } = $errUtils.cypressErrByPath('shadow.no_shadow_root') err.message = message err.docsUrl = docsUrl + err.docsUrlTitle = docsUrlTitle break } default: diff --git a/packages/driver/src/cypress/error_utils.ts b/packages/driver/src/cypress/error_utils.ts index f7195914252..8d6074d1866 100644 --- a/packages/driver/src/cypress/error_utils.ts +++ b/packages/driver/src/cypress/error_utils.ts @@ -17,7 +17,7 @@ import $stackUtils, { StackAndCodeFrameIndex } from './stack_utils' import $utils from './utils' import type { HandlerType } from './runner' -const ERROR_PROPS = ['message', 'type', 'name', 'stack', 'parsedStack', 'fileName', 'lineNumber', 'columnNumber', 'host', 'uncaught', 'actual', 'expected', 'showDiff', 'isPending', 'isRecovered', 'docsUrl', 'codeFrame'] as const +const ERROR_PROPS = ['message', 'type', 'name', 'stack', 'parsedStack', 'fileName', 'lineNumber', 'columnNumber', 'host', 'uncaught', 'actual', 'expected', 'showDiff', 'isPending', 'isRecovered', 'docsUrl', 'codeFrame', 'docsUrlTitle'] as const const ERR_PREPARED_FOR_SERIALIZATION = Symbol('ERR_PREPARED_FOR_SERIALIZATION') const crossOriginScriptRe = /^script error/i @@ -329,6 +329,7 @@ export class InternalCypressError extends Error { export class CypressError extends Error { docsUrl?: string + docsUrlTitle?: string | null retry?: boolean userInvocationStack?: any onFail?: Function @@ -407,6 +408,23 @@ const docsUrlByParents = (msgPath) => { return docsUrlByParents(msgPath) } +// recursively try for a default docsUrlTitle +const docsUrlTitleByParents = (msgPath) => { + msgPath = msgPath.split('.').slice(0, -1).join('.') + + if (!msgPath) { + return // reached root + } + + const obj = _.get(allErrorMessages, msgPath) + + if (obj.hasOwnProperty('docsUrlTitle')) { + return obj.docsUrlTitle + } + + return docsUrlTitleByParents(msgPath) +} + const errByPath = (msgPath, args?) => { let msgValue = _.get(allErrorMessages, msgPath) @@ -427,10 +445,12 @@ const errByPath = (msgPath, args?) => { } const docsUrl = (msgObj.hasOwnProperty('docsUrl') && msgObj.docsUrl) || docsUrlByParents(msgPath) + const docsUrlTitle = (msgObj.hasOwnProperty('docsUrlTitle') && msgObj.docsUrlTitle) || docsUrlTitleByParents(msgPath) return cypressErr({ message: replaceErrMsgTokens(msgObj.message, args), docsUrl: docsUrl ? replaceErrMsgTokens(docsUrl, args) : undefined, + docsUrlTitle: docsUrlTitle ? replaceErrMsgTokens(docsUrlTitle, args) : undefined, }) } @@ -544,6 +564,7 @@ export interface ErrorFromErrorEvent { const errorFromErrorEvent = (event): ErrorFromErrorEvent => { let { message, filename, lineno, colno, error } = event let docsUrl = error?.docsUrl + let docsUrlTitle = error?.docsUrlTitle // reset the message on a cross origin script error // since no details are accessible @@ -552,6 +573,7 @@ const errorFromErrorEvent = (event): ErrorFromErrorEvent => { message = crossOriginErr.message docsUrl = crossOriginErr.docsUrl + docsUrlTitle = crossOriginErr.docsUrlTitle } // it's possible the error was thrown as a string (throw 'some error') @@ -561,6 +583,7 @@ const errorFromErrorEvent = (event): ErrorFromErrorEvent => { })) as CypressError err.docsUrl = docsUrl + err.docsUrlTitle = docsUrlTitle // makeErrFromObj clones the error, so the original doesn't get mutated return { diff --git a/packages/reporter/src/errors/err-model.ts b/packages/reporter/src/errors/err-model.ts index 64539a9991c..ae65ab28754 100644 --- a/packages/reporter/src/errors/err-model.ts +++ b/packages/reporter/src/errors/err-model.ts @@ -31,6 +31,7 @@ export interface ErrProps { docsUrl: string | string[] templateType: string codeFrame: CodeFrame + docsUrlTitle: string | null } export default class Err { @@ -43,6 +44,7 @@ export default class Err { // @ts-ignore codeFrame: CodeFrame isRecovered: boolean = false + docsUrlTitle: string | null = null constructor (props?: Partial) { makeObservable(this, { @@ -56,6 +58,7 @@ export default class Err { isRecovered: observable, displayMessage: computed, isCommandErr: computed, + docsUrlTitle: observable, }) this.update(props) @@ -79,6 +82,7 @@ export default class Err { if (props.parsedStack) this.parsedStack = props.parsedStack if (props.templateType) this.templateType = props.templateType if (props.codeFrame) this.codeFrame = props.codeFrame + if (props.docsUrlTitle) this.docsUrlTitle = props.docsUrlTitle this.isRecovered = !!props.isRecovered } } diff --git a/packages/reporter/src/errors/test-error.tsx b/packages/reporter/src/errors/test-error.tsx index 57acf85d2df..a34e9d024ae 100644 --- a/packages/reporter/src/errors/test-error.tsx +++ b/packages/reporter/src/errors/test-error.tsx @@ -20,9 +20,10 @@ import { IconChevronRightMedium } from '@cypress-design/react-icon' interface DocsUrlProps { url: string | string[] + title: string | null } -const DocsUrl = ({ url }: DocsUrlProps) => { +const DocsUrl = ({ url, title }: DocsUrlProps) => { if (!url) return null const openUrl = (url: string) => (e: React.MouseEvent) => { @@ -35,7 +36,7 @@ const DocsUrl = ({ url }: DocsUrlProps) => { return _.map(urlArray, (url) => ( - Learn more + {title || 'Learn more'} )) } @@ -93,7 +94,7 @@ const TestError: React.FC = ({ err, groupLevel = 0, testId, comm
- +
{codeFrame && } {err.stack && diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 30843db92b6..5a194bed0a0 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -668,7 +668,9 @@ export class SocketBase { // if one does not already exist for the user's default browser. const start = (process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open') - return require('child_process').exec(`${start} ${url}`) + // url needs to be quoted because if there're multiples query params then the url will be split + // and the query params will be lost + return require('child_process').exec(`${start} "${url}"`) }) socket.on('get:user:editor', (cb) => { From a25eb14fbbe2ca1c7712b8691cf78b3e5b13f642 Mon Sep 17 00:00:00 2001 From: estrada9166 Date: Thu, 25 Sep 2025 14:42:12 -0500 Subject: [PATCH 2/5] Add test --- packages/reporter/cypress/e2e/test_errors.cy.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/reporter/cypress/e2e/test_errors.cy.ts b/packages/reporter/cypress/e2e/test_errors.cy.ts index 728569b7b41..512f50a0b61 100644 --- a/packages/reporter/cypress/e2e/test_errors.cy.ts +++ b/packages/reporter/cypress/e2e/test_errors.cy.ts @@ -309,4 +309,21 @@ describe('test errors', () => { .should('have.class', 'language-text') }) }) + + describe('docs url', () => { + it('renders docs url with default title', () => { + setError(commandErr) + + cy.get('.runnable-err-docs-url').should('have.attr', 'href', commandErr.docsUrl) + cy.get('.runnable-err-docs-url').should('have.text', 'Learn more') + }) + + it('renders docs url with custom title', () => { + commandErr.docsUrlTitle = 'Custom title' + setError(commandErr) + + cy.get('.runnable-err-docs-url').should('have.attr', 'href', commandErr.docsUrl) + cy.get('.runnable-err-docs-url').should('have.text', 'Custom title') + }) + }) }) From a125f14bf749147f3e1b5e7d2a586f91a51d99e6 Mon Sep 17 00:00:00 2001 From: estrada9166 Date: Fri, 26 Sep 2025 14:52:56 -0500 Subject: [PATCH 3/5] Pass docs as second argument of the error --- packages/driver/src/cypress/error_utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/driver/src/cypress/error_utils.ts b/packages/driver/src/cypress/error_utils.ts index 8d6074d1866..68fed8752e2 100644 --- a/packages/driver/src/cypress/error_utils.ts +++ b/packages/driver/src/cypress/error_utils.ts @@ -288,7 +288,7 @@ const throwErr = (err, options: any = {}): never => { } const throwErrByPath = (errPath, options: any = {}): never => { - const err = errByPath(errPath, options.args) + const err = errByPath(errPath, options.args, options.docs) if (options.stack) { err.stack = $stackUtils.replacedStack(err, options.stack) @@ -425,7 +425,7 @@ const docsUrlTitleByParents = (msgPath) => { return docsUrlTitleByParents(msgPath) } -const errByPath = (msgPath, args?) => { +const errByPath = (msgPath, args?, docs?) => { let msgValue = _.get(allErrorMessages, msgPath) if (!msgValue) { @@ -435,7 +435,7 @@ const errByPath = (msgPath, args?) => { let msgObj = msgValue if (_.isFunction(msgValue)) { - msgObj = msgValue(args) + msgObj = msgValue(args, docs) } if (_.isString(msgObj)) { From f4e6ee51cdf520f0c7fc061c99a13ec1b6166f31 Mon Sep 17 00:00:00 2001 From: estrada9166 Date: Fri, 26 Sep 2025 15:52:42 -0500 Subject: [PATCH 4/5] Revert changes --- packages/driver/src/cypress/error_utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/driver/src/cypress/error_utils.ts b/packages/driver/src/cypress/error_utils.ts index 68fed8752e2..8d6074d1866 100644 --- a/packages/driver/src/cypress/error_utils.ts +++ b/packages/driver/src/cypress/error_utils.ts @@ -288,7 +288,7 @@ const throwErr = (err, options: any = {}): never => { } const throwErrByPath = (errPath, options: any = {}): never => { - const err = errByPath(errPath, options.args, options.docs) + const err = errByPath(errPath, options.args) if (options.stack) { err.stack = $stackUtils.replacedStack(err, options.stack) @@ -425,7 +425,7 @@ const docsUrlTitleByParents = (msgPath) => { return docsUrlTitleByParents(msgPath) } -const errByPath = (msgPath, args?, docs?) => { +const errByPath = (msgPath, args?) => { let msgValue = _.get(allErrorMessages, msgPath) if (!msgValue) { @@ -435,7 +435,7 @@ const errByPath = (msgPath, args?, docs?) => { let msgObj = msgValue if (_.isFunction(msgValue)) { - msgObj = msgValue(args, docs) + msgObj = msgValue(args) } if (_.isString(msgObj)) { From df828e3878f769e23c6537e2b8dcf09e57dfa188 Mon Sep 17 00:00:00 2001 From: estrada9166 Date: Mon, 29 Sep 2025 13:12:23 -0500 Subject: [PATCH 5/5] Use openExternal for links --- packages/server/lib/socket-base.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 5a194bed0a0..90c08bc48bd 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -20,6 +20,7 @@ import runEvents from './plugins/run_events' import type { OTLPTraceExporterCloud } from '@packages/telemetry' import { telemetry } from '@packages/telemetry' import type { Automation } from './automation' +import { openExternal } from './gui/links' import type { Socket } from '@packages/socket' @@ -658,19 +659,8 @@ export class SocketBase { socket.on('external:open', (url: string) => { debug('received external:open %o', { url }) - // using this instead of require('electron').shell.openExternal - // because CT runner does not spawn an electron shell - // if we eventually decide to exclusively launch CT from - // the desktop-gui electron shell, we should update this to use - // electron.shell.openExternal. - - // cross platform way to open a new tab in default browser, or a new browser window - // if one does not already exist for the user's default browser. - const start = (process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open') - - // url needs to be quoted because if there're multiples query params then the url will be split - // and the query params will be lost - return require('child_process').exec(`${start} "${url}"`) + + return openExternal(url) }) socket.on('get:user:editor', (cb) => {