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
This commit is contained in:
Jennifer Shehane
2024-01-02 14:37:17 -05:00
committed by GitHub
parent 9f8dc10a08
commit 79564a1092
8 changed files with 234 additions and 58 deletions

View File

@@ -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:**

View File

@@ -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()

View File

@@ -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')

View File

@@ -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 <strong><strong><em>abcdef</em></strong></strong> to match <strong>/__.*abcdef.*__/</strong>')
expect(result).to.equal('expected <strong>abcdef</strong> to equal <strong>abcdef</strong>')
})
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 <strong>abcdef</strong> to not equal <strong>abcde</strong>')
})
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 <strong>__*abcdef*__</strong> to match <strong>/__.*abcdef.*__/</strong>')
})
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 <strong>__*abcdef*__</strong> not to match <strong>/__.*abcde.*__/</strong>')
})
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 <strong>***abcdef***</strong> to equal <strong>***abcdef***</strong>')
})
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 <strong>***abcdef***</strong> to not equal <strong>***abcde***</strong>')
})
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 <strong>hello\n world `code block`</strong> to equal <strong>hello\n world `code block`</strong>')
})
it('bolds asterisks using "to contain"', () => {
const specialMessage = 'expected **glob*glob** to contain *****'
const result = formattedMessage(specialMessage, 'assert')
expect(result).to.equal('expected <strong>glob*glob</strong> to contain <strong>*</strong>')
})
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 <strong>span</strong> to have CSS property <strong>background-color</strong> with the value <strong>rgb(0, 0, 0)</strong>, but the value was <strong>rgba(0, 0, 0, 0)</strong>')
})
it('bolds asterisks with simple assertions', () => {
const specialMessage = 'expected **dom** to be visible'
const result = formattedMessage(specialMessage, 'assert')
expect(result).to.equal('expected <strong>dom</strong> to be visible')
})
it('bolds assertions and displays html correctly', () => {
// expected <button#increment> to be enabled
const specialMessage = 'expected **<button#increment>** to be enabled'
const result = formattedMessage(specialMessage, 'assert')
expect(result).to.equal('expected <strong>&lt;button#increment&gt;</strong> 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 <em><strong>abcdef</strong></em> to equal <strong>***abcdef***</strong>')
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 <em><strong>abcdef</strong></em> to contain <em><strong>abcdef</strong></em>')
})
it('maintains initial spaces on new lines', () => {
const specialMessage = 'hello\n world `code block`'
const result = formattedMessage(specialMessage)
expect(result).to.equal('hello<br>\n world <code>code block</code>')
expect(result).to.equal(`message\nhere <code>code block</code> with <em>formatting</em>`)
})
})
})

View File

@@ -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",

View File

@@ -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 **<span>** to exist in the DOM'
// `expected **glob*glob** to contain *****`
// `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 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) => '&#32;'.repeat(initialSpaces.length)) // &#32 is the HTML entity for a space
// we don't want <br> in our error messages, but allow it in Cypress.log
const converted = type === 'error' ? md.renderInline(spaceEscapedText) : mdBreaks.renderInline(spaceEscapedText)
const assertion = (split[1] && [`<strong>${split[1].trim()}</strong>`]) || []
// the command message is formatted as 'expected <actual> to {assertion} <expected>'
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> -> &lt;p&gt;
const HTMLEscapedString = mdOnlyHTML.renderInline(s)
return HTMLEscapedString.replace(asterisksRegex, `<strong>$1</strong>`)
})
}
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 && <span
className='command-message-text'
dangerouslySetInnerHTML={{ __html: formattedMessage(model.displayMessage) }}
dangerouslySetInnerHTML={{ __html: formattedMessage(model.displayMessage, model.name) }}
/>}
</span>
))

View File

@@ -90,7 +90,7 @@ const TestError = (props: TestErrorProps) => {
{groupPlaceholder}
<div className='runnable-err-content'>
<div className='runnable-err-message'>
<span dangerouslySetInnerHTML={{ __html: formattedMessage(err.message, 'error') }} />
<span dangerouslySetInnerHTML={{ __html: formattedMessage(err.message) }} />
<DocsUrl url={err.docsUrl} />
</div>
{codeFrame && <ErrorCodeFrame codeFrame={codeFrame} />}

View File

@@ -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"