diff --git a/cli/__snapshots__/cli_spec.js b/cli/__snapshots__/cli_spec.js index 9092186504ba..5eaf5f414e5c 100644 --- a/cli/__snapshots__/cli_spec.js +++ b/cli/__snapshots__/cli_spec.js @@ -16,24 +16,24 @@ exports['shows help for open --foo 1'] = ` Opens Cypress in the interactive GUI. Options: - -b, --browser path to a custom browser to be added to the + -b, --browser path to a custom browser to be added to the list of available browsers in Cypress - -c, --config sets configuration values. separate multiple - values with a comma. overrides any value in + -c, --config sets configuration values. separate multiple + values with a comma. overrides any value in cypress.json. - -C, --config-file path to JSON file where configuration values - are set. defaults to "cypress.json". pass + -C, --config-file path to JSON file where configuration values + are set. defaults to "cypress.json". pass "false" to disable. -d, --detached [bool] runs Cypress application in detached mode - -e, --env sets environment variables. separate - multiple values with a comma. overrides any + -e, --env sets environment variables. separate + multiple values with a comma. overrides any value in cypress.json or cypress.env.json - --global force Cypress into global mode as if its + --global force Cypress into global mode as if its globally installed - -p, --port runs Cypress on a specific port. overrides + -p, --port runs Cypress on a specific port. overrides any value in cypress.json. -P, --project path to the project - --dev runs cypress in development and bypasses + --dev runs cypress in development and bypasses binary check -h, --help output usage information ------- @@ -198,9 +198,9 @@ exports['cli help command shows help 1'] = ` version prints Cypress version run [options] Runs Cypress tests from the CLI without the GUI open [options] Opens Cypress in the interactive GUI. - install [options] Installs the Cypress executable matching this package's + install [options] Installs the Cypress executable matching this package's version - verify [options] Verifies that Cypress is installed correctly and + verify [options] Verifies that Cypress is installed correctly and executable cache [options] Manages the Cypress binary cache ------- @@ -233,9 +233,9 @@ exports['cli help command shows help for -h 1'] = ` version prints Cypress version run [options] Runs Cypress tests from the CLI without the GUI open [options] Opens Cypress in the interactive GUI. - install [options] Installs the Cypress executable matching this package's + install [options] Installs the Cypress executable matching this package's version - verify [options] Verifies that Cypress is installed correctly and + verify [options] Verifies that Cypress is installed correctly and executable cache [options] Manages the Cypress binary cache ------- @@ -268,9 +268,9 @@ exports['cli help command shows help for --help 1'] = ` version prints Cypress version run [options] Runs Cypress tests from the CLI without the GUI open [options] Opens Cypress in the interactive GUI. - install [options] Installs the Cypress executable matching this package's + install [options] Installs the Cypress executable matching this package's version - verify [options] Verifies that Cypress is installed correctly and + verify [options] Verifies that Cypress is installed correctly and executable cache [options] Manages the Cypress binary cache ------- @@ -304,9 +304,9 @@ exports['cli unknown command shows usage and exits 1'] = ` version prints Cypress version run [options] Runs Cypress tests from the CLI without the GUI open [options] Opens Cypress in the interactive GUI. - install [options] Installs the Cypress executable matching this package's + install [options] Installs the Cypress executable matching this package's version - verify [options] Verifies that Cypress is installed correctly and + verify [options] Verifies that Cypress is installed correctly and executable cache [options] Manages the Cypress binary cache ------- diff --git a/cli/package.json b/cli/package.json index 6c770ac5d9a8..a12c73513060 100644 --- a/cli/package.json +++ b/cli/package.json @@ -30,11 +30,11 @@ "@cypress/xvfb": "1.2.4", "@types/sizzle": "2.3.2", "arch": "2.1.1", - "bluebird": "3.7.1", + "bluebird": "3.7.2", "cachedir": "2.3.0", "chalk": "3.0.0", "check-more-types": "2.24.0", - "commander": "4.0.1", + "commander": "4.1.0", "common-tags": "1.8.0", "debug": "4.1.1", "eventemitter2": "4.1.2", @@ -42,7 +42,7 @@ "executable": "4.1.1", "extract-zip": "1.6.7", "fs-extra": "8.1.0", - "getos": "3.1.1", + "getos": "3.1.4", "is-ci": "2.0.0", "is-installed-globally": "0.3.1", "lazy-ass": "1.6.0", diff --git a/cli/types/index.d.ts b/cli/types/index.d.ts index b8d618c2c5ad..e3a19378fc59 100644 --- a/cli/types/index.d.ts +++ b/cli/types/index.d.ts @@ -594,7 +594,7 @@ declare namespace Cypress { * // tries to find the given text for up to 1 second * cy.contains('my text to find', {timeout: 1000}) */ - contains(content: string | number | RegExp, options?: Partial): Chainable + contains(content: string | number | RegExp, options?: Partial): Chainable /** * Get the child DOM element that contains given text. * @@ -612,7 +612,7 @@ declare namespace Cypress { * // yields
    ...
* cy.contains('ul', 'apples') */ - contains(selector: K, text: string | number | RegExp, options?: Partial): Chainable> + contains(selector: K, text: string | number | RegExp, options?: Partial): Chainable> /** * Get the DOM element using CSS "selector" containing the text or regular expression. * @@ -621,7 +621,7 @@ declare namespace Cypress { * // yields <... class="foo">... apples ... * cy.contains('.foo', 'apples') */ - contains(selector: string, text: string | number | RegExp, options?: Partial): Chainable> + contains(selector: string, text: string | number | RegExp, options?: Partial): Chainable> /** * Double-click a DOM element. @@ -1995,6 +1995,18 @@ declare namespace Cypress { timeout: number } + /** + * Options that check case sensitivity + */ + interface CaseMatchable { + /** + * Check case sensitivity + * + * @default true + */ + matchCase: boolean + } + /** * Options that control how long the Test Runner waits for an XHR request and response to succeed */ diff --git a/packages/driver/src/cy/commands/querying.js b/packages/driver/src/cy/commands/querying.js index c7f405e02782..16c73230cba3 100644 --- a/packages/driver/src/cy/commands/querying.js +++ b/packages/driver/src/cy/commands/querying.js @@ -13,6 +13,8 @@ const restoreContains = () => { return $expr.contains = $contains } +const whitespaces = /\s+/g + module.exports = (Commands, Cypress, cy) => { // restore initially when a run starts restoreContains() @@ -407,7 +409,11 @@ module.exports = (Commands, Cypress, cy) => { filter = '' } - _.defaults(options, { log: true }) + if (options.matchCase === true && _.isRegExp(text) && text.flags.includes('i')) { + $utils.throwErrByPath('contains.regex_conflict') + } + + _.defaults(options, { log: true, matchCase: true }) if (!(_.isString(text) || _.isFinite(text) || _.isRegExp(text))) { $utils.throwErrByPath('contains.invalid_argument') @@ -477,10 +483,40 @@ module.exports = (Commands, Cypress, cy) => { options._log.set({ $el }) } + // When multiple space characters are considered as a single whitespace in all tags except
.
+      const normalizeWhitespaces = (elem) => {
+        let testText = elem.textContent || elem.innerText || $.text(elem)
+
+        if (elem.tagName === 'PRE') {
+          return testText
+        }
+
+        return testText.replace(whitespaces, ' ')
+      }
+
       if (_.isRegExp(text)) {
+        if (options.matchCase === false && !text.flags.includes('i')) {
+          text = new RegExp(text.source, text.flags + 'i') // eslint-disable-line prefer-template
+        }
+
         // taken from jquery's normal contains method
         $expr.contains = (elem) => {
-          return text.test(elem.textContent || elem.innerText || $.text(elem))
+          let testText = normalizeWhitespaces(elem)
+
+          return text.test(testText)
+        }
+      }
+
+      if (_.isString(text)) {
+        $expr.contains = (elem) => {
+          let testText = normalizeWhitespaces(elem)
+
+          if (!options.matchCase) {
+            testText = testText.toLowerCase()
+            text = text.toLowerCase()
+          }
+
+          return testText.includes(text)
         }
       }
 
diff --git a/packages/driver/src/cypress/error_messages.coffee b/packages/driver/src/cypress/error_messages.coffee
index 38febed47396..8b2517f20f66 100644
--- a/packages/driver/src/cypress/error_messages.coffee
+++ b/packages/driver/src/cypress/error_messages.coffee
@@ -148,6 +148,7 @@ module.exports = {
     empty_string: "#{cmd('contains')} cannot be passed an empty string."
     invalid_argument: "#{cmd('contains')} can only accept a string, number or regular expression."
     length_option: "#{cmd('contains')} cannot be passed a length option because it will only ever return 1 element."
+    regex_conflict: "You passed a regular expression with the case-insensitive (i) flag and { matchCase: true } to #{cmd('contains')}. Those options conflict with each other, so please choose one or the other."
 
   cookies:
     backend_error: """
diff --git a/packages/driver/test/cypress/integration/commands/querying_spec.js b/packages/driver/test/cypress/integration/commands/querying_spec.js
index 78cfc78109da..adaf0a927060 100644
--- a/packages/driver/test/cypress/integration/commands/querying_spec.js
+++ b/packages/driver/test/cypress/integration/commands/querying_spec.js
@@ -1817,21 +1817,129 @@ describe('src/cy/commands/querying', () => {
       })
     })
 
-    // NOTE: not sure why this is skipped... last edit was 3 years ago...
-    // @bkucera maybe take a look at this
-    describe.skip('handles whitespace', () => {
+    describe('handles whitespace', () => {
       it('finds el with new lines', () => {
         const btn = $(`\
