Skip to content

Commit 377238c

Browse files
committed
fix: Check for test tags in titles
Fixes #392
1 parent 3ecf3ab commit 377238c

File tree

2 files changed

+158
-1
lines changed

2 files changed

+158
-1
lines changed

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

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,68 @@ runTSRuleTester('valid-test-tags', validTestTags, {
6060
code: "test.only('my test', { tag: 'e2e' }, async ({ page }) => {})",
6161
errors: [{ messageId: 'invalidTagFormat' }],
6262
},
63+
// Title with unknown tag (not in allowedTags)
64+
{
65+
code: "test('@e2e my test', async ({ page }) => {})",
66+
errors: [{ data: { tag: '@e2e' }, messageId: 'unknownTag' }],
67+
options: [{ allowedTags: ['@regression', '@smoke'] }],
68+
},
69+
{
70+
code: "test('@e2e @login my test', async ({ page }) => {})",
71+
errors: [
72+
{ data: { tag: '@e2e' }, messageId: 'unknownTag' },
73+
{ data: { tag: '@login' }, messageId: 'unknownTag' },
74+
],
75+
options: [{ allowedTags: ['@regression', '@smoke'] }],
76+
},
77+
{
78+
code: "test('@my-tag-abc my test', async ({ page }) => {})",
79+
errors: [{ data: { tag: '@my-tag-abc' }, messageId: 'unknownTag' }],
80+
options: [{ allowedTags: ['@regression', /^@my-tag-\d+$/] }],
81+
},
82+
// Title with disallowed tag
83+
{
84+
code: "test('@skip my test', async ({ page }) => {})",
85+
errors: [{ data: { tag: '@skip' }, messageId: 'disallowedTag' }],
86+
options: [{ disallowedTags: ['@skip', '@todo'] }],
87+
},
88+
{
89+
code: "test('@skip @todo my test', async ({ page }) => {})",
90+
errors: [
91+
{ data: { tag: '@skip' }, messageId: 'disallowedTag' },
92+
{ data: { tag: '@todo' }, messageId: 'disallowedTag' },
93+
],
94+
options: [{ disallowedTags: ['@skip', '@todo'] }],
95+
},
96+
{
97+
code: "test('@temp-123 my test', async ({ page }) => {})",
98+
errors: [{ data: { tag: '@temp-123' }, messageId: 'disallowedTag' }],
99+
options: [{ disallowedTags: ['@skip', /^@temp-/] }],
100+
},
101+
{
102+
code: "test.fixme('@e2e my test', async ({ page }) => {})",
103+
errors: [{ data: { tag: '@e2e' }, messageId: 'unknownTag' }],
104+
options: [{ allowedTags: ['@regression', '@smoke'] }],
105+
},
106+
{
107+
code: "test.only('@skip my test', async ({ page }) => {})",
108+
errors: [{ data: { tag: '@skip' }, messageId: 'disallowedTag' }],
109+
options: [{ disallowedTags: ['@skip', '@todo'] }],
110+
},
111+
{
112+
code: "test.step('@e2e my step', async () => {})",
113+
errors: [{ data: { tag: '@e2e' }, messageId: 'unknownTag' }],
114+
options: [{ allowedTags: ['@regression', '@smoke'] }],
115+
},
116+
// Mixed invalid tags in title and options
117+
{
118+
code: "test('@e2e my test', { tag: '@e2e' }, async ({ page }) => {})",
119+
errors: [
120+
{ data: { tag: '@e2e' }, messageId: 'unknownTag' },
121+
{ data: { tag: '@e2e' }, messageId: 'unknownTag' },
122+
],
123+
options: [{ allowedTags: ['@regression', '@smoke'] }],
124+
},
63125
],
64126
valid: [
65127
// Basic tag validation
@@ -137,5 +199,79 @@ runTSRuleTester('valid-test-tags', validTestTags, {
137199
{
138200
code: "test.only('my test', { tag: '@e2e', annotation: { type: 'issue', description: 'BUG-123' } }, async ({ page }) => {})",
139201
},
202+
// Title with valid tags
203+
{
204+
code: "test('@e2e my test', async ({ page }) => {})",
205+
},
206+
{
207+
code: "test('@e2e @login my test', async ({ page }) => {})",
208+
},
209+
{
210+
code: "test('my test @regression @smoke', async ({ page }) => {})",
211+
},
212+
{
213+
code: "test.describe('@suite my describe block', () => {})",
214+
},
215+
{
216+
code: "test.step('@step my step', async () => {})",
217+
},
218+
// Title with valid tags and options
219+
{
220+
code: "test('@e2e my test', { tag: '@regression' }, async ({ page }) => {})",
221+
},
222+
// Title with valid tags using allowedTags
223+
{
224+
code: "test('@regression my test', async ({ page }) => {})",
225+
options: [{ allowedTags: ['@regression', '@smoke'] }],
226+
},
227+
{
228+
code: "test('@my-tag-123 my test', async ({ page }) => {})",
229+
options: [{ allowedTags: ['@regression', /^@my-tag-\d+$/] }],
230+
},
231+
// Title with valid tags not in disallowedTags
232+
{
233+
code: "test('@e2e my test', async ({ page }) => {})",
234+
options: [{ disallowedTags: ['@skip', '@todo'] }],
235+
},
236+
{
237+
code: "test('@my-tag-123 my test', async ({ page }) => {})",
238+
options: [{ disallowedTags: ['@skip', /^@temp-/] }],
239+
},
240+
// Title with valid tags in test variants
241+
{
242+
code: "test.skip('@e2e my test', async ({ page }) => {})",
243+
},
244+
{
245+
code: "test.fixme('@e2e my test', async ({ page }) => {})",
246+
},
247+
{
248+
code: "test.only('@e2e my test', async ({ page }) => {})",
249+
},
250+
// Title without tags (valid)
251+
{
252+
code: "test('my test without tags', async ({ page }) => {})",
253+
},
254+
{
255+
code: "test('my test with spaces and no @tags', async ({ page }) => {})",
256+
},
257+
// Title with common words that are not tags (valid)
258+
{
259+
code: "test('e2e my test', async ({ page }) => {})",
260+
},
261+
{
262+
code: "test('e2e @login my test', async ({ page }) => {})",
263+
},
264+
{
265+
code: "test('my test e2e @smoke', async ({ page }) => {})",
266+
},
267+
{
268+
code: "test.skip('e2e my test', async ({ page }) => {})",
269+
},
270+
{
271+
code: "test.describe('e2e my suite', () => {})",
272+
},
273+
{
274+
code: "test.step('my step', async () => {})",
275+
},
140276
],
141277
})

