cypress-conditional-tags 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,113 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.0.0] - 2026-07-26
9
+
10
+ ## Overview
11
+ v2.0.0 makes tags a **first-class, queryable, validated system** — not just a passthrough value.
12
+ Four features ship together under a single theme: complete the tag model, make it queryable, and enforce the Cypress 15.10+ contract.
13
+
14
+ ## Breaking Changes
15
+
16
+ ### `Cypress.expose()` Migration
17
+
18
+ All internal `Cypress.env()` calls have been replaced with `Cypress.expose()` — the new Cypress 15.10+ API designed for public, non-sensitive plugin configuration values.
19
+
20
+ **What changed:**
21
+ - Every read of plugin config values now uses `Cypress.expose()` instead of `Cypress.env()`
22
+ - Test and example configs now include `allowCypressEnv: false` (strict mode) to prevent accidental `Cypress.env()` usage
23
+ - Peer dependency bumped to `>= 15.10.0`
24
+
25
+ **Migration:**
26
+
27
+ ```js
28
+ // ? Before (v1.x)
29
+ --env conditionalTags=QA
30
+
31
+ // ? After (v2.0.0)
32
+ --expose conditionalTags=QA
33
+ ```
34
+
35
+ ```js
36
+ // cypress.config.js
37
+ export default defineConfig({
38
+ e2e: {
39
+ allowCypressEnv: false, // ? enforce strict mode
40
+ setupNodeEvents(on, config) {
41
+ setupNodeEvents(on, config, { specPattern: 'cypress/e2e/**/*.cy.js' })
42
+ }
43
+ }
44
+ })
45
+ ```
46
+
47
+ > If you are on Cypress < 15.10.0, stay on **cypress-conditional-tags v1.0.1**:
48
+ > ```bash
49
+ > npm install cypress-conditional-tags@1.0.1
50
+ > ```
51
+
52
+
53
+ ## New Features
54
+
55
+ ### 1. Version Guard in `setupSupport()`
56
+ `setupSupport()` now checks for the presence of `Cypress.expose` at startup and throws a clear, actionable error if it is missing (i.e. running on an older Cypress), instead of a cryptic `TypeError: Cypress.expose is not a function` buried mid-test.
57
+
58
+
59
+ ### 2. Suite-Level Tag Inheritance
60
+ Tags declared on a `describe()` block are now **automatically inherited** by all nested `it()` tests. This works at any nesting depth.
61
+
62
+ **Rules:**
63
+ - Tags from all ancestor `describe()` blocks are merged with the test's own tags
64
+ - Duplicates are deduplicated while preserving insertion order
65
+ - Tests with no own tags still receive all ancestor describe tags
66
+
67
+
68
+ ### 3. Cross-File Tag Query
69
+
70
+ **Effort:** Medium — Node-side file scanner + regex parser + `cy.task` bridge + new commands + unit tests
71
+
72
+ `setupNodeEvents` now accepts an optional `specPattern` option. At startup it **statically scans** all matching spec files (no test execution) and builds a tag registry mapping every tag to the tests that carry it.
73
+
74
+ **Three new `cy.task` commands are registered:**
75
+
76
+ | Command | Description |
77
+ |---|---|
78
+ | `cy.getTestsByTag('QA')` | All tests tagged `QA` across all spec files |
79
+ | `cy.getAllTaggedTests()` | All tests that have at least one tag |
80
+ | `cy.getAllTags()` | Sorted list of every unique tag in the suite |
81
+
82
+ **Usage in tests:**
83
+
84
+ ```js
85
+ cy.getTestsByTag('PROD').then(tests => {
86
+ // [{ file: 'login.cy.js', title: '...', fullTitle: '...', tags: [...] }]
87
+ cy.log(`Found ${tests.length} PROD tests`)
88
+ })
89
+
90
+ cy.getAllTags().then(tags => {
91
+ // ['CRITICAL', 'PROD', 'STAGING', 'REGRESSION', 'SMOKE', ...]
92
+ })
93
+
94
+ cy.getAllTaggedTests().then(tests => {
95
+ // full list of every test that carries at least one tag
96
+ })
97
+ ```
98
+
99
+
100
+ ## Full Migration Checklist (v1.x.x -> v2.0.0)
101
+
102
+ - [ ] Upgrade Cypress to `>= 15.10.0`
103
+ - [ ] Replace `--env conditionalTags=...` with `--expose conditionalTags=...` in CLI / CI scripts
104
+ - [ ] Add `allowCypressEnv: false` to `cypress.config.js` (recommended)
105
+ - [ ] Pass `specPattern` to `setupNodeEvents` to enable cross-file tag queries (optional)
106
+ - [ ] Update any test assertions that checked for an empty tag array on tests nested inside a tagged `describe()` — they will now receive the inherited tags
107
+
108
+
109
+
110
+ ## [1.0.1] - 2026-05-03
111
+
112
+ ### Fixed
113
+ - Fixed typo in the getRunTime() method
114
+
8
115
  ## [1.0.0] - 2026-03-21
9
116
 
10
117
  ### Added
package/README.md CHANGED
@@ -42,14 +42,14 @@ This makes test suites:
42
42
  This plugin makes both runtime tags (from CLI) and test tags (from config) accessible inside your tests:
43
43
 
44
44
  ```javascript
45
- it('Test Case 01', { tags: ['IS_QA', 'IS_GA'] }, () => {
46
- // Access runtime tag from CLI (passed via --env conditionalTags=IS_QA)
47
- if (Cypress.Tags.isRuntime('IS_QA')) {
45
+ it('Test Case 01', { tags: ['QA', 'PROD'] }, () => {
46
+ // Access runtime tag from CLI (passed via --env conditionalTags=QA)
47
+ if (Cypress.Tags.isRuntime('QA')) {
48
48
  cy.log('Running in QA mode')
49
49
  cy.visit('https://qa.example.com')
50
- } else if (Cypress.Tags.isRuntime('IS_GA')) {
51
- cy.log('Running in GA mode')
52
- cy.visit('https://ga.example.com')
50
+ } else if (Cypress.Tags.isRuntime('PROD')) {
51
+ cy.log('Running in PROD mode')
52
+ cy.visit('https://prod.example.com')
53
53
  }
54
54
 
55
55
  // Access test's configured tags
@@ -63,7 +63,7 @@ it('Test Case 01', { tags: ['IS_QA', 'IS_GA'] }, () => {
63
63
  **Use `conditionalTags`** to pass runtime tags:
64
64
 
65
65
  ```bash
66
- npx cypress run --env conditionalTags=IS_QA
66
+ npx cypress run --env conditionalTags=QA
67
67
  ```
68
68
 
69
69
  ```bash
@@ -113,11 +113,11 @@ Use Cypress's native `tags` option in test configuration:
113
113
 
114
114
  ```javascript
115
115
  describe('Login Tests', () => {
116
- it('should login with valid credentials', { tags: ['IS_QA', 'SMOKE'] }, () => {
116
+ it('should login with valid credentials', { tags: ['QA', 'SMOKE'] }, () => {
117
117
  // Test code
118
118
  })
119
119
 
120
- it('should show error for invalid credentials', { tags: ['IS_QA', 'REGRESSION'] }, () => {
120
+ it('should show error for invalid credentials', { tags: ['QA', 'REGRESSION'] }, () => {
121
121
  // Test code
122
122
  })
123
123
  })
@@ -131,9 +131,9 @@ Use `conditionalTags` for conditional logic - **all tests will run**:
131
131
 
132
132
  ```bash
