Skip to content

Commit 663e2d7

Browse files
aaron-goffAaron Goffmskelton
authored
Add valid-test-tags rule (#358)
* Add valid-test-tags rule and associated docs and tests * Format --------- Co-authored-by: Aaron Goff <[email protected]> Co-authored-by: Mark Skelton <[email protected]>
1 parent 12b0832 commit 663e2d7

File tree

5 files changed

+423
-0
lines changed

5 files changed

+423
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,4 @@ CLI option\
167167
| [valid-expect-in-promise](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-expect-in-promise.md) | Require promises that have expectations in their chain to be valid || | |
168168
| [valid-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-expect.md) | Enforce valid `expect()` usage || | |
169169
| [valid-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-title.md) | Enforce valid titles || 🔧 | |
170+
| [valid-test-tags](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-test-tags.md) | Enforce valid tag format in test blocks || | |

docs/rules/valid-test-tags.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Valid Test Tags
2+
3+
This rule ensures that test tags in Playwright test files follow the correct
4+
format and meet any configured requirements.
5+
6+
## Rule Details
7+
8+
This rule enforces the following:
9+
10+
1. Tags must start with `@` (e.g., `@e2e`, `@regression`)
11+
2. (Optional, exclusive of 3) Tags must match one of the values in the
12+
`allowedTags` property
13+
3. (Optional, exclusive of 2) Tags must not match one of the values in the
14+
`disallowedTags` property
15+
16+
### Examples
17+
18+
```ts
19+
// Valid
20+
test('my test', { tag: '@e2e' }, async ({ page }) => {})
21+
test('my test', { tag: ['@e2e', '@login'] }, async ({ page }) => {})
22+
test.describe('my suite', { tag: '@regression' }, () => {})
23+
test.step('my step', { tag: '@critical' }, async () => {})
24+
25+
// Valid with test.skip, test.fixme, test.only
26+
test.skip('my test', { tag: '@e2e' }, async ({ page }) => {})
27+
test.fixme('my test', { tag: '@e2e' }, async ({ page }) => {})
28+
test.only('my test', { tag: '@e2e' }, async ({ page }) => {})
29+
30+
// Valid with annotation
31+
test(
32+
'my test',
33+
{
34+
tag: '@e2e',
35+
annotation: { type: 'issue', description: 'BUG-123' },
36+
},
37+
async ({ page }) => {},
38+
)
39+
40+
// Valid with array of annotations
41+
test(
42+
'my test',
43+
{
44+
tag: '@e2e',
45+
annotation: [{ type: 'issue', description: 'BUG-123' }, { type: 'flaky' }],
46+
},
47+
async ({ page }) => {},
48+
)
49+
50+
// Invalid
51+
test('my test', { tag: 'e2e' }, async ({ page }) => {}) // Missing @ prefix
52+
test('my test', { tag: ['e2e', 'login'] }, async ({ page }) => {}) // Missing @ prefix
53+
```
54+
55+
## Options
56+
57+
This rule accepts an options object with the following properties:
58+
59+
```ts
60+
type RuleOptions = {
61+
allowedTags?: (string | RegExp)[] // List of allowed tags or patterns
62+
disallowedTags?: (string | RegExp)[] // List of disallowed tags or patterns
63+
}
64+
```
65+
66+
### `allowedTags`
67+
68+
When specified, only the listed tags are allowed. You can use either exact
69+
strings or regular expressions to match patterns.
70+
71+
```ts
72+
// Only allow specific tags
73+
{
74+
"rules": {
75+
"playwright/valid-test-tags": ["error", { "allowedTags": ["@e2e", "@regression"] }]
76+
}
77+
}
78+
79+
// Allow tags matching a pattern
80+
{
81+
"rules": {
82+
"playwright/valid-test-tags": ["error", { "allowedTags": ["@e2e", /^@my-tag-\d+$/] }]
83+
}
84+
}
85+
```
86+
87+
### `disallowedTags`
88+
89+
When specified, the listed tags are not allowed. You can use either exact
90+
strings or regular expressions to match patterns.
91+
92+
```ts
93+
// Disallow specific tags
94+
{
95+
"rules": {
96+
"playwright/valid-test-tags": ["error", { "disallowedTags": ["@skip", "@todo"] }]
97+
}
98+
}
99+
100+
// Disallow tags matching a pattern
101+
{
102+
"rules": {
103+
"playwright/valid-test-tags": ["error", { "disallowedTags": ["@skip", /^@temp-/] }]
104+
}
105+
}
106+
```
107+
108+
Note: You cannot use both `allowedTags` and `disallowedTags` together. Choose
109+
one approach based on your needs.
110+
111+
## Further Reading
112+
113+
- [Playwright Test Tags Documentation](https://playwright.dev/docs/test-annotations#tag-tests)
114+
- [Playwright Test Annotations Documentation](https://playwright.dev/docs/test-annotations)

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import requireTopLevelDescribe from './rules/require-top-level-describe.js'
4747
import validDescribeCallback from './rules/valid-describe-callback.js'
4848
import validExpect from './rules/valid-expect.js'
4949
import validExpectInPromise from './rules/valid-expect-in-promise.js'
50+
import validTestTags from './rules/valid-test-tags.js'
5051
import validTitle from './rules/valid-title.js'
5152

5253
const index = {
@@ -100,6 +101,7 @@ const index = {
100101
'valid-describe-callback': validDescribeCallback,
101102
'valid-expect': validExpect,
102103
'valid-expect-in-promise': validExpectInPromise,
104+
'valid-test-tags': validTestTags,
103105
'valid-title': validTitle,
104106
},
105107
}

src/rules/valid-test-tags.test.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { runTSRuleTester } from '../utils/rule-tester.js'
2+
import validTestTags from './valid-test-tags.js'
3+
4+
runTSRuleTester('valid-test-tags', validTestTags, {
5+
invalid: [
6+
// Tag without @ prefix
7+
{
8+
code: "test('my test', { tag: 'e2e' }, async ({ page }) => {})",
9+
errors: [{ messageId: 'invalidTagFormat' }],
10+
},
11+
// Invalid tag value type
12+
{
13+
code: "test('my test', { tag: 123 }, async ({ page }) => {})",
14+
errors: [{ messageId: 'invalidTagValue' }],
15+
},
16+
// Array of tags without @ prefix
17+
{
18+
code: "test('my test', { tag: ['e2e', 'login'] }, async ({ page }) => {})",
19+
errors: [
20+
{ messageId: 'invalidTagFormat' },
21+
{ messageId: 'invalidTagFormat' },
22+
],
23+
},
24+
// Tag not in allowedTags list
25+
{
26+
code: "test('my test', { tag: '@e2e' }, async ({ page }) => {})",
27+
errors: [{ data: { tag: '@e2e' }, messageId: 'unknownTag' }],
28+
options: [{ allowedTags: ['@regression', '@smoke'] }],
29+
},
30+
// Tag in disallowedTags list
31+
{
32+
code: "test('my test', { tag: '@skip' }, async ({ page }) => {})",
33+
errors: [{ data: { tag: '@skip' }, messageId: 'disallowedTag' }],
34+
options: [{ disallowedTags: ['@skip', '@todo'] }],
35+
},
36+
// Tag matching disallowed pattern
37+
{
38+
code: "test('my test', { tag: '@temp-123' }, async ({ page }) => {})",
39+
errors: [{ data: { tag: '@temp-123' }, messageId: 'disallowedTag' }],
40+
options: [{ disallowedTags: ['@skip', /^@temp-/] }],
41+
},
42+
// Tag not matching allowed pattern
43+
{
44+
code: "test('my test', { tag: '@my-tag-abc' }, async ({ page }) => {})",
45+
errors: [{ data: { tag: '@my-tag-abc' }, messageId: 'unknownTag' }],
46+
options: [{ allowedTags: ['@regression', /^@my-tag-\d+$/] }],
47+
},
48+
// Invalid tag in test.skip
49+
{
50+
code: "test.skip('my test', { tag: 'e2e' }, async ({ page }) => {})",
51+
errors: [{ messageId: 'invalidTagFormat' }],
52+
},
53+
// Invalid tag in test.fixme
54+
{
55+
code: "test.fixme('my test', { tag: 'e2e' }, async ({ page }) => {})",
56+
errors: [{ messageId: 'invalidTagFormat' }],
57+
},
58+
// Invalid tag in test.only
59+
{
60+
code: "test.only('my test', { tag: 'e2e' }, async ({ page }) => {})",
61+
errors: [{ messageId: 'invalidTagFormat' }],
62+
},
63+
],
64+
valid: [
65+
// Basic tag validation
66+
{
67+
code: "test('my test', { tag: '@e2e' }, async ({ page }) => {})",
68+
},
69+
{
70+
code: "test('my test', { tag: ['@e2e', '@login'] }, async ({ page }) => {})",
71+
},
72+
{
73+
code: "test.describe('my suite', { tag: '@regression' }, () => {})",
74+
},
75+
{
76+
code: "test.step('my step', { tag: '@critical' }, async () => {})",
77+
},
78+
// No tag (valid)
79+
{
80+
code: "test('my test', async ({ page }) => {})",
81+
},
82+
// Other options without tag (valid)
83+
{
84+
code: "test('my test', { timeout: 5000 }, async ({ page }) => {})",
85+
},
86+
// Allowed tags
87+
{
88+
code: "test('my test', { tag: '@regression' }, async ({ page }) => {})",
89+
options: [{ allowedTags: ['@regression', '@smoke'] }],
90+
},
91+
{
92+
code: "test('my test', { tag: '@my-tag-123' }, async ({ page }) => {})",
93+
options: [{ allowedTags: ['@regression', /^@my-tag-\d+$/] }],
94+
},
95+
// Not in disallowed tags
96+
{
97+
code: "test('my test', { tag: '@e2e' }, async ({ page }) => {})",
98+
options: [{ disallowedTags: ['@skip', '@todo'] }],
99+
},
100+
{
101+
code: "test('my test', { tag: '@my-tag-123' }, async ({ page }) => {})",
102+
options: [{ disallowedTags: ['@skip', /^@temp-/] }],
103+
},
104+
// Valid tags with test.skip
105+
{
106+
code: "test.skip('my test', { tag: '@e2e' }, async ({ page }) => {})",
107+
},
108+
// Valid tags with test.fixme
109+
{
110+
code: "test.fixme('my test', { tag: '@e2e' }, async ({ page }) => {})",
111+
},
112+
// Valid tags with test.only
113+
{
114+
code: "test.only('my test', { tag: '@e2e' }, async ({ page }) => {})",
115+
},
116+
// Tag with annotation
117+
{
118+
code: "test('my test', { tag: '@e2e', annotation: { type: 'issue', description: 'BUG-123' } }, async ({ page }) => {})",
119+
},
120+
// Tag with array of annotations
121+
{
122+
code: "test('my test', { tag: '@e2e', annotation: [{ type: 'issue', description: 'BUG-123' }, { type: 'flaky' }] }, async ({ page }) => {})",
123+
},
124+
// Array of tags with annotation
125+
{
126+
code: "test('my test', { tag: ['@e2e', '@login'], annotation: { type: 'issue', description: 'BUG-123' } }, async ({ page }) => {})",
127+
},
128+
// Tag with annotation in test.skip
129+
{
130+
code: "test.skip('my test', { tag: '@e2e', annotation: { type: 'issue', description: 'BUG-123' } }, async ({ page }) => {})",
131+
},
132+
// Tag with annotation in test.fixme
133+
{
134+
code: "test.fixme('my test', { tag: '@e2e', annotation: { type: 'issue', description: 'BUG-123' } }, async ({ page }) => {})",
135+
},
136+
// Tag with annotation in test.only
137+
{
138+
code: "test.only('my test', { tag: '@e2e', annotation: { type: 'issue', description: 'BUG-123' } }, async ({ page }) => {})",
139+
},
140+
],
141+
})

0 commit comments

Comments
 (0)