cypress-conditional-tags 1.0.1 → 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 +102 -0
- package/README.md +40 -33
- package/package.json +7 -4
- package/src/index.d.ts +89 -18
- package/src/index.js +366 -111
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,108 @@ 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
|
+
|
|
8
110
|
## [1.0.1] - 2026-05-03
|
|
9
111
|
|
|
10
112
|
### Fixed
|
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: ['
|
|
46
|
-
// Access runtime tag from CLI (passed via --env conditionalTags=
|
|
47
|
-
if (Cypress.Tags.isRuntime('
|
|
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('
|
|
51
|
-
cy.log('Running in
|
|
52
|
-
cy.visit('https://
|
|
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=
|
|
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: ['
|
|
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: ['
|
|
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=
|
|
134
|
+
npx cypress run --env conditionalTags=QA
|
|
135
135
|
|
|
136
|
-
# Result: All tests execute, but Cypress.Tags.isRuntime('
|
|
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=
|
|
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('
|
|
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: ['
|
|
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) // '
|
|
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('
|
|
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('
|
|
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) // ['
|
|
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: ['
|
|
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('
|
|
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: ['
|
|
267
|
-
const baseUrl = Cypress.Tags.isRuntime('
|
|
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: ['
|
|
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: ['
|
|
329
|
+
it('should test features', { tags: ['QA', 'NEW_FEATURE'] }, () => {
|
|
330
330
|
const features = {
|
|
331
|
-
newCheckout: Cypress.Tags.isRuntime('
|
|
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('
|
|
353
|
-
| `isAnyRuntime(...tags)` | Check if any runtime tag matches | `boolean` | `Cypress.Tags.isAnyRuntime('
|
|
354
|
-
| `isAllRuntime(...tags)` | Check if all runtime tags match | `boolean` | `Cypress.Tags.isAllRuntime('
|
|
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('
|
|
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: ['
|
|
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('
|
|
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**: `
|
|
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 `
|
|
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": "
|
|
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
|
|
19
|
+
"cypress": ">=15.10.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"cypress": "^15.
|
|
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
|
-
]
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"glob": "^10.0.0"
|
|
47
|
+
}
|
|
45
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
|
-
* @
|
|
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
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
|
11
|
-
* @returns{string|null}
|
|
36
|
+
* Supports both 'conditionalTags' (preferred) and 'grepTags' (fallback)
|
|
37
|
+
* @returns {string|null}
|
|
12
38
|
*/
|
|
13
39
|
function getRuntimeTag() {
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
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
|
-
*
|
|
33
|
-
* @returns{string[]} - Array of runtime tags
|
|
55
|
+
* @returns {string[]}
|
|
34
56
|
*/
|
|
35
57
|
function getAllRuntimeTags() {
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const
|
|
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
64
|
if (typeof tags === 'string') {
|
|
44
|
-
return tags.split(/[+,]/).map(
|
|
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
|
|
57
|
-
* @returns{boolean}
|
|
75
|
+
* @param {string} tag
|
|
76
|
+
* @returns {boolean}
|
|
58
77
|
*/
|
|
59
78
|
function isRuntimeTag(tag) {
|
|
60
|
-
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
86
|
-
* @returns{string[]}
|
|
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
|
|
99
|
-
* @returns{boolean}
|
|
120
|
+
* @param {string} tag
|
|
121
|
+
* @returns {boolean}
|
|
100
122
|
*/
|
|
101
123
|
function hasTestTag(tag) {
|
|
102
|
-
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
157
|
+
return runtimeTags.some(rt => testTags.includes(rt));
|
|
137
158
|
}
|
|
138
159
|
|
|
139
160
|
/**
|
|
140
|
-
* Store
|
|
141
|
-
* @param{string}testId
|
|
142
|
-
* @param{string[]}
|
|
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
|
|
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
|
-
|
|
167
|
-
|
|
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
|
|
198
|
+
if (currentTest) {
|
|
173
199
|
const testId = currentTest.id || currentTest.title;
|
|
174
|
-
storeTestTags(testId,
|
|
200
|
+
storeTestTags(testId, mergedTags);
|
|
175
201
|
}
|
|
176
202
|
|
|
177
|
-
|
|
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
|
|
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
|
|
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
|
|
219
|
+
} else if (configOrFn && typeof configOrFn === 'object') {
|
|
220
|
+
config = configOrFn;
|
|
198
221
|
suiteFn = fn;
|
|
199
222
|
}
|
|
200
223
|
|
|
201
|
-
|
|
202
|
-
// For now, we focus on test-level tags
|
|
224
|
+
const suiteTags = config.tags || [];
|
|
203
225
|
|
|
204
|
-
|
|
226
|
+
return originalDescribe(title, config, function() {
|
|
227
|
+
// Push this suite's tags onto the stack before nested tests register
|
|
228
|
+
describeScopeStack.push(suiteTags);
|
|
229
|
+
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
//
|
|
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
|
|
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]
|
|
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
|
|
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
|
|
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]
|
|
496
|
+
global.context[modifier] = wrapDescribe(originalDescribe[modifier]);
|
|
249
497
|
}
|
|
250
498
|
});
|
|
251
499
|
}
|
|
252
500
|
|
|
253
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
267
|
-
|
|
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('
|
|
271
|
-
return cy.
|
|
519
|
+
Cypress.Commands.add('getAllTaggedTests', () => {
|
|
520
|
+
return cy.task('getAllTaggedTests', null, { log: false });
|
|
272
521
|
});
|
|
273
522
|
|
|
274
|
-
Cypress.Commands.add('
|
|
275
|
-
return cy.
|
|
523
|
+
Cypress.Commands.add('getAllTags', () => {
|
|
524
|
+
return cy.task('getAllTags', null, { log: false });
|
|
276
525
|
});
|
|
277
526
|
|
|
278
|
-
//
|
|
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
|
|
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
|
};
|