├── .github └── workflows │ ├── badges.yml │ └── ci.yml ├── .gitignore ├── .prettierrc.json ├── README.md ├── cypress.config.js ├── cypress ├── e2e │ ├── before-spec.js │ ├── burn-spec.js │ ├── config-spec.js │ ├── config-tags-spec.js │ ├── describe-tags-spec.js │ ├── each-spec.js │ ├── effective-tags │ │ ├── spec-a.js │ │ └── spec-b.js │ ├── extra-specs │ │ ├── a.cy.js │ │ └── b.cy.js │ ├── inherits-tag-spec.js │ ├── multiple-registrations.js │ ├── negative-grep-spec.js │ ├── nested-describe-spec.js │ ├── omit-and-skip-spec.js │ ├── skip-spec.js │ ├── spec-parameter │ │ ├── a.cy.js │ │ └── b.cy.js │ ├── spec.js │ ├── specify-spec.js │ ├── tags │ │ ├── test1.spec.js │ │ ├── test2.spec.js │ │ └── test3.spec.js │ ├── test-tags │ │ └── test-has-tags.cy.js │ ├── this-spec.js │ ├── ts-spec.ts │ └── unit.js ├── support │ └── e2e.js └── tsconfig.json ├── expects ├── README.md ├── all-pending.json ├── before.json ├── burn-spec.json ├── config-spec.json ├── config-tags-spec.json ├── describe-tags-invert-spec.json ├── describe-tags-spec-untagged.json ├── describe-tags-spec.json ├── each-spec.json ├── effective-tags │ ├── spec-a-smoke.json │ └── spec-a.json ├── extra-specs.json ├── grep-filter-specs-tag.json ├── grep-filter-specs.json ├── grep-untagged.json ├── hello-burn.json ├── hello-or-works-2.json ├── hello.json ├── inherits-tag-spec.json ├── invert-tag1.json ├── multiple-registrations.json ├── negative-grep-no-projects.json ├── negative-grep-projects.json ├── nested-describe-inheriting-names-spec.json ├── nested-describe-inheriting-tags-spec.json ├── nested-describe-spec.json ├── no-hello-no-works2.json ├── no-hello.json ├── number1.json ├── omit-and-skip.json ├── omit-filtered.json ├── pending.json ├── prefix-tag1.json ├── spec-parameter.json ├── specify.json ├── tag1-and-tag2.json ├── tag1-without-tag2.json ├── tag1.json ├── tag2.json ├── tags-and.json ├── tags-or-filter.json ├── tags-or.json ├── test-npm-module.js ├── this-spec.json ├── ts-spec.json ├── works-2.json ├── works-and-tag1.json └── works-hello-no-2.json ├── images ├── config.png ├── debug.png ├── includes-pending.png └── omit-pending.png ├── jsconfig.json ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── file-utils.js ├── index.d.ts ├── plugin.js ├── support.js ├── tags-are-strings.d.ts └── utils.js ├── test-cy-v9-required-tags ├── cypress.json ├── cypress │ ├── integration │ │ ├── spec1.js │ │ └── spec2.js │ ├── plugins │ │ └── index.js │ └── support │ │ └── index.js ├── expects │ ├── misc.json │ └── second.json ├── package-lock.json └── package.json ├── test-cy-v9 ├── cypress.json ├── cypress │ ├── integration │ │ ├── spec1.js │ │ └── spec2.js │ ├── plugins │ │ └── index.js │ └── support │ │ └── index.js ├── expects │ ├── both.json │ └── misc.json ├── package-lock.json └── package.json └── tests ├── required-tags-only ├── cypress.config.js ├── e2e │ ├── clean.cy.js │ ├── scrape.cy.js │ ├── spec-a.cy.js │ └── spec-b.cy.js ├── expect-scrape.json ├── expect-skip.json └── support │ └── e2e.js ├── required-tags-suite ├── cypress.config.js ├── e2e │ ├── scrape.cy.js │ ├── spec-a.cy.js │ └── spec-b.cy.js ├── expect-scrape.json └── support │ └── e2e.js └── required-tags ├── cypress.config.js ├── e2e └── spec.cy.js ├── expect-all.json ├── expect-with-required-tag.json └── expect.json /.github/workflows/badges.yml: -------------------------------------------------------------------------------- 1 | name: badges 2 | on: 3 | schedule: 4 | # update badges every night 5 | # because we have a few badges that are linked 6 | # to the external repositories 7 | - cron: '0 3 * * *' 8 | 9 | jobs: 10 | badges: 11 | name: Badges 12 | runs-on: ubuntu-24.04 13 | steps: 14 | - name: Checkout 🛎 15 | uses: actions/checkout@v4 16 | 17 | - name: Update version badges 🏷 18 | run: npm run badges 19 | 20 | - name: Commit any changed files 💾 21 | uses: stefanzweifel/git-auto-commit-action@v5 22 | with: 23 | commit_message: Updated badges 24 | branch: main 25 | file_pattern: README.md 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: push 3 | jobs: 4 | test: 5 | runs-on: ubuntu-24.04 6 | steps: 7 | - name: Checkout 🛎 8 | uses: actions/checkout@v4 9 | 10 | - name: Run Cypress tests 🧪 11 | # https://github.com/cypress-io/github-action 12 | uses: cypress-io/github-action@v6 13 | with: 14 | build: npm run stop-only 15 | config: 'specPattern=**/unit.js' 16 | 17 | dependencies: 18 | if: false 19 | runs-on: ubuntu-24.04 20 | steps: 21 | - name: Checkout 🛎 22 | uses: actions/checkout@v4 23 | - name: check dependencies 📦 24 | # only confirm the prod dependencies are good 25 | run: npm run deps 26 | 27 | # TODO: run all e2e tests against the expected results 28 | test-one-tag: 29 | runs-on: ubuntu-24.04 30 | steps: 31 | - name: Checkout 🛎 32 | uses: actions/checkout@v4 33 | 34 | - name: Run single tag 🧪 35 | uses: cypress-io/github-action@v6 36 | with: 37 | command: npx cypress-expect run --env grepTags=@tag1 --expect-exactly expects/tag1.json 38 | 39 | test-prefix-tags: 40 | runs-on: ubuntu-24.04 41 | steps: 42 | - name: Checkout 🛎 43 | uses: actions/checkout@v4 44 | 45 | - name: Run single tag 🧪 46 | uses: cypress-io/github-action@v6 47 | with: 48 | # the plugin should automatically prefix tags with "@" character 49 | command: npx cypress-expect run --env grepTags=tag1,grepPrefixAt=true --expect-exactly expects/prefix-tag1.json 50 | 51 | test-spec-parameter: 52 | runs-on: ubuntu-24.04 53 | steps: 54 | - name: Checkout 🛎 55 | uses: actions/checkout@v4 56 | 57 | - name: Do not eliminate --specs 🧪 58 | uses: cypress-io/github-action@v6 59 | env: 60 | DEBUG: cy-grep 61 | # the same pattern as --spec 62 | CYPRESS_grepSpec: cypress/e2e/spec-parameter/a.cy.js 63 | with: 64 | # https://github.com/bahmutov/cy-grep/issues/33 65 | command: | 66 | npx cypress-expect run \ 67 | --config specPattern=cypress/e2e/spec-parameter/*.cy.js \ 68 | --spec cypress/e2e/spec-parameter/a.cy.js \ 69 | --env grepTags=@b,grepFilterSpecs=true \ 70 | --expect-exactly expects/spec-parameter.json 71 | 72 | test-extra-specs: 73 | runs-on: ubuntu-24.04 74 | steps: 75 | - name: Checkout 🛎 76 | uses: actions/checkout@v4 77 | 78 | - name: Run extra specs without filtering 🧪 79 | uses: cypress-io/github-action@v6 80 | env: 81 | DEBUG: cy-grep 82 | CYPRESS_grepExtraSpecs: cypress/e2e/extra-specs/*.cy.js 83 | with: 84 | # https://github.com/bahmutov/cy-grep/issues/33 85 | command: | 86 | npx cypress-expect run \ 87 | --config specPattern=cypress/e2e/extra-specs/*.cy.js \ 88 | --env grepTags=@a,grepFilterSpecs=true \ 89 | --expect-exactly expects/extra-specs.json 90 | 91 | test-extra-specs-comma-list: 92 | runs-on: ubuntu-24.04 93 | steps: 94 | - name: Checkout 🛎 95 | uses: actions/checkout@v4 96 | 97 | - name: Run extra specs without filtering 🧪 98 | uses: cypress-io/github-action@v6 99 | env: 100 | DEBUG: cy-grep 101 | CYPRESS_grepExtraSpecs: cypress/e2e/extra-specs/a.cy.js,cypress/e2e/extra-specs/b.cy.js 102 | with: 103 | # https://github.com/bahmutov/cy-grep/issues/33 104 | command: | 105 | npx cypress-expect run \ 106 | --config specPattern=cypress/e2e/extra-specs/*.cy.js \ 107 | --env grepTags=@a,grepFilterSpecs=true \ 108 | --expect-exactly expects/extra-specs.json 109 | 110 | test-one-spec-filter: 111 | runs-on: ubuntu-24.04 112 | steps: 113 | - name: Checkout 🛎 114 | uses: actions/checkout@v4 115 | 116 | - name: Run single tag 🧪 117 | uses: cypress-io/github-action@v6 118 | with: 119 | command: npx cypress-expect run \ 120 | --env grepTags=A,grepFilterSpecs=true,grepOmitFiltered=true \ 121 | --config specPattern=cypress/e2e/effective-tags/*.js \ 122 | --expect-exactly expects/effective-tags/spec-a.json 123 | 124 | test-has-access-to-its-test-tags: 125 | runs-on: ubuntu-24.04 126 | steps: 127 | - name: Checkout 🛎 128 | uses: actions/checkout@v4 129 | 130 | - name: Run tests for "projects" 🧪 131 | uses: cypress-io/github-action@v6 132 | with: 133 | config: 'specPattern="cypress/e2e/test-tags/*.cy.js"' 134 | 135 | test-parent-suite-name: 136 | runs-on: ubuntu-24.04 137 | steps: 138 | - name: Checkout 🛎 139 | uses: actions/checkout@v4 140 | 141 | - name: Run tests for "projects" 🧪 142 | uses: cypress-io/github-action@v6 143 | with: 144 | # the "projects" word is in the parent suite title 145 | command: npx cypress-expect run \ 146 | --env grep=projects,grepFilterSpecs=true,grepOmitFiltered=true \ 147 | --config specPattern=cypress/e2e/negative-grep-spec.js \ 148 | --expect-exactly expects/negative-grep-projects.json 149 | 150 | - name: Run tests without "projects" 🧪 151 | uses: cypress-io/github-action@v6 152 | with: 153 | # should skip tests inside the suite "projects" 154 | command: npx cypress-expect run \ 155 | --env grep=-projects,grepFilterSpecs=true,grepOmitFiltered=true \ 156 | --config specPattern=cypress/e2e/negative-grep-spec.js \ 157 | --expect-exactly expects/negative-grep-no-projects.json 158 | 159 | # https://github.com/bahmutov/cy-grep/issues/4 160 | test-effective-tags-AND-filter: 161 | runs-on: ubuntu-24.04 162 | steps: 163 | - name: Checkout 🛎 164 | uses: actions/checkout@v4 165 | 166 | - name: Run single tag 🧪 167 | uses: cypress-io/github-action@v6 168 | with: 169 | command: npx cypress-expect run \ 170 | --env grepTags=A+smoke,grepFilterSpecs=true,grepOmitFiltered=true \ 171 | --config specPattern=cypress/e2e/effective-tags/*.js \ 172 | --expect-exactly expects/effective-tags/spec-a-smoke.json 173 | 174 | test-required-tags: 175 | runs-on: ubuntu-24.04 176 | steps: 177 | - name: Checkout 🛎 178 | uses: actions/checkout@v4 179 | 180 | - name: Install Cypress 🧪 181 | uses: cypress-io/github-action@v6 182 | with: 183 | runTests: false 184 | 185 | - name: running the tests without the required tag 186 | run: | 187 | npx cypress-expect run \ 188 | --project tests/required-tags \ 189 | --expect-exactly ./tests/required-tags/expect.json 190 | 191 | - name: running the tests WITH the required tag 192 | run: | 193 | npx cypress-expect run \ 194 | --project tests/required-tags \ 195 | --env grepTags=@special \ 196 | --expect-exactly ./tests/required-tags/expect-with-required-tag.json 197 | 198 | test-tag-not-found: 199 | runs-on: ubuntu-24.04 200 | steps: 201 | - name: Checkout 🛎 202 | uses: actions/checkout@v4 203 | 204 | - name: Install Cypress 🧪 205 | uses: cypress-io/github-action@v6 206 | with: 207 | runTests: false 208 | 209 | - name: looking for tag that does not exist 210 | # should show a warning and run all specs 211 | run: | 212 | npx cypress-expect run \ 213 | --project tests/required-tags \ 214 | --env grepTags=@wrong-tag,grepFilterSpecs=true \ 215 | --expect-exactly ./tests/required-tags/expect-all.json 216 | 217 | test-required-tags-only: 218 | runs-on: ubuntu-24.04 219 | steps: 220 | - name: Checkout 🛎 221 | uses: actions/checkout@v4 222 | 223 | - name: Install Cypress 🧪 224 | uses: cypress-io/github-action@v6 225 | with: 226 | runTests: false 227 | 228 | - name: skips specs with required tags 229 | run: | 230 | npx cypress-expect run \ 231 | --project tests/required-tags-only \ 232 | --expect-exactly ./tests/required-tags-only/expect-skip.json 233 | 234 | - name: running the tests WITH the scrape required tag 235 | run: | 236 | npx cypress-expect run \ 237 | --project tests/required-tags-only \ 238 | --env grepTags=@scrape \ 239 | --expect-exactly ./tests/required-tags-only/expect-scrape.json 240 | 241 | test-required-tags-parent-suite: 242 | runs-on: ubuntu-24.04 243 | steps: 244 | - name: Checkout 🛎 245 | uses: actions/checkout@v4 246 | 247 | - name: Install Cypress 🧪 248 | uses: cypress-io/github-action@v6 249 | with: 250 | runTests: false 251 | 252 | - name: running the tests WITH the scrape required tag 253 | run: | 254 | npx cypress-expect run \ 255 | --project tests/required-tags-suite \ 256 | --env grepTags=@scrape \ 257 | --expect-exactly ./tests/required-tags-suite/expect-scrape.json 258 | 259 | test-cypress-v9: 260 | runs-on: ubuntu-24.04 261 | steps: 262 | - name: Checkout 🛎 263 | uses: actions/checkout@v4 264 | 265 | - name: Install dependencies 🧪 266 | uses: cypress-io/github-action@v6 267 | with: 268 | runTests: false 269 | 270 | - name: Install Cypress v9 dependencies 🧪 271 | uses: cypress-io/github-action@v6 272 | with: 273 | working-directory: ./test-cy-v9 274 | cache-key: cy-v9-${{ hashFiles('package-lock.json') }} 275 | runTests: false 276 | 277 | - name: Grep tags in Cypress v9 folder 278 | run: npm run test-expects 279 | working-directory: ./test-cy-v9 280 | 281 | - name: Grep tags in Cypress v9 folder with --spec argument 282 | run: npm run test-expects-spec 283 | working-directory: ./test-cy-v9 284 | 285 | - name: Runs without any grep filters 286 | run: npm run test-expects-both 287 | working-directory: ./test-cy-v9 288 | 289 | test-cypress-v9-required-tags: 290 | runs-on: ubuntu-24.04 291 | steps: 292 | - name: Checkout 🛎 293 | uses: actions/checkout@v4 294 | 295 | - name: Install dependencies 🧪 296 | uses: cypress-io/github-action@v6 297 | with: 298 | runTests: false 299 | 300 | - name: Install Cypress v9 dependencies 🧪 301 | uses: cypress-io/github-action@v6 302 | with: 303 | working-directory: ./test-cy-v9-required-tags 304 | cache-key: cy-v9-${{ hashFiles('package-lock.json') }} 305 | runTests: false 306 | 307 | - name: Grep required test tags in Cypress v9 folder 308 | run: npm run test-required-tag-test 309 | working-directory: ./test-cy-v9-required-tags 310 | 311 | - name: Grep required suite tags in Cypress v9 folder 312 | run: npm run test-required-tag-suite 313 | working-directory: ./test-cy-v9-required-tags 314 | 315 | release: 316 | needs: [ 317 | # comment out for now, need to wait for the new version 318 | # of @babel/traverse, see command "npm run deps" 319 | # dependencies, 320 | test, 321 | test-one-tag, 322 | test-one-spec-filter, 323 | test-effective-tags-AND-filter, 324 | test-required-tags, 325 | test-required-tags-only, 326 | test-required-tags-parent-suite, 327 | test-cypress-v9, 328 | test-cypress-v9-required-tags, 329 | test-parent-suite-name, 330 | test-prefix-tags, 331 | test-spec-parameter, 332 | test-has-access-to-its-test-tags, 333 | test-extra-specs, 334 | test-extra-specs-comma-list, 335 | ] 336 | runs-on: ubuntu-24.04 337 | if: github.ref == 'refs/heads/main' 338 | steps: 339 | - name: Checkout 🛎 340 | uses: actions/checkout@v4 341 | 342 | - name: Use Node 20+ 343 | # https://github.com/actions/setup-node 344 | uses: actions/setup-node@v4 345 | with: 346 | node-version: '22' 347 | 348 | - name: Install only the semantic release 📦 349 | run: npm install semantic-release 350 | 351 | - name: Semantic Release 🚀 352 | uses: cycjimmy/semantic-release-action@v4 353 | with: 354 | branch: main 355 | env: 356 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 357 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 358 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/videos/ 3 | cypress/screenshots/ 4 | */cypress/videos/ 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @bahmutov/cy-grep ![cypress version](https://img.shields.io/badge/cypress-14.4.1-brightgreen) 2 | 3 | > Filter tests using substring or tag 4 | 5 | ```shell 6 | # run only tests with "hello" in their names 7 | npx cypress run --env grep=hello 8 | 9 | ✓ hello world 10 | - works 11 | - works 2 @tag1 12 | - works 2 @tag1 @tag2 13 | 14 | 1 passing (38ms) 15 | 3 pending 16 | ``` 17 | 18 | All other tests will be marked pending, see why in the [Cypress test statuses](https://on.cypress.io/writing-and-organizing-tests#Test-statuses) blog post. 19 | 20 | If you have multiple spec files, all specs will be loaded, and every test will be filtered the same way, since the grep is run-time operation and cannot eliminate the spec files without loading them. If you want to run only specific tests, use the built-in [--spec](https://on.cypress.io/command-line#cypress-run-spec-lt-spec-gt) CLI argument. 21 | 22 | ## Training 23 | 24 | Watch the video [intro to cypress-grep plugin](https://www.youtube.com/watch?v=HS-Px-Sghd8) and study my 🎓 Cypress course [Cypress Plugins](https://cypress.tips/courses/cypress-plugins): 25 | 26 | - [Lesson k1: Set up the test grep plugin](https://cypress.tips/courses/cypress-plugins/lessons/k1) 27 | - [Lesson k2: Filter the tests using test and suite tags](https://cypress.tips/courses/cypress-plugins/lessons/k2) 28 | - [Lesson k3: Filter the specs without the tag we want to run](https://cypress.tips/courses/cypress-plugins/lessons/k3) 29 | - [Lesson k4: Filter the tests to run using several tags](https://cypress.tips/courses/cypress-plugins/lessons/k4) 30 | - [Lesson k5: Filter the tests to run using OR of several tags](https://cypress.tips/courses/cypress-plugins/lessons/k5) 31 | - [Lesson k6: Repeat selected tests N times](https://cypress.tips/courses/cypress-plugins/lessons/k6) 32 | - [Lesson k7: Pick the tests to run in the interactive mode](https://cypress.tips/courses/cypress-plugins/lessons/k7) 33 | - [Lesson k8: Automatically prefix tags with the "@" character](https://cypress.tips/courses/cypress-plugins/lessons/k8) 34 | - [Lesson k9: Set baseUrl depending on the test tag](https://cypress.tips/courses/cypress-plugins/lessons/k9) 35 | - [Lesson k10: Limit test tags to specific values](https://cypress.tips/courses/cypress-plugins/lessons/k10) 36 | - [Lesson k11: Use TypeScript enum for test tags](https://cypress.tips/courses/cypress-plugins/lessons/k11) 37 | - [Lesson k12: Validate user-supplied test tags](https://cypress.tips/courses/cypress-plugins/lessons/k12) 38 | 39 | Combine this plugin with [cypress-split](https://github.com/bahmutov/cypress-split) plugin to first pre-filter specs, then run them in parallel: lesson [Lesson c2: Combine split and grep](https://cypress.tips/courses/cypress-split/lessons/c2) from my course [Cypress-split plugin](https://cypress.tips/courses/cypress-split). 40 | 41 | ## Table of Contents 42 | 43 | 44 | 45 | - [@bahmutov/cy-grep ](#bahmutovcy-grep-) 46 | - [Training](#training) 47 | - [Table of Contents](#table-of-contents) 48 | - [Install](#install) 49 | - [Support file](#support-file) 50 | - [Config file](#config-file) 51 | - [Install in Cypress versions before 10](#install-in-cypress-versions-before-10) 52 | - [Usage Overview](#usage-overview) 53 | - [Filter by test title](#filter-by-test-title) 54 | - [OR substring matching](#or-substring-matching) 55 | - [Test suites](#test-suites) 56 | - [Invert filter](#invert-filter) 57 | - [Filter with tags](#filter-with-tags) 58 | - [Tags in the test config object](#tags-in-the-test-config-object) 59 | - [AND tags](#and-tags) 60 | - [OR tags](#or-tags) 61 | - [Inverted tags](#inverted-tags) 62 | - [NOT tags](#not-tags) 63 | - [Tags in test suites](#tags-in-test-suites) 64 | - [Grep untagged tests](#grep-untagged-tests) 65 | - [Access the tags in the test](#access-the-tags-in-the-test) 66 | - [Pre-filter specs (grepFilterSpecs)](#pre-filter-specs-grepfilterspecs) 67 | - [Omit filtered tests (grepOmitFiltered)](#omit-filtered-tests-grepomitfiltered) 68 | - [Disable grep](#disable-grep) 69 | - [Burn (repeat) tests](#burn-repeat-tests) 70 | - [grepExtraSpecs](#grepextraspecs) 71 | - [Required tags](#required-tags) 72 | - [Negative grep](#negative-grep) 73 | - [TypeScript support](#typescript-support) 74 | - [grepPrefixAt](#grepprefixat) 75 | - [grepSpec](#grepspec) 76 | - [General advice](#general-advice) 77 | - [DevTools console](#devtools-console) 78 | - [grepFailed](#grepfailed) 79 | - [Debugging](#debugging) 80 | - [Log messages](#log-messages) 81 | - [Debugging in the plugin](#debugging-in-the-plugin) 82 | - [Debugging in the browser](#debugging-in-the-browser) 83 | - [Examples](#examples) 84 | - [See also](#see-also) 85 | - [cy-grep vs cypress-grep vs @cypress/grep](#cy-grep-vs-cypress-grep-vs-cypressgrep) 86 | - [Major migrations](#major-migrations) 87 | - [v1 to v2](#v1-to-v2) 88 | - [Small Print](#small-print) 89 | 90 | 91 | 92 | ## Install 93 | 94 | Assuming you have Cypress installed, add this module as a dev dependency. 95 | 96 | ```shell 97 | # using NPM 98 | npm i -D @bahmutov/cy-grep 99 | # using Yarn 100 | yarn add -D @bahmutov/cy-grep 101 | ``` 102 | 103 | **Note**: @bahmutov/cy-grep should work with all Cypress versions, but I mostly test it on the newest versions. 104 | 105 | ### Support file 106 | 107 | **required:** load this module from the [support file](https://on.cypress.io/writing-and-organizing-tests#Support-file) or at the top of the spec file if not using the support file. You import the registration function and then call it: 108 | 109 | ```js 110 | // cypress/support/e2e.js 111 | 112 | // load and register the grep feature using "require" function 113 | // https://github.com/bahmutov/cy-grep 114 | const registerCypressGrep = require('@bahmutov/cy-grep') 115 | registerCypressGrep() 116 | 117 | // if you want to use the "import" keyword 118 | // note: `./index.d.ts` currently extends the global Cypress types and 119 | // does not define `registerCypressGrep` so the import path is directly 120 | // pointed to the support file 121 | import registerCypressGrep from '@bahmutov/cy-grep/src/support' 122 | registerCypressGrep() 123 | 124 | // "import" with `@ts-ignore` 125 | // @see error 2306 https://github.com/microsoft/TypeScript/blob/3fcd1b51a1e6b16d007b368229af03455c7d5794/src/compiler/diagnosticMessages.json#L1635 126 | // @ts-ignore 127 | import registerCypressGrep from '@bahmutov/cy-grep' 128 | registerCypressGrep() 129 | ``` 130 | 131 | ### Config file 132 | 133 | **optional:** load and register this module from the [config file](https://docs.cypress.io/guides/references/configuration#setupNodeEvents): 134 | 135 | ```js 136 | // cypress.config.js 137 | { 138 | e2e: { 139 | setupNodeEvents(on, config) { 140 | require('@bahmutov/cy-grep/src/plugin')(config); 141 | // IMPORTANT: return the config object 142 | return config; 143 | }, 144 | } 145 | } 146 | ``` 147 | 148 | Installing the plugin via `setupNodeEvents()` is required to enable the [grepFilterSpecs](#pre-filter-specs-grepfilterspecs) feature. 149 | 150 | **Tip:** you probably want to set these `env` settings in your config file 151 | 152 | ```js 153 | module.exports = defineConfig({ 154 | env: { grepFilterSpecs: true, grepOmitFiltered: true }, 155 | ... 156 | }) 157 | ``` 158 | 159 | Trying to call the plugin function without any arguments or with more than a single argument throws an error 160 | 161 | ```js 162 | // ERROR: forgot the config file 163 | setupNodeEvents(on, config) { 164 | require('@bahmutov/cy-grep/src/plugin')(); 165 | } 166 | // ERROR: too many arguments 167 | setupNodeEvents(on, config) { 168 | require('@bahmutov/cy-grep/src/plugin')(on, config); 169 | } 170 | ``` 171 | 172 | ### Install in Cypress versions before 10 173 | 174 | See [test-cy-v9](./test-cy-v9/) for example 175 | 176 | ```js 177 | // cypress/plugins/index.js 178 | module.exports = (on, config) => { 179 | // `on` is used to hook into various events Cypress emits 180 | // `config` is the resolved Cypress config 181 | require('@bahmutov/cy-grep/src/plugin')(config) 182 | // IMPORTANT: return the config object 183 | return config 184 | } 185 | ``` 186 | 187 | ```js 188 | // cypress/support/index.js 189 | require('@bahmutov/cy-grep')() 190 | ``` 191 | 192 | Put the common settings into `cypress.json` 193 | 194 | ```json 195 | { 196 | "env": { 197 | "grepOmitFiltered": true, 198 | "grepFilterSpecs": true 199 | } 200 | } 201 | ``` 202 | 203 | ## Usage Overview 204 | 205 | You can filter tests to run using part of their title via `grep`, and via explicit tags via `grepTags` Cypress environment variables. 206 | 207 | Most likely you will pass these environment variables from the command line. For example, to only run tests with "login" in their title and tagged "smoke", you would run: 208 | 209 | Here are a few examples: 210 | 211 | ```shell 212 | # run only the tests with "auth user" in the title 213 | $ npx cypress run --env grep="auth user" 214 | # run tests with "hello" or "auth user" in their titles 215 | # by separating them with ";" character 216 | $ npx cypress run --env grep="hello; auth user" 217 | # run tests tagged @fast 218 | $ npx cypress run --env grepTags=@fast 219 | # run only the tests tagged "smoke" 220 | # that have "login" in their titles 221 | $ npx cypress run --env grep=login,grepTags=smoke 222 | # only run the specs that have any tests with "user" in their titles 223 | $ npx cypress run --env grep=user,grepFilterSpecs=true 224 | # only run the specs that have any tests tagged "@smoke" 225 | $ npx cypress run --env grepTags=@smoke,grepFilterSpecs=true 226 | # run only tests that do not have any tags 227 | # and are not inside suites that have any tags 228 | $ npx cypress run --env grepUntagged=true 229 | ``` 230 | 231 | You can use any way to modify the environment values `grep` and `grepTags`, except the run-time `Cypress.env('grep')` (because it is too late at run-time). You can set the `grep` value in the `cypress.json` file to run only tests with the substring `viewport` in their names 232 | 233 | ```json 234 | { 235 | "env": { 236 | "grep": "viewport" 237 | } 238 | } 239 | ``` 240 | 241 | You can also set the `env.grep` object in the plugin file, but remember to return the changed config object: 242 | 243 | ```js 244 | // cypress/plugin/index.js 245 | module.exports = (on, config) => { 246 | config.env.grep = 'viewport' 247 | return config 248 | } 249 | ``` 250 | 251 | You can also set the grep and grepTags from the DevTools console while running Cypress in the interactive mode `cypress open`, see [DevTools Console section](#devtools-console). 252 | 253 | ## Filter by test title 254 | 255 | ```shell 256 | # run all tests with "hello" in their title 257 | $ npx cypress run --env grep=hello 258 | # run all tests with "hello world" in their title 259 | $ npx cypress run --env grep="hello world" 260 | ``` 261 | 262 | ### OR substring matching 263 | 264 | You can pass multiple title substrings to match separating them with `;` character. Each substring is trimmed. 265 | 266 | ```shell 267 | # run all tests with "hello world" or "auth user" in their title 268 | $ npx cypress run --env grep="hello world; auth user" 269 | ``` 270 | 271 | ### Test suites 272 | 273 | The filter is also applied to the "describe" blocks. In that case, the tests look up if any of their outer suites are enabled. 274 | 275 | ```js 276 | describe('block for config', () => { 277 | it('should run', () => {}) 278 | 279 | it('should also work', () => {}) 280 | }) 281 | ``` 282 | 283 | ``` 284 | # run any tests in the blocks including "config" 285 | --env grep=config 286 | ``` 287 | 288 | **Note:** global function `describe` and `context` are aliases and both supported by this plugin. 289 | 290 | ### Invert filter 291 | 292 | ```shell 293 | # run all tests WITHOUT "hello world" in their title 294 | $ npx cypress run --env grep="-hello world" 295 | # run tests with "hello", but without "world" in the titles 296 | $ npx cypress run --env grep="hello; -world" 297 | ``` 298 | 299 | ## Filter with tags 300 | 301 | You can select tests to run or skip using tags by passing `--env grepTags=...` value. 302 | 303 | ``` 304 | # enable the tests with tag "one" or "two" 305 | --env grepTags="one two" 306 | # enable the tests with both tags "one" and "two" 307 | --env grepTags="one+two" 308 | # enable the tests with "hello" in the title and tag "smoke" 309 | --env grep=hello,grepTags=smoke 310 | ``` 311 | 312 | If you can pass commas in the environment variable `grepTags`, you can use `,` to separate the tags 313 | 314 | ``` 315 | # enable the tests with tag "one" or "two" 316 | CYPRESS_grepTags=one,two npx cypress run 317 | ``` 318 | 319 | If a specific tag is not found in the specs, you will get a warning in the terminal: 320 | 321 | ``` 322 | $ npx cypress run --env grepTags=@wrong-tag 323 | cy-grep: could not find the tag "@wrong-tag" in any of the specs 324 | ``` 325 | 326 | ### Tags in the test config object 327 | 328 | Cypress tests can have their own [test config object](https://on.cypress.io/configuration#Test-Configuration), and when using this plugin you can put the test tags there, either as a single tag string or as an array of tags. 329 | 330 | ```js 331 | it('works as an array', { tags: ['config', 'some-other-tag'] }, () => { 332 | expect(true).to.be.true 333 | }) 334 | 335 | it('works as a string', { tags: 'config' }, () => { 336 | expect(true).to.be.true 337 | }) 338 | ``` 339 | 340 | You can run both of these tests using `--env grepTags=config` string. 341 | 342 | ### AND tags 343 | 344 | Use `+` to require both tags to be present 345 | 346 | ``` 347 | --env grepTags=@smoke+@fast 348 | ``` 349 | 350 | ### OR tags 351 | 352 | You can run tests that match one tag or another using spaces. Make sure to quote the grep string! 353 | 354 | ``` 355 | # run tests with tags "@slow" or "@critical" in their names 356 | --env grepTags='@slow @critical' 357 | ``` 358 | 359 | ### Inverted tags 360 | 361 | You can skip running the tests with specific tag using the invert option: prefix the tag with the character `-`. 362 | 363 | ``` 364 | # do not run any tests with tag "@slow" 365 | --env grepTags=-@slow 366 | ``` 367 | 368 | If you want to run all tests with tag `@slow` but without tag `@smoke`: 369 | 370 | ``` 371 | --env grepTags=@slow+-@smoke 372 | ``` 373 | 374 | **Note:** Inverted tag filter is not compatible with the `grepFilterSpecs` option 375 | 376 | ### NOT tags 377 | 378 | You can skip running the tests with specific tag, even if they have a tag that should run, using the not option: prefix the tag with `--`. 379 | 380 | Note this is the same as appending `+-` to each tag. May be useful with large number of tags. 381 | 382 | If you want to run tests with tags `@slow` or `@regression` but without tag `@smoke` 383 | 384 | ``` 385 | --env grepTags='@slow @regression --@smoke' 386 | ``` 387 | 388 | which is equivalent to 389 | 390 | ``` 391 | --env grepTags='@slow+-@smoke @regression+-@smoke' 392 | ``` 393 | 394 | ### Tags in test suites 395 | 396 | The tags are also applied to the "describe" blocks. In that case, the tests look up if any of their outer suites are enabled. 397 | 398 | ```js 399 | describe('block with config tag', { tags: '@smoke' }, () => {}) 400 | ``` 401 | 402 | ``` 403 | # run any tests in the blocks having "@smoke" tag 404 | --env grepTags=@smoke 405 | # skip any blocks with "@smoke" tag 406 | --env grepTags=-@smoke 407 | ``` 408 | 409 | See the [cypress/integration/describe-tags-spec.js](./cypress/integration/describe-tags-spec.js) file. 410 | 411 | **Note:** global function `describe` and `context` are aliases and both supported by this plugin. 412 | 413 | ### Grep untagged tests 414 | 415 | Sometimes you want to run only the tests without any tags, and these tests are inside the describe blocks without any tags. 416 | 417 | ``` 418 | $ npx cypress run --env grepUntagged=true 419 | ``` 420 | 421 | ### Access the tags in the test 422 | 423 | You can check the current test's tags (including its parent suites) by checking the `Cypress.env('testTags')` list 424 | 425 | ```js 426 | describe('parent', { tags: ['@p1', '@p2'] }, () => { 427 | describe('child', { tags: '@c1' }, () => { 428 | it('has all effective test tags', { tags: '@t1' }, () => { 429 | const tags = Cypress.env('testTags') 430 | // includes tags from the parent suites and the test itself 431 | expect(tags, 'tags').to.deep.equal(['@p1', '@p2', '@c1', '@t1']) 432 | }) 433 | }) 434 | }) 435 | ``` 436 | 437 | ## Pre-filter specs (grepFilterSpecs) 438 | 439 | By default, when using `grep` and `grepTags` all specs are executed, and inside each the filters are applied. This can be very wasteful, if only a few specs contain the `grep` in the test titles. Thus when doing the positive `grep`, you can pre-filter specs using the `grepFilterSpecs=true` parameter. 440 | 441 | ``` 442 | # filter all specs first, and only run the ones with 443 | # suite or test titles containing the string "it loads" 444 | $ npx cypress run --env grep="it loads",grepFilterSpecs=true 445 | # filter all specs files, only run the specs with a tag "@smoke" 446 | $ npx cypress run --env grepTags=@smoke,grepFilterSpecs=true 447 | ``` 448 | 449 | **Note 1:** this requires installing this plugin in your project's plugin file, see the [Install](#install). 450 | 451 | **Note 2:** the `grepFilterSpecs` option is only compatible with the positive `grep` and `grepTags` options, not with the negative (inverted) "-..." filter. 452 | 453 | **Note 3:** if there are no files remaining after filtering, the plugin prints a warning and leaves all files unchanged to avoid the test runner erroring with "No specs found". 454 | 455 | **Tip:** you can set this environment variable in the [config file](https://docs.cypress.io/guides/references/configuration) file to enable it by default and skip using the environment variable: 456 | 457 | ```js 458 | // config file 459 | { 460 | "e2e": { 461 | "env": { 462 | "grepFilterSpecs": true 463 | } 464 | } 465 | } 466 | ``` 467 | 468 | ## Omit filtered tests (grepOmitFiltered) 469 | 470 | By default, all filtered tests are made _pending_ using `it.skip` method. If you want to completely omit them, pass the environment variable `grepOmitFiltered=true`. 471 | 472 | Pending filtered tests 473 | 474 | ``` 475 | cypress run --env grep="works 2" 476 | ``` 477 | 478 | ![Pending tests](./images/includes-pending.png) 479 | 480 | Omit filtered tests 481 | 482 | ``` 483 | cypress run --env grep="works 2",grepOmitFiltered=true 484 | ``` 485 | 486 | ![Only running tests remaining](./images/omit-pending.png) 487 | 488 | **Tip:** you can set this environment variable in the config file (usually `cypress.config.js`) file to enable it by default and skip using the environment variable: 489 | 490 | ```json 491 | { 492 | "env": { 493 | "grepOmitFiltered": true 494 | } 495 | } 496 | ``` 497 | 498 | ## Disable grep 499 | 500 | If you specify the `grep` parameters the [config file](https://docs.cypress.io/guides/references/configuration), you can disable it from the command line 501 | 502 | ``` 503 | $ npx cypress run --env grep=,grepTags=,burn= 504 | ``` 505 | 506 | ## Burn (repeat) tests 507 | 508 | You can burn the filtered tests to make sure they are flake-free 509 | 510 | ``` 511 | npx cypress run --env grep="hello world",burn=5 512 | ``` 513 | 514 | You can pass the number of times to run the tests via environment name `burn` or `grepBurn` or `grep-burn`. Note, if a lot of tests match the grep and grep tags, a lot of tests will be burnt! 515 | 516 | If you do not specify the "grep" or "grep tags" option, the "burn" will repeat _every_ test. 517 | 518 | ## grepExtraSpecs 519 | 520 | Sometimes you want to pre-filter specs using tags AND then run extra specs without any filtering. You can set the list of specs / patterns using the `grepExtraSpecs` env string. For example, to filter specs using tag `@a` plus run the spec "b.cy.js": 521 | 522 | ``` 523 | npx cypress run --env grepTags=@a,grepExtraSpecs=cypress/e2e/b.cy.js 524 | ``` 525 | 526 | ## Required tags 527 | 528 | Sometimes you might want to run a test or a suite of tests _only_ if a specific tag or tags are present. For example, you might have a test that cleans the data. This test is meant to run nightly, not on every test run. Thus you can set a `required` tag: 529 | 530 | ```js 531 | it('cleans up the data', { requiredTags: '@nightly' }, () => {...}) 532 | ``` 533 | 534 | When you run the tests now, this test will be skipped, as if it were `it.skip`. It will only run if you use the tag `@nightly`, for example: `npx cypress run --env grepTags=@nightly`. 535 | 536 | If `grepFilterSpecs=true` and a spec has only required tags, and you are running without any tags, the the spec will be skipped completely. 537 | 538 | Read the blog posts 📝 [Required Tags](https://glebbahmutov.com/blog/required-tags/) and [Use Required Test Tags Instead Of Skipping Tests](https://glebbahmutov.com/blog/required-tags-instead-of-skipped-tests/). 539 | 540 | ## Negative grep 541 | 542 | When grepping tests by title, the parent suite title is included. For example if this is the spec 543 | 544 | ```js 545 | describe('users', () => { 546 | it('works 1', () => {}) 547 | it('works 2', () => {}) 548 | it('works 3', () => {}) 549 | }) 550 | 551 | describe('projects', () => { 552 | it('load 1', () => {}) 553 | it('load 2', () => {}) 554 | it('load 3', () => {}) 555 | }) 556 | ``` 557 | 558 | You can run the tests inside the suite "projects" by using `--env grep=projects` and you can skip the tests in the suite `projects` by using `--env grep=-projects`. 559 | 560 | ## TypeScript support 561 | 562 | Because the Cypress test config object type definition does not have the `tags` property we are using above, the TypeScript linter will show an error. Just add an ignore comment above the test: 563 | 564 | ```js 565 | // @ts-ignore 566 | it('runs on deploy', { tags: 'smoke' }, () => { 567 | ... 568 | }) 569 | ``` 570 | 571 | If you want to allow any strings to be test tags, simply include [src/tags-are-strings.d.ts](./src/tags-are-strings.d.ts) included with this package in your project TS config: 572 | 573 | ```json 574 | { 575 | "include": [ 576 | "cypress/**/*", 577 | "node_modules/@bahmutov/cy-grep/src/tags-are-strings.d.ts" 578 | ], 579 | "compilerOptions": { 580 | "types": ["cypress", "@bahmutov/cy-grep"] 581 | } 582 | } 583 | ``` 584 | 585 | If you want to provide your _own_ list of allowed tags, create a `.d.ts` file or extend your `index.d.ts` file 586 | 587 | ```ts 588 | // your project's index.d.ts file 589 | /// 590 | 591 | /** 592 | * The only allowed test tags in this project 593 | */ 594 | type AllowedTag = '@smoke' | '@misc' | '@new-todo' 595 | 596 | declare namespace Cypress { 597 | interface SuiteConfigOverrides { 598 | tags?: AllowedTag | AllowedTag[] 599 | requiredTags?: AllowedTag | AllowedTag[] 600 | } 601 | 602 | interface TestConfigOverrides { 603 | tags?: AllowedTag | AllowedTag[] 604 | requiredTags?: AllowedTag | AllowedTag[] 605 | } 606 | } 607 | ``` 608 | 609 | ## grepPrefixAt 610 | 611 | Using test tags that start with `@` is so common, you can enforce it using the env option `grepPrefixAt: true`. This lets you use `@tag1,@tag2, ...` or `tag1,tag2, ...` when calling. 612 | 613 | ``` 614 | # use grepPrefixAt in your env settings object 615 | # use { tags: '@tag1' } in your tests 616 | 617 | # then these two are equivalent 618 | --env grepTags=@tag1 619 | --env grepTags=tag1 620 | ``` 621 | 622 | ## grepSpec 623 | 624 | If the user selected spec(s) to run, then it might conflict with `grepFilterSpecs=true` that filters the specs. There is no way to "know" if the user used `--spec <...>` option when the plugin runs, see issues [33](https://github.com/bahmutov/cy-grep/issues/33) and [26032](https://github.com/cypress-io/cypress/issues/26032). Thus if you use `--spec pattern`, you need to pass it to the plugin via `CYPRESS_grepSpec=pattern` or `--env grepSpec=pattern` option. 625 | 626 | ``` 627 | cypress run --spec a.cy.js --env grepTags=...,grepSpec=a.cy.js 628 | ``` 629 | 630 | ## General advice 631 | 632 | - keep it simple. 633 | - I like using `@` as tag prefix to make the tags searchable 634 | 635 | ```js 636 | // ✅ good practice 637 | describe('auth', { tags: '@critical' }, () => ...) 638 | it('works', { tags: '@smoke' }, () => ...) 639 | it('works quickly', { tags: ['@smoke', '@fast'] }, () => ...) 640 | 641 | // 🚨 NOT GOING TO WORK 642 | // ERROR: treated as a single tag, 643 | // probably want an array instead 644 | it('works', { tags: '@smoke @fast' }, () => ...) 645 | ``` 646 | 647 | Grepping the tests 648 | 649 | ```shell 650 | # run the tests by title 651 | $ npx cypress run --env grep="works quickly" 652 | # run all tests tagged @smoke 653 | $ npx cypress run --env grepTags=@smoke 654 | # run all tests except tagged @smoke 655 | $ npx cypress run --env grepTags=-@smoke 656 | # run all tests that have tag @fast but do not have tag @smoke 657 | $ npx cypress run --env grepTags=@fast+-@smoke 658 | ``` 659 | 660 | I would run all tests by default, and grep tests from the command line. For example, I could run the smoke tests first using grep plugin, and if the smoke tests pass, then run all the tests. See the video [How I organize pull request workflows by running smoke tests first](https://www.youtube.com/watch?v=SFW7Ecj5TNE) and its [pull request workflow file](https://github.com/bahmutov/cypress-grep-example/blob/main/.github/workflows/pr.yml). 661 | 662 | ## DevTools console 663 | 664 | You can set the grep string from the DevTools Console. This plugin adds method `Cypress.grep` and `Cypress.grepTags` to set the grep strings and restart the tests 665 | 666 | ```js 667 | // filter tests by title substring 668 | Cypress.grep('hello world') 669 | // run filtered tests 100 times 670 | Cypress.grep('hello world', null, 100) 671 | // filter tests by tag string 672 | // in this case will run tests with tag @smoke OR @fast 673 | Cypress.grep(null, '@smoke @fast') 674 | // run tests tagged @smoke AND @fast 675 | Cypress.grep(null, '@smoke+@fast') 676 | // run tests with title containing "hello" and tag @smoke 677 | Cypress.grep('hello', '@smoke') 678 | // run tests with title containing "hello" and tag @smoke 10 times 679 | Cypress.grep('hello', '@smoke', 10) 680 | ``` 681 | 682 | - to remove the grep strings enter `Cypress.grep()` 683 | 684 | ### grepFailed 685 | 686 | Once the tests finish, you can run just the failed tests from DevTools console 687 | 688 | ```text 689 | > Cypress.grepFailed() 690 | ``` 691 | 692 | **Tip:** use `Cypress.grep()` to reset and run all the tests 693 | 694 | 📝 Read the blog post [Run Just The Failed Tests In Cypress](https://glebbahmutov.com/blog/run-failed-tests/). 695 | 696 | ## Debugging 697 | 698 | When debugging a problem, first make sure you are using the expected version of this plugin, as some features might be only available in the [later releases](https://github.com/bahmutov/cy-grep/releases). 699 | 700 | ``` 701 | # get the plugin's version using NPM 702 | $ npm ls @bahmutov/cy-grep 703 | ... 704 | └── @bahmutov/cy-grep@1.1.0 705 | 706 | # get the plugin's version using Yarn 707 | $ yarn why @bahmutov/cy-grep 708 | ... 709 | => Found "@bahmutov/cy-grep@1.1.0" 710 | info Has been hoisted to "@bahmutov/cy-grep" 711 | info This module exists because it's specified in "devDependencies". 712 | ... 713 | ``` 714 | 715 | Second, make sure you are passing the values to the plugin correctly by inspecting the "Settings" tab in the Cypress Desktop GUI screen. You should see the values you have passed in the "Config" object under the `env` property. For example, if I start the Test Runner with 716 | 717 | ```text 718 | $ npx cypress open --env grep=works,grepFilterTests=true 719 | ``` 720 | 721 | Then I expect to see the grep string and the "filter tests" flag in the `env` object. 722 | 723 | ![Values in the env object](./images/config.png) 724 | 725 | ### Log messages 726 | 727 | This module uses [debug](https://github.com/visionmedia/debug#readme) to log verbose messages. You can enable the debug messages in the plugin file (runs when discovering specs to filter), and inside the browser to see how it determines which tests to run and to skip. When opening a new issue, please provide the debug logs from the plugin (if any) and from the browser. 728 | 729 | ### Debugging in the plugin 730 | 731 | Start Cypress with the environment variable `DEBUG=cy-grep`. You will see a few messages from this plugin in the terminal output: 732 | 733 | ``` 734 | $ DEBUG=cy-grep npx cypress run --env grep=works,grepFilterSpecs=true 735 | cy-grep: tests with "works" in their names 736 | cy-grep: filtering specs using "works" in the title 737 | cy-grep Cypress config env object: { grep: 'works', grepFilterSpecs: true } 738 | ... 739 | cy-grep found 1 spec files +5ms 740 | cy-grep [ 'spec.js' ] +0ms 741 | cy-grep spec file spec.js +5ms 742 | cy-grep suite and test names: [ 'hello world', 'works', 'works 2 @tag1', 743 | 'works 2 @tag1 @tag2', 'works @tag2' ] +0ms 744 | cy-grep found "works" in 1 specs +0ms 745 | cy-grep [ 'spec.js' ] +0ms 746 | ``` 747 | 748 | ### Debugging in the browser 749 | 750 | To enable debug console messages in the browser, from the DevTools console set `localStorage.debug='cy-grep'` and run the tests again. 751 | 752 | To see how to debug this plugin, watch the video [Debug cypress-grep Plugin](https://youtu.be/4YMAERddHYA) but use the string `cy-grep` 753 | 754 | ## Examples 755 | 756 | - [cypress-grep-example](https://github.com/bahmutov/cypress-grep-example) 757 | - [todo-graphql-example](https://github.com/bahmutov/todo-graphql-example) 758 | 759 | ## See also 760 | 761 | - [cypress-select-tests](https://github.com/bahmutov/cypress-select-tests) 762 | - [cypress-skip-test](https://github.com/cypress-io/cypress-skip-test) 763 | - 📝 Read the blog post [Cypress GitHub Actions Slash Command](https://glebbahmutov.com/blog/cypress-slash-command/) 764 | - 📝 Read the blog post [Type Check Your Test Tags](https://glebbahmutov.com/blog/type-check-test-tags/) 765 | - plugin [dennisbergevin/cypress-plugin-last-failed](https://github.com/dennisbergevin/cypress-plugin-last-failed) 766 | - plugin [dennisbergevin/cypress-cli-select](https://github.com/dennisbergevin/cypress-cli-select) 767 | - plugin [dennisbergevin/cypress-plugin-grep-boxes](https://github.com/dennisbergevin/cypress-plugin-grep-boxes) 768 | 769 | ## cy-grep vs cypress-grep vs @cypress/grep 770 | 771 | Many years ago I wrote a plugin `cypress-grep`. When I left the company Cypress, I transferred that MIT-licensed plugin to the Cypress GitHub organization. They moved it to the Cypress monorepo and renamed the NPM module `@cypress/grep`. I still use this grep plugin in some projects. When Cypress v10 was released, it broke some of the things in the plugin. Since I needed to fix it quickly and the monorepo setup is suboptimal, I forked the plugin back to my own repo `bahmutov/cy-grep` (this repo) and released under NPM name `@bahmutov/cy-grep`. 772 | 773 | I plan to maintain the plugin `@bahmutov/cy-grep` in the future, since I rely on it myself **a lot**. 774 | 775 | ## Major migrations 776 | 777 | ### v1 to v2 778 | 779 | Adding a type for `tags` and `requiredTags` moved from default to its own `.d.ts` file. 780 | 781 | **v1** 782 | 783 | For example, the plugin `@bahmutov/cy-grep@v1` simply could add itself to the `types` list in your `tsconfig.json` / `jsconfig.json` file 784 | 785 | ```json 786 | { 787 | "include": ["cypress/**/*"], 788 | "compilerOptions": { 789 | "types": ["cypress", "@bahmutov/cy-grep"] 790 | } 791 | } 792 | ``` 793 | 794 | This made `tags` property a string, so you could use `it('works', { tags: '@smoke' }, () => ...)` 795 | 796 | **v2** 797 | 798 | If you want to use _any_ strings as tags, you need to add the file [src/tags-are-strings.d.ts](./src/tags-are-strings.d.ts) 799 | 800 | ```json 801 | { 802 | "include": [ 803 | "cypress/**/*", 804 | "node_modules/@bahmutov/cy-grep/src/tags-are-strings.d.ts" 805 | ], 806 | "compilerOptions": { 807 | "types": ["cypress", "@bahmutov/cy-grep"] 808 | } 809 | } 810 | ``` 811 | 812 | **Note:** you still want to include the `@bahmutov/cy-grep` default types, since they provide additional static methods, like `Cypress.grep` 813 | 814 | ## Small Print 815 | 816 | Author: Gleb Bahmutov <gleb.bahmutov@gmail.com> © 2022 817 | 818 | - [@bahmutov](https://twitter.com/bahmutov) 819 | - [glebbahmutov.com](https://glebbahmutov.com) 820 | - [blog](https://glebbahmutov.com/blog) 821 | - [videos](https://www.youtube.com/glebbahmutov) 822 | - [presentations](https://slides.com/bahmutov) 823 | - [cypress.tips](https://cypress.tips) 824 | - [Cypress Tips & Tricks Newsletter](https://cypresstips.substack.com/) 825 | - [my Cypress courses](https://cypress.tips/courses) 826 | 827 | License: MIT - do anything with the code, but don't blame me if it does not work. 828 | 829 | Support: if you find any problems with this module, email / tweet / 830 | [open issue](https://github.com/bahmutov/cy-grep/issues) on Github 831 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | defaultCommandTimeout: 1000, 6 | setupNodeEvents(on, config) { 7 | require('./src/plugin')(config) 8 | 9 | return config 10 | }, 11 | specPattern: 'cypress/**/spec.js', 12 | }, 13 | env: { 14 | // set different values for testing 15 | // https://github.com/bahmutov/cy-grep/issues/138 16 | // grepFilterSpecs: true, 17 | // grepOmitFiltered: true, 18 | }, 19 | fixturesFolder: false, 20 | video: false, 21 | browser: 'electron', 22 | }) 23 | -------------------------------------------------------------------------------- /cypress/e2e/before-spec.js: -------------------------------------------------------------------------------- 1 | describe('Runs before and beforeEach when first test is skipped', () => { 2 | let count = 0 3 | 4 | before(() => { 5 | count++ 6 | }) 7 | 8 | beforeEach(() => { 9 | count++ 10 | }) 11 | 12 | it('A', { tags: ['@core'] }, () => {}) 13 | 14 | it('B', { tags: ['@core', '@staging'] }, () => { 15 | expect(count).to.equal(2) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /cypress/e2e/burn-spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // if we specify just the burn parameter 4 | // then this test will be repeated N times 5 | describe('burning a test N times', () => { 6 | it('repeats', () => {}) 7 | 8 | it('second test', () => {}) 9 | }) 10 | -------------------------------------------------------------------------------- /cypress/e2e/config-spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | describe('tests that use config object', () => { 4 | it('still works @config', { baseUrl: 'http://localhost:8000' }, () => { 5 | expect(Cypress.config('baseUrl')).to.equal('http://localhost:8000') 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /cypress/e2e/config-tags-spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | describe('tags in the config object', () => { 4 | it('works as an array', { tags: ['config', 'some-other-tag'] }, () => { 5 | expect(true).to.be.true 6 | }) 7 | 8 | it('works as a string', { tags: 'config' }, () => { 9 | expect(true).to.be.true 10 | }) 11 | 12 | it('does not use tags', () => { 13 | // so it fails 14 | expect(true).to.be.false 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /cypress/e2e/describe-tags-spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // @ts-check 4 | 5 | describe('block with no tags', () => { 6 | it('inside describe 1', () => {}) 7 | 8 | it('inside describe 2', () => {}) 9 | }) 10 | 11 | describe('block with tag smoke', { tags: '@smoke' }, () => { 12 | it('inside describe 3', () => {}) 13 | 14 | it('inside describe 4', () => {}) 15 | }) 16 | 17 | describe('block without any tags', () => { 18 | // note the parent suite has no tags 19 | // so this test should run when using --env grepTags=@smoke 20 | it('test with tag smoke', { tags: '@smoke' }, () => {}) 21 | }) 22 | 23 | it('is a test outside any suites', () => {}) 24 | -------------------------------------------------------------------------------- /cypress/e2e/each-spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // https://github.com/bahmutov/cypress-each 4 | import 'cypress-each' 5 | 6 | describe('tests that use .each work', () => { 7 | // creating tests dynamically works with "cy-grep" 8 | it.each([1, 2, 3])('test for %d', (x) => { 9 | expect(x).to.be.oneOf([1, 2, 3]) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /cypress/e2e/effective-tags/spec-a.js: -------------------------------------------------------------------------------- 1 | describe('spec a', { tags: 'A' }, () => { 2 | it('runs test', () => {}) 3 | it('runs smoke test', { tags: 'smoke' }, () => {}) 4 | }) 5 | -------------------------------------------------------------------------------- /cypress/e2e/effective-tags/spec-b.js: -------------------------------------------------------------------------------- 1 | describe('spec b', { tags: 'B' }, () => { 2 | it('runs test', () => {}) 3 | it('runs smoke test', { tags: 'smoke' }, () => {}) 4 | }) 5 | -------------------------------------------------------------------------------- /cypress/e2e/extra-specs/a.cy.js: -------------------------------------------------------------------------------- 1 | it('works A', { tags: '@a' }, () => {}) 2 | -------------------------------------------------------------------------------- /cypress/e2e/extra-specs/b.cy.js: -------------------------------------------------------------------------------- 1 | it('works B', () => {}) 2 | -------------------------------------------------------------------------------- /cypress/e2e/inherits-tag-spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('Screen A', { tags: ['@sanity', '@screen-a'] }, () => { 4 | it('loads', { tags: ['@screen-b'] }, () => { 5 | // do something that eventually sends the page to screen b. 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /cypress/e2e/multiple-registrations.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // register the plugin multiple times 4 | // to simulate including from support and spec files 5 | // https://github.com/cypress-io/cypress-grep/issues/59 6 | require('../../src/support')() 7 | require('../../src/support')() 8 | require('../../src/support')() 9 | 10 | it('hello world', () => {}) 11 | -------------------------------------------------------------------------------- /cypress/e2e/negative-grep-spec.js: -------------------------------------------------------------------------------- 1 | describe('users', () => { 2 | it('works 1', () => {}) 3 | it('works 2', () => {}) 4 | it('works 3', () => {}) 5 | }) 6 | 7 | describe('projects', () => { 8 | it('load 1', () => {}) 9 | it('load 2', () => {}) 10 | it('load 3', () => {}) 11 | }) 12 | -------------------------------------------------------------------------------- /cypress/e2e/nested-describe-spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // @ts-check 4 | describe('grand', () => { 5 | context('outer', { tags: '@smoke' }, () => { 6 | describe('inner', () => { 7 | it('runs', () => {}) 8 | }) 9 | }) 10 | }) 11 | 12 | describe('top', { tags: '@smoke' }, () => { 13 | describe('middle', () => { 14 | context('bottom', { tags: ['@integration', '@fast'] }, () => { 15 | it('runs too', () => {}); 16 | }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /cypress/e2e/omit-and-skip-spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // @ts-check 4 | describe('Page', () => { 5 | describe('List', { tags: ['@us1'] }, () => { 6 | it.skip('first test', () => {}) 7 | it('second test', () => {}) 8 | it('third test', () => {}) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /cypress/e2e/skip-spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | describe('tests that use .skip', () => { 3 | // use a template literal 4 | it(`works`, () => {}) 5 | 6 | it.skip('is pending', () => {}) 7 | 8 | it.skip('is pending again', () => {}) 9 | }) 10 | -------------------------------------------------------------------------------- /cypress/e2e/spec-parameter/a.cy.js: -------------------------------------------------------------------------------- 1 | it('works A', { tags: '@a' }, () => {}) 2 | -------------------------------------------------------------------------------- /cypress/e2e/spec-parameter/b.cy.js: -------------------------------------------------------------------------------- 1 | it('works B', { tags: '@b' }, () => {}) 2 | -------------------------------------------------------------------------------- /cypress/e2e/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // the standard list of tests we run 4 | 5 | it('hello world', () => {}) 6 | 7 | it('works', () => {}) 8 | 9 | it('works 2 @tag1', { tags: '@tag1' }, () => {}) 10 | 11 | it('works 2 @tag1 @tag2', { tags: ['@tag1', '@tag2'] }, () => { 12 | // confirm we have test tags 13 | expect(Cypress.env('testTags'), 'test tags').to.deep.equal(['@tag1', '@tag2']) 14 | }) 15 | 16 | it('works @tag2', { tags: '@tag2' }, () => {}) 17 | 18 | // a failed test if needed 19 | // comment out when done 20 | // describe('a failing suite', () => { 21 | // it('bad test', () => { 22 | // expect(false).to.be.true 23 | // }) 24 | // }) 25 | -------------------------------------------------------------------------------- /cypress/e2e/specify-spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // specify is the same as it() 4 | 5 | specify('hello world', () => {}) 6 | 7 | specify('works', () => {}) 8 | 9 | specify('works 2 @tag1', { tags: '@tag1' }, () => {}) 10 | 11 | specify('works 2 @tag1 @tag2', { tags: ['@tag1', '@tag2'] }, () => {}) 12 | 13 | specify('works @tag2', { tags: '@tag2' }, () => {}) 14 | -------------------------------------------------------------------------------- /cypress/e2e/tags/test1.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | it('Test 1', { tags: ['smoke', 'regression'] }, () => { 4 | expect(true).to.be.true 5 | }) 6 | -------------------------------------------------------------------------------- /cypress/e2e/tags/test2.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | it('Test 2', { tags: ['high', 'smoke'] }, () => { 4 | expect(true).to.be.true 5 | }) 6 | -------------------------------------------------------------------------------- /cypress/e2e/tags/test3.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | it('Test 3', { tags: ['smoke'] }, () => { 4 | expect(true).to.be.true 5 | }) 6 | -------------------------------------------------------------------------------- /cypress/e2e/test-tags/test-has-tags.cy.js: -------------------------------------------------------------------------------- 1 | it('has no test tags', () => { 2 | const tags = Cypress.env('testTags') 3 | expect(tags, 'tags').to.deep.equal([]) 4 | }) 5 | 6 | // to truly test this, need to execute with the tags 7 | it('has required tags', { requiredTags: '@r1' }, () => { 8 | const tags = Cypress.env('testTags') 9 | expect(tags, 'required tags').to.deep.equal(['@r1']) 10 | }) 11 | 12 | describe('parent', { tags: ['@p1', '@p2'] }, () => { 13 | describe('child', { tags: '@c1' }, () => { 14 | it('has all effective test tags', { tags: '@t1' }, () => { 15 | const tags = Cypress.env('testTags') 16 | // includes tags from the parent suites and the test itself 17 | expect(tags, 'tags').to.deep.equal(['@p1', '@p2', '@c1', '@t1']) 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /cypress/e2e/this-spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | describe('this context', () => { 3 | beforeEach(() => { 4 | cy.wrap(42).as('life') 5 | }) 6 | 7 | it('preserves the test context', function () { 8 | expect(this).to.be.an('object') 9 | expect(this.life).to.equal(42) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /cypress/e2e/ts-spec.ts: -------------------------------------------------------------------------------- 1 | describe('TypeScript spec', () => { 2 | it('works', () => { 3 | type Person = { 4 | name: string 5 | } 6 | 7 | const person: Person = { 8 | name: 'Joe', 9 | } 10 | 11 | cy.wrap(person).should('have.property', 'name', 'Joe') 12 | }) 13 | 14 | it('loads', () => { 15 | const n: number = 1 16 | cy.wrap(n).should('eq', 1) 17 | }) 18 | 19 | it('loads interfaces', () => { 20 | interface Person { 21 | name: string 22 | } 23 | 24 | const p: Person = { 25 | name: 'Joe', 26 | } 27 | cy.wrap(p).should('have.property', 'name', 'Joe') 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /cypress/e2e/unit.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | 4 | import { 5 | parseGrep, 6 | parseTitleGrep, 7 | parseFullTitleGrep, 8 | parseTagsGrep, 9 | shouldTestRun, 10 | shouldTestRunTags, 11 | shouldTestRunRequiredTags, 12 | shouldTestRunTitle, 13 | getMentionedTags, 14 | } from '../../src/utils' 15 | 16 | // print full objects in the assertions 17 | chai.config.truncateThreshold = 500 18 | 19 | describe('utils', () => { 20 | context('getMentionedTags', () => { 21 | it('returns unique tags', () => { 22 | const tags = getMentionedTags('@tag1+@tag2+@tag3') 23 | expect(tags).to.deep.equal(['@tag1', '@tag2', '@tag3']) 24 | }) 25 | 26 | it('sorts returned tags', () => { 27 | const tags = getMentionedTags('x y a') 28 | expect(tags).to.deep.equal(['a', 'x', 'y']) 29 | }) 30 | 31 | it('handles -', () => { 32 | const tags = getMentionedTags('@smoke+@screen-b') 33 | expect(tags).to.deep.equal(['@screen-b', '@smoke']) 34 | }) 35 | 36 | it('handles extra spaces', () => { 37 | const tags = getMentionedTags(' @tag1 -@tag2 ') 38 | expect(tags).to.deep.equal(['@tag1', '@tag2']) 39 | }) 40 | 41 | it('handles extra commas and empty values', () => { 42 | const tags = getMentionedTags(',,,@tag2,') 43 | expect(tags).to.deep.equal(['@tag2']) 44 | }) 45 | 46 | describe('grepPrefixAt', () => { 47 | const grepPrefixAt = true 48 | 49 | it('returns unique tags', () => { 50 | const tags = getMentionedTags('tag1+tag2+tag3', grepPrefixAt) 51 | expect(tags).to.deep.equal(['@tag1', '@tag2', '@tag3']) 52 | }) 53 | }) 54 | }) 55 | 56 | context('parseTitleGrep', () => { 57 | it('grabs the positive title', () => { 58 | const parsed = parseTitleGrep('hello w') 59 | 60 | expect(parsed).to.deep.equal({ 61 | title: 'hello w', 62 | invert: false, 63 | }) 64 | }) 65 | 66 | it('trims the string', () => { 67 | const parsed = parseTitleGrep(' hello w ') 68 | 69 | expect(parsed).to.deep.equal({ 70 | title: 'hello w', 71 | invert: false, 72 | }) 73 | }) 74 | 75 | it('inverts the string', () => { 76 | const parsed = parseTitleGrep('-hello w') 77 | 78 | expect(parsed).to.deep.equal({ 79 | title: 'hello w', 80 | invert: true, 81 | }) 82 | }) 83 | 84 | it('trims the inverted the string', () => { 85 | const parsed = parseTitleGrep(' -hello w ') 86 | 87 | expect(parsed).to.deep.equal({ 88 | title: 'hello w', 89 | invert: true, 90 | }) 91 | }) 92 | 93 | it('returns null for undefined input', () => { 94 | const parsed = parseTitleGrep(undefined) 95 | 96 | expect(parsed).to.equal(null) 97 | }) 98 | }) 99 | 100 | context('parseFullTitleGrep', () => { 101 | it('returns list of title greps', () => { 102 | const parsed = parseFullTitleGrep('hello; one; -two') 103 | 104 | expect(parsed).to.deep.equal([ 105 | { title: 'hello', invert: false }, 106 | { title: 'one', invert: false }, 107 | { title: 'two', invert: true }, 108 | ]) 109 | }) 110 | }) 111 | 112 | context('parseTagsGrep', () => { 113 | it('allows arrays of tags', () => { 114 | const parsed = parseTagsGrep(['@tag1+@tag2', '@tag3']) 115 | 116 | expect(parsed).to.deep.equal([ 117 | // two OR parts 118 | [ 119 | // with 3 AND parts 120 | { tag: '@tag1', invert: false }, 121 | { tag: '@tag2', invert: false }, 122 | ], 123 | [{ tag: '@tag3', invert: false }], 124 | ]) 125 | }) 126 | 127 | it('parses AND tags', () => { 128 | // run only the tests with all 3 tags 129 | const parsed = parseTagsGrep('@tag1+@tag2+@tag3') 130 | 131 | expect(parsed).to.deep.equal([ 132 | // single OR part 133 | [ 134 | // with 3 AND parts 135 | { tag: '@tag1', invert: false }, 136 | { tag: '@tag2', invert: false }, 137 | { tag: '@tag3', invert: false }, 138 | ], 139 | ]) 140 | }) 141 | 142 | it('handles dashes in the tag', () => { 143 | const parsed = parseTagsGrep('@smoke+@screen-b') 144 | 145 | expect(parsed).to.deep.equal([ 146 | [ 147 | { tag: '@smoke', invert: false }, 148 | { tag: '@screen-b', invert: false }, 149 | ], 150 | ]) 151 | }) 152 | 153 | it('parses OR tags spaces', () => { 154 | // run tests with tag1 OR tag2 or tag3 155 | const parsed = parseTagsGrep('@tag1 @tag2 @tag3') 156 | 157 | expect(parsed).to.deep.equal([ 158 | [{ tag: '@tag1', invert: false }], 159 | [{ tag: '@tag2', invert: false }], 160 | [{ tag: '@tag3', invert: false }], 161 | ]) 162 | }) 163 | 164 | it('parses OR tags commas', () => { 165 | // run tests with tag1 OR tag2 or tag3 166 | const parsed = parseTagsGrep('@tag1,@tag2,@tag3') 167 | 168 | expect(parsed).to.deep.equal([ 169 | [{ tag: '@tag1', invert: false }], 170 | [{ tag: '@tag2', invert: false }], 171 | [{ tag: '@tag3', invert: false }], 172 | ]) 173 | }) 174 | 175 | it('parses inverted tag', () => { 176 | const parsed = parseTagsGrep('-@tag1') 177 | 178 | expect(parsed).to.deep.equal([[{ tag: '@tag1', invert: true }]]) 179 | }) 180 | 181 | it('parses tag1 but not tag2 with space', () => { 182 | const parsed = parseTagsGrep('@tag1 -@tag2') 183 | 184 | expect(parsed).to.deep.equal([ 185 | [{ tag: '@tag1', invert: false }], 186 | [{ tag: '@tag2', invert: true }], 187 | ]) 188 | }) 189 | 190 | it('forgives extra spaces', () => { 191 | const parsed = parseTagsGrep(' @tag1 -@tag2 ') 192 | 193 | expect(parsed).to.deep.equal([ 194 | [{ tag: '@tag1', invert: false }], 195 | [{ tag: '@tag2', invert: true }], 196 | ]) 197 | }) 198 | 199 | it('parses tag1 but not tag2 with comma', () => { 200 | const parsed = parseTagsGrep('@tag1,-@tag2') 201 | 202 | expect(parsed).to.deep.equal([ 203 | [{ tag: '@tag1', invert: false }], 204 | [{ tag: '@tag2', invert: true }], 205 | ]) 206 | }) 207 | 208 | it('filters out empty tags', () => { 209 | const parsed = parseTagsGrep(',, @tag1,-@tag2,, ,, ,') 210 | 211 | expect(parsed).to.deep.equal([ 212 | [{ tag: '@tag1', invert: false }], 213 | [{ tag: '@tag2', invert: true }], 214 | ]) 215 | }) 216 | 217 | // TODO: would need to change the tokenizer 218 | it.skip('parses tag1 but not tag2', () => { 219 | const parsed = parseTagsGrep('@tag1-@tag2') 220 | 221 | expect(parsed).to.deep.equal([ 222 | [ 223 | { tag: '@tag1', invert: false }, 224 | { tag: '@tag2', invert: true }, 225 | ], 226 | ]) 227 | }) 228 | 229 | it('allows all tags to be inverted', () => { 230 | const parsed = parseTagsGrep('--@tag1,--@tag2') 231 | 232 | expect(parsed).to.deep.equal([ 233 | [ 234 | { tag: '@tag1', invert: true }, 235 | { tag: '@tag2', invert: true }, 236 | ], 237 | ]) 238 | }) 239 | 240 | describe('grepPrefixAt', () => { 241 | const grepPrefixAt = true 242 | 243 | it('parses AND tags', () => { 244 | // run only the tests with all 3 tags 245 | const parsed = parseTagsGrep('tag1+tag2+tag3', grepPrefixAt) 246 | 247 | expect(parsed).to.deep.equal([ 248 | // single OR part 249 | [ 250 | // with 3 AND parts 251 | { tag: '@tag1', invert: false }, 252 | { tag: '@tag2', invert: false }, 253 | { tag: '@tag3', invert: false }, 254 | ], 255 | ]) 256 | }) 257 | 258 | it('parses tag1 but not tag2 with space', () => { 259 | const parsed = parseTagsGrep('tag1 -tag2', grepPrefixAt) 260 | 261 | expect(parsed).to.deep.equal([ 262 | [{ tag: '@tag1', invert: false }], 263 | [{ tag: '@tag2', invert: true }], 264 | ]) 265 | }) 266 | }) 267 | }) 268 | 269 | context('parseGrep', () => { 270 | // no need to exhaustively test the parsing 271 | // since we want to confirm it works via test names 272 | // and not through the implementation details of 273 | // the parsed object 274 | 275 | it('creates just the title grep', () => { 276 | const parsed = parseGrep('hello w') 277 | 278 | expect(parsed).to.deep.equal({ 279 | title: [ 280 | { 281 | title: 'hello w', 282 | invert: false, 283 | }, 284 | ], 285 | tags: [], 286 | }) 287 | }) 288 | 289 | it('creates object from the grep string only', () => { 290 | const parsed = parseGrep('hello w') 291 | 292 | expect(parsed).to.deep.equal({ 293 | title: [ 294 | { 295 | title: 'hello w', 296 | invert: false, 297 | }, 298 | ], 299 | tags: [], 300 | }) 301 | 302 | // check how the parsed grep works against specific tests 303 | expect(shouldTestRun(parsed, 'hello w')).to.equal(true) 304 | expect(shouldTestRun(parsed, 'hello no')).to.equal(false) 305 | }) 306 | 307 | it('matches one of the titles', () => { 308 | // also should trim each title 309 | const parsed = parseGrep(' hello w; work 2 ') 310 | 311 | expect(parsed).to.deep.equal({ 312 | title: [ 313 | { 314 | title: 'hello w', 315 | invert: false, 316 | }, 317 | { 318 | title: 'work 2', 319 | invert: false, 320 | }, 321 | ], 322 | tags: [], 323 | }) 324 | 325 | // check how the parsed grep works against specific tests 326 | expect(shouldTestRun(parsed, 'hello w')).to.equal(true) 327 | expect(shouldTestRun(parsed, 'this work 2 works')).to.equal(true) 328 | expect(shouldTestRun(parsed, 'hello no')).to.equal(false) 329 | }) 330 | 331 | it('creates object from the grep string and tags', () => { 332 | const parsed = parseGrep('hello w', '@tag1+@tag2+@tag3') 333 | 334 | expect(parsed).to.deep.equal({ 335 | title: [ 336 | { 337 | title: 'hello w', 338 | invert: false, 339 | }, 340 | ], 341 | tags: [ 342 | // single OR part 343 | [ 344 | // with 3 AND parts 345 | { tag: '@tag1', invert: false }, 346 | { tag: '@tag2', invert: false }, 347 | { tag: '@tag3', invert: false }, 348 | ], 349 | ], 350 | }) 351 | 352 | // check how the parsed grep works against specific tests 353 | expect(shouldTestRun(parsed, 'hello w'), 'needs tags').to.equal(false) 354 | expect(shouldTestRun(parsed, 'hello no')).to.equal(false) 355 | // not every tag is present 356 | expect(shouldTestRun(parsed, '', ['@tag1', '@tag2'])).to.equal(false) 357 | expect(shouldTestRun(parsed, '', ['@tag1', '@tag2', '@tag3'])).to.equal( 358 | true, 359 | ) 360 | expect( 361 | shouldTestRun(parsed, '', ['@tag1', '@tag2', '@tag3', '@tag4']), 362 | ).to.equal(true) 363 | 364 | // title matches, but tags do not 365 | expect(shouldTestRun(parsed, 'hello w', ['@tag1', '@tag2'])).to.equal( 366 | false, 367 | ) 368 | 369 | // tags and title match 370 | expect( 371 | shouldTestRun(parsed, 'hello w', ['@tag1', '@tag2', '@tag3']), 372 | ).to.equal(true) 373 | }) 374 | }) 375 | 376 | context('shouldTestRunTags', () => { 377 | // when the user types "used" string 378 | // and the test has the given tags, make sure 379 | // our parsing and decision logic computes the expected result 380 | const shouldIt = (used, tags, expected) => { 381 | const parsedTags = parseTagsGrep(used) 382 | 383 | expect( 384 | shouldTestRunTags(parsedTags, tags), 385 | `"${used}" against "${tags}"`, 386 | ).to.equal(expected) 387 | } 388 | 389 | it('handles AND tags', () => { 390 | shouldIt('smoke+slow', ['fast', 'smoke'], false) 391 | shouldIt('smoke+slow', ['mobile', 'smoke', 'slow'], true) 392 | shouldIt('smoke+slow', ['slow', 'extra', 'smoke'], true) 393 | shouldIt('smoke+slow', ['smoke'], false) 394 | }) 395 | 396 | it('handles OR tags', () => { 397 | // smoke OR slow 398 | shouldIt('smoke slow', ['fast', 'smoke'], true) 399 | shouldIt('smoke', ['mobile', 'smoke', 'slow'], true) 400 | shouldIt('slow', ['slow', 'extra', 'smoke'], true) 401 | shouldIt('smoke', ['smoke'], true) 402 | shouldIt('smoke', ['slow'], false) 403 | }) 404 | 405 | it('handles invert tag', () => { 406 | // should not run - we are excluding the "slow" 407 | shouldIt('smoke+-slow', ['smoke', 'slow'], false) 408 | shouldIt('mobile+-slow', ['smoke', 'slow'], false) 409 | shouldIt('smoke -slow', ['smoke', 'fast'], true) 410 | shouldIt('-slow', ['smoke', 'slow'], false) 411 | shouldIt('-slow', ['smoke'], true) 412 | // no tags in the test 413 | shouldIt('-slow', [], true) 414 | }) 415 | }) 416 | 417 | context('shouldTestRunRequiredTags', () => { 418 | const shouldIt = (used, requiredTags, expected) => { 419 | const parsedTags = parseTagsGrep(used) 420 | 421 | expect( 422 | shouldTestRunRequiredTags(parsedTags, requiredTags), 423 | `"${used}" against only "${requiredTags}"`, 424 | ).to.equal(expected) 425 | } 426 | 427 | it('tags is included', () => { 428 | shouldIt('smoke', ['smoke'], true) 429 | shouldIt('nice smoke', ['smoke'], true) 430 | shouldIt('all different tags and smoke', ['smoke'], true) 431 | }) 432 | 433 | it('two tags are both listed', () => { 434 | shouldIt('two one', ['one', 'two'], true) 435 | }) 436 | 437 | it('has no only tags', () => { 438 | shouldIt('nice smoke', [], true) 439 | }) 440 | 441 | it('tag is not listed', () => { 442 | shouldIt('nope', ['smoke'], false) 443 | shouldIt('all different tags', ['smoke'], false) 444 | }) 445 | 446 | it('one of two needed flags', () => { 447 | // the test needs both "one" and "two" to run 448 | shouldIt('smoke one', ['one', 'two'], false) 449 | shouldIt('smoke two', ['one', 'two'], false) 450 | }) 451 | }) 452 | 453 | context('combination of tags and required tags', () => { 454 | const checkName = (grep, grepTags) => { 455 | const parsed = parseGrep(grep, grepTags) 456 | 457 | expect(parsed).to.be.an('object') 458 | 459 | return (testName, testTags = [], requiredTags = []) => { 460 | expect(testName, 'test title').to.be.a('string') 461 | expect(testTags, 'test tags').to.be.an('array') 462 | 463 | return shouldTestRun(parsed, testName, testTags, false, requiredTags) 464 | } 465 | } 466 | 467 | it('simple tags', () => { 468 | // command line grep tags 469 | const t = checkName(null, 'tag1') 470 | 471 | // test information (title, tags, requiredTags) 472 | expect(t('my test', ['tag1'])).to.be.true 473 | expect(t('my test', ['tag2'])).to.be.false 474 | }) 475 | 476 | it('tags plus requiredTags prevent run', () => { 477 | // command line grep tags 478 | const t = checkName(null, 'tag1') 479 | 480 | // test information (title, tags, requiredTags) 481 | expect(t('my test', ['tag1'])).to.be.true 482 | // if the test specified "requiredTags" to require only1 483 | expect(t('my test', ['tag1'], ['only1'])).to.be.false 484 | }) 485 | 486 | it('tags plus requiredTags allow run', () => { 487 | // command line grep tags 488 | const t = checkName(null, 'tag1 only1') 489 | 490 | expect(t('my test', ['tag1'])).to.be.true 491 | // the test tagged tag1 and requiring only1 tag 492 | // will run when the user passes "only1" 493 | expect(t('my test', ['tag1'], ['only1'])).to.be.true 494 | }) 495 | 496 | it('empty tags plus requiredTags allow run', () => { 497 | // command line grep tags 498 | const t = checkName(null, 'only1') 499 | 500 | expect(t('my test', ['tag1'])).to.be.false 501 | expect(t('my test', ['tag1'], [])).to.be.false 502 | // the test tagged tag1 and requiring only1 tag 503 | // will run when the user passes "only1" 504 | expect(t('my test', [], ['only1'])).to.be.true 505 | expect(t('my test', ['tag1'], ['only1'])).to.be.true 506 | }) 507 | 508 | it('several only tags', () => { 509 | // command line grep tags 510 | const t = checkName(null, 'only1 only2') 511 | 512 | expect(t('my test', ['tag1'])).to.be.false 513 | expect(t('my test', ['tag1'], [])).to.be.false 514 | expect(t('my test', [], ['only1'])).to.be.true 515 | expect(t('my test', [], ['only2'])).to.be.true 516 | expect(t('my test', ['tag1'], ['only2', 'only1'])).to.be.true 517 | }) 518 | }) 519 | 520 | context('shouldTestRun', () => { 521 | // a little utility function to parse the given grep string 522 | // and apply the first argument in shouldTestRun 523 | const checkName = (grep, grepTags) => { 524 | const parsed = parseGrep(grep, grepTags) 525 | 526 | expect(parsed).to.be.an('object') 527 | 528 | return (testName, testTags = []) => { 529 | expect(testName, 'test title').to.be.a('string') 530 | expect(testTags, 'test tags').to.be.an('array') 531 | 532 | return shouldTestRun(parsed, testName, testTags) 533 | } 534 | } 535 | 536 | it('simple tag', () => { 537 | const parsed = parseGrep('@tag1') 538 | 539 | expect(shouldTestRun(parsed, 'no tag1 here')).to.be.false 540 | expect(shouldTestRun(parsed, 'has @tag1 in the name')).to.be.true 541 | }) 542 | 543 | it('with invert title', () => { 544 | const t = checkName('-hello') 545 | 546 | expect(t('no greetings')).to.be.true 547 | expect(t('has hello world')).to.be.false 548 | }) 549 | 550 | it('with invert option', () => { 551 | const t = checkName(null, '-@tag1') 552 | 553 | expect(t('no tags here')).to.be.true 554 | expect(t('has tag1', ['@tag1'])).to.be.false 555 | expect(t('has other tags', ['@tag2'])).to.be.true 556 | }) 557 | 558 | it('with AND option', () => { 559 | const t = checkName('', '@tag1+@tag2') 560 | 561 | expect(t('no tag1 here')).to.be.false 562 | expect(t('has only @tag1', ['@tag1'])).to.be.false 563 | expect(t('has only @tag2', ['@tag2'])).to.be.false 564 | expect(t('has both tags', ['@tag1', '@tag2'])).to.be.true 565 | }) 566 | 567 | it('with OR option', () => { 568 | const t = checkName(null, '@tag1 @tag2') 569 | 570 | expect(t('no tag1 here')).to.be.false 571 | expect(t('has only @tag1 in the name', ['@tag1'])).to.be.true 572 | expect(t('has only @tag2 in the name', ['@tag2'])).to.be.true 573 | expect(t('has @tag1 and @tag2 in the name', ['@tag1', '@tag2'])).to.be 574 | .true 575 | }) 576 | 577 | it('OR with AND option', () => { 578 | const t = checkName(null, '@tag1 @tag2+@tag3') 579 | 580 | expect(t('no tag1 here')).to.be.false 581 | expect(t('has only @tag1 in the name', ['@tag1'])).to.be.true 582 | expect(t('has only @tag2 in the name', ['@tag2'])).to.be.false 583 | expect(t('has only @tag2 in the name and also @tag3', ['@tag2', '@tag3'])) 584 | .to.be.true 585 | 586 | expect( 587 | t('has @tag1 and @tag2 and @tag3 in the name', [ 588 | '@tag1', 589 | '@tag2', 590 | '@tag3', 591 | ]), 592 | ).to.be.true 593 | }) 594 | 595 | it('Multiple invert strings and a simple one', () => { 596 | const t = checkName('-name;-hey;number') 597 | 598 | expect(t('number should only be matches without a n-a-m-e')).to.be.true 599 | expect(t("number can't be name")).to.be.false 600 | expect(t('The man needs a name')).to.be.false 601 | expect(t('number hey name')).to.be.false 602 | expect(t('numbers hey name')).to.be.false 603 | expect(t('number hsey nsame')).to.be.true 604 | expect(t('This wont match')).to.be.false 605 | }) 606 | 607 | it('Only inverted strings', () => { 608 | const t = checkName('-name;-hey') 609 | 610 | expect(t("I'm matched")).to.be.true 611 | expect(t("hey! I'm not")).to.be.false 612 | expect(t('My name is weird')).to.be.false 613 | }) 614 | }) 615 | 616 | context('parseFullTitleGrep', () => { 617 | const shouldIt = (search, testName, expected) => { 618 | const parsed = parseFullTitleGrep(search) 619 | 620 | expect( 621 | shouldTestRunTitle(parsed, testName), 622 | `"${search}" against title "${testName}"`, 623 | ).to.equal(expected) 624 | } 625 | 626 | it('passes for substring', () => { 627 | shouldIt('hello w', 'hello world', true) 628 | shouldIt('-hello w', 'hello world', false) 629 | }) 630 | }) 631 | }) 632 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | import cypressGrep from '../../src/support' 5 | // register the grep feature 6 | // https://github.com/bahmutov/cy-grep 7 | cypressGrep() 8 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["cypress"] 4 | }, 5 | "include": ["e2e/**/*.ts"] 6 | } 7 | -------------------------------------------------------------------------------- /expects/README.md: -------------------------------------------------------------------------------- 1 | Different JSON files with expected test statuses when running Cypress tests with different grep argument. 2 | 3 | Used via [cypress-expect](https://github.com/bahmutov/cypress-expect) with `--expect` option to run on CI to verify the correct tests were pending or passing. 4 | -------------------------------------------------------------------------------- /expects/all-pending.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello world": "pending", 3 | "works": "pending", 4 | "works 2 @tag1": "pending", 5 | "works 2 @tag1 @tag2": "pending", 6 | "works @tag2": "pending" 7 | } 8 | -------------------------------------------------------------------------------- /expects/before.json: -------------------------------------------------------------------------------- 1 | { 2 | "Runs before and beforeEach when first test is skipped": { 3 | "A": "pending", 4 | "B": "passed" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /expects/burn-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "burning a test N times": { 3 | "repeats: burning 1 of 5": "pass", 4 | "repeats: burning 2 of 5": "pass", 5 | "repeats: burning 3 of 5": "pass", 6 | "repeats: burning 4 of 5": "pass", 7 | "repeats: burning 5 of 5": "pass", 8 | "second test: burning 1 of 5": "pass", 9 | "second test: burning 2 of 5": "pass", 10 | "second test: burning 3 of 5": "pass", 11 | "second test: burning 4 of 5": "pass", 12 | "second test: burning 5 of 5": "pass" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /expects/config-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests that use config object": { 3 | "still works @config": "passed" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /expects/config-tags-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags in the config object": { 3 | "works as an array": "passed", 4 | "works as a string": "passed", 5 | "does not use tags": "pending" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /expects/describe-tags-invert-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "block with no tags": { 3 | "inside describe 1": "passing", 4 | "inside describe 2": "passing" 5 | }, 6 | "block with tag smoke": { 7 | "inside describe 3": "pending", 8 | "inside describe 4": "pending" 9 | }, 10 | "block without any tags": { 11 | "test with tag smoke": "pending" 12 | }, 13 | "is a test outside any suites": "passing" 14 | } 15 | -------------------------------------------------------------------------------- /expects/describe-tags-spec-untagged.json: -------------------------------------------------------------------------------- 1 | { 2 | "block with no tags": { 3 | "inside describe 1": "passing", 4 | "inside describe 2": "passing" 5 | }, 6 | "block with tag smoke": { 7 | "inside describe 3": "pending", 8 | "inside describe 4": "pending" 9 | }, 10 | "block without any tags": { 11 | "test with tag smoke": "pending" 12 | }, 13 | "is a test outside any suites": "passing" 14 | } 15 | -------------------------------------------------------------------------------- /expects/describe-tags-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "block with no tags": { 3 | "inside describe 1": "pending", 4 | "inside describe 2": "pending" 5 | }, 6 | "block with tag smoke": { 7 | "inside describe 3": "passed", 8 | "inside describe 4": "passed" 9 | }, 10 | "block without any tags": { 11 | "test with tag smoke": "passed" 12 | }, 13 | "is a test outside any suites": "pending" 14 | } 15 | -------------------------------------------------------------------------------- /expects/each-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests that use .each work": { 3 | "test for 1": "pending", 4 | "test for 2": "passing", 5 | "test for 3": "pending" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /expects/effective-tags/spec-a-smoke.json: -------------------------------------------------------------------------------- 1 | { 2 | "cypress": { 3 | "e2e": { 4 | "effective-tags": { 5 | "spec-a.js": { 6 | "spec a": { 7 | "runs smoke test": "pass" 8 | } 9 | } 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /expects/effective-tags/spec-a.json: -------------------------------------------------------------------------------- 1 | { 2 | "cypress": { 3 | "e2e": { 4 | "effective-tags": { 5 | "spec-a.js": { 6 | "spec a": { 7 | "runs test": "pass", 8 | "runs smoke test": "pass" 9 | } 10 | } 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /expects/extra-specs.json: -------------------------------------------------------------------------------- 1 | { 2 | "cypress": { 3 | "e2e": { 4 | "extra-specs": { 5 | "a.cy.js": { 6 | "works A": "passing" 7 | }, 8 | "b.cy.js": { 9 | "works B": "passing" 10 | } 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /expects/grep-filter-specs-tag.json: -------------------------------------------------------------------------------- 1 | { 2 | "block with no tags": { 3 | "inside describe 1": "pending", 4 | "inside describe 2": "pending" 5 | }, 6 | "block with tag smoke": { 7 | "inside describe 3": "passing", 8 | "inside describe 4": "passing" 9 | }, 10 | "block without any tags": { 11 | "test with tag smoke": "passing" 12 | }, 13 | "is a test outside any suites": "pending", 14 | "grand": { 15 | "outer": { 16 | "inner": { 17 | "runs": "passing" 18 | } 19 | } 20 | }, 21 | "top": { 22 | "middle": { 23 | "bottom": { 24 | "runs too": "passing" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /expects/grep-filter-specs.json: -------------------------------------------------------------------------------- 1 | { 2 | "is a test outside any suites": "passing", 3 | "block with no tags": { 4 | "inside describe 1": "pending", 5 | "inside describe 2": "pending" 6 | }, 7 | "block with tag smoke": { 8 | "inside describe 3": "pending", 9 | "inside describe 4": "pending" 10 | }, 11 | "block without any tags": { 12 | "test with tag smoke": "pending" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /expects/grep-untagged.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello world": "passing", 3 | "works": "passing", 4 | "works 2 @tag1": "pending", 5 | "works 2 @tag1 @tag2": "pending", 6 | "works @tag2": "pending" 7 | } 8 | -------------------------------------------------------------------------------- /expects/hello-burn.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello world: burning 1 of 3": "passed", 3 | "hello world: burning 2 of 3": "passed", 4 | "hello world: burning 3 of 3": "passed", 5 | "works": "pending", 6 | "works 2 @tag1": "pending", 7 | "works 2 @tag1 @tag2": "pending", 8 | "works @tag2": "pending" 9 | } 10 | -------------------------------------------------------------------------------- /expects/hello-or-works-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello world": "passed", 3 | "works": "pending", 4 | "works 2 @tag1": "passed", 5 | "works 2 @tag1 @tag2": "passed", 6 | "works @tag2": "pending" 7 | } 8 | -------------------------------------------------------------------------------- /expects/hello.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello world": "passed", 3 | "works": "pending", 4 | "works 2 @tag1": "pending", 5 | "works 2 @tag1 @tag2": "pending", 6 | "works @tag2": "pending" 7 | } 8 | -------------------------------------------------------------------------------- /expects/inherits-tag-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "Screen A": { 3 | "loads": "passed" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /expects/invert-tag1.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello world": "passing", 3 | "works": "passing", 4 | "works 2 @tag1": "pending", 5 | "works 2 @tag1 @tag2": "pending", 6 | "works @tag2": "passing" 7 | } 8 | -------------------------------------------------------------------------------- /expects/multiple-registrations.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello world: burning 1 of 3": "passed", 3 | "hello world: burning 2 of 3": "passed", 4 | "hello world: burning 3 of 3": "passed" 5 | } 6 | -------------------------------------------------------------------------------- /expects/negative-grep-no-projects.json: -------------------------------------------------------------------------------- 1 | { 2 | "cypress": { 3 | "e2e": { 4 | "negative-grep-spec.js": { 5 | "users": { 6 | "works 1": "pass", 7 | "works 2": "pass", 8 | "works 3": "pass" 9 | } 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /expects/negative-grep-projects.json: -------------------------------------------------------------------------------- 1 | { 2 | "cypress": { 3 | "e2e": { 4 | "negative-grep-spec.js": { 5 | "projects": { 6 | "load 1": "pass", 7 | "load 2": "pass", 8 | "load 3": "pass" 9 | } 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /expects/nested-describe-inheriting-names-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "grand": { 3 | "outer": { 4 | "inner": { 5 | "runs": "pending" 6 | } 7 | } 8 | }, 9 | "top": { 10 | "middle": { 11 | "bottom": { 12 | "runs too": "passing" 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /expects/nested-describe-inheriting-tags-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "grand": { 3 | "outer": { 4 | "inner": { 5 | "runs": "pending" 6 | } 7 | } 8 | }, 9 | "top": { 10 | "middle": { 11 | "bottom": { 12 | "runs too": "passing" 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /expects/nested-describe-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "grand": { 3 | "outer": { 4 | "inner": { 5 | "runs": "passing" 6 | } 7 | } 8 | }, 9 | "top": { 10 | "middle": { 11 | "bottom": { 12 | "runs too": "passing" 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /expects/no-hello-no-works2.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello world": "pending", 3 | "works": "passed", 4 | "works 2 @tag1": "pending", 5 | "works 2 @tag1 @tag2": "pending", 6 | "works @tag2": "passed" 7 | } 8 | -------------------------------------------------------------------------------- /expects/no-hello.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello world": "pending", 3 | "works": "passed", 4 | "works 2 @tag1": "passed", 5 | "works 2 @tag1 @tag2": "passed", 6 | "works @tag2": "passed" 7 | } 8 | -------------------------------------------------------------------------------- /expects/number1.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello world": "pending", 3 | "works": "pending", 4 | "works 2 @tag1": "passed", 5 | "works 2 @tag1 @tag2": "passed", 6 | "works @tag2": "pending" 7 | } 8 | -------------------------------------------------------------------------------- /expects/omit-and-skip.json: -------------------------------------------------------------------------------- 1 | { 2 | "Page": { 3 | "List": { 4 | "first test": "pending", 5 | "second test": "passed", 6 | "third test": "passed" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /expects/omit-filtered.json: -------------------------------------------------------------------------------- 1 | { 2 | "works 2 @tag1": "passed", 3 | "works 2 @tag1 @tag2": "passed" 4 | } 5 | -------------------------------------------------------------------------------- /expects/pending.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests that use .skip": { 3 | "works": "pending", 4 | "is pending": "pending", 5 | "is pending again": "pending" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /expects/prefix-tag1.json: -------------------------------------------------------------------------------- 1 | { 2 | "cypress": { 3 | "e2e": { 4 | "spec.js": { 5 | "hello world": "pending", 6 | "works": "pending", 7 | "works 2 @tag1": "passed", 8 | "works 2 @tag1 @tag2": "passed", 9 | "works @tag2": "pending" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /expects/spec-parameter.json: -------------------------------------------------------------------------------- 1 | { 2 | "cypress": { 3 | "e2e": { 4 | "spec-parameter": { 5 | "a.cy.js": { 6 | "works A": "pending" 7 | } 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /expects/specify.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello world": "pending", 3 | "works": "pending", 4 | "works 2 @tag1": "passed", 5 | "works 2 @tag1 @tag2": "passed", 6 | "works @tag2": "pending" 7 | } 8 | -------------------------------------------------------------------------------- /expects/tag1-and-tag2.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello world": "pending", 3 | "works": "pending", 4 | "works 2 @tag1": "pending", 5 | "works 2 @tag1 @tag2": "passed", 6 | "works @tag2": "pending" 7 | } 8 | -------------------------------------------------------------------------------- /expects/tag1-without-tag2.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello world": "pending", 3 | "works": "pending", 4 | "works 2 @tag1": "passed", 5 | "works 2 @tag1 @tag2": "pending", 6 | "works @tag2": "pending" 7 | } 8 | -------------------------------------------------------------------------------- /expects/tag1.json: -------------------------------------------------------------------------------- 1 | { 2 | "cypress": { 3 | "e2e": { 4 | "spec.js": { 5 | "hello world": "pending", 6 | "works": "pending", 7 | "works 2 @tag1": "passed", 8 | "works 2 @tag1 @tag2": "passed", 9 | "works @tag2": "pending" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /expects/tag2.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello world": "pending", 3 | "works": "pending", 4 | "works 2 @tag1": "pending", 5 | "works 2 @tag1 @tag2": "passed" 6 | } 7 | -------------------------------------------------------------------------------- /expects/tags-and.json: -------------------------------------------------------------------------------- 1 | { 2 | "Test 1": "pending", 3 | "Test 2": "passing", 4 | "Test 3": "pending" 5 | } 6 | -------------------------------------------------------------------------------- /expects/tags-or-filter.json: -------------------------------------------------------------------------------- 1 | { 2 | "Test 1": "passing", 3 | "Test 2": "passing" 4 | } 5 | -------------------------------------------------------------------------------- /expects/tags-or.json: -------------------------------------------------------------------------------- 1 | { 2 | "Test 1": "passing", 3 | "Test 2": "passing", 4 | "Test 3": "passing" 5 | } 6 | -------------------------------------------------------------------------------- /expects/test-npm-module.js: -------------------------------------------------------------------------------- 1 | // https://github.com/cypress-io/cypress-grep/issues/41 2 | // shows how to pass grep parameters using Cypress NPM Module API 3 | // https://on.cypress.io/module-api 4 | const cypress = require('cypress') 5 | 6 | cypress 7 | .run({ 8 | env: { 9 | grep: 'works', 10 | grepTags: '@tag2', 11 | }, 12 | }) 13 | .then((results) => { 14 | // TODO use cypress-expects to compare the test results 15 | if (results.totalTests !== 5) { 16 | console.error('expected 5 tests total, got %d', results.totalTests) 17 | process.exit(1) 18 | } 19 | 20 | if (results.totalPassed !== 2) { 21 | console.error('expected 2 tests passed, got %d', results.totalPassed) 22 | process.exit(1) 23 | } 24 | 25 | if (results.totalPending !== 3) { 26 | console.error('expected 3 tests pending, got %d', results.totalPending) 27 | process.exit(1) 28 | } 29 | 30 | console.log(results.runs[0]) 31 | }) 32 | -------------------------------------------------------------------------------- /expects/this-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "this context": { 3 | "preserves the test context: burning 1 of 3": "passed", 4 | "preserves the test context: burning 2 of 3": "passed", 5 | "preserves the test context: burning 3 of 3": "passed" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /expects/ts-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "TypeScript spec": { 3 | "loads": "passing", 4 | "loads interfaces": "passing" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /expects/works-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello world": "pending", 3 | "works": "pending", 4 | "works 2 @tag1": "passing", 5 | "works 2 @tag1 @tag2": "passing", 6 | "works @tag2": "pending" 7 | } 8 | -------------------------------------------------------------------------------- /expects/works-and-tag1.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello world": "pending", 3 | "works": "pending", 4 | "works 2 @tag1": "passing", 5 | "works 2 @tag1 @tag2": "passing", 6 | "works @tag2": "pending" 7 | } 8 | -------------------------------------------------------------------------------- /expects/works-hello-no-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello world": "passed", 3 | "works": "passed", 4 | "works 2 @tag1": "pending", 5 | "works 2 @tag1 @tag2": "pending", 6 | "works @tag2": "pending" 7 | } 8 | -------------------------------------------------------------------------------- /images/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cy-grep/fdff3fb9f88137005de8f351b3e16350d108af01/images/config.png -------------------------------------------------------------------------------- /images/debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cy-grep/fdff3fb9f88137005de8f351b3e16350d108af01/images/debug.png -------------------------------------------------------------------------------- /images/includes-pending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cy-grep/fdff3fb9f88137005de8f351b3e16350d108af01/images/includes-pending.png -------------------------------------------------------------------------------- /images/omit-pending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cy-grep/fdff3fb9f88137005de8f351b3e16350d108af01/images/omit-pending.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["cypress"], 4 | "resolveJsonModule": true, 5 | "moduleResolution": "node", 6 | "target": "ES6" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bahmutov/cy-grep", 3 | "version": "0.0.0-development", 4 | "description": "Filter Cypress tests using title or tags", 5 | "main": "src/support.js", 6 | "scripts": { 7 | "cy:run": "cypress run --config specPattern='**/unit.js'", 8 | "cy:open": "cypress open --e2e -b electron --config specPattern='**/unit.js'", 9 | "cy:open:tags": "cypress open --e2e -b electron --config specPattern='cypress/e2e/test-tags/*.cy.js'", 10 | "badges": "npx -p dependency-version-badge update-badge cypress", 11 | "semantic-release": "semantic-release", 12 | "deps": "npm audit --report --omit dev", 13 | "stop-only": "stop-only --folder cypress/e2e" 14 | }, 15 | "dependencies": { 16 | "cypress-plugin-config": "^1.2.0", 17 | "debug": "^4.3.2", 18 | "find-cypress-specs": "^1.35.1", 19 | "find-test-names": "1.29.15", 20 | "globby": "^11.1.0" 21 | }, 22 | "devDependencies": { 23 | "cypress": "14.4.1", 24 | "cypress-each": "^1.11.0", 25 | "cypress-expect": "^3.1.0", 26 | "prettier": "^3.0.0", 27 | "semantic-release": "^24.0.0", 28 | "stop-only": "^3.3.1", 29 | "typescript": "^5.0.0" 30 | }, 31 | "peerDependencies": { 32 | "cypress": ">=8" 33 | }, 34 | "files": [ 35 | "src" 36 | ], 37 | "types": "src/index.d.ts", 38 | "license": "MIT", 39 | "repository": { 40 | "type": "git", 41 | "url": "https://github.com/bahmutov/cy-grep.git" 42 | }, 43 | "homepage": "https://github.com/bahmutov/cy-grep", 44 | "author": "Gleb Bahmutov ", 45 | "bugs": { 46 | "url": "https://github.com/bahmutov/cy-grep/issues" 47 | }, 48 | "keywords": [ 49 | "cypress", 50 | "grep" 51 | ], 52 | "publishConfig": { 53 | "access": "public" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "automerge": true, 4 | "major": { 5 | "automerge": false 6 | }, 7 | "minor": { 8 | "automerge": true 9 | }, 10 | "prConcurrentLimit": 3, 11 | "prHourlyLimit": 2, 12 | "schedule": ["after 10pm and before 5am on every weekday", "every weekend"], 13 | "masterIssue": true, 14 | "labels": ["type: dependencies", "renovate"], 15 | "ignorePaths": ["**/node_modules/**", "**/test*/**"] 16 | } 17 | -------------------------------------------------------------------------------- /src/file-utils.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const globby = require('globby') 4 | const debug = require('debug')('cy-grep') 5 | 6 | function resolveFilePattern(pattern) { 7 | if (pattern.includes('*')) { 8 | const globbyOptions = { 9 | sort: true, 10 | objectMode: false, 11 | } 12 | debug('globby options "%s" %o', pattern, globbyOptions) 13 | 14 | const files = globby.sync(pattern, globbyOptions) 15 | debug('found %d file(s) %o', files.length, files) 16 | return files 17 | } else { 18 | return pattern 19 | } 20 | } 21 | 22 | function resolveFilePatterns(patterns) { 23 | const extraSpecsList = patterns 24 | .split(',') 25 | .map((s) => s.trim()) 26 | .filter(Boolean) 27 | debug('extra specs list %o', extraSpecsList) 28 | 29 | return extraSpecsList.flatMap(resolveFilePattern) 30 | } 31 | 32 | module.exports = { 33 | resolveFilePattern, 34 | resolveFilePatterns, 35 | } 36 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace Cypress { 4 | interface Cypress { 5 | /** 6 | * Runs only the tests that contain the given "grep" string 7 | * in the title, or have specific tags. 8 | * @param grep Part of the test title 9 | * @param tags Tags to filter tests by 10 | * @param burn Number of times to repeat each test 11 | */ 12 | grep?: (grep?: string, tags?: string, burn?: string) => void 13 | /** 14 | * Take the current test statuses and run only the failed tests. 15 | * Run this static method from the DevTools console. 16 | * Tip: use Cypress.grep() to reset and run all tests. 17 | * @example Cypress.grepFailed() 18 | */ 19 | grepFailed?: () => void 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const debug = require('debug')('cy-grep') 3 | 4 | const { getSpecs } = require('find-cypress-specs') 5 | const { getTestNames, findEffectiveTestTags } = require('find-test-names') 6 | const fs = require('fs') 7 | const path = require('path') 8 | const { version } = require('../package.json') 9 | const { parseGrep, shouldTestRun, getMentionedTags } = require('./utils') 10 | const { resolveFilePatterns } = require('./file-utils') 11 | const minimatch = require('minimatch') 12 | 13 | const MINIMATCH_OPTIONS = { dot: true, matchBase: true } 14 | 15 | const isCypressV9 = (config) => !('specPattern' in config) 16 | 17 | function getGrepSettings(config) { 18 | const { env } = config 19 | 20 | debug('cy-grep plugin version %s', version) 21 | debug('Cypress config env object: %o', env) 22 | 23 | const grep = env.grep ? String(env.grep) : undefined 24 | 25 | if (grep) { 26 | console.log('cy-grep: tests with "%s" in their names', grep.trim()) 27 | } 28 | 29 | const grepPrefixAt = env.grepPrefixAt || env['grep-prefix-at'] 30 | 31 | const grepTags = env.grepTags || env['grep-tags'] 32 | 33 | if (grepTags) { 34 | console.log('cy-grep: filtering using tag(s) "%s"', grepTags) 35 | const parsedGrep = parseGrep(null, grepTags, grepPrefixAt) 36 | 37 | debug('parsed grep tags %o', parsedGrep.tags) 38 | } 39 | 40 | const grepBurn = env.grepBurn || env['grep-burn'] || env.burn 41 | 42 | if (grepBurn) { 43 | console.log('cy-grep: running filtered tests %d times', grepBurn) 44 | } 45 | 46 | const grepUntagged = env.grepUntagged || env['grep-untagged'] 47 | 48 | if (grepUntagged) { 49 | console.log('cy-grep: running untagged tests') 50 | } 51 | 52 | const omitFiltered = env.grepOmitFiltered || env['grep-omit-filtered'] 53 | 54 | if (omitFiltered) { 55 | console.log('cy-grep: will omit filtered tests') 56 | } 57 | 58 | const grepFilterSpecs = env.grepFilterSpecs === true 59 | 60 | if (grepPrefixAt) { 61 | console.log('cy-grep: all tags will be forced to start with @') 62 | } 63 | 64 | return { grep, grepTags, grepFilterSpecs, grepPrefixAt } 65 | } 66 | 67 | /** 68 | * Prints the cy-grep environment values if any. 69 | * @param {Cypress.ConfigOptions} config 70 | */ 71 | function cypressGrepPlugin(config) { 72 | if (arguments.length === 0) { 73 | throw new Error( 74 | 'ERROR: forgot the config file, see https://github.com/bahmutov/cy-grep', 75 | ) 76 | } 77 | if (arguments.length > 1) { 78 | throw new Error( 79 | 'ERROR: too many arguments, see https://github.com/bahmutov/cy-grep', 80 | ) 81 | } 82 | 83 | if (!config || !config.env) { 84 | return config 85 | } 86 | 87 | const { grep, grepTags, grepFilterSpecs, grepPrefixAt } = 88 | getGrepSettings(config) 89 | 90 | if (grepFilterSpecs) { 91 | let specFiles = getSpecs(config) 92 | 93 | debug('found %d spec file(s)', specFiles.length) 94 | debug('%o', specFiles) 95 | const specPattern = config.env.grepSpec || config.env.grepSpecs 96 | if (specPattern) { 97 | debug('custom spec pattern: %s', specPattern) 98 | // https://github.com/bahmutov/cy-grep/issues/33 99 | // the user set a custom "--spec <...>" parameter to select specs to run 100 | // so we need to pre-filter all found specFiles 101 | specFiles = specFiles.filter((specFilename) => 102 | minimatch(specFilename, specPattern, MINIMATCH_OPTIONS), 103 | ) 104 | debug('pre-filtered specs %d %o', specFiles.length, specFiles) 105 | } 106 | 107 | let greppedSpecs = [] 108 | 109 | if (grep) { 110 | console.log('cy-grep: filtering specs using "%s" in the title', grep) 111 | const parsedGrep = parseGrep(grep, undefined, grepPrefixAt) 112 | 113 | debug('parsed grep %o', parsedGrep) 114 | greppedSpecs = specFiles.filter((specFile) => { 115 | const text = fs.readFileSync(specFile, { encoding: 'utf8' }) 116 | 117 | try { 118 | const result = getTestNames(text, true) 119 | const testNames = result.fullTestNames 120 | 121 | debug('spec file %s', specFile) 122 | debug('full test names: %o', testNames) 123 | 124 | return testNames.some((name) => { 125 | const shouldRun = shouldTestRun(parsedGrep, name) 126 | 127 | return shouldRun 128 | }) 129 | } catch (err) { 130 | debug(err.message) 131 | debug(err.stack) 132 | console.error('Could not determine test names in file: %s', specFile) 133 | console.error('Will run it to let the grep filter the tests') 134 | 135 | return true 136 | } 137 | }) 138 | 139 | debug('found grep "%s" in %d specs', grep, greppedSpecs.length) 140 | debug('%o', greppedSpecs) 141 | } else if (grepTags) { 142 | const parsedGrep = parseGrep(null, grepTags, grepPrefixAt) 143 | debug('parsed grep tags %o', parsedGrep) 144 | const mentionedTags = getMentionedTags(grepTags, grepPrefixAt) 145 | debug('user mentioned tags %o', mentionedTags) 146 | // unique tags found across all specs we search 147 | const foundTags = new Set() 148 | 149 | greppedSpecs = specFiles.filter((specFile) => { 150 | const text = fs.readFileSync(specFile, { encoding: 'utf8' }) 151 | 152 | try { 153 | const testTags = findEffectiveTestTags(text) 154 | // we get back a single object with keys being full test titles 155 | // and the values being arrays of effective test tags 156 | debug('spec file %s', specFile) 157 | debug('effective test tags %o', testTags) 158 | 159 | // remember all found tags 160 | Object.entries(testTags).forEach(([testTitle, tags]) => { 161 | tags.effectiveTags.forEach((tag) => { 162 | foundTags.add(tag) 163 | }) 164 | tags.requiredTags.forEach((tag) => { 165 | foundTags.add(tag) 166 | }) 167 | }); 168 | 169 | return Object.keys(testTags).some((testTitle) => { 170 | const effectiveTags = testTags[testTitle].effectiveTags 171 | const requiredTags = testTags[testTitle].requiredTags 172 | 173 | return shouldTestRun( 174 | parsedGrep, 175 | undefined, 176 | effectiveTags, 177 | false, 178 | requiredTags, 179 | ) 180 | }) 181 | } catch (err) { 182 | console.error('Could not determine test names in file: %s', specFile) 183 | console.error('Will run it to let the grep filter the tests') 184 | 185 | return true 186 | } 187 | }) 188 | 189 | debug('found grep tags "%s" in %d specs', grepTags, greppedSpecs.length) 190 | debug('%o', greppedSpecs) 191 | 192 | debug('all found tags across the specs %o', ...foundTags) 193 | debug('user mentioned tags %o', mentionedTags) 194 | mentionedTags.forEach((tag) => { 195 | if (!foundTags.has(tag)) { 196 | console.warn( 197 | 'cy-grep: could not find the tag "%s" in any of the specs', 198 | tag, 199 | ) 200 | } 201 | }) 202 | } else { 203 | // we have no tags to grep 204 | debug('will try eliminating specs with required tags') 205 | 206 | greppedSpecs = specFiles.filter((specFile) => { 207 | const text = fs.readFileSync(specFile, { encoding: 'utf8' }) 208 | 209 | try { 210 | const testTags = findEffectiveTestTags(text) 211 | debug('spec file %s', specFile) 212 | debug('effective test tags %o', testTags) 213 | // eliminate all tests with required tags, since we have no tags right now 214 | const testsWithoutRequiredTags = Object.keys(testTags).filter( 215 | (testTitle) => { 216 | return testTags[testTitle].requiredTags.length === 0 217 | }, 218 | ) 219 | // if there are any tests remaining, we should run this spec 220 | // (we should not run empty specs where all tests have required tags) 221 | return testsWithoutRequiredTags.length 222 | } catch (err) { 223 | console.error('Could not determine test names in file: %s', specFile) 224 | console.error('Will run it to let the grep filter the tests') 225 | 226 | return true 227 | } 228 | }) 229 | } 230 | 231 | const extraSpecsPattern = config.env.grepExtraSpecs 232 | if (extraSpecsPattern) { 233 | debug('processing the extra specs pattern "%s"', extraSpecsPattern) 234 | const extraSpecs = resolveFilePatterns(extraSpecsPattern) 235 | // update the config env object with resolved extra specs 236 | const resolvedExtraSpecs = [] 237 | extraSpecs.forEach((specFilename) => { 238 | if (!greppedSpecs.includes(specFilename)) { 239 | greppedSpecs.push(specFilename) 240 | resolvedExtraSpecs.push(specFilename) 241 | debug('added extra spec %s', specFilename) 242 | } 243 | }) 244 | 245 | config.env.grepExtraSpecs = resolvedExtraSpecs 246 | } 247 | 248 | if (greppedSpecs.length) { 249 | if (isCypressV9(config)) { 250 | debug('setting selected %d specs (< v10)', greppedSpecs.length) 251 | // @ts-ignore 252 | const integrationFolder = config.integrationFolder 253 | const relativeNames = greppedSpecs.map((filename) => 254 | path.relative(integrationFolder, filename), 255 | ) 256 | const relativeSpecs = relativeNames.join(', ') 257 | debug( 258 | 'specs in the integration folder %s %s', 259 | integrationFolder, 260 | relativeSpecs, 261 | ) 262 | // @ts-ignore 263 | config.testFiles = relativeNames 264 | } else { 265 | debug('setting selected %d specs (>= v10)', greppedSpecs.length) 266 | // @ts-ignore 267 | config.specPattern = greppedSpecs 268 | } 269 | } else { 270 | // hmm, we filtered out all specs, probably something is wrong 271 | console.warn('cy-grep: grep and/or grepTags has eliminated all specs') 272 | grep ? console.warn('cy-grep: title: %s', grep) : null 273 | grepTags ? console.warn('cy-grep: tags: %s', grepTags) : null 274 | console.warn('cy-grep: Will leave all specs to run to filter at run-time') 275 | } 276 | } 277 | 278 | return config 279 | } 280 | 281 | module.exports = cypressGrepPlugin 282 | -------------------------------------------------------------------------------- /src/support.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /// 3 | 4 | const { parseGrep, shouldTestRun } = require('./utils') 5 | // @ts-ignore 6 | const { version } = require('../package.json') 7 | const { 8 | getPluginConfigValue, 9 | setPluginConfigValue, 10 | } = require('cypress-plugin-config') 11 | // to debug in the browser, set the "localStorage.debug='cy-grep'" 12 | const debug = require('debug')('cy-grep') 13 | 14 | debug.log = console.info.bind(console) 15 | 16 | // preserve the real "it" function 17 | const _it = it 18 | const _describe = describe 19 | // keeps all collected test tags by the full test title 20 | // includes both the test tags and the suite tags 21 | // and the required test tags 22 | const testTree = {} 23 | 24 | beforeEach(() => { 25 | // set the test tags for the current test 26 | const testTitle = Cypress.currentTest.titlePath.join(' ') 27 | const info = testTree[testTitle] 28 | if (info) { 29 | const allTags = info.effectiveTestTags.concat(info.requiredTestTags) 30 | Cypress.env('testTags', allTags) 31 | } else { 32 | Cypress.env('testTags', null) 33 | } 34 | }) 35 | 36 | /** 37 | * Wraps the "it" and "describe" functions that support tags. 38 | * @see https://github.com/bahmutov/cy-grep 39 | */ 40 | function registerCyGrep() { 41 | /** @type {string} Part of the test title go grep */ 42 | let grep = getPluginConfigValue('grep') 43 | 44 | if (grep) { 45 | grep = String(grep).trim() 46 | } 47 | 48 | /** @type {string} Raw tags to grep string */ 49 | const grepTags = 50 | getPluginConfigValue('grepTags') || getPluginConfigValue('grep-tags') 51 | 52 | const burnSpecified = 53 | getPluginConfigValue('grepBurn') || 54 | getPluginConfigValue('grep-burn') || 55 | getPluginConfigValue('burn') 56 | 57 | const grepUntagged = 58 | getPluginConfigValue('grepUntagged') || 59 | getPluginConfigValue('grep-untagged') 60 | 61 | const extraSpecs = getPluginConfigValue('grepExtraSpecs') 62 | 63 | // if (!grep && !grepTags && !burnSpecified && !grepUntagged) { 64 | // nothing to do, the user has no specified the "grep" string 65 | // debug('Nothing to grep, version %s', version) 66 | 67 | // return 68 | // } 69 | 70 | /** @type {number} Number of times to repeat each running test */ 71 | const grepBurn = 72 | getPluginConfigValue('grepBurn') || 73 | getPluginConfigValue('grep-burn') || 74 | getPluginConfigValue('burn') || 75 | 1 76 | 77 | /** @type {boolean} Omit filtered tests completely */ 78 | const omitFiltered = 79 | getPluginConfigValue('grepOmitFiltered') || 80 | getPluginConfigValue('grep-omit-filtered') 81 | 82 | const grepPrefixAt = 83 | getPluginConfigValue('grepPrefixAt') || 84 | getPluginConfigValue('grep-prefix-at') 85 | 86 | debug('grep %o', { 87 | grep, 88 | grepTags, 89 | grepBurn, 90 | omitFiltered, 91 | grepPrefixAt, 92 | version, 93 | }) 94 | if (!Cypress._.isInteger(grepBurn) || grepBurn < 1) { 95 | throw new Error(`Invalid grep burn value: ${grepBurn}`) 96 | } 97 | 98 | const parsedGrep = parseGrep(grep, grepTags, grepPrefixAt) 99 | 100 | debug('parsed grep %o', parsedGrep) 101 | 102 | // prevent multiple registrations 103 | if (it.name === 'itGrep') { 104 | debug('already registered cy-grep') 105 | 106 | return 107 | } 108 | 109 | it = function itGrep(name, options, callback) { 110 | if (typeof options === 'function') { 111 | // the test has format it('...', cb) 112 | callback = options 113 | options = {} 114 | } 115 | 116 | if (!callback) { 117 | // the pending test by itself 118 | return _it(name, options) 119 | } 120 | 121 | let configTags = options && options.tags 122 | if (typeof configTags === 'string') { 123 | configTags = [configTags] 124 | } 125 | let configRequiredTags = options && options.requiredTags 126 | if (typeof configRequiredTags === 'string') { 127 | configRequiredTags = [configRequiredTags] 128 | } 129 | 130 | if (extraSpecs?.length && extraSpecs.includes(Cypress.spec.relative)) { 131 | // the user wants to run all tests in this extra spec file 132 | return _it(name, options, callback) 133 | } 134 | 135 | const nameToGrep = suiteStack 136 | .map((item) => item.name) 137 | .concat(name) 138 | .join(' ') 139 | const effectiveTestTags = suiteStack 140 | .flatMap((item) => item.tags) 141 | .concat(configTags) 142 | .filter(Boolean) 143 | const requiredTestTags = suiteStack 144 | .flatMap((item) => item.requiredTags) 145 | .concat(configRequiredTags) 146 | .filter(Boolean) 147 | debug({ nameToGrep, effectiveTestTags, requiredTestTags }) 148 | testTree[nameToGrep] = { effectiveTestTags, requiredTestTags } 149 | 150 | const shouldRun = shouldTestRun( 151 | parsedGrep, 152 | nameToGrep, 153 | effectiveTestTags, 154 | grepUntagged, 155 | requiredTestTags, 156 | ) 157 | 158 | if (effectiveTestTags && effectiveTestTags.length) { 159 | debug( 160 | 'should test "%s" with tags %s run? %s', 161 | name, 162 | effectiveTestTags.join(','), 163 | shouldRun, 164 | ) 165 | } else { 166 | debug('should test without tags "%s" run? %s', nameToGrep, shouldRun) 167 | } 168 | 169 | if (shouldRun) { 170 | if (grepBurn > 1) { 171 | // repeat the same test to make sure it is solid 172 | return Cypress._.times(grepBurn, (k) => { 173 | const fullName = `${name}: burning ${k + 1} of ${grepBurn}` 174 | 175 | _it(fullName, options, callback) 176 | }) 177 | } 178 | 179 | return _it(name, options, callback) 180 | } 181 | 182 | if (omitFiltered) { 183 | // omit the filtered tests completely 184 | // in order to be compatible with Mocha, create fake method and test object 185 | return { 186 | parent: { 187 | appendOnlyTest: () => {}, 188 | }, 189 | } 190 | } 191 | 192 | // skip tests without grep string in their names 193 | return _it.skip(name, options, callback) 194 | } 195 | 196 | // list of "describe" suites for the current test 197 | // when we encounter a new suite, we push it to the stack 198 | // when the "describe" function exits, we pop it 199 | // Thus a test can look up the tags from its parent suites 200 | const suiteStack = [] 201 | 202 | describe = function describeGrep(name, options, callback) { 203 | if (typeof options === 'function') { 204 | // the block has format describe('...', cb) 205 | callback = options 206 | options = {} 207 | } 208 | 209 | const stackItem = { name } 210 | 211 | suiteStack.push(stackItem) 212 | 213 | if (!callback) { 214 | // the pending suite by itself 215 | const result = _describe(name, options) 216 | 217 | suiteStack.pop() 218 | 219 | return result 220 | } 221 | 222 | let configTags = options && options.tags 223 | if (typeof configTags === 'string') { 224 | configTags = [configTags] 225 | } 226 | let requiredTags = options && options.requiredTags 227 | if (typeof requiredTags === 'string') { 228 | requiredTags = [requiredTags] 229 | } 230 | 231 | if (!configTags || !configTags.length) { 232 | if (!requiredTags || !requiredTags.length) { 233 | // if the describe suite does not have explicit tags 234 | // move on, since the tests inside can have their own tags 235 | _describe(name, options, callback) 236 | suiteStack.pop() 237 | 238 | return 239 | } 240 | } 241 | 242 | // when looking at the suite of the tests, I found 243 | // that using the name is quickly becoming very confusing 244 | // and thus we need to use the explicit tags 245 | stackItem.tags = configTags 246 | stackItem.requiredTags = requiredTags 247 | debug('stack item', stackItem) 248 | 249 | _describe(name, options, callback) 250 | suiteStack.pop() 251 | 252 | return 253 | } 254 | 255 | // overwrite "context" which is an alias to "describe" 256 | context = describe 257 | 258 | // overwrite "specify" which is an alias to "it" 259 | specify = it 260 | 261 | // keep the ".skip", ".only" methods the same as before 262 | it.skip = _it.skip 263 | it.only = _it.only 264 | // preserve "it.each" method if found 265 | if (typeof _it.each === 'function') { 266 | it.each = _it.each 267 | } 268 | 269 | describe.skip = _describe.skip 270 | describe.only = _describe.only 271 | if (typeof _describe.each === 'function') { 272 | describe.each = _describe.each 273 | } 274 | } 275 | 276 | function restartTests() { 277 | setTimeout(() => { 278 | window.top.document.querySelector('.reporter .restart').click() 279 | }, 0) 280 | } 281 | 282 | if (!Cypress.grep) { 283 | /** 284 | * A utility method to set the grep and run the tests from 285 | * the DevTools console. Restarts the test runner 286 | * @example 287 | * // run only the tests with "hello w" in the title 288 | * Cypress.grep('hello w') 289 | * // runs only tests tagged both "@smoke" and "@fast" 290 | * Cypress.grep(null, '@smoke+@fast') 291 | * // runs the grepped tests 100 times 292 | * Cypress.grep('add items', null, 100) 293 | * // remove all current grep settings 294 | * // and run all tests 295 | * Cypress.grep() 296 | */ 297 | Cypress.grep = function grep(grep, tags, burn) { 298 | setPluginConfigValue('grep', grep) 299 | setPluginConfigValue('grepTags', tags) 300 | setPluginConfigValue('grepBurn', burn) 301 | // remove any aliased values 302 | setPluginConfigValue('grep-tags', null) 303 | setPluginConfigValue('grep-burn', null) 304 | setPluginConfigValue('burn', null) 305 | 306 | debug('set new grep to "%o" restarting tests', { grep, tags, burn }) 307 | restartTests() 308 | } 309 | } 310 | 311 | if (!Cypress.grepFailed) { 312 | Cypress.grepFailed = function () { 313 | // @ts-ignore 314 | let root = Cypress.state('runnable') 315 | while (root.parent) { 316 | root = root.parent 317 | } 318 | const failedTestTitles = [] 319 | 320 | function findFailedTests(suite) { 321 | suite.tests.forEach((test) => { 322 | if (test.state === 'failed') { 323 | // TODO use the full test title 324 | failedTestTitles.push(test.title) 325 | } 326 | }) 327 | suite.suites.forEach((suite) => { 328 | findFailedTests(suite) 329 | }) 330 | } 331 | findFailedTests(root) 332 | 333 | if (!failedTestTitles.length) { 334 | console.log('No failed tests found') 335 | } else { 336 | console.log('running only the failed tests') 337 | console.log(failedTestTitles) 338 | const grepTitles = failedTestTitles.join(';') 339 | // @ts-ignore 340 | Cypress.grep(grepTitles) 341 | } 342 | } 343 | } 344 | 345 | module.exports = registerCyGrep 346 | -------------------------------------------------------------------------------- /src/tags-are-strings.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // default types for test "tags" and "requiredTags" properties 4 | // tags could be a single string or an array of strings 5 | // see https://github.com/bahmutov/cy-grep?tab=readme-ov-file#typescript-support 6 | 7 | declare namespace Cypress { 8 | interface SuiteConfigOverrides { 9 | /** 10 | * List of tags for this suite 11 | * @example a single tag 12 | * describe('block with config tag', { tags: '@smoke' }, () => {}) 13 | * @example multiple tags 14 | * describe('block with config tag', { tags: ['@smoke', '@slow'] }, () => {}) 15 | * @see https://github.com/bahmutov/cy-grep 16 | */ 17 | tags?: string | string[] 18 | /** 19 | * Provide a tag or a list of tags that is required for this suite to run. 20 | * @example describe('mobile tests', { requiredTags: '@mobile' }, () => {}) 21 | * @see https://github.com/bahmutov/cy-grep 22 | */ 23 | requiredTags?: string | string[] 24 | } 25 | 26 | // specify additional properties in the TestConfig object 27 | // in our case we will add "tags" property 28 | interface TestConfigOverrides { 29 | /** 30 | * List of tags for this test 31 | * @example a single tag 32 | * it('logs in', { tags: '@smoke' }, () => { ... }) 33 | * @example multiple tags 34 | * it('works', { tags: ['@smoke', '@slow'] }, () => { ... }) 35 | * @see https://github.com/bahmutov/cy-grep 36 | */ 37 | tags?: string | string[] 38 | /** 39 | * Provide a tag or a list of tags that is required for this test to run. 40 | * @example it('cleans the data', { requiredTags: '@nightly' }, () => {}) 41 | * @see https://github.com/bahmutov/cy-grep 42 | */ 43 | requiredTags?: string | string[] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | // Universal code - should run in Node or in the browser 4 | 5 | /** 6 | * Parses test title grep string. 7 | * The string can have "-" in front of it to invert the match. 8 | * @param {string} s Input substring of the test title 9 | */ 10 | function parseTitleGrep(s) { 11 | if (!s || typeof s !== 'string') { 12 | return null 13 | } 14 | 15 | s = s.trim() 16 | if (s.startsWith('-')) { 17 | return { 18 | title: s.substring(1), 19 | invert: true, 20 | } 21 | } 22 | 23 | return { 24 | title: s, 25 | invert: false, 26 | } 27 | } 28 | 29 | function parseFullTitleGrep(s) { 30 | if (!s || typeof s !== 'string') { 31 | return [] 32 | } 33 | 34 | // separate each title 35 | return s.split(';').map(parseTitleGrep) 36 | } 37 | 38 | /** 39 | * Parses tags to grep for. 40 | * @param {string|string[]} s Tags string like "@tag1+@tag2", or array of tags 41 | * @param {boolean} grepPrefixAt Prefix all tags with "@" if needed 42 | * @example 43 | * parseTagsGrep('@tag1+@tag2') 44 | * @example 45 | * parseTagsGrep(['@tag1', '@tag2']) 46 | */ 47 | function parseTagsGrep(s, grepPrefixAt = false) { 48 | if (!s) { 49 | return [] 50 | } 51 | 52 | if (Array.isArray(s)) { 53 | s = s.join(',') 54 | } 55 | const explicitNotTags = [] 56 | 57 | // top level split - using space or comma, each part is OR 58 | const ORS = s 59 | .split(/[ ,]/) 60 | // remove any empty tags 61 | .filter(Boolean) 62 | .map((part) => { 63 | // now every part is an AND 64 | if (part.startsWith('--')) { 65 | explicitNotTags.push({ 66 | tag: part.slice(2), 67 | invert: true, 68 | }) 69 | 70 | return 71 | } 72 | 73 | const parsed = part.split('+').map((tag) => { 74 | if (tag.startsWith('-')) { 75 | return { 76 | tag: tag.slice(1), 77 | invert: true, 78 | } 79 | } 80 | 81 | return { 82 | tag, 83 | invert: false, 84 | } 85 | }) 86 | 87 | return parsed 88 | }) 89 | 90 | // filter out undefined from explicit not tags 91 | const ORS_filtered = ORS.filter((x) => x !== undefined) 92 | 93 | if (explicitNotTags.length > 0) { 94 | ORS_filtered.forEach((OR, index) => { 95 | ORS_filtered[index] = OR.concat(explicitNotTags) 96 | }) 97 | 98 | if (ORS_filtered.length === 0) { 99 | ORS_filtered[0] = explicitNotTags 100 | } 101 | } 102 | 103 | if (grepPrefixAt) { 104 | const prefix = '@' 105 | ORS_filtered.forEach((OR) => { 106 | OR.forEach((token) => { 107 | if (token.tag && !token.tag.startsWith(prefix)) { 108 | token.tag = prefix + token.tag 109 | } 110 | }) 111 | }) 112 | } 113 | 114 | return ORS_filtered 115 | } 116 | 117 | /** 118 | * Given a user string of tags to find, with various connectors, 119 | * returns the list of just the tags themselves. Could be used to 120 | * quickly filter test specs or find misspelled tags. 121 | * @param {string} s String of tags passed by the user 122 | * @param {boolean} grepPrefixAt Enforce the `@` character at the start of each tag 123 | * @returns {string[]} list of unique tags 124 | */ 125 | function getMentionedTags(s, grepPrefixAt = false) { 126 | if (!s) { 127 | return [] 128 | } 129 | const spaced = s.replaceAll(/[+,]/g, ' ') 130 | const tags = spaced 131 | .split(' ') 132 | .map((s) => s.trim()) 133 | .filter(Boolean) 134 | // remove any "-" at the start of the tag 135 | // because these are to signal inverted tags 136 | .map((s) => (s.startsWith('-') ? s.slice(1) : s)) 137 | .map((tag) => { 138 | if (grepPrefixAt) { 139 | if (!tag.startsWith('@')) { 140 | return '@' + tag 141 | } 142 | } 143 | 144 | return tag 145 | }) 146 | const uniqueTags = [...new Set(tags)] 147 | return uniqueTags.sort() 148 | } 149 | 150 | function shouldTestRunRequiredTags(parsedGrepTags, requiredTags = []) { 151 | if (!requiredTags.length) { 152 | // there are no tags to check 153 | return true 154 | } 155 | 156 | return requiredTags.every((onlyTag) => { 157 | return parsedGrepTags.some((orPart) => { 158 | return orPart.some((p) => { 159 | return !p.invert && p.tag === onlyTag 160 | }) 161 | }) 162 | }) 163 | } 164 | 165 | function shouldTestRunTags(parsedGrepTags, tags = []) { 166 | if (!parsedGrepTags.length) { 167 | // there are no parsed tags to search for, the test should run 168 | return true 169 | } 170 | 171 | // now the test has tags and the parsed tags are present 172 | 173 | // top levels are OR 174 | const onePartMatched = parsedGrepTags.some((orPart) => { 175 | const everyAndPartMatched = orPart.every((p) => { 176 | if (p.invert) { 177 | return !tags.includes(p.tag) 178 | } 179 | 180 | return tags.includes(p.tag) 181 | }) 182 | // console.log('every part matched %o?', orPart, everyAndPartMatched) 183 | 184 | return everyAndPartMatched 185 | }) 186 | 187 | // console.log('onePartMatched', onePartMatched) 188 | return onePartMatched 189 | } 190 | 191 | function shouldTestRunTitle(parsedGrep, testName) { 192 | if (!testName) { 193 | // if there is no title, let it run 194 | return true 195 | } 196 | 197 | if (!parsedGrep) { 198 | return true 199 | } 200 | 201 | if (!Array.isArray(parsedGrep)) { 202 | console.error('Invalid parsed title grep') 203 | console.error(parsedGrep) 204 | throw new Error('Expected title grep to be an array') 205 | } 206 | 207 | if (!parsedGrep.length) { 208 | return true 209 | } 210 | 211 | const inverted = parsedGrep.filter((g) => g.invert) 212 | const straight = parsedGrep.filter((g) => !g.invert) 213 | 214 | return ( 215 | inverted.every((titleGrep) => !testName.includes(titleGrep.title)) && 216 | (!straight.length || 217 | straight.some((titleGrep) => testName.includes(titleGrep.title))) 218 | ) 219 | } 220 | 221 | // note: tags take precedence over the test name 222 | /** 223 | * Returns boolean if the test with the given name and effective tags 224 | * should run, given the runtime grep (parsed) structure. 225 | * @param {string|undefined} testName The full test title 226 | * @param {string[]} tags The effective test tags 227 | * @param {string[]} requiredTags The effective "required" test tags 228 | */ 229 | function shouldTestRun( 230 | parsedGrep, 231 | testName, 232 | tags = [], 233 | grepUntagged = false, 234 | requiredTags = [], 235 | ) { 236 | if (grepUntagged) { 237 | return !tags.length 238 | } 239 | 240 | if (Array.isArray(testName)) { 241 | // the caller passed tags only, no test name 242 | tags = testName 243 | testName = undefined 244 | } 245 | 246 | const combinedTagsAndRequiredTags = [...tags, ...requiredTags] 247 | 248 | return ( 249 | shouldTestRunTitle(parsedGrep.title, testName) && 250 | shouldTestRunTags(parsedGrep.tags, combinedTagsAndRequiredTags) && 251 | shouldTestRunRequiredTags(parsedGrep.tags, requiredTags) 252 | ) 253 | } 254 | 255 | function parseGrep(titlePart, tags, grepPrefixAt) { 256 | return { 257 | title: parseFullTitleGrep(titlePart), 258 | tags: parseTagsGrep(tags, grepPrefixAt), 259 | } 260 | } 261 | 262 | module.exports = { 263 | parseGrep, 264 | parseTitleGrep, 265 | parseFullTitleGrep, 266 | parseTagsGrep, 267 | shouldTestRun, 268 | shouldTestRunTags, 269 | shouldTestRunRequiredTags, 270 | shouldTestRunTitle, 271 | getMentionedTags, 272 | } 273 | -------------------------------------------------------------------------------- /test-cy-v9-required-tags/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "fixturesFolder": false, 3 | "video": false, 4 | "env": { 5 | "grepOmitFiltered": true, 6 | "grepFilterSpecs": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test-cy-v9-required-tags/cypress/integration/spec1.js: -------------------------------------------------------------------------------- 1 | describe('spec1', () => { 2 | it('first test', { requiredTags: '@misc' }, () => {}) 3 | 4 | it('second test', () => {}) 5 | }) 6 | -------------------------------------------------------------------------------- /test-cy-v9-required-tags/cypress/integration/spec2.js: -------------------------------------------------------------------------------- 1 | describe('spec2', { requiredTags: '@second' }, () => { 2 | it('test A') 3 | 4 | it('test B', () => {}) 5 | }) 6 | -------------------------------------------------------------------------------- /test-cy-v9-required-tags/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (on, config) => { 2 | // `on` is used to hook into various events Cypress emits 3 | // `config` is the resolved Cypress config 4 | require('../../../src/plugin')(config) 5 | // IMPORTANT: return the config object 6 | return config 7 | } 8 | -------------------------------------------------------------------------------- /test-cy-v9-required-tags/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | require('../../..')() 2 | -------------------------------------------------------------------------------- /test-cy-v9-required-tags/expects/misc.json: -------------------------------------------------------------------------------- 1 | { 2 | "cypress": { 3 | "integration": { 4 | "spec1.js": { 5 | "spec1": { 6 | "first test": "pass" 7 | } 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test-cy-v9-required-tags/expects/second.json: -------------------------------------------------------------------------------- 1 | { 2 | "cypress": { 3 | "integration": { 4 | "spec2.js": { 5 | "spec2": { 6 | "test A": "pending", 7 | "test B": "pass" 8 | } 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test-cy-v9-required-tags/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-cy-v9-required-tags", 3 | "version": "1.0.0", 4 | "description": "cy-grep with Cypress v9 and required tags", 5 | "private": true, 6 | "scripts": { 7 | "test": "DEBUG=cy-grep cypress run --env grepTags=@misc", 8 | "test-required-tag-test": "npx cypress-expect run --env grepTags=@misc --expect-exactly expects/misc.json", 9 | "test-required-tag-suite": "DEBUG=cy-grep npx cypress-expect run --env grepTags=@second --expect-exactly expects/second.json", 10 | "open": "cypress open" 11 | }, 12 | "devDependencies": { 13 | "cypress": "^9.7.0", 14 | "cypress-expect": "^3.1.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test-cy-v9/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "fixturesFolder": false, 3 | "video": false, 4 | "env": { 5 | "grepOmitFiltered": true, 6 | "grepFilterSpecs": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test-cy-v9/cypress/integration/spec1.js: -------------------------------------------------------------------------------- 1 | describe('spec1', () => { 2 | it('first test', { tags: '@misc' }, () => {}) 3 | 4 | it('second test', () => {}) 5 | }) 6 | -------------------------------------------------------------------------------- /test-cy-v9/cypress/integration/spec2.js: -------------------------------------------------------------------------------- 1 | describe('spec2', () => { 2 | it('test A') 3 | 4 | it('test B', () => {}) 5 | }) 6 | -------------------------------------------------------------------------------- /test-cy-v9/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (on, config) => { 2 | // `on` is used to hook into various events Cypress emits 3 | // `config` is the resolved Cypress config 4 | require('../../../src/plugin')(config) 5 | // IMPORTANT: return the config object 6 | return config 7 | } 8 | -------------------------------------------------------------------------------- /test-cy-v9/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | require('../../..')() 2 | -------------------------------------------------------------------------------- /test-cy-v9/expects/both.json: -------------------------------------------------------------------------------- 1 | { 2 | "cypress": { 3 | "integration": { 4 | "spec1.js": { 5 | "spec1": { 6 | "first test": "pass", 7 | "second test": "pass" 8 | } 9 | }, 10 | "spec2.js": { 11 | "spec2": { 12 | "test A": "pending", 13 | "test B": "pass" 14 | } 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test-cy-v9/expects/misc.json: -------------------------------------------------------------------------------- 1 | { 2 | "cypress": { 3 | "integration": { 4 | "spec1.js": { 5 | "spec1": { 6 | "first test": "pass" 7 | } 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test-cy-v9/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-cy-v9", 3 | "version": "1.0.0", 4 | "description": "cy-grep with Cypress v9", 5 | "private": true, 6 | "scripts": { 7 | "test": "DEBUG=cy-grep cypress run --env grepTags=@misc", 8 | "test-expects": "npx cypress-expect run --env grepTags=@misc --expect-exactly expects/misc.json", 9 | "test-expects-spec": "npx cypress-expect run --env grepTags=@misc --spec cypress/integration/spec1.js --expect-exactly expects/misc.json", 10 | "test-expects-both": "npx cypress-expect run --expect-exactly expects/both.json", 11 | "open": "cypress open" 12 | }, 13 | "devDependencies": { 14 | "cypress": "^9.7.0", 15 | "cypress-expect": "^3.1.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/required-tags-only/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | env: { grepFilterSpecs: true, grepOmitFiltered: true }, 5 | e2e: { 6 | defaultCommandTimeout: 1000, 7 | setupNodeEvents(on, config) { 8 | require('../../src/plugin')(config) 9 | return config 10 | }, 11 | specPattern: 'e2e/*.cy.js', 12 | supportFile: './support/e2e.js', 13 | }, 14 | fixturesFolder: false, 15 | video: false, 16 | }) 17 | -------------------------------------------------------------------------------- /tests/required-tags-only/e2e/clean.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('clean', () => { 4 | it('old data', { requiredTags: '@nightly' }, () => { 5 | // pretend we are deleting old test data 6 | cy.wait(5000) 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /tests/required-tags-only/e2e/scrape.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('scrape', () => { 4 | it('scrapes the blog', { requiredTags: '@scrape' }, () => { 5 | // pretend we are scraping the blog posts 6 | cy.wait(5000) 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /tests/required-tags-only/e2e/spec-a.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('spec A', () => { 4 | it('works', () => {}) 5 | 6 | it('works some more', () => {}) 7 | }) 8 | -------------------------------------------------------------------------------- /tests/required-tags-only/e2e/spec-b.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('spec B', () => { 4 | it('works', () => {}) 5 | 6 | it('works some more', () => {}) 7 | }) 8 | -------------------------------------------------------------------------------- /tests/required-tags-only/expect-scrape.json: -------------------------------------------------------------------------------- 1 | { 2 | "e2e": { 3 | "scrape.cy.js": { 4 | "scrape": { 5 | "scrapes the blog": "pass" 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/required-tags-only/expect-skip.json: -------------------------------------------------------------------------------- 1 | { 2 | "e2e": { 3 | "spec-a.cy.js": { 4 | "spec A": { 5 | "works": "pass", 6 | "works some more": "pass" 7 | } 8 | }, 9 | "spec-b.cy.js": { 10 | "spec B": { 11 | "works": "pass", 12 | "works some more": "pass" 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/required-tags-only/support/e2e.js: -------------------------------------------------------------------------------- 1 | import registerGrep from '../../../src/support' 2 | registerGrep() 3 | -------------------------------------------------------------------------------- /tests/required-tags-suite/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | env: { grepFilterSpecs: true, grepOmitFiltered: true }, 5 | e2e: { 6 | defaultCommandTimeout: 1000, 7 | setupNodeEvents(on, config) { 8 | require('../../src/plugin')(config) 9 | return config 10 | }, 11 | specPattern: 'e2e/*.cy.js', 12 | supportFile: './support/e2e.js', 13 | }, 14 | fixturesFolder: false, 15 | video: false, 16 | }) 17 | -------------------------------------------------------------------------------- /tests/required-tags-suite/e2e/scrape.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // the required tag should apply to both tests inside 4 | describe('scrape', { requiredTags: '@scrape' }, () => { 5 | it('scrapes the blog 1', () => { 6 | // pretend we are scraping the blog posts 7 | cy.wait(1000) 8 | }) 9 | 10 | it('scrapes the blog 2', () => { 11 | // pretend we are scraping the blog posts 12 | cy.wait(1000) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /tests/required-tags-suite/e2e/spec-a.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('spec A', () => { 4 | it('works', () => {}) 5 | 6 | it('works some more', () => {}) 7 | }) 8 | -------------------------------------------------------------------------------- /tests/required-tags-suite/e2e/spec-b.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('spec B', () => { 4 | it('works', () => {}) 5 | 6 | it('works some more', () => {}) 7 | }) 8 | -------------------------------------------------------------------------------- /tests/required-tags-suite/expect-scrape.json: -------------------------------------------------------------------------------- 1 | { 2 | "e2e": { 3 | "scrape.cy.js": { 4 | "scrape": { 5 | "scrapes the blog 1": "pass", 6 | "scrapes the blog 2": "pass" 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/required-tags-suite/support/e2e.js: -------------------------------------------------------------------------------- 1 | import registerGrep from '../../../src/support' 2 | registerGrep() 3 | -------------------------------------------------------------------------------- /tests/required-tags/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | defaultCommandTimeout: 1000, 6 | setupNodeEvents(on, config) { 7 | require('../../src/plugin')(config) 8 | return config 9 | }, 10 | supportFile: false, 11 | specPattern: 'e2e/*.cy.js', 12 | }, 13 | fixturesFolder: false, 14 | video: false, 15 | }) 16 | -------------------------------------------------------------------------------- /tests/required-tags/e2e/spec.cy.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import registerGrep from '../../../src/support' 4 | registerGrep() 5 | 6 | it('runs always', () => {}) 7 | 8 | // this test should ONLY run when the tag "@special" is on 9 | it('runs only when tag "special" is on', { requiredTags: '@special' }, () => {}) 10 | -------------------------------------------------------------------------------- /tests/required-tags/expect-all.json: -------------------------------------------------------------------------------- 1 | { 2 | "e2e": { 3 | "spec.cy.js": { 4 | "runs always": "pending", 5 | "runs only when tag \"special\" is on": "pending" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/required-tags/expect-with-required-tag.json: -------------------------------------------------------------------------------- 1 | { 2 | "e2e": { 3 | "spec.cy.js": { 4 | "runs always": "pending", 5 | "runs only when tag \"special\" is on": "pass" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/required-tags/expect.json: -------------------------------------------------------------------------------- 1 | { 2 | "e2e": { 3 | "spec.cy.js": { 4 | "runs always": "pass", 5 | "runs only when tag \"special\" is on": "pending" 6 | } 7 | } 8 | } 9 | --------------------------------------------------------------------------------