From 79564a10929031913093aee548bf40c338103a39 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Tue, 2 Jan 2024 14:37:17 -0500 Subject: [PATCH] fix: Properly handle formatting and escaping markdown for commands and assertions in Command Log (#28583) * bump markdown-it * Update code to wrap assertions back in strong html * Update the logic to be more broad in not applying markdown formmated for any assertions * Add code covering complex assertions * Add code/cases to handle html elements in assertions * Fix studio tests - these should be 'expected' * fix the regex and clean up code * Add condition for 'not to' since the terminology is switched on match * add changelog entry * fix date format in changelog * remove space * Fix bug with studio displaying generated assertions incorrectly * revert studio changes to track in a different PR * remove old 'type' arg that was passed for formattedMessage before * Update code to not format markdown for commands that accept URLs * changelog update --- cli/CHANGELOG.md | 1 + packages/app/cypress/e2e/studio/studio.cy.ts | 1 - packages/reporter/cypress/e2e/commands.cy.ts | 50 +++++++ .../cypress/e2e/unit/formatted_message.cy.ts | 122 +++++++++++++++--- packages/reporter/package.json | 2 +- packages/reporter/src/commands/command.tsx | 57 ++++++-- packages/reporter/src/errors/test-error.tsx | 2 +- yarn.lock | 57 ++++---- 8 files changed, 234 insertions(+), 58 deletions(-) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index f4f98b9669..7e74d481ed 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -6,6 +6,7 @@ _Released 1/2/2024 (PENDING)_ **Bugfixes:** - Now 'node_modules' will not be ignored if a project path or a provided path to spec files contains it. Fixes [#23616](https://github.com/cypress-io/cypress/issues/23616). +- Updated display of assertions and commands with a URL argument to escape markdown formatting so that values are displayed as is and assertion values display as bold. Fixes [#24960](https://github.com/cypress-io/cypress/issues/24960) and [#28100](https://github.com/cypress-io/cypress/issues/28100). - When generating assertions via Cypress Studio, the preview of the generated assertions now correctly displays the past tense of 'expected' instead of 'expect'. Fixed in [#28593](https://github.com/cypress-io/cypress/pull/28593). **Dependency Updates:** diff --git a/packages/app/cypress/e2e/studio/studio.cy.ts b/packages/app/cypress/e2e/studio/studio.cy.ts index 617494c877..d60bae8cd6 100644 --- a/packages/app/cypress/e2e/studio/studio.cy.ts +++ b/packages/app/cypress/e2e/studio/studio.cy.ts @@ -91,7 +91,6 @@ it('visits a basic html page', () => { }) assertStudioHookCount(2) - cy.getAutIframe().within(() => { cy.get('#increment').rightclick().then(() => { cy.get('.__cypress-studio-assertions-menu').shadow().contains('be visible').realClick() diff --git a/packages/reporter/cypress/e2e/commands.cy.ts b/packages/reporter/cypress/e2e/commands.cy.ts index 50da592c05..3b793d02e5 100644 --- a/packages/reporter/cypress/e2e/commands.cy.ts +++ b/packages/reporter/cypress/e2e/commands.cy.ts @@ -185,6 +185,56 @@ describe('commands', { viewportHeight: 1000 }, () => { cy.percySnapshot() }) + it('displays assertions formatted', () => { + addCommand(runner, { + id: 1291, + name: 'assert', + type: 'child', + message: 'expected **value** to equal **value**', + state: 'passed', + timeout: 4000, + group: 1229, + groupLevel: 2, + wallClockStartedAt: inProgressStartedAt, + }) + + addCommand(runner, { + id: 1292, + name: 'assert', + type: 'child', + message: 'expected **value** to match **value**', + state: 'passed', + timeout: 4000, + group: 1229, + groupLevel: 2, + wallClockStartedAt: inProgressStartedAt, + }) + + addCommand(runner, { + id: 1293, + name: 'assert', + type: 'child', + message: 'expected **_value_** to contain **_value_**', + state: 'passed', + timeout: 4000, + group: 1229, + groupLevel: 2, + wallClockStartedAt: inProgressStartedAt, + }) + + cy.contains('.command-message-text', 'to equal') + .find('strong').should('have.length', 2) + .and('contain', 'value') + .and('not.contain', '*') + + cy.contains('.command-message-text', 'to match') + .find('strong').should('have.length', 2) + .and('contain', 'value') + .and('not.contain', '*') + + cy.percySnapshot() + }) + it('includes the type class', () => { cy.contains('#exists').closest('.command-wrapper') .should('have.class', 'command-type-parent') diff --git a/packages/reporter/cypress/e2e/unit/formatted_message.cy.ts b/packages/reporter/cypress/e2e/unit/formatted_message.cy.ts index 833b1f45fe..ceb276c0c9 100644 --- a/packages/reporter/cypress/e2e/unit/formatted_message.cy.ts +++ b/packages/reporter/cypress/e2e/unit/formatted_message.cy.ts @@ -7,31 +7,115 @@ describe('formattedMessage', () => { expect(result).to.equal('') }) - it('maintains special characters when using "to match"', () => { - const specialMessage = 'expected **__*abcdef*__** to match /__.*abcdef.*__/' - const result = formattedMessage(specialMessage) + describe('when assertion', () => { + it('does not display extraneous "*" for to equal assertions', () => { + const specialMessage = 'expected **abcdef** to equal **abcdef**' + const result = formattedMessage(specialMessage, 'assert') - expect(result).to.equal('expected abcdef to match /__.*abcdef.*__/') + expect(result).to.equal('expected abcdef to equal abcdef') + }) + + it('does not display extraneous "*" for to not equal assertions', () => { + const specialMessage = 'expected **abcdef** to not equal **abcde**' + const result = formattedMessage(specialMessage, 'assert') + + expect(result).to.equal('expected abcdef to not equal abcde') + }) + + it('maintains special characters when using "to match"', () => { + const specialMessage = 'expected **__*abcdef*__** to match **/__.*abcdef.*__/**' + const result = formattedMessage(specialMessage, 'assert') + + expect(result).to.equal('expected __*abcdef*__ to match /__.*abcdef.*__/') + }) + + it('maintains special characters when using "to not match"', () => { + const specialMessage = 'expected **__*abcdef*__** not to match **/__.*abcde.*__/**' + const result = formattedMessage(specialMessage, 'assert') + + expect(result).to.equal('expected __*abcdef*__ not to match /__.*abcde.*__/') + }) + + it('maintains special characters when using "to equal"', () => { + const specialMessage = 'expected *****abcdef***** to equal *****abcdef*****' + const result = formattedMessage(specialMessage, 'assert') + + expect(result).to.equal('expected ***abcdef*** to equal ***abcdef***') + }) + + it('maintains special characters when using "to not equal"', () => { + const specialMessage = 'expected *****abcdef***** to not equal *****abcde*****' + const result = formattedMessage(specialMessage, 'assert') + + expect(result).to.equal('expected ***abcdef*** to not equal ***abcde***') + }) + + it('maintains initial spaces on new lines', () => { + const specialMessage = 'expected **hello\n world `code block`** to equal **hello\n world `code block`**' + const result = formattedMessage(specialMessage, 'assert') + + expect(result).to.equal('expected hello\n world `code block` to equal hello\n world `code block`') + }) + + it('bolds asterisks using "to contain"', () => { + const specialMessage = 'expected **glob*glob** to contain *****' + const result = formattedMessage(specialMessage, 'assert') + + expect(result).to.equal('expected glob*glob to contain *') + }) + + it('bolds asterisks with complex assertions', () => { + const specialMessage = 'expected **span** to have CSS property **background-color** with the value **rgb(0, 0, 0)**, but the value was **rgba(0, 0, 0, 0)**' + const result = formattedMessage(specialMessage) + + expect(result).to.equal('expected span to have CSS property background-color with the value rgb(0, 0, 0), but the value was rgba(0, 0, 0, 0)') + }) + + it('bolds asterisks with simple assertions', () => { + const specialMessage = 'expected **dom** to be visible' + const result = formattedMessage(specialMessage, 'assert') + + expect(result).to.equal('expected dom to be visible') + }) + + it('bolds assertions and displays html correctly', () => { + // expected to be enabled + const specialMessage = 'expected **** to be enabled' + const result = formattedMessage(specialMessage, 'assert') + + expect(result).to.equal('expected <button#increment> to be enabled') + }) }) - it('maintains special characters when using "to contain"', () => { - const specialMessage = 'expected ***abcdef*** to equal ***abcdef***' - const result = formattedMessage(specialMessage) + describe('when command that accepts url', () => { + it('cy.visit does not do markdown formatting', () => { + const specialMessage = 'http://www.test.com/__Path__99~~path~~' + const result = formattedMessage(specialMessage, 'visit') - expect(result).to.equal('expected abcdef to equal ***abcdef***') + expect(result).to.equal('http://www.test.com/__Path__99~~path~~') + }) + + it('cy.request does not do markdown formatting', () => { + const specialMessage = 'http://www.test.com/__Path__99~~path~~' + const result = formattedMessage(specialMessage, 'request') + + expect(result).to.equal('http://www.test.com/__Path__99~~path~~') + }) + + it('cy.origin does not do markdown formatting', () => { + const specialMessage = 'http://www.test.com/__Path__99~~path~~' + const result = formattedMessage(specialMessage, 'origin') + + expect(result).to.equal('http://www.test.com/__Path__99~~path~~') + }) }) - it('does NOT maintain special characters when "to equal" or "to match" are not in assertion', () => { - const specialMessage = 'expected ***abcdef*** to contain ***abcdef***' - const result = formattedMessage(specialMessage) + describe('when not an assertion', () => { + it('displays special characters as markdown when not assertion', () => { + const specialMessage = 'message\n here `code block` with *formatting*' + const result = formattedMessage(specialMessage) - expect(result).to.equal('expected abcdef to contain abcdef') - }) - - it('maintains initial spaces on new lines', () => { - const specialMessage = 'hello\n world `code block`' - const result = formattedMessage(specialMessage) - - expect(result).to.equal('hello
\n world code block') + expect(result).to.equal(`message\nhere code block with formatting`) + }) }) }) diff --git a/packages/reporter/package.json b/packages/reporter/package.json index aab1df7254..e4fa6235b6 100644 --- a/packages/reporter/package.json +++ b/packages/reporter/package.json @@ -26,7 +26,7 @@ "cypress-multi-reporters": "1.4.0", "cypress-real-events": "1.6.0", "lodash": "^4.17.21", - "markdown-it": "11.0.1", + "markdown-it": "^14.0.0", "mobx": "5.15.4", "mobx-react": "6.1.8", "prismjs": "1.21.0", diff --git a/packages/reporter/src/commands/command.tsx b/packages/reporter/src/commands/command.tsx index 116ffd8e33..e5f470914d 100644 --- a/packages/reporter/src/commands/command.tsx +++ b/packages/reporter/src/commands/command.tsx @@ -27,23 +27,54 @@ import RunningIcon from '@packages/frontend-shared/src/assets/icons/status-runni const displayName = (model: CommandModel) => model.displayName || model.name const nameClassName = (name: string) => name.replace(/(\s+)/g, '-') -const mdBreaks = new Markdown({ breaks: true }) const md = new Markdown() +const mdOnlyHTML = new Markdown('zero').enable(['html_inline', 'html_block']) -export const formattedMessage = (message: string, type: string) => { +const asterisksRegex = /^\*\*(.+?)\*\*$/gs +// regex to match everything outside of expected/actual values like: +// 'expected **** to exist in the DOM' +// `expected **glob*glob** to contain *****` +// `expected **** to have CSS property **background-color** with the value **rgb(0, 0, 0)**, but the value was **rgba(0, 0, 0, 0)**` +const assertionRegex = /expected | to[^\*]+| not[^\*]+| with[^\*]+|, but[^\*]+/g + +// used to format the display of command messages and error messages +// we use markdown syntax within our error messages (code ticks, urls, etc) +// and cy.log and Cypress.log supports markdown formatting +export const formattedMessage = (message: string, name?: string) => { if (!message) return '' - const searchText = ['to match', 'to equal'] - const regex = new RegExp(searchText.join('|')) - const split = message.split(regex) - const matchingText = searchText.find((text) => message.includes(text)) - const textToConvert = [split[0].trim(), ...(matchingText ? [matchingText] : [])].join(' ') - const spaceEscapedText = textToConvert.replace(/^ +/gm, (initialSpaces) => ' '.repeat(initialSpaces.length)) // is the HTML entity for a space - // we don't want
in our error messages, but allow it in Cypress.log - const converted = type === 'error' ? md.renderInline(spaceEscapedText) : mdBreaks.renderInline(spaceEscapedText) - const assertion = (split[1] && [`${split[1].trim()}`]) || [] + // the command message is formatted as 'expected to {assertion} ' + const assertionArray = message.match(assertionRegex) - return [converted, ...assertion].join(' ') + const expectedActualArray = () => { + // get the expected and actual values of assertions + const splitTrim = message.split(assertionRegex).filter(Boolean).map((s) => s.trim()) + + // replace outside double asterisks with strong tags + return splitTrim.map((s) => { + // we want to escape HTML chars so that they display + // correctly in the command log:

-> <p> + const HTMLEscapedString = mdOnlyHTML.renderInline(s) + + return HTMLEscapedString.replace(asterisksRegex, `$1`) + }) + } + + if (name === 'assert' && assertionArray) { + // for assertions print the exact text so that characters like _ and * + // are not escaped in the assertion display when comparing values + const result = assertionArray.flatMap((s, index) => [s, expectedActualArray()[index]]) + + return result.join('') + } + + // if the command has url args, don't format those chars like __ and ~~ + if (name === 'visit' || name === 'request' || name === 'origin') { + return message + } + + // format markdown for everything else + return md.renderInline(message) } const invisibleMessage = (model: CommandModel) => { @@ -228,7 +259,7 @@ const Message = observer(({ model }: MessageProps) => ( )} {!!model.displayMessage && } )) diff --git a/packages/reporter/src/errors/test-error.tsx b/packages/reporter/src/errors/test-error.tsx index cfac67a02e..0526e1be31 100644 --- a/packages/reporter/src/errors/test-error.tsx +++ b/packages/reporter/src/errors/test-error.tsx @@ -90,7 +90,7 @@ const TestError = (props: TestErrorProps) => { {groupPlaceholder}

- +
{codeFrame && } diff --git a/yarn.lock b/yarn.lock index 9ff61f226f..74d8cd23e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13490,11 +13490,6 @@ entities@^4.2.0, entities@^4.4.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== -entities@~2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" - integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== - entities@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" @@ -19366,13 +19361,6 @@ lines-and-columns@~2.0.3: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-2.0.3.tgz#b2f0badedb556b747020ab8ea7f0373e22efac1b" integrity sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w== -linkify-it@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8" - integrity sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ== - dependencies: - uc.micro "^1.0.1" - linkify-it@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-4.0.1.tgz#01f1d5e508190d06669982ba31a7d9f56a5751ec" @@ -19380,6 +19368,13 @@ linkify-it@^4.0.1: dependencies: uc.micro "^1.0.1" +linkify-it@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" + integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== + dependencies: + uc.micro "^2.0.0" + lint-staged@11.1.2: version "11.1.2" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-11.1.2.tgz#4dd78782ae43ee6ebf2969cad9af67a46b33cd90" @@ -20166,17 +20161,6 @@ map-visit@^1.0.0: promise "7.0.4" socket-retry-connect "0.0.1" -markdown-it@11.0.1: - version "11.0.1" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-11.0.1.tgz#b54f15ec2a2193efa66dda1eb4173baea08993d6" - integrity sha512-aU1TzmBKcWNNYvH9pjq6u92BML+Hz3h5S/QpfTFwiQF852pLT+9qHsrhM9JYipkOXZxGn+sGH8oyJE9FD9WezQ== - dependencies: - argparse "^1.0.7" - entities "~2.0.0" - linkify-it "^3.0.1" - mdurl "^1.0.1" - uc.micro "^1.0.5" - markdown-it@13.0.1: version "13.0.1" resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.1.tgz#c6ecc431cacf1a5da531423fc6a42807814af430" @@ -20188,6 +20172,18 @@ markdown-it@13.0.1: mdurl "^1.0.1" uc.micro "^1.0.5" +markdown-it@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.0.0.tgz#b4b2ddeb0f925e88d981f84c183b59bac9e3741b" + integrity sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw== + dependencies: + argparse "^2.0.1" + entities "^4.4.0" + linkify-it "^5.0.0" + mdurl "^2.0.0" + punycode.js "^2.3.1" + uc.micro "^2.0.0" + marked-terminal@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-5.1.1.tgz#d2edc2991841d893ee943b44b40b2ee9518b4d9f" @@ -20263,6 +20259,11 @@ mdurl@^1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= +mdurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" + integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -24337,6 +24338,11 @@ pumpify@1.5.1, pumpify@^1.3.3, pumpify@^1.3.5: inherits "^2.0.3" pump "^2.0.0" +punycode.js@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" + integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== + punycode@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" @@ -28951,6 +28957,11 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== +uc.micro@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.0.0.tgz#84b3c335c12b1497fd9e80fcd3bfa7634c363ff1" + integrity sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig== + uglify-js@^2.6: version "2.8.29" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"