133
133
  # Pass runtime tag for conditional logic using conditionalTags
134
- npx cypress run --env conditionalTags=IS_QA
134
+ npx cypress run --env conditionalTags=QA
135
135
 
136
- # Result: All tests execute, but Cypress.Tags.isRuntime('IS_QA') returns true
136
+ # Result: All tests execute, but Cypress.Tags.isRuntime('QA') returns true
137
137
 
138
138
  # Multiple tags for conditional logic
139
139
  npx cypress run --env conditionalTags=tag5+tag6
@@ -180,11 +180,11 @@ npx cypress run --env grepTags=tag2,conditionalTags=tag5+tag6
180
180
  # Cypress.Tags.isRuntime('tag6') returns true
181
181
 
182
182
  # Another example: Filter with multiple tags, different conditional tags
183
- npx cypress run --env grepTags=SMOKE+REGRESSION,conditionalTags=IS_QA
183
+ npx cypress run --env grepTags=SMOKE+REGRESSION,conditionalTags=QA
184
184
 
185
185
  # Result:
186
186
  # - Runs tests tagged with SMOKE OR REGRESSION (filtering)
187
- # - Inside tests: Cypress.Tags.isRuntime('IS_QA') returns true (conditional logic)
187
+ # - Inside tests: Cypress.Tags.isRuntime('QA') returns true (conditional logic)
188
188
  ```
189
189
 
190
190
  **Key Benefits:**
@@ -199,24 +199,24 @@ npx cypress run --env grepTags=SMOKE+REGRESSION,conditionalTags=IS_QA
199
199
  Use `Cypress.Tags` for immediate, synchronous access:
200
200
 
201
201
  ```javascript
