diff --git a/packages/driver/src/cypress/script_utils.js b/packages/driver/src/cypress/script_utils.js index 4daa4f66f8..b78f18b623 100644 --- a/packages/driver/src/cypress/script_utils.js +++ b/packages/driver/src/cypress/script_utils.js @@ -12,7 +12,7 @@ const fetchScript = (scriptWindow, script) => { } const extractSourceMap = ([script, contents]) => { - script.fullyQualifiedUrl = `${window.top.location.origin}${script.relativeUrl}` + script.fullyQualifiedUrl = `${window.top.location.origin}${script.relativeUrl}`.replace(/ /g, '%20') const sourceMap = $sourceMapUtils.extractSourceMap(script, contents) diff --git a/packages/reporter/cypress/fixtures/command_error.json b/packages/reporter/cypress/fixtures/command_error.json index 8c7f4a0fe7..8d3305669d 100644 --- a/packages/reporter/cypress/fixtures/command_error.json +++ b/packages/reporter/cypress/fixtures/command_error.json @@ -1,7 +1,7 @@ { "name": "CommandError", "message": "`foo` \\`bar\\` **baz** *fizz* ** buzz **", - "stack": "Some Error\n at foo.bar (my/app.js:2:7)\n at baz.qux (cypress/integration/foo_spec.js:5:2)\n From previous event:\n at bar.baz (my/app.js:8:11)\n ", + "stack": "Some Error\n at foo.bar (my/app.js:2:7)\n at baz.qux (cypress/integration/foo_spec.js:5:2)\n at space (cypress/integration/a b.js:34:99)\n From previous event:\n at bar.baz (my/app.js:8:11)\n ", "parsedStack": [ { "message": "Some Error", @@ -35,6 +35,15 @@ "column": 2, "whitespace": " " }, + { + "originalFile": "cypress/integration/a%20b.js", + "relativeFile": "cypress/integration/a%20b.js", + "absoluteFile": "/me/dev/cypress/integration/a%20b.js", + "function": "space", + "line": 34, + "column": 99, + "whitespace": " " + }, { "message": "At previous event:", "whitespace": " " diff --git a/packages/reporter/cypress/integration/test_errors_spec.ts b/packages/reporter/cypress/integration/test_errors_spec.ts index 2a1c25efc3..00f6c74c5e 100644 --- a/packages/reporter/cypress/integration/test_errors_spec.ts +++ b/packages/reporter/cypress/integration/test_errors_spec.ts @@ -95,20 +95,23 @@ describe('test errors', () => { cy.get('.runnable-err-stack-trace').within(() => { cy.get('.err-stack-line') - .should('have.length', 9) + .should('have.length', 10) .first().should('have.text', 'at foo.bar (my/app.js:2:7)') cy.get('.err-stack-line') .eq(1).should('have.text', ' at baz.qux (cypress/integration/foo_spec.js:5:2)') cy.get('.err-stack-line') - .eq(2).should('have.text', 'At previous event:') + .eq(2).should('have.text', ' at space (cypress/integration/a b.js:34:99)') cy.get('.err-stack-line') - .eq(3).should('have.text', ' at bar.baz (http://localhost:1234/me/dev/my/app.js:8:11)') + .eq(3).should('have.text', 'At previous event:') cy.get('.err-stack-line') - .eq(4).should('have.text', ' at callFn (cypress://../driver/src/cypress/runner.js:9:12)') + .eq(4).should('have.text', ' at bar.baz (http://localhost:1234/me/dev/my/app.js:8:11)') + + cy.get('.err-stack-line') + .eq(5).should('have.text', ' at callFn (cypress://../driver/src/cypress/runner.js:9:12)') }) }) @@ -124,12 +127,15 @@ describe('test errors', () => { cy.contains('View stack trace').click() cy.get('.runnable-err-stack-trace .runnable-err-file-path') - .should('have.length', 2) + .should('have.length', 3) .first() .should('have.text', 'my/app.js:2:7') cy.get('.runnable-err-stack-trace .runnable-err-file-path').eq(1) .should('have.text', 'cypress/integration/foo_spec.js:5:2') + + cy.get('.runnable-err-stack-trace .runnable-err-file-path').eq(2) + .should('have.text', 'cypress/integration/a b.js:34:99') }) it('does not turn cypress:// files into links', () => { diff --git a/packages/reporter/src/errors/decode-file-paths.ts b/packages/reporter/src/errors/decode-file-paths.ts new file mode 100644 index 0000000000..134c90afee --- /dev/null +++ b/packages/reporter/src/errors/decode-file-paths.ts @@ -0,0 +1,12 @@ +import { FileDetails } from '@packages/ui-components' + +export function decodeFilePaths (fileDetails: FileDetails) { + const { absoluteFile, originalFile, relativeFile } = fileDetails + + return { + ...fileDetails, + absoluteFile: absoluteFile ? absoluteFile.replace(/%20/g, ' ') : absoluteFile, + originalFile: originalFile ? originalFile.replace(/%20/g, ' ') : originalFile, + relativeFile: relativeFile ? relativeFile.replace(/%20/g, ' ') : relativeFile, + } +} diff --git a/packages/reporter/src/errors/error-code-frame.tsx b/packages/reporter/src/errors/error-code-frame.tsx index 1bcbf1c69d..e167fd5f17 100644 --- a/packages/reporter/src/errors/error-code-frame.tsx +++ b/packages/reporter/src/errors/error-code-frame.tsx @@ -4,6 +4,7 @@ import Prism from 'prismjs' import { CodeFrame } from './err-model' import FileNameOpener from '../lib/file-name-opener' +import { decodeFilePaths } from './decode-file-paths' interface Props { codeFrame: CodeFrame @@ -24,7 +25,7 @@ class ErrorCodeFrame extends Component { return (
- +
           {frame}
         
diff --git a/packages/reporter/src/errors/error-stack.tsx b/packages/reporter/src/errors/error-stack.tsx index f273eafd46..ba0a0fec1e 100644 --- a/packages/reporter/src/errors/error-stack.tsx +++ b/packages/reporter/src/errors/error-stack.tsx @@ -3,6 +3,7 @@ import { observer } from 'mobx-react' import React, { ReactElement } from 'react' import FileNameOpener from '../lib/file-name-opener' +import { decodeFilePaths } from './decode-file-paths' import Err, { ParsedStackFileLine, ParsedStackMessageLine } from './err-model' const cypressLineRegex = /(cypress:\/\/|cypress_runner\.js)/ @@ -13,21 +14,26 @@ interface Props { type StringOrElement = string | ReactElement +const isMessageLine = (stackLine: ParsedStackFileLine | ParsedStackMessageLine) => { + return (stackLine as ParsedStackMessageLine).message != null +} + const ErrorStack = observer(({ err }: Props) => { if (!err.parsedStack) return <>err.stack // only display stack lines beyond the original message, since it's already - // displayed above this + // displayed above this in the UI let foundFirstStackLine = false const stackLines = _.filter(err.parsedStack, (line) => { if (foundFirstStackLine) return true - if ((line as ParsedStackMessageLine).message != null) return false + if (isMessageLine(line)) return false foundFirstStackLine = true return true }) + // instead of having every line indented, get rid of the smallest amount of // whitespace common to each line so the stack is aligned left but lines // with extra whitespace still have it @@ -42,38 +48,39 @@ const ErrorStack = observer(({ err }: Props) => { let stopLinking = false const lines = _.map(stackLines, (stackLine, index) => { - const { originalFile, function: fn, line, column, absoluteFile } = stackLine as ParsedStackFileLine - const key = `${originalFile}${index}` - const whitespace = stackLine.whitespace.slice(commonWhitespaceLength) - if ((stackLine as ParsedStackMessageLine).message != null) { + if (isMessageLine(stackLine)) { + const message = (stackLine as ParsedStackMessageLine).message + // we append some errors with 'node internals', which we don't want to link // so stop linking anything after 'From Node.js Internals' - if ((stackLine as ParsedStackMessageLine).message.includes('From Node')) { + if (message.includes('From Node')) { stopLinking = true } - return makeLine(key, [whitespace, (stackLine as ParsedStackMessageLine).message]) + return makeLine(`${message}${index}`, [whitespace, message]) } - if (stopLinking) { - // we have already shown a good link, so no need to make - // clickable links deep in the woods of the stack trace + const { originalFile, function: fn, line, column, absoluteFile } = stackLine as ParsedStackFileLine + const key = `${originalFile}${index}` + + const dontLink = ( + // don't link to Node files, opening them in IDE won't work + stopLinking + // sometimes we can determine the file on disk, but if there are no + // source maps or the file was transpiled in the browser, there + // is no absoluteFile to link to + || !absoluteFile + // don't link to cypress internals, opening them in IDE won't work + || cypressLineRegex.test(originalFile || '') + ) + + if (dontLink) { return makeLine(key, [whitespace, `at ${fn} (${originalFile}:${line}:${column})`]) } - // decide if can show "open file in IDE" link or not - // sometimes we can determine the file on disk, but if - // there are no source maps, or the file was transpiled in the browser - // then all we can do is show the link as is without making it clickable - if (!absoluteFile) { - return makeLine(key, [whitespace, `at ${fn} (${originalFile}:${line}:${column})`]) - } - - if (cypressLineRegex.test(originalFile || '')) { - return makeLine(key, [whitespace, `at ${fn} (${originalFile}:${line}:${column})`]) - } + stackLine = decodeFilePaths(stackLine as ParsedStackFileLine) as ParsedStackFileLine const link = (