@bahmutov/cy-grep 1.2.1 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @bahmutov/cy-grep ![cypress version](https://img.shields.io/badge/cypress-12.1.0-brightgreen)
1
+ # @bahmutov/cy-grep ![cypress version](https://img.shields.io/badge/cypress-12.3.0-brightgreen)
2
2
 
3
3
  > Filter tests using substring or tag
4
4
 
@@ -46,6 +46,7 @@ Table of Contents
46
46
  - [Omit filtered tests (grepOmitFiltered)](#omit-filtered-tests-grepomitfiltered)
47
47
  - [Disable grep](#disable-grep)
48
48
  - [Burn (repeat) tests](#burn-repeat-tests)
49
+ - [Required tags](#required-tags)
49
50
  - [TypeScript support](#typescript-support)
50
51
  - [General advice](#general-advice)
51
52
  - [DevTools console](#devtools-console)
@@ -118,6 +119,15 @@ registerCypressGrep()
118
119
 
119
120
  Installing the plugin via `setupNodeEvents()` is required to enable the [grepFilterSpecs](#pre-filter-specs-grepfilterspecs) feature.
120
121
 
122
+ **Tip:** you probably want to set these `env` settings in your config file
123
+
124
+ ```js
125
+ module.exports = defineConfig({
126
+ env: { grepFilterSpecs: true, grepOmitFiltered: true },
127
+ ...
128
+ })
129
+ ```
130
+
121
131
  ## Usage Overview
122
132
 
123
133
  You can filter tests to run using part of their title via `grep`, and via explicit tags via `grepTags` Cypress environment variables.
@@ -410,6 +420,16 @@ You can pass the number of times to run the tests via environment name `burn` or
410
420
 
411
421
  If you do not specify the "grep" or "grep tags" option, the "burn" will repeat _every_ test.
412
422
 
423
+ ## Required tags
424
+
425
+ 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:
426
+
427
+ ```js
428
+ it('cleans up the data', { requiredTags: '@nightly' }, () => {...})
429
+ ```
430
+
431
+ 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`.
432
+
413
433
  ## TypeScript support
414
434
 
415
435
  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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bahmutov/cy-grep",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "Filter Cypress tests using title or tags",
5
5
  "main": "src/support.js",
6
6
  "scripts": {
@@ -12,7 +12,7 @@
12
12
  "dependencies": {
13
13
  "cypress-plugin-config": "^1.2.0",
14
14
  "debug": "^4.3.2",
15
- "find-test-names": "^1.23.0",
15
+ "find-test-names": "1.25.0",
16
16
  "globby": "^11.0.4"
17
17
  },
18
18
  "devDependencies": {
@@ -20,7 +20,7 @@
20
20
  "cypress-each": "^1.11.0",
21
21
  "cypress-expect": "^2.5.3",
22
22
  "prettier": "^2.8.1",
23
- "semantic-release": "^20.0.2",
23
+ "semantic-release": "^20.0.4",
24
24
  "typescript": "^4.7.4"
25
25
  },
26
26
  "peerDependencies": {
package/src/index.d.ts CHANGED
@@ -8,8 +8,15 @@ declare namespace Cypress {
8
8
  * describe('block with config tag', { tags: '@smoke' }, () => {})
9
9
  * @example multiple tags
10
10
  * describe('block with config tag', { tags: ['@smoke', '@slow'] }, () => {})
11
+ * @see https://github.com/bahmutov/cy-grep
11
12
  */
12
13
  tags?: string | string[]
14
+ /**
15
+ * Provide a tag or a list of tags that is required for this suite to run.
16
+ * @example describe('mobile tests', { requiredTags: '@mobile' }, () => {})
17
+ * @see https://github.com/bahmutov/cy-grep
18
+ */
19
+ requiredTags?: string | string[]
13
20
  }
14
21
 
15
22
  // specify additional properties in the TestConfig object
@@ -21,8 +28,15 @@ declare namespace Cypress {
21
28
  * it('logs in', { tags: '@smoke' }, () => { ... })
22
29
  * @example multiple tags
23
30
  * it('works', { tags: ['@smoke', '@slow'] }, () => { ... })
31
+ * @see https://github.com/bahmutov/cy-grep
24
32
  */
25
33
  tags?: string | string[]
34
+ /**
35
+ * Provide a tag or a list of tags that is required for this test to run.
36
+ * @example it('cleans the data', { requiredTags: '@nightly' }, () => {})
37
+ * @see https://github.com/bahmutov/cy-grep
38
+ */
39
+ requiredTags?: string | string[]
26
40
  }
27
41
 
28
42
  interface Cypress {
package/src/plugin.js CHANGED
@@ -124,7 +124,14 @@ function cypressGrepPlugin(config) {
124
124
  debug('effective test tags %o', testTags)
125
125
  return Object.keys(testTags).some((testTitle) => {
126
126
  const effectiveTags = testTags[testTitle].effectiveTags
127
- return shouldTestRun(parsedGrep, null, effectiveTags)
127
+ const requiredTags = testTags[testTitle].requiredTags
128
+ return shouldTestRun(
129
+ parsedGrep,
130
+ null,
131
+ effectiveTags,
132
+ false,
133
+ requiredTags,
134
+ )
128
135
  })
129
136
  } catch (err) {
130
137
  console.error('Could not determine test names in file: %s', specFile)
package/src/support.js CHANGED
@@ -21,7 +21,7 @@ const _describe = describe
21
21
  * Wraps the "it" and "describe" functions that support tags.
22
22
  * @see https://github.com/bahmutov/cy-grep
23
23
  */
24
- function cypressGrep() {
24
+ function registerCyGrep() {
25
25
  /** @type {string} Part of the test title go grep */
26
26
  let grep = getPluginConfigValue('grep')
27
27
 
@@ -42,12 +42,12 @@ function cypressGrep() {
42
42
  getPluginConfigValue('grepUntagged') ||
43
43
  getPluginConfigValue('grep-untagged')
44
44
 
45
- if (!grep && !grepTags && !burnSpecified && !grepUntagged) {
46
- // nothing to do, the user has no specified the "grep" string
47
- debug('Nothing to grep, version %s', version)
45
+ // if (!grep && !grepTags && !burnSpecified && !grepUntagged) {
46
+ // nothing to do, the user has no specified the "grep" string
47
+ // debug('Nothing to grep, version %s', version)
48
48
 
49
- return
50
- }
49
+ // return
50
+ // }
51
51
 
52
52
  /** @type {number} Number of times to repeat each running test */
53
53
  const grepBurn =
@@ -90,32 +90,41 @@ function cypressGrep() {
90
90
  }
91
91
 
92
92
  let configTags = options && options.tags
93
-
94
93
  if (typeof configTags === 'string') {
95
94
  configTags = [configTags]
96
95
  }
96
+ let configRequiredTags = options && options.requiredTags
97
+ if (typeof configRequiredTags === 'string') {
98
+ configRequiredTags = [configRequiredTags]
99
+ }
97
100
 
98
101
  const nameToGrep = suiteStack
99
102
  .map((item) => item.name)
100
103
  .concat(name)
101
104
  .join(' ')
102
- const tagsToGrep = suiteStack
105
+ const effectiveTestTags = suiteStack
103
106
  .flatMap((item) => item.tags)
104
107
  .concat(configTags)
105
108
  .filter(Boolean)
109
+ const requiredTestTags = suiteStack
110
+ .flatMap((item) => item.requiredTags)
111
+ .concat(configRequiredTags)
112
+ .filter(Boolean)
113
+ // console.log({ nameToGrep, effectiveTestTags, requiredTestTags })
106
114
 
107
115
  const shouldRun = shouldTestRun(
108
116
  parsedGrep,
109
117
  nameToGrep,
110
- tagsToGrep,
118
+ effectiveTestTags,
111
119
  grepUntagged,
120
+ requiredTestTags,
112
121
  )
113
122
 
114
- if (tagsToGrep && tagsToGrep.length) {
123
+ if (effectiveTestTags && effectiveTestTags.length) {
115
124
  debug(
116
125
  'should test "%s" with tags %s run? %s',
117
126
  name,
118
- tagsToGrep.join(','),
127
+ effectiveTestTags.join(','),
119
128
  shouldRun,
120
129
  )
121
130
  } else {
@@ -171,7 +180,6 @@ function cypressGrep() {
171
180
  }
172
181
 
173
182
  let configTags = options && options.tags
174
-
175
183
  if (typeof configTags === 'string') {
176
184
  configTags = [configTags]
177
185
  }
@@ -285,4 +293,4 @@ if (!Cypress.grepFailed) {
285
293
  }
286
294
  }
287
295
 
288
- module.exports = cypressGrep
296
+ module.exports = registerCyGrep
package/src/utils.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * The string can have "-" in front of it to invert the match.
8
8
  * @param {string} s Input substring of the test title
9
9
  */
10
- function parseTitleGrep (s) {
10
+ function parseTitleGrep(s) {
11
11
  if (!s || typeof s !== 'string') {
12
12
  return null
13
13
  }
@@ -26,7 +26,7 @@ function parseTitleGrep (s) {
26
26
  }
27
27
  }
28
28
 
29
- function parseFullTitleGrep (s) {
29
+ function parseFullTitleGrep(s) {
30
30
  if (!s || typeof s !== 'string') {
31
31
  return []
32
32
  }
@@ -39,7 +39,7 @@ function parseFullTitleGrep (s) {
39
39
  * Parses tags to grep for.
40
40
  * @param {string} s Tags string like "@tag1+@tag2"
41
41
  */
42
- function parseTagsGrep (s) {
42
+ function parseTagsGrep(s) {
43
43
  if (!s) {
44
44
  return []
45
45
  }
@@ -48,37 +48,37 @@ function parseTagsGrep (s) {
48
48
 
49
49
  // top level split - using space or comma, each part is OR
50
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
- })
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
61
 
62
- return
63
- }
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
+ }
64
72
 
65
- const parsed = part.split('+').map((tag) => {
66
- if (tag.startsWith('-')) {
67
73
  return {
68
- tag: tag.slice(1),
69
- invert: true,
74
+ tag,
75
+ invert: false,
70
76
  }
71
- }
77
+ })
72
78
 
73
- return {
74
- tag,
75
- invert: false,
76
- }
79
+ return parsed
77
80
  })
78
81
 
79
- return parsed
80
- })
81
-
82
82
  // filter out undefined from explicit not tags
83
83
  const ORS_filtered = ORS.filter((x) => x !== undefined)
84
84
 
@@ -88,14 +88,29 @@ function parseTagsGrep (s) {
88
88
  })
89
89
 
90
90
  if (ORS_filtered.length === 0) {
91
- ORS_filtered[ 0 ] = explicitNotTags
91
+ ORS_filtered[0] = explicitNotTags
92
92
  }
93
93
  }
94
94
 
95
95
  return ORS_filtered
96
96
  }
97
97
 
98
- function shouldTestRunTags (parsedGrepTags, tags = []) {
98
+ function shouldTestRunRequiredTags(parsedGrepTags, requiredTags = []) {
99
+ if (!requiredTags.length) {
100
+ // there are no tags to check
101
+ return true
102
+ }
103
+
104
+ return requiredTags.every((onlyTag) => {
105
+ return parsedGrepTags.some((orPart) => {
106
+ return orPart.some((p) => {
107
+ return !p.invert && p.tag === onlyTag
108
+ })
109
+ })
110
+ })
111
+ }
112
+
113
+ function shouldTestRunTags(parsedGrepTags, tags = []) {
99
114
  if (!parsedGrepTags.length) {
100
115
  // there are no parsed tags to search for, the test should run
101
116
  return true
@@ -121,7 +136,7 @@ function shouldTestRunTags (parsedGrepTags, tags = []) {
121
136
  return onePartMatched
122
137
  }
123
138
 
124
- function shouldTestRunTitle (parsedGrep, testName) {
139
+ function shouldTestRunTitle(parsedGrep, testName) {
125
140
  if (!testName) {
126
141
  // if there is no title, let it run
127
142
  return true
@@ -152,7 +167,20 @@ function shouldTestRunTitle (parsedGrep, testName) {
152
167
  }
153
168
 
154
169
  // note: tags take precedence over the test name
155
- function shouldTestRun (parsedGrep, testName, tags = [], grepUntagged = false) {
170
+ /**
171
+ * Returns boolean if the test with the given name and effective tags
172
+ * should run, given the runtime grep (parsed) structure.
173
+ * @param {string|undefined} testName The full test title
174
+ * @param {string[]} tags The effective test tags
175
+ * @param {string[]} requiredTags The effective "required" test tags
176
+ */
177
+ function shouldTestRun(
178
+ parsedGrep,
179
+ testName,
180
+ tags = [],
181
+ grepUntagged = false,
182
+ requiredTags = [],
183
+ ) {
156
184
  if (grepUntagged) {
157
185
  return !tags.length
158
186
  }
@@ -163,13 +191,16 @@ function shouldTestRun (parsedGrep, testName, tags = [], grepUntagged = false) {
163
191
  testName = undefined
164
192
  }
165
193
 
194
+ const combinedTagsAndRequiredTags = [...tags, ...requiredTags]
195
+
166
196
  return (
167
197
  shouldTestRunTitle(parsedGrep.title, testName) &&
168
- shouldTestRunTags(parsedGrep.tags, tags)
198
+ shouldTestRunTags(parsedGrep.tags, combinedTagsAndRequiredTags) &&
199
+ shouldTestRunRequiredTags(parsedGrep.tags, requiredTags)
169
200
  )
170
201
  }
171
202
 
172
- function parseGrep (titlePart, tags) {
203
+ function parseGrep(titlePart, tags) {
173
204
  return {
174
205
  title: parseFullTitleGrep(titlePart),
175
206
  tags: parseTagsGrep(tags),
@@ -183,5 +214,6 @@ module.exports = {
183
214
  parseTagsGrep,
184
215
  shouldTestRun,
185
216
  shouldTestRunTags,
217
+ shouldTestRunRequiredTags,
186
218
  shouldTestRunTitle,
187
219
  }