Skip to content

Commit d82fa37

Browse files
committed
[@kbn/eslint-plugin-eslint] Add ESlint rule to allow only a single describe call per fixture
1 parent 6ed6ea4 commit d82fa37

File tree

4 files changed

+291
-0
lines changed

4 files changed

+291
-0
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2701,6 +2701,7 @@ module.exports = {
27012701
],
27022702
rules: {
27032703
'@kbn/eslint/scout_no_describe_configure': 'error',
2704+
'@kbn/eslint/scout_max_one_describe': 'error',
27042705
'@kbn/eslint/require_include_in_check_a11y': 'warn',
27052706
},
27062707
},

packages/kbn-eslint-plugin-eslint/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ module.exports = {
2424
no_deprecated_imports: require('./rules/no_deprecated_imports'),
2525
deployment_agnostic_test_context: require('./rules/deployment_agnostic_test_context'),
2626
scout_no_describe_configure: require('./rules/scout_no_describe_configure'),
27+
scout_max_one_describe: require('./rules/scout_max_one_describe'),
2728
require_kbn_fs: require('./rules/require_kbn_fs'),
2829
require_include_in_check_a11y: require('./rules/require_include_in_check_a11y'),
2930
},
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
/** @typedef {import("eslint").Rule.RuleModule} Rule */
11+
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.CallExpression} CallExpression */
12+
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.MemberExpression} MemberExpression */
13+
14+
const ERROR_MSG = 'Only one describe block is allowed per test type (apiTest, test, or spaceTest).';
15+
16+
const TEST_FIXTURES = new Set(['apiTest', 'test', 'spaceTest']);
17+
18+
/**
19+
* Checks if a node represents apiTest.describe(), test.describe(), or spaceTest.describe()
20+
* @param {CallExpression} node
21+
* @returns {string | null} The test type ('apiTest', 'test', 'spaceTest') or null if not a match
22+
*/
23+
const getDescribeTestType = (node) => {
24+
// Check for *.describe() pattern
25+
if (
26+
node.callee.type === 'MemberExpression' &&
27+
node.callee.property.type === 'Identifier' &&
28+
node.callee.property.name === 'describe' &&
29+
node.callee.object.type === 'Identifier'
30+
) {
31+
const objectName = node.callee.object.name;
32+
if (TEST_FIXTURES.has(objectName)) {
33+
return objectName;
34+
}
35+
}
36+
return null;
37+
};
38+
39+
/** @type {Rule} */
40+
module.exports = {
41+
meta: {
42+
type: 'problem',
43+
docs: {
44+
description:
45+
'Ensure at most one describe block per test type (apiTest, test, or spaceTest) in Scout tests',
46+
category: 'Best Practices',
47+
},
48+
fixable: null,
49+
schema: [],
50+
},
51+
create: (context) => {
52+
// Track count of each test type's describe calls
53+
const describeCounts = {
54+
apiTest: 0,
55+
test: 0,
56+
spaceTest: 0,
57+
};
58+
59+
return {
60+
CallExpression(node) {
61+
const testType = getDescribeTestType(node);
62+
if (testType) {
63+
describeCounts[testType]++;
64+
// Report error if this is the second or subsequent occurrence
65+
if (describeCounts[testType] > 1) {
66+
context.report({
67+
node,
68+
message: ERROR_MSG,
69+
});
70+
}
71+
}
72+
},
73+
};
74+
},
75+
};
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
const { RuleTester } = require('eslint');
11+
const rule = require('./scout_max_one_describe');
12+
const dedent = require('dedent');
13+
14+
const ERROR_MSG = 'Only one describe block is allowed per test type (apiTest, test, or spaceTest).';
15+
16+
const ruleTester = new RuleTester({
17+
parser: require.resolve('@typescript-eslint/parser'),
18+
parserOptions: {
19+
sourceType: 'module',
20+
ecmaVersion: 2018,
21+
ecmaFeatures: {
22+
jsx: true,
23+
},
24+
},
25+
});
26+
27+
ruleTester.run('@kbn/eslint/scout_max_one_describe', rule, {
28+
valid: [
29+
// No describe blocks at all
30+
{
31+
code: dedent`
32+
test('should work', () => {
33+
expect(true).toBe(true);
34+
});
35+
`,
36+
},
37+
// Single apiTest.describe()
38+
{
39+
code: dedent`
40+
apiTest.describe('my API test suite', () => {
41+
apiTest('should work', () => {
42+
expect(true).toBe(true);
43+
});
44+
});
45+
`,
46+
},
47+
// Single test.describe()
48+
{
49+
code: dedent`
50+
test.describe('my test suite', () => {
51+
test('should work', () => {
52+
expect(true).toBe(true);
53+
});
54+
});
55+
`,
56+
},
57+
// Single spaceTest.describe()
58+
{
59+
code: dedent`
60+
spaceTest.describe('my space-aware test suite', () => {
61+
spaceTest('should work', () => {
62+
expect(true).toBe(true);
63+
});
64+
});
65+
`,
66+
},
67+
// Different test types can each have one describe
68+
{
69+
code: dedent`
70+
test.describe('test suite', () => {
71+
test('should work', () => {
72+
expect(true).toBe(true);
73+
});
74+
});
75+
76+
apiTest.describe('api test suite', () => {
77+
apiTest('should work', () => {
78+
expect(true).toBe(true);
79+
});
80+
});
81+
`,
82+
},
83+
// Nested describe blocks (only one at top level per type)
84+
{
85+
code: dedent`
86+
test.describe('outer suite', () => {
87+
describe('inner suite', () => {
88+
test('should work', () => {
89+
expect(true).toBe(true);
90+
});
91+
});
92+
});
93+
`,
94+
},
95+
// Regular describe() without test type prefix
96+
{
97+
code: dedent`
98+
describe('my test suite', () => {
99+
describe('nested suite', () => {
100+
test('should work', () => {
101+
expect(true).toBe(true);
102+
});
103+
});
104+
});
105+
`,
106+
},
107+
],
108+
109+
invalid: [
110+
// Two apiTest.describe() calls
111+
{
112+
code: dedent`
113+
apiTest.describe('first suite', () => {
114+
apiTest('test 1', () => {});
115+
});
116+
117+
apiTest.describe('second suite', () => {
118+
apiTest('test 2', () => {});
119+
});
120+
`,
121+
errors: [
122+
{
123+
message: ERROR_MSG,
124+
},
125+
],
126+
},
127+
// Two test.describe() calls
128+
{
129+
code: dedent`
130+
test.describe('first suite', () => {
131+
test('test 1', () => {});
132+
});
133+
134+
test.describe('second suite', () => {
135+
test('test 2', () => {});
136+
});
137+
`,
138+
errors: [
139+
{
140+
message: ERROR_MSG,
141+
},
142+
],
143+
},
144+
// Two spaceTest.describe() calls
145+
{
146+
code: dedent`
147+
spaceTest.describe('first suite', () => {
148+
spaceTest('test 1', () => {});
149+
});
150+
151+
spaceTest.describe('second suite', () => {
152+
spaceTest('test 2', () => {});
153+
});
154+
`,
155+
errors: [
156+
{
157+
message: ERROR_MSG,
158+
},
159+
],
160+
},
161+
// Three test.describe() calls - should report 2 errors
162+
{
163+
code: dedent`
164+
test.describe('first suite', () => {
165+
test('test 1', () => {});
166+
});
167+
168+
test.describe('second suite', () => {
169+
test('test 2', () => {});
170+
});
171+
172+
test.describe('third suite', () => {
173+
test('test 3', () => {});
174+
});
175+
`,
176+
errors: [
177+
{
178+
message: ERROR_MSG,
179+
},
180+
{
181+
message: ERROR_MSG,
182+
},
183+
],
184+
},
185+
// Multiple types with violations
186+
{
187+
code: dedent`
188+
test.describe('test suite 1', () => {
189+
test('test 1', () => {});
190+
});
191+
192+
test.describe('test suite 2', () => {
193+
test('test 2', () => {});
194+
});
195+
196+
apiTest.describe('api suite 1', () => {
197+
apiTest('test 1', () => {});
198+
});
199+
200+
apiTest.describe('api suite 2', () => {
201+
apiTest('test 2', () => {});
202+
});
203+
`,
204+
errors: [
205+
{
206+
message: ERROR_MSG,
207+
},
208+
{
209+
message: ERROR_MSG,
210+
},
211+
],
212+
},
213+
],
214+
});

0 commit comments

Comments
 (0)