src/rules/valid-test-tags.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export default createRule({
3030
}
3131
}
3232

33+
const extractTagsFromTitle = (title: string): string[] => {
34+
// Only extract tags that start with @ - these are the actual tags
35+
return title.match(/@[\S]+/g) || []
36+
}
37+
3338
const validateTag = (tag: string, node: Rule.Node) => {
3439
if (!tag.startsWith('@')) {
3540
context.report({
@@ -75,6 +80,21 @@ export default createRule({
7580
const { type } = call
7681
if (type !== 'test' && type !== 'describe' && type !== 'step') return
7782

83+
// Check for tags in the title (first argument)
84+
if (node.arguments.length > 0) {
85+
const titleArg = node.arguments[0]
86+
if (
87+
titleArg &&
88+
titleArg.type === 'Literal' &&
89+
typeof titleArg.value === 'string'
90+
) {
91+
const titleTags = extractTagsFromTitle(titleArg.value)
92+
for (const tag of titleTags) {
93+
validateTag(tag, node)
94+
}
95+
}
96+
}
97+
7898
// Check if there's an options object as the second argument
7999
if (node.arguments.length < 2) return
80100
const optionsArg = node.arguments[1]
@@ -125,7 +145,8 @@ export default createRule({
125145
},
126146
meta: {
127147
docs: {
128-
description: 'Enforce valid tag format in Playwright test blocks',
148+
description:
149+
'Enforce valid tag format in Playwright test blocks and titles',
129150
recommended: true,
130151
},
131152
messages: {

0 commit comments

Comments
 (0)