-\
 `).appendTo(cy.$$('body'))
 
+        cy.get('#whitespace1').contains('White space')
         cy.contains('White space').then(($btn) => {
           expect($btn.get(0)).to.eq(btn.get(0))
         })
       })
+
+      it('finds el with new lines + spaces', () => {
+        const btn = $(`\
+\
+`).appendTo(cy.$$('body'))
+
+        cy.get('#whitespace2').contains('White space')
+        cy.contains('White space').then(($btn) => {
+          expect($btn.get(0)).to.eq(btn.get(0))
+        })
+      })
+
+      it('finds el with multiple spaces', () => {
+        const btn = $(`\
+\
+`).appendTo(cy.$$('body'))
+
+        cy.get('#whitespace3').contains('White space')
+        cy.contains('White space').then(($btn) => {
+          expect($btn.get(0)).to.eq(btn.get(0))
+        })
+      })
+
+      it('finds el with regex', () => {
+        const btn = $(`\
+\
+`).appendTo(cy.$$('body'))
+
+        cy.get('#whitespace4').contains('White space')
+        cy.contains(/White space/).then(($btn) => {
+          expect($btn.get(0)).to.eq(btn.get(0))
+        })
+      })
+
+      it('does not normalize text in pre tag', () => {
+        $(`\
+
+White
+space
+
\ +`).appendTo(cy.$$('body')) + + cy.contains('White space').should('not.match', 'pre') + cy.get('#whitespace5').contains('White\nspace') + }) + + it('finds el with leading/trailing spaces', () => { + const btn = $(``).appendTo(cy.$$('body')) + + cy.get('#whitespace6').contains('White space') + cy.contains('White space').then(($btn) => { + expect($btn.get(0)).to.eq(btn.get(0)) + }) + }) + }) + + describe('case sensitivity', () => { + beforeEach(() => { + $('').appendTo(cy.$$('body')) + }) + + it('is case sensitive when matchCase is undefined', () => { + cy.get('#test-button').contains('Test') + }) + + it('is case sensitive when matchCase is true', () => { + cy.get('#test-button').contains('Test', { + matchCase: true, + }) + }) + + it('is case insensitive when matchCase is false', () => { + cy.get('#test-button').contains('test', { + matchCase: false, + }) + + cy.get('#test-button').contains(/Test/, { + matchCase: false, + }) + }) + + it('does not crash when matchCase: false is used with regex flag, i', () => { + cy.get('#test-button').contains(/Test/i, { + matchCase: false, + }) + }) + + it('throws when content has "i" flag while matchCase: true', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.eq('You passed a regular expression with the case-insensitive (i) flag and { matchCase: true } to cy.contains(). Those options conflict with each other, so please choose one or the other.') + + done() + }) + + cy.get('#test-button').contains(/Test/i, { + matchCase: true, + }) + }) + + it('passes when "i" flag is used with undefined option', () => { + cy.get('#test-button').contains(/Test/i) + }) }) describe('subject contains text nodes', () => {