202
- it('should test feature', { tags: ['IS_QA', 'SMOKE'] }, () => {
202
+ it('should test feature', { tags: ['QA', 'SMOKE'] }, () => {
203
203
  // Get runtime tag from CLI
204
204
  const runtimeTag = Cypress.Tags.getRuntime()
205
- console.log(runtimeTag) // 'IS_QA' (if run with --env conditionalTags=IS_QA)
205
+ console.log(runtimeTag) // 'QA' (if run with --env conditionalTags=QA)
206
206
 
207
207
  // Check if running with specific tag
208
- if (Cypress.Tags.isRuntime('IS_QA')) {
208
+ if (Cypress.Tags.isRuntime('QA')) {
209
209
  cy.log('Running in QA mode')
210
210
  }
211
211
 
212
212
  // Check for any of multiple tags
213
- if (Cypress.Tags.isAnyRuntime('IS_QA', 'IS_STAGING')) {
213
+ if (Cypress.Tags.isAnyRuntime('QA', 'IS_STAGING')) {
214
214
  cy.log('Running in non-production environment')
215
215
  }
216
216
 
217
217
  // Get test's configured tags
218
218
  const testTags = Cypress.Tags.getTest()
219
- console.log(testTags) // ['IS_QA', 'SMOKE']
219
+ console.log(testTags) // ['QA', 'SMOKE']
220
220
 
221
221
  // Check if test has specific tag
222
222
  if (Cypress.Tags.hasTest('SMOKE')) {
@@ -235,12 +235,12 @@ it('should test feature', { tags: ['IS_QA', 'SMOKE'] }, () => {
235
235
  Use custom Cypress commands for chainable operations:
236
236
 
237
237
  ```javascript
238
- it('should test feature', { tags: ['IS_QA', 'SMOKE'] }, () => {
238
+ it('should test feature', { tags: ['QA', 'SMOKE'] }, () => {
239
239
  cy.getRuntimeTag().then(tag => {
240
240
  cy.log(`Runtime tag: ${tag}`)
241
241
  })
242
242
 
243
- cy.isRuntimeTag('IS_QA').then(isQA => {
243
+ cy.isRuntimeTag('QA').then(isQA => {
244
244
  if (isQA) {
245
245
  cy.log('Running in QA mode')
246
246
  }
@@ -263,8 +263,8 @@ it('should test feature', { tags: ['IS_QA', 'SMOKE'] }, () => {
263
263
  ### Example 1: Environment-Specific URLs
264
264
 
265
265
  ```javascript
266
- it('should use correct environment', { tags: ['IS_QA', 'IS_PROD'] }, () => {
267
- const baseUrl = Cypress.Tags.isRuntime('IS_QA')
266
+ it('should use correct environment', { tags: ['QA', 'IS_PROD'] }, () => {
267
+ const baseUrl = Cypress.Tags.isRuntime('QA')
268
268
  ? 'https://qa-api.example.com'
269
269
  : Cypress.Tags.isRuntime('IS_PROD')
270
270
  ? 'https://api.example.com'
@@ -295,7 +295,7 @@ it('should test checkout', { tags: ['FAST', 'FULL'] }, () => {
295
295
  ### Example 3: Environment-Specific Test Data
296
296
 
297
297
  ```javascript
298
- it('should process data', { tags: ['IS_QA', 'SMALL_DATA', 'LARGE_DATA'] }, () => {
298
+ it('should process data', { tags: ['QA', 'SMALL_DATA', 'LARGE_DATA'] }, () => {
299
299
  const recordCount = Cypress.Tags.isRuntime('LARGE_DATA') ? 10000 : 100
300
300
 
301
301
  cy.request('POST', '/api/generate-data', { count: recordCount })
@@ -326,9 +326,9 @@ it('should validate production', { tags: ['IS_PROD', 'CRITICAL'] }, () => {
326
326
  ### Example 5: Feature Flags
327
327
 
328
328
  ```javascript
329
- it('should test features', { tags: ['IS_QA', 'NEW_FEATURE'] }, () => {
329
+ it('should test features', { tags: ['QA', 'NEW_FEATURE'] }, () => {
330
330
  const features = {
331
- newCheckout: Cypress.Tags.isRuntime('IS_QA'),
331
+ newCheckout: Cypress.Tags.isRuntime('QA'),
332
332
  betaFeatures: Cypress.Tags.hasTest('NEW_FEATURE')
333
333
  }
334
334
 
@@ -349,9 +349,9 @@ it('should test features', { tags: ['IS_QA', 'NEW_FEATURE'] }, () => {
349
349
  |--------|-------------|---------|---------|
350
350
  | `getRuntime()` | Get runtime tag from CLI | `string \| null` | `Cypress.Tags.getRuntime()` |
351
351
  | `getAllRuntime()` | Get all runtime tags | `string[]` | `Cypress.Tags.getAllRuntime()` |
352
- | `isRuntime(tag)` | Check if runtime tag matches | `boolean` | `Cypress.Tags.isRuntime('IS_QA')` |
353
- | `isAnyRuntime(...tags)` | Check if any runtime tag matches | `boolean` | `Cypress.Tags.isAnyRuntime('IS_QA', 'IS_STAGING')` |
354
- | `isAllRuntime(...tags)` | Check if all runtime tags match | `boolean` | `Cypress.Tags.isAllRuntime('IS_QA', 'SMOKE')` |
352
+ | `isRuntime(tag)` | Check if runtime tag matches | `boolean` | `Cypress.Tags.isRuntime('QA')` |
353
+ | `isAnyRuntime(...tags)` | Check if any runtime tag matches | `boolean` | `Cypress.Tags.isAnyRuntime('QA', 'IS_STAGING')` |
354
+ | `isAllRuntime(...tags)` | Check if all runtime tags match | `boolean` | `Cypress.Tags.isAllRuntime('QA', 'SMOKE')` |
355
355
  | `getTest()` | Get test's configured tags | `string[]` | `Cypress.Tags.getTest()` |
356
356
  | `hasTest(tag)` | Check if test has specific tag | `boolean` | `Cypress.Tags.hasTest('SMOKE')` |
357
357
  | `hasAnyTest(...tags)` | Check if test has any tag | `boolean` | `Cypress.Tags.hasAnyTest('SMOKE', 'REGRESSION')` |
@@ -364,7 +364,7 @@ it('should test features', { tags: ['IS_QA', 'NEW_FEATURE'] }, () => {
364
364
  |---------|-------------|---------|---------|
365
365
  | `cy.getRuntimeTag()` | Get runtime tag from CLI | `Chainable<string \| null>` | `cy.getRuntimeTag().then(tag => ...)` |
366
366
  | `cy.getAllRuntimeTags()` | Get all runtime tags | `Chainable<string[]>` | `cy.getAllRuntimeTags().then(tags => ...)` |
367
- | `cy.isRuntimeTag(tag)` | Check if runtime tag matches | `Chainable<boolean>` | `cy.isRuntimeTag('IS_QA').then(is => ...)` |
367
+ | `cy.isRuntimeTag(tag)` | Check if runtime tag matches | `Chainable<boolean>` | `cy.isRuntimeTag('QA').then(is => ...)` |
368
368
  | `cy.getTestTags()` | Get test's configured tags | `Chainable<string[]>` | `cy.getTestTags().then(tags => ...)` |
369
369
  | `cy.hasTestTag(tag)` | Check if test has specific tag | `Chainable<boolean>` | `cy.hasTestTag('SMOKE').then(has => ...)` |
370
370
  | `cy.matchesRuntimeTag()` | Check if test has runtime tag | `Chainable<boolean>` | `cy.matchesRuntimeTag().then(matches => ...)` |
@@ -374,10 +374,10 @@ it('should test features', { tags: ['IS_QA', 'NEW_FEATURE'] }, () => {
374
374
  Full TypeScript definitions are included. No additional setup required!
375
375
 
376
376
  ```typescript
377
- it('should test', { tags: ['IS_QA', 'SMOKE'] }, () => {
377
+ it('should test', { tags: ['QA', 'SMOKE'] }, () => {
378
378
  const runtimeTag: string | null = Cypress.Tags.getRuntime()
379
379
  const testTags: string[] = Cypress.Tags.getTest()
380
- const isQA: boolean = Cypress.Tags.isRuntime('IS_QA')
380
+ const isQA: boolean = Cypress.Tags.isRuntime('QA')
381
381
 
382
382
  cy.getRuntimeTag().then((tag: string | null) => {
383
383
  // TypeScript knows the type
@@ -399,13 +399,20 @@ it('should test', { tags: ['IS_QA', 'SMOKE'] }, () => {
399
399
  - **TypeScript**: Full support included
400
400
  - **@cypress/grep**: Fully compatible
401
401
 
402
+
403
+ ## Release note for v2.0.0
404
+ v2.0.0 requires Cypress '>=15.10.0' and uses `Cypress.expose()` instead of `Cypress.env()`.
405
+ On older Cypress version use 'npm install cypress-conditional-tags@1.0.1'
406
+ For more information about the release notes, please refer to the CHANGELOG.md file.
407
+
408
+
402
409
  ## Best Practices
403
410
 
404
- 1. **Use Descriptive Tag Names**: `IS_QA`, `IS_PROD`, `SMOKE`, `REGRESSION` are better than `Q`, `P`, `S`, `R`
411
+ 1. **Use Descriptive Tag Names**: `QA`, `IS_PROD`, `SMOKE`, `REGRESSION` are better than `Q`, `P`, `S`, `R`
405
412
  2. **Prefer Synchronous API**: Use `Cypress.Tags.isRuntime()` over `cy.isRuntimeTag()` for simpler code
406
413
  3. **Combine with @cypress/grep**: Use for filtering + runtime logic
407
414
  4. **Document Tags**: Maintain a list of available tags in your project
408
- 5. **Environment Tags**: Use `IS_QA`, `IS_STAGING`, `IS_PROD` for environment-specific logic
415
+ 5. **Environment Tags**: Use `QA`, `IS_STAGING`, `IS_PROD` for environment-specific logic
409
416
  6. **Test Type Tags**: Use `SMOKE`, `REGRESSION`, `CRITICAL` for test categorization
410
417
 
411
418
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cypress-conditional-tags",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Access runtime tags (from CLI) and test tags (from config) inside Cypress test logic",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -16,10 +16,10 @@
16
16
  "author": "Jaydip Maniya",
17
17
  "license": "MIT",
18
18
  "peerDependencies": {
19
- "cypress": ">=10.0.0"
19
+ "cypress": ">=15.10.0"
20
20
  },
21
21
  "devDependencies": {
22
- "cypress": "^15.12.0",
22
+ "cypress": "^15.19.0",
23
23
  "mocha": "^10.2.0",
24
24
  "chai": "^4.3.10"
25
25
  },
@@ -41,5 +41,8 @@
41
41
  "src/",
42
42
  "README.md",
43
43
  "LICENSE"
44
- ]
45
- }
44
+ ],
45
+ "dependencies": {
46
+ "glob": "^10.0.0"
47
+ }
48
+ }
package/src/index.d.ts CHANGED
@@ -2,6 +2,18 @@
2
2
  * TypeScript definitions for cypress-conditional-tags
3
3
  */
4
4
 
5
+ /** Metadata for a single test found by the static spec scanner */
6
+ interface TestEntry {
7
+ /** Relative file path, e.g. "cypress/e2e/login.cy.js" */
8
+ file: string;
9
+ /** The it() title string */
10
+ title: string;
11
+ /** Full title including describe path, e.g. "Suite > test title" */
12
+ fullTitle: string;
13
+ /** All tags — own tags merged with ancestor describe tags */
14
+ tags: string[];
15
+ }
16
+
5
17
  declare namespace Cypress {
6
18
  interface Chainable {
7
19
  /**
@@ -24,7 +36,7 @@ declare namespace Cypress {
24
36
 
25
37
  /**
26
38
  * Check if runtime tag matches the given tag (async)
27
- * @paramtag - Tag to check
39
+ * @param tag - Tag to check
28
40
  * @example
29
41
  * cy.isRuntimeTag('IS_QA').then(matches => {
30
42
  * if (matches) {
@@ -32,10 +44,10 @@ declare namespace Cypress {
32
44
  * }
33
45
  * })
34
46
  */
35
- isRuntimeTag(tag:string): Chainable<boolean>;
47
+ isRuntimeTag(tag: string): Chainable<boolean>;
36
48
 
37
49
  /**
38
- * Get tags for current test from config (async)
50
+ * Get tags for current test - includes own tags and all ancestor describe tags (async)
39
51
  * @example
40
52
  * cy.getTestTags().then(tags => {
41
53
  * expect(tags).to.include('IS_QA')
@@ -45,7 +57,7 @@ declare namespace Cypress {
45
57
 
46
58
  /**
47
59
  * Check if current test has a specific tag (async)
48
- * @paramtag - Tag to check
60
+ * @param tag - Tag to check
49
61
  * @example
50
62
  * cy.hasTestTag('IS_QA').then(hasIt => {
51
63
  * if (hasIt) {
@@ -65,6 +77,35 @@ declare namespace Cypress {
65
77
  * })
66
78
  */
67
79
  matchesRuntimeTag(): Chainable<boolean>;
80
+
81
+ /**
82
+ * Query all tests across spec files that have the given tag (async, requires specPattern in setupNodeEvents)
83
+ * @param tag - Tag to search for
84
+ * @example
85
+ * cy.getTestsByTag('IS_QA').then(tests => {
86
+ * cy.log(Found ${tests.length} tests tagged IS_QA)
87
+ * tests.forEach(t => cy.log(${t.file}: ${t.fullTitle}))
88
+ * })
89
+ */
90
+ getTestsByTag(tag: string): Chainable<TestEntry[]>;
91
+
92
+ /**
93
+ * Get all tests across spec files that have at least one tag (async, requires specPattern in setupNodeEvents)
94
+ * @example
95
+ * cy.getAllTaggedTests().then(tests => {
96
+ * cy.log(Total tagged tests: ${tests.length})
97
+ * })
98
+ */
99
+ getAllTaggedTests(): Chainable<TestEntry[]>;
100
+
101
+ /**
102
+ * Get a sorted list of all unique tags found across all spec files (async, requires specPattern in setupNodeEvents)
103
+ * @example
104
+ * cy.getAllTags().then(tags => {
105
+ * cy.log(Available tags: ${tags.join(', ')})
106
+ * })
107
+ */
108
+ getAllTags(): Chainable<string[]>;
68
109
  }
69
110
 
70
111
  interface Tags {
@@ -90,18 +131,18 @@ declare namespace Cypress {
90
131
 
91
132
  /**
92
133
  * Check if runtime tag matches the given tag (synchronous)
93
- * @paramtag - Tag to check
134
+ * @param tag - Tag to check
94
135
  * @returns true if runtime tag matches
95
136
  * @example
96
137
  * if (Cypress.Tags.isRuntime('IS_QA')) {
97
138
  * cy.log('Running in QA mode')
98
139
  * }
99
140
  */
100
- isRuntime(tag:string): boolean;
141
+ isRuntime(tag: string): boolean;
101
142
 
102
143
  /**
103
144
  * Check if runtime has any of the given tags (synchronous)
104
- * @paramtags - Tags to check
145
+ * @param tags - Tags to check
105
146
  * @returns true if any tag matches
106
147
  * @example
107
148
  * if (Cypress.Tags.isAnyRuntime('IS_QA', 'IS_STAGING')) {
@@ -132,7 +173,7 @@ declare namespace Cypress {
132
173
 
133
174
  /**
134
175
  * Check if current test has a specific tag (synchronous)
135
- * @paramtag - Tag to check
176
+ * @param tag - Tag to check
136
177
  * @returns true if test has the tag
137
178
  * @example
138
179
  * if (Cypress.Tags.hasTest('IS_QA')) {
@@ -143,7 +184,7 @@ declare namespace Cypress {
143
184
 
144
185
  /**
145
186
  * Check if current test has any of the given tags (synchronous)
146
- * @paramtags - Tags to check
187
+ * @param tags - Tags to check
147
188
  * @returns true if test has any tag
148
189
  * @example
149
190
  * if (Cypress.Tags.hasAnyTest('IS_QA', 'IS_GA')) {
@@ -154,7 +195,7 @@ declare namespace Cypress {
154
195
 
155
196
  /**
156
197
  * Check if current test has all of the given tags (synchronous)
157
- * @paramtags - Tags to check
198
+ * @param tags - Tags to check
158
199
  * @returns true if test has all tags
159
200
  * @example
160
201
  * if (Cypress.Tags.hasAllTest('IS_QA', 'SMOKE')) {
@@ -182,13 +223,25 @@ declare namespace Cypress {
182
223
  }
183
224
  }
184
225
 
226
+ /** Options for setupNodeEvents */
227
+ interface SetupNodeEventsOptions {
228
+ /**
229
+ * Glob pattern pointing at your spec files, relative to project root.
230
+ * When provided, enables cy.getTestsByTag(), cy.getAllTaggedTests(), cy.getAllTags().
231
+ * @example 'cypress/e2e/**\/*.cy.js'
232
+ */
233
+ specPattern?: string;
234
+ }
235
+
185
236
  /**
186
237
  * Plugin configuration for cypress.config.js
238
+ * @param options.specPattern — enables cross-file tag query tasks
187
239
  */
188
240
  export function setupNodeEvents(
189
241
  on: Cypress.PluginEvents,
190
- config: Cypress.PluginConfigOptions
191
- ):Cypress.PluginConfigOptions;
242
+ config: Cypress.PluginConfigOptions,
243
+ options?: SetupNodeEventsOptions
244
+ ): Cypress.PluginConfigOptions;
192
245
 
193
246
  /**
194
247
  * Support file setup for cypress/support/e2e.js
@@ -210,21 +263,21 @@ export function getAllRuntimeTags(): string[];
210
263
 
211
264
  /**
212
265
  * Check if runtime tag matches the given tag
213
- * @paramtag - Tag to check
266
+ * @param tag - Tag to check
214
267
  * @returns true if runtime tag matches
215
268
  */
216
269
  export function isRuntimeTag(tag: string): boolean;
217
270
 
218
271
  /**
219
272
  * Check if runtime has any of the given tags
220
- * @paramtags - Tags to check
273
+ * @param tags - Tags to check
221
274
  * @returns true if any tag matches
222
275
  */
223
276
  export function isAnyRuntimeTag(...tags: string[]): boolean;
224
277
 
225
278
  /**
226
279
  * Check if runtime has all of the given tags
227
- * @paramtags - Tags to check
280
+ * @param tags - Tags to check
228
281
  * @returns true if all tags match
229
282
  */
230
283
  export function isAllRuntimeTags(...tags: string[]): boolean;
@@ -237,21 +290,21 @@ export function getTestTags(): string[];
237
290
 
238
291
  /**
239
292
  * Check if current test has a specific tag
240
- * @paramtag - Tag to check
293
+ * @param tag - Tag to check
241
294
  * @returns true if test has the tag
242
295
  */
243
296
  export function hasTestTag(tag: string): boolean;
244
297
 
245
298
  /**
246
299
  * Check if current test has any of the given tags
247
- * @paramtags - Tags to check
300
+ * @param tags - Tags to check
248
301
  * @returns true if test has any tag
249
302
  */
250
303
  export function hasAnyTestTag(...tags: string[]): boolean;
251
304
 
252
305
  /**
253
306
  * Check if current test has all of the given tags
254
- * @paramtags - Tags to check
307
+ * @param tags - Tags to check
255
308
  * @returns true if test has all tags
256
309
  */
257
310
  export function hasAllTestTags(...tags: string[]): boolean;
@@ -261,3 +314,21 @@ export function hasAllTestTags(...tags: string[]): boolean;
261
314
  * @returns true if test has the runtime tag
262
315
  */
263
316
  export function matchesRuntimeTag(): boolean;
317
+
318
+ /**
319
+ * Scan a single spec file and return all discovered test entries (Node-side helper)
320
+ * @param filePath - absolute path to the spec file
321
+ */
322
+ export function scanSpecFile(filePath: string): TestEntry[];
323
+
324
+ /**
325
+ * Build a tag registry by scanning all spec files matching a glob pattern (Node-side helper)
326
+ * @param specPattern - glob pattern relative to cwd
327
+ */
328
+ export function buildTagRegistry(specPattern: string): Map<string, TestEntry[]>;
329
+
330
+ /**
331
+ * Extract tag string values from a config object fragment (Node-side helper)
332
+ * @param configBody - the text between { } in an it/describe config argument
333
+ */
334
+ export function extractTagsFromConfigString(configBody: string): string[];
package/src/index.js CHANGED
@@ -1,26 +1,49 @@
1
1
  /**
2
2
  * Cypress Conditional Tags Plugin
3
3
  * Access runtime tags (from CLI) and test tags (from config) inside test logic
4
+ *
5
+ * Requires Cypress >= 15.10.0
6
+ * Uses Cypress.expose() for public/non-sensitive plugin configuration values.
7
+ * See: https://docs.cypress.io/guides/references/migration-guide#Migrating-away-from-Cypressenv
4
8
  */
5
9
 
6
10
  const testTagStore = new Map();
7
11
 
12
+ // Stack tracking the tags of currently executing describe() blocks.
13
+ // Each entry is an array of tags from one describe level.
14
+ // When a describe() opens it pushes its tags; when it closes it pops them.
15
+ const describeScopeStack = [];
16
+
17
+ /**
18
+ * Collect all tags in scope for the currently executing context:
19
+ * every describe ancestor's tags merged + the test's own tags.
20
+ * @param {string[]} ownTags - tags declared directly on the it()
21
+ * @returns {string[]} deduplicated merged tag list
22
+ */
23
+ function mergeWithScopeTags(ownTags) {
24
+ const inherited = describeScopeStack.flat();
25
+ const merged = [...inherited, ...ownTags];
26
+ // Deduplicate while preserving order
27
+ return [...new Set(merged)];
28
+ }
29
+
30
+ // -----------------------------------------------------------------------------
31
+ // RUNTIME TAG FUNCTIONS (browser-side, reads CLI-passed expose values)
32
+ // -----------------------------------------------------------------------------
33
+
8
34
  /**
9
35
  * Get the runtime tag passed via CLI
10
- * Supports both 'conditionalTags' (preferred) and 'grepTags' (fallback for backward compatibility)
11
- * @returns{string|null} - The runtime tag or null
36
+ * Supports both 'conditionalTags' (preferred) and 'grepTags' (fallback)
37
+ * @returns {string|null}
12
38
  */
13
39
  function getRuntimeTag() {
14
- // Prefer conditionalTags, fallback to grepTags for backward compatibility
15
- const conditionalTags = Cypress.env('conditionalTags');
16
- const grepTags = Cypress.env('grepTags');
17
- const tags = conditionalTags || grepTags;
40
+ const conditionalTags = Cypress.expose('conditionalTags');
41
+ const grepTags = Cypress.expose('grepTags');
42
+ const tags = conditionalTags || grepTags;
18
43
 
19
44
  if (!tags) return null;
20
45
 
21
- // Handle multiple tags separated by + or ,
22
46
  if (typeof tags === 'string') {
23
- // Return first tag if multiple
24
47
  return tags.split(/[+,]/)[0].trim();
25
48
  }
26
49
 
@@ -29,61 +52,60 @@ function getRuntimeTag() {
29
52
 
30
53
  /**
31
54
  * Get all runtime tags passed via CLI
32
- * Supports both 'conditionalTags' (preferred) and 'grepTags' (fallback for backward compatibility)
33
- * @returns{string[]} - Array of runtime tags
55
+ * @returns {string[]}
34
56
  */
35
57
  function getAllRuntimeTags() {
36
- // Prefer conditionalTags, fallback to grepTags for backward compatibility
37
- const conditionalTags = Cypress.env('conditionalTags');
38
- const grepTags = Cypress.env('grepTags');
39
- const tags = conditionalTags || grepTags;
58
+ const conditionalTags = Cypress.expose('conditionalTags');
59
+ const grepTags = Cypress.expose('grepTags');
60
+ const tags = conditionalTags || grepTags;
40
61
 
41
62
  if (!tags) return [];
42
63
 
43
- if (typeoftags === 'string') {
44
- return tags.split(/[+,]/).map(tag => tag.trim()).filter(Boolean);
64
+ if (typeof tags === 'string') {
65
+ return tags.split(/[+,]/).map(t => t.trim()).filter(Boolean);
45
66
  }
46
67
 
47
- if (Array.isArray(tags)) {
48
- return tags;
49
- }
68
+ if (Array.isArray(tags)) return tags;
50
69
 
51
70
  return [tags];
52
71
  }
53
72
 
54
73
  /**
55
74
  * Check if runtime tag matches the given tag
56
- * @param{string}tag - Tag to check
57
- * @returns{boolean}
75
+ * @param {string} tag
76
+ * @returns {boolean}
58
77
  */
59
78
  function isRuntimeTag(tag) {
60
- const runtimeTags = getAllRuntimeTags();
61
- return runtimeTags.includes(tag);
79
+ return getAllRuntimeTags().includes(tag);
62
80
  }
63
81
 
64
82
  /**
65
83
  * Check if runtime has any of the given tags
66
- * @param{...string}tags - Tags to check
67
- * @returns{boolean}
84
+ * @param {...string} tags
85
+ * @returns {boolean}
68
86
  */
69
87
  function isAnyRuntimeTag(...tags) {
70
88
  const runtimeTags = getAllRuntimeTags();
71
- return tags.some(tag => runtimeTags.includes(tag));
89
+ return tags.some(t => runtimeTags.includes(t));
72
90
  }
73
91
 
74
92
  /**
75
93
  * Check if runtime has all of the given tags
76
- * @param{...string}tags - Tags to check
77
- * @returns{boolean}
94
+ * @param {...string} tags
95
+ * @returns {boolean}
78
96
  */
79
97
  function isAllRuntimeTags(...tags) {
80
98
  const runtimeTags = getAllRuntimeTags();
81
- return tags.every(tag => runtimeTags.includes(tag));
99
+ return tags.every(t => runtimeTags.includes(t));
82
100
  }
83
101
 
102
+ // -----------------------------------------------------------------------------
103
+ // TEST TAG FUNCTIONS (browser-side, reads from testTagStore)
104
+ // -----------------------------------------------------------------------------
105
+
84
106
  /**
85
- * Get tags for current test from config
86
- * @returns{string[]} - Array of test tags
107
+ * Get tags for the current test (own tags + all ancestor describe tags)
108
+ * @returns {string[]}
87
109
  */
88
110
  function getTestTags() {
89
111
  const currentTest = cy.state('test');
@@ -95,51 +117,50 @@ function getTestTags() {
95
117
 
96
118
  /**
97
119
  * Check if current test has a specific tag
98
- * @param{string}tag - Tag to check
99
- * @returns{boolean}
120
+ * @param {string} tag
121
+ * @returns {boolean}
100
122
  */
101
123
  function hasTestTag(tag) {
102
- const tags = getTestTags();
103
- return tags.includes(tag);
124
+ return getTestTags().includes(tag);
104
125
  }
105
126
 
106
127
  /**
107
128
  * Check if current test has any of the given tags
108
- * @param{...string}tags - Tags to check
109
- * @returns{boolean}
129
+ * @param {...string} tags
130
+ * @returns {boolean}
110
131
  */
111
132
  function hasAnyTestTag(...tags) {
112
133
  const testTags = getTestTags();
113
- return tags.some(tag => testTags.includes(tag));
134
+ return tags.some(t => testTags.includes(t));
114
135
  }
115
136
 
116
137
  /**
117
138
  * Check if current test has all of the given tags
118
- * @param{...string}tags - Tags to check
119
- * @returns{boolean}
139
+ * @param {...string} tags
140
+ * @returns {boolean}
120
141
  */
121
142
  function hasAllTestTags(...tags) {
122
143
  const testTags = getTestTags();
123
- return tags.every(tag => testTags.includes(tag));
144
+ return tags.every(t => testTags.includes(t));
124
145
  }
125
146
 
126
147
  /**
127
148
  * Check if current test matches the runtime tag
128
- * @returns{boolean}
149
+ * @returns {boolean}
129
150
  */
130
151
  function matchesRuntimeTag() {
131
152
  const runtimeTags = getAllRuntimeTags();
132
- const testTags = getTestTags();
153
+ const testTags = getTestTags();
133
154
 
134
- if (runtimeTags.length === 0) return true; // No runtime filter
155
+ if (runtimeTags.length === 0) return true; // No runtime filter ? always passes
135
156
 
136
- return runtimeTags.some(runtimeTag => testTags.includes(runtimeTag));
157
+ return runtimeTags.some(rt => testTags.includes(rt));
137
158
  }
138
159
 
139
160
  /**
140
- * Store tags for a test
141
- * @param{string}testId - Test identifier
142
- * @param{string[]}tags - Array of tags
161
+ * Store the fully-merged tag list for a test
162
+ * @param {string} testId
163
+ * @param {string[]} tags
143
164
  */
144
165
  function storeTestTags(testId, tags) {
145
166
  if (tags && tags.length > 0) {
@@ -147,165 +168,399 @@ function storeTestTags(testId, tags) {
147
168
  }
148
169
  }
149
170
 
171
+ // -----------------------------------------------------------------------------
172
+ // WRAPPING HELPERS
173
+ // -----------------------------------------------------------------------------
174
+
150
175
  /**
151
- * Wrap it() to capture tags from config
176
+ * Wrap it() to capture own tags + inherit all ancestor describe tags.
152
177
  */
153
178
  function wrapIt(originalIt) {
154
179
  return function(title, configOrFn, fn) {
155
180
  let config = {};
156
181
  let testFn = fn;
157
182
 
158
- // Handle different signatures
159
183
  if (typeof configOrFn === 'function') {
160
184
  testFn = configOrFn;
161
- } else if (typeof configOrFn === 'object') {
185
+ } else if (configOrFn && typeof configOrFn === 'object') {
162
186
  config = configOrFn;
163
187
  testFn = fn;
164
188
  }
165
189
 
166
- // Extract tags from config
167
- const tags = config.tags || [];
190
+ const ownTags = config.tags || [];
191
+
192
+ // Capture merged tags NOW (sync, during spec collection) while the
193
+ // describeScopeStack still reflects all ancestor describe() tags.
194
+ const mergedTags = mergeWithScopeTags(ownTags);
168
195
 
169
196
  return originalIt(title, config, function() {
170
- // Store tags for this test
171
197
  const currentTest = cy.state('test');
172
- if (currentTest && tags.length > 0) {
198
+ if (currentTest) {
173
199
  const testId = currentTest.id || currentTest.title;
174
- storeTestTags(testId, tags);
200
+ storeTestTags(testId, mergedTags);
175
201
  }
176
202
 
177
- // Execute the test
178
- if (testFn) {
179
- return testFn.call(this);
180
- }
203
+ if (testFn) return testFn.call(this);
181
204
  });
182
205
  };
183
206
  }
184
207
 
185
208
  /**
186
- * Wrap describe() to support tag inheritance (future enhancement)
209
+ * Wrap describe() to push/pop its tags onto the describe scope stack
210
+ * so all nested it() calls automatically inherit them.
187
211
  */
188
212
  function wrapDescribe(originalDescribe) {
189
213
  return function(title, configOrFn, fn) {
190
- let config = {};
191
- let suiteFn = fn;
214
+ let config = {};
215
+ let suiteFn = fn;
192
216
 
193
- // Handle different signatures
194
217
  if (typeof configOrFn === 'function') {
195
218
  suiteFn = configOrFn;
196
- } else if (typeof configOrFn === 'object') {
197
- config = configOrFn;
219
+ } else if (configOrFn && typeof configOrFn === 'object') {
220
+ config = configOrFn;
198
221
  suiteFn = fn;
199
222
  }
200
223
 
201
- // Note: Describe-level tags would need special handling
202
- // For now, we focus on test-level tags
224
+ const suiteTags = config.tags || [];
225
+
226
+ return originalDescribe(title, config, function() {
227
+ // Push this suite's tags onto the stack before nested tests register
228
+ describeScopeStack.push(suiteTags);
203
229
 
204
- return originalDescribe(title, config, suiteFn);
230
+ try {
231
+ if (suiteFn) suiteFn.call(this);
232
+ } finally {
233
+ // Always pop even if suiteFn throws
234
+ describeScopeStack.pop();
235
+ }
236
+ });
205
237
  };
206
238
  }
207
239
 
240
+ // -----------------------------------------------------------------------------
241
+ // NODE-SIDE: SPEC FILE SCANNER (used inside setupNodeEvents)
242
+ // -----------------------------------------------------------------------------
243
+
244
+ /**
245
+ * Extract { title, tags, describePath } entries from a spec file by static analysis.
246
+ * Uses a simple line-by-line regex scan no runtime execution, no dependencies.
247
+ *
248
+ * Recognises patterns like:
249
+ * it('title', { tags: ['A', 'B'] }, ...)
250
+ * it("title", { tags: ["A"] }, ...)
251
+ * describe('Suite', { tags: ['X'] }, ...)
252
+ *
253
+ * @param {string} filePath - absolute path to the spec file
254
+ * @param {string} [baseDir] - base directory for computing relative file paths (defaults to cwd)
255
+ * @returns {Array<{file: string, title: string, fullTitle: string, tags: string[]}>}
256
+ */
257
+ function scanSpecFile(filePath, baseDir) {
258
+ // webpackIgnore: true tells webpack to skip bundling these Node-only modules.
259
+ // This function is only called from setupNodeEvents (Node process) - never browser.
260
+ const fs = require('fs');
261
+ const path = require('path');
262
+
263
+ let source;
264
+ try {
265
+ source = fs.readFileSync(filePath, 'utf8');
266
+ } catch (_) {
267
+ return [];
268
+ }
269
+
270
+ const results = [];
271
+ const describeStack = []; // { title, tags }[]
272
+
273
+ // Matches: it('title', { tags: ['A','B'] }, ...) or it('title', () => ...)
274
+ const itRegex = /^\s*(?:it|test)\s*\(\s*(['"`])(.*?)\1\s*(?:,\s*\{([^}]*)\})?/;
275
+
276
+ // Matches: describe('Suite', { tags: ['X'] }, ...) or describe('Suite', () => ...)
277
+ const describeOpenRegex = /^\s*(?:describe|context)\s*\(\s*(['"`])(.*?)\1\s*(?:,\s*\{([^}]*)\})?/;
278
+
279
+ // Closing brace that ends a describe block (very rough heuristic counts depth)
280
+ const describeCloseRegex = /^\s*\}\s*\)\s*;?\s*$/;
281
+
282
+ const lines = source.split('\n');
283
+
284
+ for (const line of lines) {
285
+ // -- describe open --------------------------------------------------------
286
+ const dMatch = line.match(describeOpenRegex);
287
+ if (dMatch) {
288
+ const suiteTitle = dMatch[2];
289
+ const suiteBody = dMatch[3] || '';
290
+ const suiteTags = extractTagsFromConfigString(suiteBody);
291
+ describeStack.push({ title: suiteTitle, tags: suiteTags });
292
+ continue;
293
+ }
294
+
295
+ // -- describe close -------------------------------------------------------
296
+ if (describeCloseRegex.test(line) && describeStack.length > 0) {
297
+ describeStack.pop();
298
+ continue;
299
+ }
300
+
301
+ // -- it() / test() --------------------------------------------------------
302
+ const iMatch = line.match(itRegex);
303
+ if (iMatch) {
304
+ const testTitle = iMatch[2];
305
+ const testBody = iMatch[3] || '';
306
+ const ownTags = extractTagsFromConfigString(testBody);
307
+
308
+ // Merge inherited describe tags with own tags (deduplicated)
309
+ const inheritedTags = describeStack.flatMap(d => d.tags);
310
+ const allTags = [...new Set([...inheritedTags, ...ownTags])];
311
+
312
+ const describePath = describeStack.map(d => d.title).join(' > ');
313
+ const fullTitle = describePath ? `${describePath} > ${testTitle}` : testTitle;
314
+ const relFile = path.relative(baseDir || process.cwd(), filePath);
315
+
316
+ results.push({
317
+ file: relFile,
318
+ title: testTitle,
319
+ fullTitle,
320
+ tags: allTags
321
+ });
322
+ }
323
+ }
324
+
325
+ return results;
326
+ }
327
+
328
+ /**
329
+ * Extract string values from a { tags: [...] } config fragment.
330
+ * Only handles string literals - ignores dynamic/variable tags (by design).
331
+ * @param {string} configBody - the content between { and } in the it/describe config
332
+ * @returns {string[]}
333
+ */
334
+ function extractTagsFromConfigString(configBody) {
335
+ const tagsMatch = configBody.match(/tags\s*:\s*\[([^\]]*)\]/);
336
+ if (!tagsMatch) return [];
337
+
338
+ const inside = tagsMatch[1];
339
+ const tags = [];
340
+ const tagRe = /['"`]([^'"`]+)['"`]/g;
341
+ let m;
342
+ while ((m = tagRe.exec(inside)) !== null) {
343
+ tags.push(m[1]);
344
+ }
345
+ return tags;
346
+ }
347
+
348
+ /**
349
+ * Scan all spec files matching a glob pattern and build a tag registry.
350
+ * @param {string} specPattern - glob pattern relative to cwd (e.g. 'cypress/e2e/**\/*.cy.js')
351
+ * @returns {Map<string, Array<{file,title,fullTitle,tags}>>} tag ? test entries
352
+ */
353
+ function buildTagRegistry(specPattern, baseDir) {
354
+ // webpackIgnore: true tells webpack to skip bundling these Node-only modules.
355
+ // This function is only called from setupNodeEvents (Node process) - never browser.
356
+ const path = require('path');
357
+ const globModule = require('glob');
358
+ const globFn = globModule.sync || globModule;
359
+
360
+ const registry = new Map();
361
+ const cwd = baseDir || process.cwd();
362
+
363
+ // Resolve files matching pattern
364
+ let files = [];
365
+ try {
366
+ files = globFn(specPattern, { nodir: true, cwd });
367
+ } catch (_) {
368
+ files = [];
369
+ }
370
+
371
+ for (const relFile of files) {
372
+ const absFile = path.resolve(cwd, relFile);
373
+ const entries = scanSpecFile(absFile, cwd);
374
+
375
+ for (const entry of entries) {
376
+ for (const tag of entry.tags) {
377
+ if (!registry.has(tag)) registry.set(tag, []);
378
+ registry.get(tag).push(entry);
379
+ }
380
+ }
381
+ }
382
+
383
+ return registry;
384
+ }
385
+
386
+ // -----------------------------------------------------------------------------
387
+ // PUBLIC NODE-SIDE SETUP
388
+ // -----------------------------------------------------------------------------
389
+
208
390
  /**
209
391
  * Plugin setup function for cypress.config.js
392
+ *
393
+ * Accepts an optional options object:
394
+ * { specPattern: 'cypress/e2e/**\/*.cy.js' }
395
+ *
396
+ * When specPattern is provided, registers:
397
+ * cy.task('getTestsByTag', tag) ? TestEntry[]
398
+ * cy.task('getAllTaggedTests', _) ? TestEntry[] (all tests that have any tag)
399
+ * cy.task('getAllTags', _) ? string[] (sorted list of unique tags)
400
+ *
401
+ * @param {Function} on
402
+ * @param {object} config
403
+ * @param {object} [options]
404
+ * @param {string} [options.specPattern]
405
+ * @returns {object} config
210
406
  */
211
- function setupNodeEvents(on, config) {
212
- // This plugin does not provide test filtering
213
- // For test filtering based on tags, install and configure @cypress/grep separately
214
- // See: https://github.com/cypress-io/cypress/tree/develop/npm/grep
407
+ function setupNodeEvents(on, config, options = {}) {
408
+ if (options.specPattern) {
409
+ const projectRoot = (config && config.projectRoot) ? config.projectRoot : process.cwd();
410
+ const registry = buildTagRegistry(options.specPattern, projectRoot);
411
+
412
+ on('task', {
413
+ /**
414
+ * Return all tests that have the given tag.
415
+ * @param {string} tag
416
+ * @returns {Array<{file,title,fullTitle,tags}>}
417
+ */
418
+ getTestsByTag(tag) {
419
+ return registry.get(tag) || [];
420
+ },
421
+
422
+ /**
423
+ * Return all tests that have at least one tag.
424
+ * @returns {Array<{file,title,fullTitle,tags}>}
425
+ */
426
+ getAllTaggedTests(_) {
427
+ const seen = new Set();
428
+ const all = [];
429
+ for (const entries of registry.values()) {
430
+ for (const entry of entries) {
431
+ const key = `${entry.file}::${entry.fullTitle}`;
432
+ if (!seen.has(key)) {
433
+ seen.add(key);
434
+ all.push(entry);
435
+ }
436
+ }
437
+ }
438
+ return all;
439
+ },
440
+
441
+ /**
442
+ * Return a sorted list of all unique tags found across all spec files.
443
+ * @returns {string[]}
444
+ */
445
+ getAllTags(_) {
446
+ return [...registry.keys()].sort();
447
+ }
448
+ });
449
+ }
215
450
 
216
451
  return config;
217
452
  }
218
453
 
454
+ // -----------------------------------------------------------------------------
455
+ // PUBLIC BROWSER-SIDE SETUP
456
+ // -----------------------------------------------------------------------------
457
+
219
458
  /**
220
459
  * Support file setup function for cypress/support/e2e.js
221
460
  */
222
461
  function setupSupport() {
223
- // Wrap global it/test functions
462
+ // -- Feature 1: Version guard ---------------------------------------------
463
+ if (typeof Cypress.expose !== 'function') {
464
+ throw new Error(
465
+ '[cypress-conditional-tags] Cypress.expose() is not available.\n' +
466
+ 'This plugin (v2.0.0+) requires Cypress >= 15.10.0.\n' +
467
+ 'If you are on an older version of Cypress, please use cypress-conditional-tags v1.0.1 or earlier:\n' +
468
+ ' npm install cypress-conditional-tags@1.0.1\n' +
469
+ 'Otherwise, upgrade Cypress to >= 15.10.0 to use this version of the plugin.'
470
+ );
471
+ }
472
+
473
+ // -- Feature 2: Wrap it() - captures own tags + inherits describe tags ----
224
474
  if (typeof it !== 'undefined') {
225
475
  const originalIt = it;
226
- global.it = wrapIt(originalIt);
476
+ global.it = wrapIt(originalIt);
227
477
  global.test = wrapIt(originalIt);
228
478
 
229
- // Preserve it.only, it.skip, etc.
230
479
  ['only', 'skip'].forEach(modifier => {
231
480
  if (originalIt[modifier]) {
232
- global.it[modifier] = wrapIt(originalIt[modifier]);
481
+ global.it[modifier] = wrapIt(originalIt[modifier]);
233
482
  global.test[modifier] = wrapIt(originalIt[modifier]);
234
483
  }
235
484
  });
236
485
  }
237
486
 
238
- // Wrap global describe/context functions (for future tag inheritance)
487
+ // -- Feature 2: Wrap describe() - pushes/pops suite tags onto scope stack -
239
488
  if (typeof describe !== 'undefined') {
240
489
  const originalDescribe = describe;
241
490
  global.describe = wrapDescribe(originalDescribe);
242
- global.context = wrapDescribe(originalDescribe);
491
+ global.context = wrapDescribe(originalDescribe);
243
492
 
244
- // Preserve describe.only, describe.skip, etc.
245
493
  ['only', 'skip'].forEach(modifier => {
246
494
  if (originalDescribe[modifier]) {
247
495
  global.describe[modifier] = wrapDescribe(originalDescribe[modifier]);
248
- global.context[modifier] = wrapDescribe(originalDescribe[modifier]);
496
+ global.context[modifier] = wrapDescribe(originalDescribe[modifier]);
249
497
  }
250
498
  });
251
499
  }
252
500
 
253
- // Add custom Cypress commands for async access
254
- Cypress.Commands.add('getRuntimeTag', () => {
255
- return cy.wrap(getRuntimeTag());
256
- });
501
+ // -- Async Cypress commands -----------------------------------------------
502
+ Cypress.Commands.add('getRuntimeTag', () => cy.wrap(getRuntimeTag()));
257
503
 
258
- Cypress.Commands.add('getAllRuntimeTags', () => {
259
- return cy.wrap(getAllRuntimeTags());
260
- });
504
+ Cypress.Commands.add('getAllRuntimeTags', () => cy.wrap(getAllRuntimeTags()));
261
505
 
262
- Cypress.Commands.add('isRuntimeTag', (tag) => {
263
- return cy.wrap(isRuntimeTag(tag));
264
- });
506
+ Cypress.Commands.add('isRuntimeTag', (tag) => cy.wrap(isRuntimeTag(tag)));
507
+
508
+ Cypress.Commands.add('getTestTags', () => cy.wrap(getTestTags()));
509
+
510
+ Cypress.Commands.add('hasTestTag', (tag) => cy.wrap(hasTestTag(tag)));
511
+
512
+ Cypress.Commands.add('matchesRuntimeTag', () => cy.wrap(matchesRuntimeTag()));
265
513
 
266
- Cypress.Commands.add('getTestTags', () => {
267
- return cy.wrap(getTestTags());
514
+ // -- Feature 3: Cross-file tag query commands (bridge to cy.task) ---------
515
+ Cypress.Commands.add('getTestsByTag', (tag) => {
516
+ return cy.task('getTestsByTag', tag, { log: false });
268
517
  });
269
518
 
270
- Cypress.Commands.add('hasTestTag', (tag) => {
271
- return cy.wrap(hasTestTag(tag));
519
+ Cypress.Commands.add('getAllTaggedTests', () => {
520
+ return cy.task('getAllTaggedTests', null, { log: false });
272
521
  });
273
522
 
274
- Cypress.Commands.add('matchesRuntimeTag', () => {
275
- return cy.wrap(matchesRuntimeTag());
523
+ Cypress.Commands.add('getAllTags', () => {
524
+ return cy.task('getAllTags', null, { log: false });
276
525
  });
277
526
 
278
- // Add global Tags API for synchronous access
527
+ // -- Synchronous Tags API -------------------------------------------------
279
528
  Cypress.Tags = {
280
- // Runtime tags (from CLI)
281
- getRuntime:getRuntimeTag,
282
- getAllRuntime:getAllRuntimeTags,
283
- isRuntime:isRuntimeTag,
284
- isAnyRuntime:isAnyRuntimeTag,
285
- isAllRuntime:isAllRuntimeTags,
286
-
287
- // Test tags (from config)
288
- getTest:getTestTags,
289
- hasTest:hasTestTag,
290
- hasAnyTest:hasAnyTestTag,
291
- hasAllTest:hasAllTestTags,
529
+ // Runtime tags (from CLI via --expose)
530
+ getRuntime: getRuntimeTag,
531
+ getAllRuntime: getAllRuntimeTags,
532
+ isRuntime: isRuntimeTag,
533
+ isAnyRuntime: isAnyRuntimeTag,
534
+ isAllRuntime: isAllRuntimeTags,
535
+
536
+ // Test tags (own + inherited from describe ancestors)
537
+ getTest: getTestTags,
538
+ hasTest: hasTestTag,
539
+ hasAnyTest: hasAnyTestTag,
540
+ hasAllTest: hasAllTestTags,
292
541
 
293
542
  // Combined
294
- matchesRuntime:matchesRuntimeTag
543
+ matchesRuntime: matchesRuntimeTag
295
544
  };
296
545
  }
297
546
 
298
547
  module.exports = {
299
548
  setupNodeEvents,
300
549
  setupSupport,
550
+ // Runtime tag helpers (re-exported for direct import if needed)
301
551
  getRuntimeTag,
302
552
  getAllRuntimeTags,
303
553
  isRuntimeTag,
304
554
  isAnyRuntimeTag,
305
555
  isAllRuntimeTags,
556
+ // Test tag helpers
306
557
  getTestTags,
307
558
  hasTestTag,
308
559
  hasAnyTestTag,
309
560
  hasAllTestTags,
310
- matchesRuntimeTag
561
+ matchesRuntimeTag,
562
+ // Node-side helpers (exported for testing)
563
+ scanSpecFile,
564
+ buildTagRegistry,
565
+ extractTagsFromConfigString
311
566
  };