@bahmutov/cy-grep 1.2.0 → 1.3.0

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)
@@ -56,9 +57,6 @@ Table of Contents
56
57
  - [Debugging in the browser](#debugging-in-the-browser)
57
58
  - [Examples](#examples)
58
59
  - [See also](#see-also)
59
- - [Migration guide](#migration-guide)
60
- - [from v1 to v2](#from-v1-to-v2)
61
- - [from v2 to v3](#from-v2-to-v3)
62
60
  - [Small Print](#small-print)
63
61
 
64
62
  <!-- /MarkdownTOC -->
@@ -217,8 +215,6 @@ $ npx cypress run --env grep="-hello world"
217
215
  $ npx cypress run --env grep="hello; -world"
218
216
  ```
219
217
 
220
- **Note:** Inverted title filter is not compatible with the `grepFilterSpecs` option
221
-
222
218
  ## Filter with tags
223
219
 
224
220
  You can select tests to run or skip using tags by passing `--env grepTags=...` value.
@@ -415,6 +411,16 @@ You can pass the number of times to run the tests via environment name `burn` or
415
411
 
416
412
  If you do not specify the "grep" or "grep tags" option, the "burn" will repeat _every_ test.
417
413
 
414
+ ## Required tags
415
+
416
+ 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:
417
+
418
+ ```js
419
+ it('cleans up the data', { requiredTags: '@nightly' }, () => {...})
420
+ ```
421
+
422
+ 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`.
423
+
418
424
  ## TypeScript support
419
425
 
420
426
  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:
@@ -547,30 +553,28 @@ This module uses [debug](https://github.com/visionmedia/debug#readme) to log ver
547
553
 
548
554
  ### Debugging in the plugin
549
555
 
550
- Start Cypress with the environment variable `DEBUG=cypress-grep`. You will see a few messages from this plugin in the terminal output:
556
+ Start Cypress with the environment variable `DEBUG=cy-grep`. You will see a few messages from this plugin in the terminal output:
551
557
 
552
558
  ```
553
- $ DEBUG=cypress-grep npx cypress run --env grep=works,grepFilterSpecs=true
554
- cypress-grep: tests with "works" in their names
555
- cypress-grep: filtering specs using "works" in the title
556
- cypress-grep Cypress config env object: { grep: 'works', grepFilterSpecs: true }
559
+ $ DEBUG=cy-grep npx cypress run --env grep=works,grepFilterSpecs=true
560
+ cy-grep: tests with "works" in their names
561
+ cy-grep: filtering specs using "works" in the title
562
+ cy-grep Cypress config env object: { grep: 'works', grepFilterSpecs: true }
557
563
  ...
558
- cypress-grep found 1 spec files +5ms
559
- cypress-grep [ 'spec.js' ] +0ms
560
- cypress-grep spec file spec.js +5ms
561
- cypress-grep suite and test names: [ 'hello world', 'works', 'works 2 @tag1',
564
+ cy-grep found 1 spec files +5ms
565
+ cy-grep [ 'spec.js' ] +0ms
566
+ cy-grep spec file spec.js +5ms
567
+ cy-grep suite and test names: [ 'hello world', 'works', 'works 2 @tag1',
562
568
  'works 2 @tag1 @tag2', 'works @tag2' ] +0ms
563
- cypress-grep found "works" in 1 specs +0ms
564
- cypress-grep [ 'spec.js' ] +0ms
569
+ cy-grep found "works" in 1 specs +0ms
570
+ cy-grep [ 'spec.js' ] +0ms
565
571
  ```
566
572
 
567
573
  ### Debugging in the browser
568
574
 
569
- To enable debug console messages in the browser, from the DevTools console set `localStorage.debug='cypress-grep'` and run the tests again.
575
+ To enable debug console messages in the browser, from the DevTools console set `localStorage.debug='cy-grep'` and run the tests again.
570
576
 
571
- ![Debug messages](./images/debug.png)
572
-
573
- To see how to debug this plugin, watch the video [Debug cypress-grep Plugin](https://youtu.be/4YMAERddHYA).
577
+ To see how to debug this plugin, watch the video [Debug cypress-grep Plugin](https://youtu.be/4YMAERddHYA) but use the string `cy-grep`
574
578
 
575
579
  ## Examples
576
580
 
@@ -582,37 +586,6 @@ To see how to debug this plugin, watch the video [Debug cypress-grep Plugin](htt
582
586
  - [cypress-select-tests](https://github.com/bahmutov/cypress-select-tests)
583
587
  - [cypress-skip-test](https://github.com/cypress-io/cypress-skip-test)
584
588
 
585
- ## Migration guide
586
-
587
- ### from v1 to v2
588
-
589
- In v2 we have separated grepping by part of the title string from tags.
590
-
591
- **v1**
592
-
593
- ```
594
- --env grep="one two"
595
- ```
596
-
597
- 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`?
598
-
599
- **v2**
600
-
601
- ```
602
- # enable the tests with string "one two" in their titles
603
- --env grep="one two"
604
- # enable the tests with tag "one" or "two"
605
- --env grepTags="one two"
606
- # enable the tests with both tags "one" and "two"
607
- --env grepTags="one+two"
608
- # enable the tests with "hello" in the title and tag "smoke"
609
- --env grep=hello,grepTags=smoke
610
- ```
611
-
612
- ### from v2 to v3
613
-
614
- Version >= 3 of cypress-grep _only_ supports Cypress >= 10.
615
-
616
589
  ## Small Print
617
590
 
618
591
  License: MIT - do anything with the code, but don't blame me if it does not work.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bahmutov/cy-grep",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Filter Cypress tests using title or tags",
5
5
  "main": "src/support.js",
6
6
  "scripts": {
@@ -12,15 +12,15 @@
12
12
  "dependencies": {
13
13
  "cypress-plugin-config": "^1.2.0",
14
14
  "debug": "^4.3.2",
15
- "find-test-names": "^1.22.2",
15
+ "find-test-names": "1.25.0",
16
16
  "globby": "^11.0.4"
17
17
  },
18
18
  "devDependencies": {
19
- "cypress": "12.1.0",
19
+ "cypress": "12.3.0",
20
20
  "cypress-each": "^1.11.0",
21
21
  "cypress-expect": "^2.5.3",
22
22
  "prettier": "^2.8.1",
23
- "semantic-release": "^19.0.5",
23
+ "semantic-release": "^20.0.3",
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
@@ -1,4 +1,4 @@
1
- const debug = require('debug')('cypress-grep')
1
+ const debug = require('debug')('cy-grep')
2
2
  const globby = require('globby')
3
3
  const { getTestNames, findEffectiveTestTags } = require('find-test-names')
4
4
  const fs = require('fs')
@@ -6,7 +6,7 @@ const { version } = require('../package.json')
6
6
  const { parseGrep, shouldTestRun } = require('./utils')
7
7
 
8
8
  /**
9
- * Prints the cypress-grep environment values if any.
9
+ * Prints the cy-grep environment values if any.
10
10
  * @param {Cypress.ConfigOptions} config
11
11
  */
12
12
  function cypressGrepPlugin(config) {
@@ -18,23 +18,23 @@ function cypressGrepPlugin(config) {
18
18
 
19
19
  if (!config.specPattern) {
20
20
  throw new Error(
21
- 'Incompatible versions detected, cypress-grep 3.0.0+ requires Cypress 10.0.0+',
21
+ 'Incompatible versions detected, cy-grep requires Cypress 10.0.0+',
22
22
  )
23
23
  }
24
24
 
25
- debug('cypress-grep plugin version %s', version)
25
+ debug('cy-grep plugin version %s', version)
26
26
  debug('Cypress config env object: %o', env)
27
27
 
28
28
  const grep = env.grep ? String(env.grep) : undefined
29
29
 
30
30
  if (grep) {
31
- console.log('cypress-grep: tests with "%s" in their names', grep.trim())
31
+ console.log('cy-grep: tests with "%s" in their names', grep.trim())
32
32
  }
33
33
 
34
34
  const grepTags = env.grepTags || env['grep-tags']
35
35
 
36
36
  if (grepTags) {
37
- console.log('cypress-grep: filtering using tag(s) "%s"', grepTags)
37
+ console.log('cy-grep: filtering using tag(s) "%s"', grepTags)
38
38
  const parsedGrep = parseGrep(null, grepTags)
39
39
 
40
40
  debug('parsed grep tags %o', parsedGrep.tags)
@@ -43,19 +43,19 @@ function cypressGrepPlugin(config) {
43
43
  const grepBurn = env.grepBurn || env['grep-burn'] || env.burn
44
44
 
45
45
  if (grepBurn) {
46
- console.log('cypress-grep: running filtered tests %d times', grepBurn)
46
+ console.log('cy-grep: running filtered tests %d times', grepBurn)
47
47
  }
48
48
 
49
49
  const grepUntagged = env.grepUntagged || env['grep-untagged']
50
50
 
51
51
  if (grepUntagged) {
52
- console.log('cypress-grep: running untagged tests')
52
+ console.log('cy-grep: running untagged tests')
53
53
  }
54
54
 
55
55
  const omitFiltered = env.grepOmitFiltered || env['grep-omit-filtered']
56
56
 
57
57
  if (omitFiltered) {
58
- console.log('cypress-grep: will omit filtered tests')
58
+ console.log('cy-grep: will omit filtered tests')
59
59
  }
60
60
 
61
61
  const { specPattern, excludeSpecPattern } = config
@@ -78,7 +78,7 @@ function cypressGrepPlugin(config) {
78
78
  let greppedSpecs = []
79
79
 
80
80
  if (grep) {
81
- console.log('cypress-grep: filtering specs using "%s" in the title', grep)
81
+ console.log('cy-grep: filtering specs using "%s" in the title', grep)
82
82
  const parsedGrep = parseGrep(grep)
83
83
 
84
84
  debug('parsed grep %o', parsedGrep)
@@ -123,7 +123,7 @@ function cypressGrepPlugin(config) {
123
123
  debug('spec file %s', specFile)
124
124
  debug('effective test tags %o', testTags)
125
125
  return Object.keys(testTags).some((testTitle) => {
126
- const effectiveTags = testTags[testTitle]
126
+ const effectiveTags = testTags[testTitle].effectiveTags
127
127
  return shouldTestRun(parsedGrep, null, effectiveTags)
128
128
  })
129
129
  } catch (err) {
package/src/support.js CHANGED
@@ -8,7 +8,8 @@ const {
8
8
  getPluginConfigValue,
9
9
  setPluginConfigValue,
10
10
  } = require('cypress-plugin-config')
11
- const debug = require('debug')('@bahmutov/cy-grep')
11
+ // to debug in the browser, set the "localStorage.debug='cy-grep'"
12
+ const debug = require('debug')('cy-grep')
12
13
 
13
14
  debug.log = console.info.bind(console)
14
15
 
@@ -18,9 +19,9 @@ const _describe = describe
18
19
 
19
20
  /**
20
21
  * Wraps the "it" and "describe" functions that support tags.
21
- * @see https://github.com/cypress-io/cypress-grep
22
+ * @see https://github.com/bahmutov/cy-grep
22
23
  */
23
- function cypressGrep() {
24
+ function registerCyGrep() {
24
25
  /** @type {string} Part of the test title go grep */
25
26
  let grep = getPluginConfigValue('grep')
26
27
 
@@ -41,12 +42,12 @@ function cypressGrep() {
41
42
  getPluginConfigValue('grepUntagged') ||
42
43
  getPluginConfigValue('grep-untagged')
43
44
 
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)
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)
47
48
 
48
- return
49
- }
49
+ // return
50
+ // }
50
51
 
51
52
  /** @type {number} Number of times to repeat each running test */
52
53
  const grepBurn =
@@ -70,9 +71,8 @@ function cypressGrep() {
70
71
  debug('parsed grep %o', parsedGrep)
71
72
 
72
73
  // prevent multiple registrations
73
- // https://github.com/cypress-io/cypress-grep/issues/59
74
74
  if (it.name === 'itGrep') {
75
- debug('already registered cypress-grep')
75
+ debug('already registered cy-grep')
76
76
 
77
77
  return
78
78
  }
@@ -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
  }
@@ -205,7 +213,6 @@ function cypressGrep() {
205
213
  it.skip = _it.skip
206
214
  it.only = _it.only
207
215
  // preserve "it.each" method if found
208
- // https://github.com/cypress-io/cypress-grep/issues/72
209
216
  if (typeof _it.each === 'function') {
210
217
  it.each = _it.each
211
218
  }
@@ -237,7 +244,6 @@ if (!Cypress.grep) {
237
244
  * // remove all current grep settings
238
245
  * // and run all tests
239
246
  * Cypress.grep()
240
- * @see "Grep from DevTools console" https://github.com/cypress-io/cypress-grep#devtools-console
241
247
  */
242
248
  Cypress.grep = function grep(grep, tags, burn) {
243
249
  setPluginConfigValue('grep', grep)
@@ -287,4 +293,4 @@ if (!Cypress.grepFailed) {
287
293
  }
288
294
  }
289
295
 
290
- 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
  }