diff --git a/packages/driver/cypress/integration/commands/assertions_spec.js b/packages/driver/cypress/integration/commands/assertions_spec.js index a17f3ce8c7a9..6318a04a4e1b 100644 --- a/packages/driver/cypress/integration/commands/assertions_spec.js +++ b/packages/driver/cypress/integration/commands/assertions_spec.js @@ -1269,6 +1269,36 @@ describe('src/cy/commands/assertions', () => { }) }) + // TODO: this suite should be merged with the suite above + describe('message formatting', () => { + const expectMarkdown = (test, message, done) => { + cy.then(() => { + test() + }) + + cy.on('log:added', (attrs, log) => { + if (attrs.name === 'assert') { + cy.removeAllListeners('log:added') + + expect(log.get('message')).to.eq(message) + + done() + } + }) + } + + // https://github.com/cypress-io/cypress/issues/19116 + it('text with backslashes', (done) => { + const text = '" expect(text).to.equal(text), + `expected **" { beforeEach(function () { this.$body = cy.$$('body') @@ -1319,6 +1349,15 @@ describe('src/cy/commands/assertions', () => { cy.get('#escape-quotes').should('contain', 'shouldn\'t') }) + + // https://github.com/cypress-io/cypress/issues/19116 + it('escapes backslashes', () => { + const $span = '"<OE_D]dQ\\' + + cy.$$($span).appendTo(cy.$$('body')) + + cy.get('#escape-backslashes').should('contain', '" { diff --git a/packages/driver/cypress/integration/commands/querying/querying_spec.js b/packages/driver/cypress/integration/commands/querying/querying_spec.js index 151198bccd5e..39b7050eb60c 100644 --- a/packages/driver/cypress/integration/commands/querying/querying_spec.js +++ b/packages/driver/cypress/integration/commands/querying/querying_spec.js @@ -1224,6 +1224,12 @@ describe('src/cy/commands/querying', () => { cy.contains(/\'/) }) + // https://github.com/cypress-io/cypress/issues/19116 + it('handles backslashes', () => { + $('
"<OE_D]dQ\\
').appendTo(cy.$$('body')) + cy.get('#backslashes').contains('" { it('returns null when no content exists', () => { cy.contains('alksjdflkasjdflkajsdf').should('not.exist').then(($el) => { diff --git a/packages/driver/src/cy/chai.ts b/packages/driver/src/cy/chai.ts index 57bf42c49a6c..49fdd466e851 100644 --- a/packages/driver/src/cy/chai.ts +++ b/packages/driver/src/cy/chai.ts @@ -8,6 +8,7 @@ import sinonChai from '@cypress/sinon-chai' import $dom from '../dom' import $utils from '../cypress/utils' +import { escapeBackslashes, escapeQuotes } from '../util/escape' import $errUtils from '../cypress/error_utils' import $stackUtils from '../cypress/stack_utils' import $chaiJquery from '../cypress/chai_jquery' @@ -29,6 +30,8 @@ const trailingWhitespaces = /\s*'\*\*/g const whitespace = /\s/g const valueHasLeadingOrTrailingWhitespaces = /\*\*'\s+|\s+'\*\*/g const imageMarkdown = /!\[.*?\]\(.*?\)/g +const doubleslashRe = /\\\\/g +const escapedDoubleslashRe = /__double_slash__/g type CreateFunc = ((specWindow, state, assertFn) => ({ chai: Chai.ChaiStatic @@ -103,6 +106,9 @@ chai.use((chai, u) => { return }) + const escapeDoubleSlash = (str: string) => str.replace(doubleslashRe, '__double_slash__') + const restoreDoubleSlash = (str: string) => str.replace(escapedDoubleslashRe, '\\\\') + // remove any single quotes between our **, // except escaped quotes, empty strings and number strings. const removeOrKeepSingleQuotesBetweenStars = (message) => { @@ -277,7 +283,9 @@ chai.use((chai, u) => { return _super.apply(this, arguments) } - const escText = $utils.escapeQuotes(text) + const escText = escapeQuotes( + escapeBackslashes(text), + ) const selector = `:contains('${escText}'), [type='submit'][value~='${escText}']` @@ -457,7 +465,9 @@ chai.use((chai, u) => { let message = chaiUtils.getMessage(this, customArgs as Chai.AssertionArgs) const actual = chaiUtils.getActual(this, customArgs as Chai.AssertionArgs) + message = escapeDoubleSlash(message) message = removeOrKeepSingleQuotesBetweenStars(message) + message = restoreDoubleSlash(message) message = escapeMarkdown(message) try { diff --git a/packages/driver/src/cypress/utils.ts b/packages/driver/src/cypress/utils.ts index 378307718790..ef4f7d0f576e 100644 --- a/packages/driver/src/cypress/utils.ts +++ b/packages/driver/src/cypress/utils.ts @@ -10,7 +10,6 @@ import { $Location } from './location' const tagOpen = /\[([a-z\s='"-]+)\]/g const tagClosed = /\[\/([a-z]+)\]/g -const quotesRe = /('|")/g const defaultOptions = { delay: 10, @@ -267,12 +266,6 @@ export default { } }, - escapeQuotes (text) { - // convert to str and escape any single - // or double quotes - return (`${text}`).replace(quotesRe, '\\$1') - }, - normalizeNumber (num) { const parsed = Number(num) diff --git a/packages/driver/src/dom/elements/find.ts b/packages/driver/src/dom/elements/find.ts index 6669a9190111..8ac5466aba1b 100644 --- a/packages/driver/src/dom/elements/find.ts +++ b/packages/driver/src/dom/elements/find.ts @@ -4,7 +4,8 @@ import $document from '../document' import $jquery from '../jquery' import { getTagName } from './elementHelpers' import { isWithinShadowRoot, getShadowElementFromPoint } from './shadow' -import { normalizeWhitespaces, escapeQuotes } from './utils' +import { normalizeWhitespaces } from './utils' +import { escapeQuotes, escapeBackslashes } from '../../util/escape' /** * Find Parents relative to an initial element @@ -228,7 +229,9 @@ export const getContainsSelector = (text, filter = '', options: { } = {}) => { const $expr = $.expr[':'] - const escapedText = escapeQuotes(text) + const escapedText = escapeQuotes( + escapeBackslashes(text), + ) // they may have written the filter as // comma separated dom els, so we want to search all diff --git a/packages/driver/src/dom/elements/utils.ts b/packages/driver/src/dom/elements/utils.ts index c319dcceab1a..66238c2b0d33 100644 --- a/packages/driver/src/dom/elements/utils.ts +++ b/packages/driver/src/dom/elements/utils.ts @@ -5,7 +5,6 @@ import $window from '../window' import $document from '../document' const whitespaces = /\s+/g -const quotesRe = /('|")/g // When multiple space characters are considered as a single whitespace in all tags except
.
 export const normalizeWhitespaces = (elem) => {
@@ -29,12 +28,6 @@ export const isSelector = ($el: JQuery, selector) => {
   return $el.is(selector)
 }
 
-export function escapeQuotes (text) {
-  // convert to str and escape any single
-  // or double quotes
-  return (`${text}`).replace(quotesRe, '\\$1')
-}
-
 export function switchCase (value, casesObj, defaultKey = 'default') {
   if (_.has(casesObj, value)) {
     return _.result(casesObj, value)
diff --git a/packages/driver/src/util/escape.ts b/packages/driver/src/util/escape.ts
new file mode 100644
index 000000000000..ae6633f5a0fc
--- /dev/null
+++ b/packages/driver/src/util/escape.ts
@@ -0,0 +1,13 @@
+const quotesRe = /('|")/g
+const backslashRe = /\\/g
+
+export function escapeQuotes (text) {
+  // convert to str and escape any single
+  // or double quotes
+  return (`${text}`).replace(quotesRe, '\\$1')
+}
+
+export function escapeBackslashes (text) {
+  // convert to str and escape any backslashes
+  return (`${text}`).replace(backslashRe, '\\\\')
+}