@bahmutov/cy-grep 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
package/README.md ADDED
@@ -0,0 +1,602 @@
1
+ # @bahmutov/cy-grep ![cypress version](https://img.shields.io/badge/cypress-12.1.0-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
+ Watch the video [intro to cypress-grep plugin](https://www.youtube.com/watch?v=HS-Px-Sghd8)
23
+
24
+ Table of Contents
25
+
26
+ <!-- MarkdownTOC autolink="true" -->
27
+
28
+ - [@bahmutov/cy-grep](#bahmutovcy-grep)
29
+ - [Install](#install)
30
+ - [Support file](#support-file)
31
+ - [Config file](#config-file)
32
+ - [Usage Overview](#usage-overview)
33
+ - [Filter by test title](#filter-by-test-title)
34
+ - [OR substring matching](#or-substring-matching)
35
+ - [Test suites](#test-suites)
36
+ - [Invert filter](#invert-filter)
37
+ - [Filter with tags](#filter-with-tags)
38
+ - [Tags in the test config object](#tags-in-the-test-config-object)
39
+ - [AND tags](#and-tags)
40
+ - [OR tags](#or-tags)
41
+ - [Inverted tags](#inverted-tags)
42
+ - [NOT tags](#not-tags)
43
+ - [Tags in test suites](#tags-in-test-suites)
44
+ - [Grep untagged tests](#grep-untagged-tests)
45
+ - [Pre-filter specs (grepFilterSpecs)](#pre-filter-specs-grepfilterspecs)
46
+ - [Omit filtered tests (grepOmitFiltered)](#omit-filtered-tests-grepomitfiltered)
47
+ - [Disable grep](#disable-grep)
48
+ - [Burn (repeat) tests](#burn-repeat-tests)
49
+ - [TypeScript support](#typescript-support)
50
+ - [General advice](#general-advice)
51
+ - [DevTools console](#devtools-console)
52
+ - [Debugging](#debugging)
53
+ - [Log messages](#log-messages)
54
+ - [Debugging in the plugin](#debugging-in-the-plugin)
55
+ - [Debugging in the browser](#debugging-in-the-browser)
56
+ - [Examples](#examples)
57
+ - [See also](#see-also)
58
+ - [Migration guide](#migration-guide)
59
+ - [from v1 to v2](#from-v1-to-v2)
60
+ - [from v2 to v3](#from-v2-to-v3)
61
+ - [Small Print](#small-print)
62
+
63
+ <!-- /MarkdownTOC -->
64
+
65
+ ## Install
66
+
67
+ Assuming you have Cypress installed, add this module as a dev dependency.
68
+
69
+ ```shell
70
+ # using NPM
71
+ npm i -D @bahmutov/cy-grep
72
+ # using Yarn
73
+ yarn add -D @bahmutov/cy-grep
74
+ ```
75
+
76
+ **Note**: @bahmutov/cy-grep only works with Cypress version >= 10.
77
+
78
+ ### Support file
79
+
80
+ **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:
81
+
82
+ ```js
83
+ // cypress/support/index.js
84
+ // load and register the grep feature using "require" function
85
+ // https://github.com/bahmutov/cy-grep
86
+ const registerCypressGrep = require('@bahmutov/cy-grep')
87
+ registerCypressGrep()
88
+
89
+ // if you want to use the "import" keyword
90
+ // note: `./index.d.ts` currently extends the global Cypress types and
91
+ // does not define `registerCypressGrep` so the import path is directly
92
+ // pointed to the `support.js` file
93
+ import registerCypressGrep from '@bahmutov/cy-grep/src/support'
94
+ registerCypressGrep()
95
+
96
+ // "import" with `@ts-ignore`
97
+ // @see error 2306 https://github.com/microsoft/TypeScript/blob/3fcd1b51a1e6b16d007b368229af03455c7d5794/src/compiler/diagnosticMessages.json#L1635
98
+ // @ts-ignore
99
+ import registerCypressGrep from '@bahmutov/cy-grep'
100
+ registerCypressGrep()
101
+ ```
102
+
103
+ ### Config file
104
+
105
+ **optional:** load and register this module from the [config file](https://docs.cypress.io/guides/references/configuration#setupNodeEvents):
106
+
107
+ ```js
108
+ // cypress.config.js
109
+ {
110
+ e2e: {
111
+ setupNodeEvents(on, config) {
112
+ require('@bahmutov/cy-grep/src/plugin')(config);
113
+ return config;
114
+ },
115
+ }
116
+ }
117
+ ```
118
+
119
+ Installing the plugin via `setupNodeEvents()` is required to enable the [grepFilterSpecs](#grepfilterspecs) feature.
120
+
121
+ ## Usage Overview
122
+
123
+ You can filter tests to run using part of their title via `grep`, and via explicit tags via `grepTags` Cypress environment variables.
124
+
125
+ 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:
126
+
127
+ Here are a few examples:
128
+
129
+ ```shell
130
+ # run only the tests with "auth user" in the title
131
+ $ npx cypress run --env grep="auth user"
132
+ # run tests with "hello" or "auth user" in their titles
133
+ # by separating them with ";" character
134
+ $ npx cypress run --env grep="hello; auth user"
135
+ # run tests tagged @fast
136
+ $ npx cypress run --env grepTags=@fast
137
+ # run only the tests tagged "smoke"
138
+ # that have "login" in their titles
139
+ $ npx cypress run --env grep=login,grepTags=smoke
140
+ # only run the specs that have any tests with "user" in their titles
141
+ $ npx cypress run --env grep=user,grepFilterSpecs=true
142
+ # only run the specs that have any tests tagged "@smoke"
143
+ $ npx cypress run --env grepTags=@smoke,grepFilterSpecs=true
144
+ # run only tests that do not have any tags
145
+ # and are not inside suites that have any tags
146
+ $ npx cypress run --env grepUntagged=true
147
+ ```
148
+
149
+ 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
150
+
151
+ ```json
152
+ {
153
+ "env": {
154
+ "grep": "viewport"
155
+ }
156
+ }
157
+ ```
158
+
159
+ You can also set the `env.grep` object in the plugin file, but remember to return the changed config object:
160
+
161
+ ```js
162
+ // cypress/plugin/index.js
163
+ module.exports = (on, config) => {
164
+ config.env.grep = 'viewport'
165
+ return config
166
+ }
167
+ ```
168
+
169
+ 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).
170
+
171
+ ## Filter by test title
172
+
173
+ ```shell
174
+ # run all tests with "hello" in their title
175
+ $ npx cypress run --env grep=hello
176
+ # run all tests with "hello world" in their title
177
+ $ npx cypress run --env grep="hello world"
178
+ ```
179
+
180
+ ### OR substring matching
181
+
182
+ You can pass multiple title substrings to match separating them with `;` character. Each substring is trimmed.
183
+
184
+ ```shell
185
+ # run all tests with "hello world" or "auth user" in their title
186
+ $ npx cypress run --env grep="hello world; auth user"
187
+ ```
188
+
189
+ ### Test suites
190
+
191
+ The filter is also applied to the "describe" blocks. In that case, the tests look up if any of their outer suites are enabled.
192
+
193
+ ```js
194
+ describe('block for config', () => {
195
+ it('should run', () => {})
196
+
197
+ it('should also work', () => {})
198
+ })
199
+ ```
200
+
201
+ ```
202
+ # run any tests in the blocks including "config"
203
+ --env grep=config
204
+ ```
205
+
206
+ **Note:** global function `describe` and `context` are aliases and both supported by this plugin.
207
+
208
+ ### Invert filter
209
+
210
+ ```shell
211
+ # run all tests WITHOUT "hello world" in their title
212
+ $ npx cypress run --env grep="-hello world"
213
+ # run tests with "hello", but without "world" in the titles
214
+ $ npx cypress run --env grep="hello; -world"
215
+ ```
216
+
217
+ **Note:** Inverted title filter is not compatible with the `grepFilterSpecs` option
218
+
219
+ ## Filter with tags
220
+
221
+ You can select tests to run or skip using tags by passing `--env grepTags=...` value.
222
+
223
+ ```
224
+ # enable the tests with tag "one" or "two"
225
+ --env grepTags="one two"
226
+ # enable the tests with both tags "one" and "two"
227
+ --env grepTags="one+two"
228
+ # enable the tests with "hello" in the title and tag "smoke"
229
+ --env grep=hello,grepTags=smoke
230
+ ```
231
+
232
+ If you can pass commas in the environment variable `grepTags`, you can use `,` to separate the tags
233
+
234
+ ```
235
+ # enable the tests with tag "one" or "two"
236
+ CYPRESS_grepTags=one,two npx cypress run
237
+ ```
238
+
239
+ ### Tags in the test config object
240
+
241
+ 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.
242
+
243
+ ```js
244
+ it('works as an array', { tags: ['config', 'some-other-tag'] }, () => {
245
+ expect(true).to.be.true
246
+ })
247
+
248
+ it('works as a string', { tags: 'config' }, () => {
249
+ expect(true).to.be.true
250
+ })
251
+ ```
252
+
253
+ You can run both of these tests using `--env grepTags=config` string.
254
+
255
+ ### AND tags
256
+
257
+ Use `+` to require both tags to be present
258
+
259
+ ```
260
+ --env grepTags=@smoke+@fast
261
+ ```
262
+
263
+ ### OR tags
264
+
265
+ You can run tests that match one tag or another using spaces. Make sure to quote the grep string!
266
+
267
+ ```
268
+ # run tests with tags "@slow" or "@critical" in their names
269
+ --env grepTags='@slow @critical'
270
+ ```
271
+
272
+ ### Inverted tags
273
+
274
+ You can skip running the tests with specific tag using the invert option: prefix the tag with the character `-`.
275
+
276
+ ```
277
+ # do not run any tests with tag "@slow"
278
+ --env grepTags=-@slow
279
+ ```
280
+
281
+ If you want to run all tests with tag `@slow` but without tag `@smoke`:
282
+
283
+ ```
284
+ --env grepTags=@slow+-@smoke
285
+ ```
286
+
287
+ **Note:** Inverted tag filter is not compatible with the `grepFilterSpecs` option
288
+
289
+ ### NOT tags
290
+
291
+ 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 `--`.
292
+
293
+ Note this is the same as appending `+-<tag to never run>` to each tag. May be useful with large number of tags.
294
+
295
+ If you want to run tests with tags `@slow` or `@regression` but without tag `@smoke`
296
+
297
+ ```
298
+ --env grepTags='@slow @regression --@smoke'
299
+ ```
300
+
301
+ which is equivalent to
302
+
303
+ ```
304
+ --env grepTags='@slow+-@smoke @regression+-@smoke'
305
+ ```
306
+
307
+ ### Tags in test suites
308
+
309
+ The tags are also applied to the "describe" blocks. In that case, the tests look up if any of their outer suites are enabled.
310
+
311
+ ```js
312
+ describe('block with config tag', { tags: '@smoke' }, () => {})
313
+ ```
314
+
315
+ ```
316
+ # run any tests in the blocks having "@smoke" tag
317
+ --env grepTags=@smoke
318
+ # skip any blocks with "@smoke" tag
319
+ --env grepTags=-@smoke
320
+ ```
321
+
322
+ See the [cypress/integration/describe-tags-spec.js](./cypress/integration/describe-tags-spec.js) file.
323
+
324
+ **Note:** global function `describe` and `context` are aliases and both supported by this plugin.
325
+
326
+ ### Grep untagged tests
327
+
328
+ Sometimes you want to run only the tests without any tags, and these tests are inside the describe blocks without any tags.
329
+
330
+ ```
331
+ $ npx cypress run --env grepUntagged=true
332
+ ```
333
+
334
+ ## Pre-filter specs (grepFilterSpecs)
335
+
336
+ 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.
337
+
338
+ ```
339
+ # filter all specs first, and only run the ones with
340
+ # suite or test titles containing the string "it loads"
341
+ $ npx cypress run --env grep="it loads",grepFilterSpecs=true
342
+ # filter all specs files, only run the specs with a tag "@smoke"
343
+ $ npx cypress run --env grepTags=@smoke,grepFilterSpecs=true
344
+ ```
345
+
346
+ **Note 1:** this requires installing this plugin in your project's plugin file, see the [Install](#install).
347
+
348
+ **Note 2:** the `grepFilterSpecs` option is only compatible with the positive `grep` and `grepTags` options, not with the negative (inverted) "-..." filter.
349
+
350
+ **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".
351
+
352
+ **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:
353
+
354
+ ```js
355
+ {
356
+ "env": {
357
+ "grepFilterSpecs": true
358
+ }
359
+ }
360
+ ```
361
+
362
+ ## Omit filtered tests (grepOmitFiltered)
363
+
364
+ 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`.
365
+
366
+ Pending filtered tests
367
+
368
+ ```
369
+ cypress run --env grep="works 2"
370
+ ```
371
+
372
+ ![Pending tests](./images/includes-pending.png)
373
+
374
+ Omit filtered tests
375
+
376
+ ```
377
+ cypress run --env grep="works 2",grepOmitFiltered=true
378
+ ```
379
+
380
+ ![Only running tests remaining](./images/omit-pending.png)
381
+
382
+ **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:
383
+
384
+ ```json
385
+ {
386
+ "env": {
387
+ "grepOmitFiltered": true
388
+ }
389
+ }
390
+ ```
391
+
392
+ ## Disable grep
393
+
394
+ If you specify the `grep` parameters the [config file](https://docs.cypress.io/guides/references/configuration), you can disable it from the command line
395
+
396
+ ```
397
+ $ npx cypress run --env grep=,grepTags=,burn=
398
+ ```
399
+
400
+ ## Burn (repeat) tests
401
+
402
+ You can burn the filtered tests to make sure they are flake-free
403
+
404
+ ```
405
+ npx cypress run --env grep="hello world",burn=5
406
+ ```
407
+
408
+ 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!
409
+
410
+ If you do not specify the "grep" or "grep tags" option, the "burn" will repeat _every_ test.
411
+
412
+ ## TypeScript support
413
+
414
+ 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:
415
+
416
+ ```js
417
+ // @ts-ignore
418
+ it('runs on deploy', { tags: 'smoke' }, () => {
419
+ ...
420
+ })
421
+ ```
422
+
423
+ This package comes with [src/index.d.ts](./src/index.d.ts) definition file that adds the property `tags` to the Cypress test overrides interface. Include this file in your specs or TS config settings. For example, you can load it using a reference comment
424
+
425
+ ```js
426
+ // cypress/integration/my-spec.js
427
+ /// <reference types="@bahmutov/cy-grep" />
428
+ ```
429
+
430
+ If you have `tsconfig.json` file, add this library to the types list
431
+
432
+ ```json
433
+ {
434
+ "compilerOptions": {
435
+ "target": "es5",
436
+ "lib": ["es5", "dom"],
437
+ "types": ["cypress", "@bahmutov/cy-grep"]
438
+ },
439
+ "include": ["**/*.ts"]
440
+ }
441
+ ```
442
+
443
+ ## General advice
444
+
445
+ - keep it simple.
446
+ - I like using `@` as tag prefix to make the tags searchable
447
+
448
+ ```js
449
+ // ✅ good practice
450
+ describe('auth', { tags: '@critical' }, () => ...)
451
+ it('works', { tags: '@smoke' }, () => ...)
452
+ it('works quickly', { tags: ['@smoke', '@fast'] }, () => ...)
453
+
454
+ // 🚨 NOT GOING TO WORK
455
+ // ERROR: treated as a single tag,
456
+ // probably want an array instead
457
+ it('works', { tags: '@smoke @fast' }, () => ...)
458
+ ```
459
+
460
+ Grepping the tests
461
+
462
+ ```shell
463
+ # run the tests by title
464
+ $ npx cypress run --env grep="works quickly"
465
+ # run all tests tagged @smoke
466
+ $ npx cypress run --env grepTags=@smoke
467
+ # run all tests except tagged @smoke
468
+ $ npx cypress run --env grepTags=-@smoke
469
+ # run all tests that have tag @fast but do not have tag @smoke
470
+ $ npx cypress run --env grepTags=@fast+-@smoke
471
+ ```
472
+
473
+ 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).
474
+
475
+ ## DevTools console
476
+
477
+ 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
478
+
479
+ ```js
480
+ // filter tests by title substring
481
+ Cypress.grep('hello world')
482
+ // run filtered tests 100 times
483
+ Cypress.grep('hello world', null, 100)
484
+ // filter tests by tag string
485
+ // in this case will run tests with tag @smoke OR @fast
486
+ Cypress.grep(null, '@smoke @fast')
487
+ // run tests tagged @smoke AND @fast
488
+ Cypress.grep(null, '@smoke+@fast')
489
+ // run tests with title containing "hello" and tag @smoke
490
+ Cypress.grep('hello', '@smoke')
491
+ // run tests with title containing "hello" and tag @smoke 10 times
492
+ Cypress.grep('hello', '@smoke', 10)
493
+ ```
494
+
495
+ - to remove the grep strings enter `Cypress.grep()`
496
+
497
+ ## Debugging
498
+
499
+ 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/cypress-io/cypress-grep/releases).
500
+
501
+ ```
502
+ # get the cypress-grep version using NPM
503
+ $ npm ls cypress-grep
504
+ ...
505
+ └── cypress-grep@2.10.1
506
+ # get the cypress-grep version using Yarn
507
+ $ yarn why cypress-grep
508
+ ...
509
+ => Found "cypress-grep@2.10.1"
510
+ info Has been hoisted to "cypress-grep"
511
+ info This module exists because it's specified in "devDependencies".
512
+ ...
513
+ ```
514
+
515
+ 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
516
+
517
+ ```text
518
+ $ npx cypress open --env grep=works,grepFilterTests=true
519
+ ```
520
+
521
+ Then I expect to see the grep string and the "filter tests" flag in the `env` object.
522
+
523
+ ![Values in the env object](./images/config.png)
524
+
525
+ ### Log messages
526
+
527
+ 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.
528
+
529
+ ### Debugging in the plugin
530
+
531
+ Start Cypress with the environment variable `DEBUG=cypress-grep`. You will see a few messages from this plugin in the terminal output:
532
+
533
+ ```
534
+ $ DEBUG=cypress-grep npx cypress run --env grep=works,grepFilterSpecs=true
535
+ cypress-grep: tests with "works" in their names
536
+ cypress-grep: filtering specs using "works" in the title
537
+ cypress-grep Cypress config env object: { grep: 'works', grepFilterSpecs: true }
538
+ ...
539
+ cypress-grep found 1 spec files +5ms
540
+ cypress-grep [ 'spec.js' ] +0ms
541
+ cypress-grep spec file spec.js +5ms
542
+ cypress-grep suite and test names: [ 'hello world', 'works', 'works 2 @tag1',
543
+ 'works 2 @tag1 @tag2', 'works @tag2' ] +0ms
544
+ cypress-grep found "works" in 1 specs +0ms
545
+ cypress-grep [ 'spec.js' ] +0ms
546
+ ```
547
+
548
+ ### Debugging in the browser
549
+
550
+ To enable debug console messages in the browser, from the DevTools console set `localStorage.debug='cypress-grep'` and run the tests again.
551
+
552
+ ![Debug messages](./images/debug.png)
553
+
554
+ To see how to debug this plugin, watch the video [Debug cypress-grep Plugin](https://youtu.be/4YMAERddHYA).
555
+
556
+ ## Examples
557
+
558
+ - [cypress-grep-example](https://github.com/bahmutov/cypress-grep-example)
559
+ - [todo-graphql-example](https://github.com/bahmutov/todo-graphql-example)
560
+
561
+ ## See also
562
+
563
+ - [cypress-select-tests](https://github.com/bahmutov/cypress-select-tests)
564
+ - [cypress-skip-test](https://github.com/cypress-io/cypress-skip-test)
565
+
566
+ ## Migration guide
567
+
568
+ ### from v1 to v2
569
+
570
+ In v2 we have separated grepping by part of the title string from tags.
571
+
572
+ **v1**
573
+
574
+ ```
575
+ --env grep="one two"
576
+ ```
577
+
578
+ The above scenario was confusing - did you want to find all tests with title containing "one two" or did you want to run tests tagged `one` or `two`?
579
+
580
+ **v2**
581
+
582
+ ```
583
+ # enable the tests with string "one two" in their titles
584
+ --env grep="one two"
585
+ # enable the tests with tag "one" or "two"
586
+ --env grepTags="one two"
587
+ # enable the tests with both tags "one" and "two"
588
+ --env grepTags="one+two"
589
+ # enable the tests with "hello" in the title and tag "smoke"
590
+ --env grep=hello,grepTags=smoke
591
+ ```
592
+
593
+ ### from v2 to v3
594
+
595
+ Version >= 3 of cypress-grep _only_ supports Cypress >= 10.
596
+
597
+ ## Small Print
598
+
599
+ License: MIT - do anything with the code, but don't blame me if it does not work.
600
+
601
+ Support: if you find any problems with this module, email / tweet /
602
+ [open issue](https://github.com/bahmutov/cy-grep/issues) on Github
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@bahmutov/cy-grep",
3
+ "version": "1.0.0",
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
+ "badges": "npx -p dependency-version-badge update-badge cypress",
10
+ "semantic-release": "semantic-release"
11
+ },
12
+ "dependencies": {
13
+ "cypress-plugin-config": "^1.2.0",
14
+ "debug": "^4.3.2",
15
+ "find-test-names": "^1.22.2",
16
+ "globby": "^11.0.4"
17
+ },
18
+ "devDependencies": {
19
+ "cypress": "12.1.0",
20
+ "cypress-each": "^1.11.0",
21
+ "cypress-expect": "^2.5.3",
22
+ "prettier": "^2.8.1",
23
+ "semantic-release": "^19.0.5",
24
+ "typescript": "^4.7.4"
25
+ },
26
+ "peerDependencies": {
27
+ "cypress": ">=10"
28
+ },
29
+ "files": [
30
+ "src"
31
+ ],
32
+ "types": "src/index.d.ts",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/bahmutov/cy-grep.git"
37
+ },
38
+ "homepage": "https://github.com/bahmutov/cy-grep",
39
+ "author": "Gleb Bahmutov <gleb.bahmutov@gmail.com>",
40
+ "bugs": {
41
+ "url": "https://github.com/bahmutov/cy-grep/issues"
42
+ },
43
+ "keywords": [
44
+ "cypress",
45
+ "grep"
46
+ ],
47
+ "publishConfig": {
48
+ "access": "public"
49
+ }
50
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ /// <reference types="cypress" />
2
+
3
+ declare namespace Cypress {
4
+ interface SuiteConfigOverrides {
5
+ /**
6
+ * List of tags for this suite
7
+ * @example a single tag
8
+ * describe('block with config tag', { tags: '@smoke' }, () => {})
9
+ * @example multiple tags
10
+ * describe('block with config tag', { tags: ['@smoke', '@slow'] }, () => {})
11
+ */
12
+ tags?: string | string[]
13
+ }
14
+
15
+ // specify additional properties in the TestConfig object
16
+ // in our case we will add "tags" property
17
+ interface TestConfigOverrides {
18
+ /**
19
+ * List of tags for this test
20
+ * @example a single tag
21
+ * it('logs in', { tags: '@smoke' }, () => { ... })
22
+ * @example multiple tags
23
+ * it('works', { tags: ['@smoke', '@slow'] }, () => { ... })
24
+ */
25
+ tags?: string | string[]
26
+ }
27
+
28
+ interface Cypress {
29
+ grep?: (grep?: string, tags?: string, burn?: string) => void
30
+ }
31
+ }
package/src/plugin.js ADDED
@@ -0,0 +1,156 @@
1
+ const debug = require('debug')('cypress-grep')
2
+ const globby = require('globby')
3
+ const { getTestNames } = require('find-test-names')
4
+ const fs = require('fs')
5
+ const { version } = require('../package.json')
6
+ const { parseGrep, shouldTestRun } = require('./utils')
7
+
8
+ /**
9
+ * Prints the cypress-grep environment values if any.
10
+ * @param {Cypress.ConfigOptions} config
11
+ */
12
+ function cypressGrepPlugin (config) {
13
+ if (!config || !config.env) {
14
+ return config
15
+ }
16
+
17
+ const { env } = config
18
+
19
+ if (!config.specPattern) {
20
+ throw new Error(
21
+ 'Incompatible versions detected, cypress-grep 3.0.0+ requires Cypress 10.0.0+',
22
+ )
23
+ }
24
+
25
+ debug('cypress-grep plugin version %s', version)
26
+ debug('Cypress config env object: %o', env)
27
+
28
+ const grep = env.grep ? String(env.grep) : undefined
29
+
30
+ if (grep) {
31
+ console.log('cypress-grep: tests with "%s" in their names', grep.trim())
32
+ }
33
+
34
+ const grepTags = env.grepTags || env['grep-tags']
35
+
36
+ if (grepTags) {
37
+ console.log('cypress-grep: filtering using tag(s) "%s"', grepTags)
38
+ const parsedGrep = parseGrep(null, grepTags)
39
+
40
+ debug('parsed grep tags %o', parsedGrep.tags)
41
+ }
42
+
43
+ const grepBurn = env.grepBurn || env['grep-burn'] || env.burn
44
+
45
+ if (grepBurn) {
46
+ console.log('cypress-grep: running filtered tests %d times', grepBurn)
47
+ }
48
+
49
+ const grepUntagged = env.grepUntagged || env['grep-untagged']
50
+
51
+ if (grepUntagged) {
52
+ console.log('cypress-grep: running untagged tests')
53
+ }
54
+
55
+ const omitFiltered = env.grepOmitFiltered || env['grep-omit-filtered']
56
+
57
+ if (omitFiltered) {
58
+ console.log('cypress-grep: will omit filtered tests')
59
+ }
60
+
61
+ const { specPattern, excludeSpecPattern } = config
62
+ const integrationFolder = env.grepIntegrationFolder || process.cwd()
63
+
64
+ const grepFilterSpecs = env.grepFilterSpecs === true
65
+
66
+ if (grepFilterSpecs) {
67
+ debug('specPattern', specPattern)
68
+ debug('excludeSpecPattern', excludeSpecPattern)
69
+ debug('integrationFolder', integrationFolder)
70
+ const specFiles = globby.sync(specPattern, {
71
+ cwd: integrationFolder,
72
+ ignore: excludeSpecPattern,
73
+ absolute: true,
74
+ })
75
+
76
+ debug('found %d spec files', specFiles.length)
77
+ debug('%o', specFiles)
78
+ let greppedSpecs = []
79
+
80
+ if (grep) {
81
+ console.log('cypress-grep: filtering specs using "%s" in the title', grep)
82
+ const parsedGrep = parseGrep(grep)
83
+
84
+ debug('parsed grep %o', parsedGrep)
85
+ greppedSpecs = specFiles.filter((specFile) => {
86
+ const text = fs.readFileSync(specFile, { encoding: 'utf8' })
87
+
88
+ try {
89
+ const names = getTestNames(text)
90
+ const testAndSuiteNames = names.suiteNames.concat(names.testNames)
91
+
92
+ debug('spec file %s', specFile)
93
+ debug('suite and test names: %o', testAndSuiteNames)
94
+
95
+ return testAndSuiteNames.some((name) => {
96
+ const shouldRun = shouldTestRun(parsedGrep, name)
97
+
98
+ return shouldRun
99
+ })
100
+ } catch (err) {
101
+ debug(err.message)
102
+ debug(err.stack)
103
+ console.error('Could not determine test names in file: %s', specFile)
104
+ console.error('Will run it to let the grep filter the tests')
105
+
106
+ return true
107
+ }
108
+ })
109
+
110
+ debug('found grep "%s" in %d specs', grep, greppedSpecs.length)
111
+ debug('%o', greppedSpecs)
112
+ } else if (grepTags) {
113
+ const parsedGrep = parseGrep(null, grepTags)
114
+
115
+ debug('parsed grep tags %o', parsedGrep)
116
+ greppedSpecs = specFiles.filter((specFile) => {
117
+ const text = fs.readFileSync(specFile, { encoding: 'utf8' })
118
+
119
+ try {
120
+ const testInfo = getTestNames(text)
121
+
122
+ debug('spec file %s', specFile)
123
+ debug('test info: %o', testInfo.tests)
124
+
125
+ return testInfo.tests.some((info) => {
126
+ const shouldRun = shouldTestRun(parsedGrep, null, info.tags)
127
+
128
+ return shouldRun
129
+ })
130
+ } catch (err) {
131
+ console.error('Could not determine test names in file: %s', specFile)
132
+ console.error('Will run it to let the grep filter the tests')
133
+
134
+ return true
135
+ }
136
+ })
137
+
138
+ debug('found grep tags "%s" in %d specs', grepTags, greppedSpecs.length)
139
+ debug('%o', greppedSpecs)
140
+ }
141
+
142
+ if (greppedSpecs.length) {
143
+ config.specPattern = greppedSpecs
144
+ } else {
145
+ // hmm, we filtered out all specs, probably something is wrong
146
+ console.warn('grep and/or grepTags has eliminated all specs')
147
+ grep ? console.warn('grep: %s', grep) : null
148
+ grepTags ? console.warn('grepTags: %s', grepTags) : null
149
+ console.warn('Will leave all specs to run to filter at run-time')
150
+ }
151
+ }
152
+
153
+ return config
154
+ }
155
+
156
+ module.exports = cypressGrepPlugin
package/src/support.js ADDED
@@ -0,0 +1,256 @@
1
+ // @ts-check
2
+ /// <reference path="./index.d.ts" />
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
+ const debug = require('debug')('@bahmutov/cy-grep')
12
+
13
+ debug.log = console.info.bind(console)
14
+
15
+ // preserve the real "it" function
16
+ const _it = it
17
+ const _describe = describe
18
+
19
+ /**
20
+ * Wraps the "it" and "describe" functions that support tags.
21
+ * @see https://github.com/cypress-io/cypress-grep
22
+ */
23
+ function cypressGrep() {
24
+ /** @type {string} Part of the test title go grep */
25
+ let grep = getPluginConfigValue('grep')
26
+
27
+ if (grep) {
28
+ grep = String(grep).trim()
29
+ }
30
+
31
+ /** @type {string} Raw tags to grep string */
32
+ const grepTags =
33
+ getPluginConfigValue('grepTags') || getPluginConfigValue('grep-tags')
34
+
35
+ const burnSpecified =
36
+ getPluginConfigValue('grepBurn') ||
37
+ getPluginConfigValue('grep-burn') ||
38
+ getPluginConfigValue('burn')
39
+
40
+ const grepUntagged =
41
+ getPluginConfigValue('grepUntagged') ||
42
+ getPluginConfigValue('grep-untagged')
43
+
44
+ if (!grep && !grepTags && !burnSpecified && !grepUntagged) {
45
+ // nothing to do, the user has no specified the "grep" string
46
+ debug('Nothing to grep, version %s', version)
47
+
48
+ return
49
+ }
50
+
51
+ /** @type {number} Number of times to repeat each running test */
52
+ const grepBurn =
53
+ getPluginConfigValue('grepBurn') ||
54
+ getPluginConfigValue('grep-burn') ||
55
+ getPluginConfigValue('burn') ||
56
+ 1
57
+
58
+ /** @type {boolean} Omit filtered tests completely */
59
+ const omitFiltered =
60
+ getPluginConfigValue('grepOmitFiltered') ||
61
+ getPluginConfigValue('grep-omit-filtered')
62
+
63
+ debug('grep %o', { grep, grepTags, grepBurn, omitFiltered, version })
64
+ if (!Cypress._.isInteger(grepBurn) || grepBurn < 1) {
65
+ throw new Error(`Invalid grep burn value: ${grepBurn}`)
66
+ }
67
+
68
+ const parsedGrep = parseGrep(grep, grepTags)
69
+
70
+ debug('parsed grep %o', parsedGrep)
71
+
72
+ // prevent multiple registrations
73
+ // https://github.com/cypress-io/cypress-grep/issues/59
74
+ if (it.name === 'itGrep') {
75
+ debug('already registered cypress-grep')
76
+
77
+ return
78
+ }
79
+
80
+ it = function itGrep(name, options, callback) {
81
+ if (typeof options === 'function') {
82
+ // the test has format it('...', cb)
83
+ callback = options
84
+ options = {}
85
+ }
86
+
87
+ if (!callback) {
88
+ // the pending test by itself
89
+ return _it(name, options)
90
+ }
91
+
92
+ let configTags = options && options.tags
93
+
94
+ if (typeof configTags === 'string') {
95
+ configTags = [configTags]
96
+ }
97
+
98
+ const nameToGrep = suiteStack
99
+ .map((item) => item.name)
100
+ .concat(name)
101
+ .join(' ')
102
+ const tagsToGrep = suiteStack
103
+ .flatMap((item) => item.tags)
104
+ .concat(configTags)
105
+ .filter(Boolean)
106
+
107
+ const shouldRun = shouldTestRun(
108
+ parsedGrep,
109
+ nameToGrep,
110
+ tagsToGrep,
111
+ grepUntagged,
112
+ )
113
+
114
+ if (tagsToGrep && tagsToGrep.length) {
115
+ debug(
116
+ 'should test "%s" with tags %s run? %s',
117
+ name,
118
+ tagsToGrep.join(','),
119
+ shouldRun,
120
+ )
121
+ } else {
122
+ debug('should test "%s" run? %s', nameToGrep, shouldRun)
123
+ }
124
+
125
+ if (shouldRun) {
126
+ if (grepBurn > 1) {
127
+ // repeat the same test to make sure it is solid
128
+ return Cypress._.times(grepBurn, (k) => {
129
+ const fullName = `${name}: burning ${k + 1} of ${grepBurn}`
130
+
131
+ _it(fullName, options, callback)
132
+ })
133
+ }
134
+
135
+ return _it(name, options, callback)
136
+ }
137
+
138
+ if (omitFiltered) {
139
+ // omit the filtered tests completely
140
+ return
141
+ }
142
+
143
+ // skip tests without grep string in their names
144
+ return _it.skip(name, options, callback)
145
+ }
146
+
147
+ // list of "describe" suites for the current test
148
+ // when we encounter a new suite, we push it to the stack
149
+ // when the "describe" function exits, we pop it
150
+ // Thus a test can look up the tags from its parent suites
151
+ const suiteStack = []
152
+
153
+ describe = function describeGrep(name, options, callback) {
154
+ if (typeof options === 'function') {
155
+ // the block has format describe('...', cb)
156
+ callback = options
157
+ options = {}
158
+ }
159
+
160
+ const stackItem = { name }
161
+
162
+ suiteStack.push(stackItem)
163
+
164
+ if (!callback) {
165
+ // the pending suite by itself
166
+ const result = _describe(name, options)
167
+
168
+ suiteStack.pop()
169
+
170
+ return result
171
+ }
172
+
173
+ let configTags = options && options.tags
174
+
175
+ if (typeof configTags === 'string') {
176
+ configTags = [configTags]
177
+ }
178
+
179
+ if (!configTags || !configTags.length) {
180
+ // if the describe suite does not have explicit tags
181
+ // move on, since the tests inside can have their own tags
182
+ _describe(name, options, callback)
183
+ suiteStack.pop()
184
+
185
+ return
186
+ }
187
+
188
+ // when looking at the suite of the tests, I found
189
+ // that using the name is quickly becoming very confusing
190
+ // and thus we need to use the explicit tags
191
+ stackItem.tags = configTags
192
+ _describe(name, options, callback)
193
+ suiteStack.pop()
194
+
195
+ return
196
+ }
197
+
198
+ // overwrite "context" which is an alias to "describe"
199
+ context = describe
200
+
201
+ // overwrite "specify" which is an alias to "it"
202
+ specify = it
203
+
204
+ // keep the ".skip", ".only" methods the same as before
205
+ it.skip = _it.skip
206
+ it.only = _it.only
207
+ // preserve "it.each" method if found
208
+ // https://github.com/cypress-io/cypress-grep/issues/72
209
+ if (typeof _it.each === 'function') {
210
+ it.each = _it.each
211
+ }
212
+
213
+ describe.skip = _describe.skip
214
+ describe.only = _describe.only
215
+ if (typeof _describe.each === 'function') {
216
+ describe.each = _describe.each
217
+ }
218
+ }
219
+
220
+ function restartTests() {
221
+ setTimeout(() => {
222
+ window.top.document.querySelector('.reporter .restart').click()
223
+ }, 0)
224
+ }
225
+
226
+ if (!Cypress.grep) {
227
+ /**
228
+ * A utility method to set the grep and run the tests from
229
+ * the DevTools console. Restarts the test runner
230
+ * @example
231
+ * // run only the tests with "hello w" in the title
232
+ * Cypress.grep('hello w')
233
+ * // runs only tests tagged both "@smoke" and "@fast"
234
+ * Cypress.grep(null, '@smoke+@fast')
235
+ * // runs the grepped tests 100 times
236
+ * Cypress.grep('add items', null, 100)
237
+ * // remove all current grep settings
238
+ * // and run all tests
239
+ * Cypress.grep()
240
+ * @see "Grep from DevTools console" https://github.com/cypress-io/cypress-grep#devtools-console
241
+ */
242
+ Cypress.grep = function grep(grep, tags, burn) {
243
+ setPluginConfigValue('grep', grep)
244
+ setPluginConfigValue('grepTags', tags)
245
+ setPluginConfigValue('grepBurn', burn)
246
+ // remove any aliased values
247
+ setPluginConfigValue('grep-tags', null)
248
+ setPluginConfigValue('grep-burn', null)
249
+ setPluginConfigValue('burn', null)
250
+
251
+ debug('set new grep to "%o" restarting tests', { grep, tags, burn })
252
+ restartTests()
253
+ }
254
+ }
255
+
256
+ module.exports = cypressGrep
package/src/utils.js ADDED
@@ -0,0 +1,187 @@
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} s Tags string like "@tag1+@tag2"
41
+ */
42
+ function parseTagsGrep (s) {
43
+ if (!s) {
44
+ return []
45
+ }
46
+
47
+ const explicitNotTags = []
48
+
49
+ // top level split - using space or comma, each part is OR
50
+ const ORS = s
51
+ .split(/[ ,]/)
52
+ // remove any empty tags
53
+ .filter(Boolean)
54
+ .map((part) => {
55
+ // now every part is an AND
56
+ if (part.startsWith('--')) {
57
+ explicitNotTags.push({
58
+ tag: part.slice(2),
59
+ invert: true,
60
+ })
61
+
62
+ return
63
+ }
64
+
65
+ const parsed = part.split('+').map((tag) => {
66
+ if (tag.startsWith('-')) {
67
+ return {
68
+ tag: tag.slice(1),
69
+ invert: true,
70
+ }
71
+ }
72
+
73
+ return {
74
+ tag,
75
+ invert: false,
76
+ }
77
+ })
78
+
79
+ return parsed
80
+ })
81
+
82
+ // filter out undefined from explicit not tags
83
+ const ORS_filtered = ORS.filter((x) => x !== undefined)
84
+
85
+ if (explicitNotTags.length > 0) {
86
+ ORS_filtered.forEach((OR, index) => {
87
+ ORS_filtered[index] = OR.concat(explicitNotTags)
88
+ })
89
+
90
+ if (ORS_filtered.length === 0) {
91
+ ORS_filtered[ 0 ] = explicitNotTags
92
+ }
93
+ }
94
+
95
+ return ORS_filtered
96
+ }
97
+
98
+ function shouldTestRunTags (parsedGrepTags, tags = []) {
99
+ if (!parsedGrepTags.length) {
100
+ // there are no parsed tags to search for, the test should run
101
+ return true
102
+ }
103
+
104
+ // now the test has tags and the parsed tags are present
105
+
106
+ // top levels are OR
107
+ const onePartMatched = parsedGrepTags.some((orPart) => {
108
+ const everyAndPartMatched = orPart.every((p) => {
109
+ if (p.invert) {
110
+ return !tags.includes(p.tag)
111
+ }
112
+
113
+ return tags.includes(p.tag)
114
+ })
115
+ // console.log('every part matched %o?', orPart, everyAndPartMatched)
116
+
117
+ return everyAndPartMatched
118
+ })
119
+
120
+ // console.log('onePartMatched', onePartMatched)
121
+ return onePartMatched
122
+ }
123
+
124
+ function shouldTestRunTitle (parsedGrep, testName) {
125
+ if (!testName) {
126
+ // if there is no title, let it run
127
+ return true
128
+ }
129
+
130
+ if (!parsedGrep) {
131
+ return true
132
+ }
133
+
134
+ if (!Array.isArray(parsedGrep)) {
135
+ console.error('Invalid parsed title grep')
136
+ console.error(parsedGrep)
137
+ throw new Error('Expected title grep to be an array')
138
+ }
139
+
140
+ if (!parsedGrep.length) {
141
+ return true
142
+ }
143
+
144
+ const inverted = parsedGrep.filter((g) => g.invert)
145
+ const straight = parsedGrep.filter((g) => !g.invert)
146
+
147
+ return (
148
+ inverted.every((titleGrep) => !testName.includes(titleGrep.title)) &&
149
+ (!straight.length ||
150
+ straight.some((titleGrep) => testName.includes(titleGrep.title)))
151
+ )
152
+ }
153
+
154
+ // note: tags take precedence over the test name
155
+ function shouldTestRun (parsedGrep, testName, tags = [], grepUntagged = false) {
156
+ if (grepUntagged) {
157
+ return !tags.length
158
+ }
159
+
160
+ if (Array.isArray(testName)) {
161
+ // the caller passed tags only, no test name
162
+ tags = testName
163
+ testName = undefined
164
+ }
165
+
166
+ return (
167
+ shouldTestRunTitle(parsedGrep.title, testName) &&
168
+ shouldTestRunTags(parsedGrep.tags, tags)
169
+ )
170
+ }
171
+
172
+ function parseGrep (titlePart, tags) {
173
+ return {
174
+ title: parseFullTitleGrep(titlePart),
175
+ tags: parseTagsGrep(tags),
176
+ }
177
+ }
178
+
179
+ module.exports = {
180
+ parseGrep,
181
+ parseTitleGrep,
182
+ parseFullTitleGrep,
183
+ parseTagsGrep,
184
+ shouldTestRun,
185
+ shouldTestRunTags,
186
+ shouldTestRunTitle,
187
+ }