eslint-config-agent 1.3.6 → 1.3.7

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
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. See [Conven
4
4
 
5
5
 
6
6
 
7
+ ## [1.3.7](https://github.com/tupe12334/eslint-config/compare/v1.3.6...v1.3.7) (2025-09-24)
8
+
9
+ ### Features
10
+
11
+ * add error handling rules from eslint-plugin-error ([082701f](https://github.com/tupe12334/eslint-config/commit/082701f8abe8730ed12afe206caf8f10825b4d1c))
12
+ * add jsx-classname-required rule to enforce className attribute on HTML elements in JSX ([3603865](https://github.com/tupe12334/eslint-config/commit/360386582f05e63d6ba1625548725b9216c4f4d6))
13
+ * integrate error handling rules from eslint-plugin-error and update configuration ([7c8b846](https://github.com/tupe12334/eslint-config/commit/7c8b84694550b92ac2a265591fac736d5ddb1180))
14
+
7
15
  ## [1.3.6](https://github.com/tupe12334/eslint-config/compare/v1.3.5...v1.3.6) (2025-09-21)
8
16
 
9
17
  ### Bug Fixes
package/index.js CHANGED
@@ -12,6 +12,7 @@ import storybookPlugin from "eslint-plugin-storybook";
12
12
  import globals from "globals";
13
13
  import allRules from "./rules/index.js";
14
14
  import { noDefaultClassExportRule } from "./rules/no-default-class-export/index.js";
15
+ import errorPlugin from "eslint-plugin-error";
15
16
 
16
17
  // Conditionally import preact plugin if available
17
18
  let preactPlugin = null;
@@ -40,6 +41,8 @@ const sharedRules = {
40
41
  "implicit-arrow-linebreak": "off",
41
42
  "arrow-body-style": "off",
42
43
  "no-continue": "off",
44
+ // Additional built-in error handling rules
45
+ "prefer-promise-reject-errors": "error",
43
46
  };
44
47
 
45
48
  // Shared no-restricted-syntax rules for both JS and TS
@@ -101,7 +104,6 @@ const sharedRestrictedSyntax = [
101
104
  ...allRules.noDefaultClassExportRules,
102
105
  ];
103
106
 
104
-
105
107
  // TypeScript-specific no-restricted-syntax rules
106
108
  const tsOnlyRestrictedSyntax = [
107
109
  {
@@ -189,9 +191,11 @@ const config = [
189
191
  "class-export": classExportPlugin,
190
192
  "single-export": singleExportPlugin,
191
193
  "required-exports": requiredExportsPlugin,
194
+ error: errorPlugin,
192
195
  custom: {
193
196
  rules: {
194
197
  "no-default-class-export": noDefaultClassExportRule,
198
+ "jsx-classname-required": allRules.jsxClassNameRequiredRule,
195
199
  },
196
200
  },
197
201
  },
@@ -199,19 +203,29 @@ const config = [
199
203
  // Use recommended-latest if available (v5+), otherwise create flat config equivalent of legacy recommended
200
204
  ...(reactHooks.configs["recommended-latest"]
201
205
  ? [reactHooks.configs["recommended-latest"]]
202
- : [{
203
- name: "react-hooks/recommended-flat",
204
- plugins: {
205
- "react-hooks": reactHooks,
206
- },
207
- rules: {
208
- "react-hooks/rules-of-hooks": "error",
209
- "react-hooks/exhaustive-deps": "warn",
206
+ : [
207
+ {
208
+ name: "react-hooks/recommended-flat",
209
+ plugins: {
210
+ "react-hooks": reactHooks,
211
+ },
212
+ rules: {
213
+ "react-hooks/rules-of-hooks": "error",
214
+ "react-hooks/exhaustive-deps": "warn",
215
+ },
210
216
  },
211
- }]
212
- ),
217
+ ]),
213
218
  js.configs.recommended,
214
219
  ...tseslint.configs.recommended,
220
+ // Error handling plugin strict config
221
+ {
222
+ plugins: {
223
+ error: errorPlugin,
224
+ },
225
+ rules: {
226
+ ...errorPlugin.configs.strict.rules,
227
+ },
228
+ },
215
229
 
216
230
  // TypeScript and TSX files
217
231
  {
@@ -248,15 +262,18 @@ const config = [
248
262
  "no-undef": "off", // TypeScript handles this
249
263
  "custom/no-default-class-export": "error",
250
264
  "single-export/single-export": "error",
251
- "required-exports/required-exports": ["error", {
252
- "variable": false, // Don't require exporting variables/constants
253
- "function": false, // Don't require exporting functions
254
- "class": true, // Require exporting classes (matches old behavior)
255
- "interface": false, // Don't require exporting interfaces
256
- "type": false, // Don't require exporting types
257
- "enum": true, // Require exporting enums (matches old behavior)
258
- "ignorePrivate": true // Ignore declarations starting with _
259
- }],
265
+ "required-exports/required-exports": [
266
+ "error",
267
+ {
268
+ variable: false, // Don't require exporting variables/constants
269
+ function: false, // Don't require exporting functions
270
+ class: true, // Require exporting classes (matches old behavior)
271
+ interface: false, // Don't require exporting interfaces
272
+ type: false, // Don't require exporting types
273
+ enum: true, // Require exporting enums (matches old behavior)
274
+ ignorePrivate: true, // Ignore declarations starting with _
275
+ },
276
+ ],
260
277
  "no-restricted-syntax": [
261
278
  "error",
262
279
  ...sharedRestrictedSyntax,
@@ -273,15 +290,18 @@ const config = [
273
290
  // Include all shared rules (like max-lines-per-function)
274
291
  ...sharedRules,
275
292
  "single-export/single-export": "off", // TSX files have their own specific export rules
276
- "required-exports/required-exports": ["error", {
277
- "variable": false, // Don't require exporting variables/constants
278
- "function": false, // Don't require exporting functions
279
- "class": true, // Require exporting classes (matches old behavior)
280
- "interface": false, // Don't require exporting interfaces
281
- "type": false, // Don't require exporting types
282
- "enum": true, // Require exporting enums (matches old behavior)
283
- "ignorePrivate": true // Ignore declarations starting with _
284
- }],
293
+ "required-exports/required-exports": [
294
+ "error",
295
+ {
296
+ variable: false, // Don't require exporting variables/constants
297
+ function: false, // Don't require exporting functions
298
+ class: true, // Require exporting classes (matches old behavior)
299
+ interface: false, // Don't require exporting interfaces
300
+ type: false, // Don't require exporting types
301
+ enum: true, // Require exporting enums (matches old behavior)
302
+ ignorePrivate: true, // Ignore declarations starting with _
303
+ },
304
+ ],
285
305
  "no-restricted-syntax": [
286
306
  "error",
287
307
  // Switch case rules as errors
@@ -343,12 +363,6 @@ const config = [
343
363
  message:
344
364
  "Function expressions containing switch statements must have explicit return type annotations.",
345
365
  },
346
- // className requirement for HTML elements
347
- {
348
- selector:
349
- 'JSXOpeningElement:not([name.name=/^[A-Z]/]):not([name.name="Fragment"]):not(:has(JSXAttribute[name.name="className"]))',
350
- message: "HTML elements must have a className attribute.",
351
- },
352
366
  ],
353
367
  },
354
368
  },
@@ -474,19 +488,19 @@ const config = [
474
488
  rules: {
475
489
  ...sharedRules,
476
490
  "single-export/single-export": "error",
477
- "required-exports/required-exports": ["error", {
478
- "variable": false, // Don't require exporting variables/constants
479
- "function": false, // Don't require exporting functions
480
- "class": true, // Require exporting classes (matches old behavior)
481
- "interface": false, // Don't require exporting interfaces (N/A for JS)
482
- "type": false, // Don't require exporting types (N/A for JS)
483
- "enum": false, // Don't require exporting enums (N/A for JS)
484
- "ignorePrivate": true // Ignore declarations starting with _
485
- }],
486
- "no-restricted-syntax": [
491
+ "required-exports/required-exports": [
487
492
  "error",
488
- ...sharedRestrictedSyntax,
493
+ {
494
+ variable: false, // Don't require exporting variables/constants
495
+ function: false, // Don't require exporting functions
496
+ class: true, // Require exporting classes (matches old behavior)
497
+ interface: false, // Don't require exporting interfaces (N/A for JS)
498
+ type: false, // Don't require exporting types (N/A for JS)
499
+ enum: false, // Don't require exporting enums (N/A for JS)
500
+ ignorePrivate: true, // Ignore declarations starting with _
501
+ },
489
502
  ],
503
+ "no-restricted-syntax": ["error", ...sharedRestrictedSyntax],
490
504
  },
491
505
  },
492
506
 
@@ -538,15 +552,18 @@ const config = [
538
552
  ...sharedRules,
539
553
  "no-undef": "off", // TypeScript handles this
540
554
  "single-export/single-export": "off", // JSX files have their own specific export rules
541
- "required-exports/required-exports": ["error", {
542
- "variable": false, // Don't require exporting variables/constants
543
- "function": false, // Don't require exporting functions
544
- "class": true, // Require exporting classes (matches old behavior)
545
- "interface": false, // Don't require exporting interfaces (N/A for JSX)
546
- "type": false, // Don't require exporting types (N/A for JSX)
547
- "enum": false, // Don't require exporting enums (N/A for JSX)
548
- "ignorePrivate": true // Ignore declarations starting with _
549
- }],
555
+ "required-exports/required-exports": [
556
+ "error",
557
+ {
558
+ variable: false, // Don't require exporting variables/constants
559
+ function: false, // Don't require exporting functions
560
+ class: true, // Require exporting classes (matches old behavior)
561
+ interface: false, // Don't require exporting interfaces (N/A for JSX)
562
+ type: false, // Don't require exporting types (N/A for JSX)
563
+ enum: false, // Don't require exporting enums (N/A for JSX)
564
+ ignorePrivate: true, // Ignore declarations starting with _
565
+ },
566
+ ],
550
567
  "no-restricted-syntax": [
551
568
  "error",
552
569
  // Switch case rules as errors
@@ -608,12 +625,6 @@ const config = [
608
625
  message:
609
626
  "Function expressions containing switch statements must have explicit return type annotations.",
610
627
  },
611
- // className requirement for HTML elements
612
- {
613
- selector:
614
- 'JSXOpeningElement:not([name.name=/^[A-Z]/]):not([name.name="Fragment"]):not(:has(JSXAttribute[name.name="className"]))',
615
- message: "HTML elements must have a className attribute.",
616
- },
617
628
  // Required export rules as errors (class rule only for JSX)
618
629
  {
619
630
  selector:
@@ -658,15 +669,18 @@ const config = [
658
669
  rules: {
659
670
  "no-undef": "off", // TypeScript handles this
660
671
  "single-export/single-export": "off", // JSX files have their own specific export rules
661
- "required-exports/required-exports": ["warn", {
662
- "variable": false, // Don't require exporting variables/constants
663
- "function": false, // Don't require exporting functions
664
- "class": true, // Require exporting classes (matches old behavior)
665
- "interface": false, // Don't require exporting interfaces (N/A for JSX)
666
- "type": false, // Don't require exporting types (N/A for JSX)
667
- "enum": false, // Don't require exporting enums (N/A for JSX)
668
- "ignorePrivate": true // Ignore declarations starting with _
669
- }],
672
+ "required-exports/required-exports": [
673
+ "warn",
674
+ {
675
+ variable: false, // Don't require exporting variables/constants
676
+ function: false, // Don't require exporting functions
677
+ class: true, // Require exporting classes (matches old behavior)
678
+ interface: false, // Don't require exporting interfaces (N/A for JSX)
679
+ type: false, // Don't require exporting types (N/A for JSX)
680
+ enum: false, // Don't require exporting enums (N/A for JSX)
681
+ ignorePrivate: true, // Ignore declarations starting with _
682
+ },
683
+ ],
670
684
  "no-restricted-syntax": [
671
685
  "warn",
672
686
  // Include shared rules but remove the multiple exports restriction and switch case rules for JSX
@@ -829,18 +843,22 @@ const config = [
829
843
  "**/test/**",
830
844
  "!**/test/export/**",
831
845
  "!**/test/required-exports/**",
846
+ "!**/test/switch-case/**",
832
847
  "**/rules/**/index.js",
833
848
  ],
834
849
  rules: {
835
- "required-exports/required-exports": ["error", {
836
- "variable": false, // Don't require exporting variables/constants
837
- "function": false, // Don't require exporting functions
838
- "class": true, // Require exporting classes (matches old behavior)
839
- "interface": false, // Don't require exporting interfaces
840
- "type": false, // Don't require exporting types
841
- "enum": true, // Require exporting enums (matches old behavior)
842
- "ignorePrivate": true // Ignore declarations starting with _
843
- }],
850
+ "required-exports/required-exports": [
851
+ "error",
852
+ {
853
+ variable: false, // Don't require exporting variables/constants
854
+ function: false, // Don't require exporting functions
855
+ class: true, // Require exporting classes (matches old behavior)
856
+ interface: false, // Don't require exporting interfaces
857
+ type: false, // Don't require exporting types
858
+ enum: true, // Require exporting enums (matches old behavior)
859
+ ignorePrivate: true, // Ignore declarations starting with _
860
+ },
861
+ ],
844
862
  "no-restricted-syntax": [
845
863
  "error",
846
864
  // Switch case rules as errors
@@ -933,14 +951,7 @@ const config = [
933
951
  files: ["**/*.{tsx,jsx}"],
934
952
  ignores: ["**/*.stories.{js,jsx,ts,tsx}"],
935
953
  rules: {
936
- "no-restricted-syntax": [
937
- "error",
938
- {
939
- selector:
940
- 'JSXOpeningElement:not([name.name=/^[A-Z]/]):not([name.name="Fragment"]):not(:has(JSXAttribute[name.name="className"]))',
941
- message: "HTML elements must have a className attribute.",
942
- },
943
- ],
954
+ "custom/jsx-classname-required": "error",
944
955
  },
945
956
  },
946
957
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-config-agent",
3
- "version": "1.3.6",
3
+ "version": "1.3.7",
4
4
  "description": "ESLint configuration package with TypeScript support",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -106,5 +106,8 @@
106
106
  "globals": "^16.3.0",
107
107
  "release-it": "^19.0.4",
108
108
  "typescript-eslint": "^8.40.0"
109
+ },
110
+ "dependencies": {
111
+ "eslint-plugin-error": "^1.1.1"
109
112
  }
110
113
  }
package/rules/index.js CHANGED
@@ -16,6 +16,7 @@ import { noTypeAssertionsConfig } from "./no-type-assertions/index.js";
16
16
  import { noExportSpecifiersConfig } from "./no-empty-exports/index.js";
17
17
  import { noClassPropertyDefaultsConfig } from "./no-class-property-defaults/index.js";
18
18
  import { noDefaultClassExportRules } from "./no-default-class-export/index.js";
19
+ import { jsxClassNameRequiredRule } from "./jsx-classname-required/index.js";
19
20
 
20
21
  // Plugin rule configurations
21
22
  import { pluginRules } from "./plugin/index.js";
@@ -36,6 +37,7 @@ const allRules = {
36
37
  noExportSpecifiersConfig,
37
38
  noClassPropertyDefaultsConfig,
38
39
  noDefaultClassExportRules,
40
+ jsxClassNameRequiredRule,
39
41
 
40
42
  // Plugin rule configurations
41
43
  pluginRules,
@@ -59,6 +61,7 @@ export {
59
61
  noExportSpecifiersConfig,
60
62
  noClassPropertyDefaultsConfig,
61
63
  noDefaultClassExportRules,
64
+ jsxClassNameRequiredRule,
62
65
 
63
66
  // Plugin rule configurations
64
67
  pluginRules,
@@ -0,0 +1,68 @@
1
+ /**
2
+ * ESLint rule to require className attribute on HTML elements in JSX
3
+ *
4
+ * This rule enforces that all HTML elements in JSX must have a className attribute.
5
+ * It excludes React components (starting with capital letters) and fragments.
6
+ *
7
+ * Examples:
8
+ * - ❌ <div>Content</div>
9
+ * - ✅ <div className="container">Content</div>
10
+ * - ✅ <MyComponent>Content</MyComponent> (ignored - React component)
11
+ * - ✅ <Fragment>Content</Fragment> (ignored - fragment)
12
+ * - ✅ <React.Fragment>Content</React.Fragment> (ignored - React fragment)
13
+ * - ✅ <React.StrictMode>Content</React.StrictMode> (ignored - React component)
14
+ */
15
+
16
+ const jsxClassNameRequiredRule = {
17
+ meta: {
18
+ type: "layout",
19
+ docs: {
20
+ description: "Require className attribute on HTML elements in JSX",
21
+ category: "Stylistic Issues",
22
+ recommended: false,
23
+ },
24
+ fixable: null,
25
+ schema: [],
26
+ messages: {
27
+ missingClassName: "HTML elements must have a className attribute.",
28
+ },
29
+ },
30
+
31
+ create(context) {
32
+ return {
33
+ JSXOpeningElement(node) {
34
+ // Skip if this is a React component (starts with capital letter)
35
+ if (node.name.type === 'JSXIdentifier' && /^[A-Z]/.test(node.name.name)) {
36
+ return;
37
+ }
38
+
39
+ // Skip if this is Fragment
40
+ if (node.name.type === 'JSXIdentifier' && node.name.name === 'Fragment') {
41
+ return;
42
+ }
43
+
44
+ // Skip if this is React.Something (like React.Fragment, React.StrictMode, etc.)
45
+ if (node.name.type === 'JSXMemberExpression' &&
46
+ node.name.object.name === 'React' &&
47
+ /^[A-Z]/.test(node.name.property.name)) {
48
+ return;
49
+ }
50
+
51
+ // Check if element has className attribute
52
+ const hasClassName = node.attributes.some(attr =>
53
+ attr.type === 'JSXAttribute' &&
54
+ attr.name.name === 'className'
55
+ );
56
+
57
+ if (!hasClassName) {
58
+ context.report({
59
+ node,
60
+ messageId: 'missingClassName',
61
+ });
62
+ }
63
+ },
64
+ };
65
+ },
66
+ };
67
+
68
+ export { jsxClassNameRequiredRule };
@@ -0,0 +1,283 @@
1
+ import { RuleTester } from "eslint";
2
+ import { jsxClassNameRequiredRule } from "./index.js";
3
+
4
+ const ruleTester = new RuleTester({
5
+ languageOptions: {
6
+ ecmaVersion: 2022,
7
+ sourceType: "module",
8
+ parser: (await import("typescript-eslint")).parser,
9
+ parserOptions: {
10
+ ecmaFeatures: {
11
+ jsx: true,
12
+ },
13
+ },
14
+ },
15
+ });
16
+
17
+ ruleTester.run('jsx-classname-required', jsxClassNameRequiredRule, {
18
+ valid: [
19
+ // HTML elements with className
20
+ {
21
+ code: '<div className="container">Content</div>',
22
+ },
23
+ {
24
+ code: '<span className="text">Text</span>',
25
+ },
26
+ {
27
+ code: '<p className="paragraph">Paragraph</p>',
28
+ },
29
+ {
30
+ code: '<button className="btn">Button</button>',
31
+ },
32
+ {
33
+ code: '<input className="input" type="text" />',
34
+ },
35
+ {
36
+ code: '<img className="image" src="test.jpg" alt="test" />',
37
+ },
38
+
39
+ // React components (should be ignored)
40
+ {
41
+ code: '<MyComponent>Content</MyComponent>',
42
+ },
43
+ {
44
+ code: '<CustomButton onClick={() => {}}>Click</CustomButton>',
45
+ },
46
+ {
47
+ code: '<AnotherComponent prop="value" />',
48
+ },
49
+
50
+ // Fragments (should be ignored)
51
+ {
52
+ code: '<Fragment>Content</Fragment>',
53
+ },
54
+ {
55
+ code: '<React.Fragment>Content</React.Fragment>',
56
+ },
57
+ {
58
+ code: '<>Content</>',
59
+ },
60
+
61
+ // React.* components (should be ignored)
62
+ {
63
+ code: '<React.StrictMode><div className="content">Content</div></React.StrictMode>',
64
+ },
65
+ {
66
+ code: '<React.Suspense fallback={<div className="loading">Loading</div>}><div className="content">Content</div></React.Suspense>',
67
+ },
68
+ {
69
+ code: '<React.Profiler id="test" onRender={() => {}}><div className="content">Content</div></React.Profiler>',
70
+ },
71
+
72
+ // Mixed cases with valid HTML elements
73
+ {
74
+ code: `
75
+ <div className="container">
76
+ <MyComponent>
77
+ <p className="text">Text</p>
78
+ </MyComponent>
79
+ <React.Fragment>
80
+ <span className="fragment-content">Fragment content</span>
81
+ </React.Fragment>
82
+ </div>
83
+ `,
84
+ },
85
+
86
+ // Nested valid cases
87
+ {
88
+ code: `
89
+ <div className="outer">
90
+ <div className="inner">
91
+ <span className="nested">Nested content</span>
92
+ </div>
93
+ </div>
94
+ `,
95
+ },
96
+ ],
97
+
98
+ invalid: [
99
+ // HTML elements without className
100
+ {
101
+ code: '<div>Content</div>',
102
+ errors: [
103
+ {
104
+ messageId: 'missingClassName',
105
+ type: 'JSXOpeningElement',
106
+ },
107
+ ],
108
+ },
109
+ {
110
+ code: '<span>Text</span>',
111
+ errors: [
112
+ {
113
+ messageId: 'missingClassName',
114
+ type: 'JSXOpeningElement',
115
+ },
116
+ ],
117
+ },
118
+ {
119
+ code: '<p>Paragraph</p>',
120
+ errors: [
121
+ {
122
+ messageId: 'missingClassName',
123
+ type: 'JSXOpeningElement',
124
+ },
125
+ ],
126
+ },
127
+ {
128
+ code: '<button>Button</button>',
129
+ errors: [
130
+ {
131
+ messageId: 'missingClassName',
132
+ type: 'JSXOpeningElement',
133
+ },
134
+ ],
135
+ },
136
+ {
137
+ code: '<input type="text" />',
138
+ errors: [
139
+ {
140
+ messageId: 'missingClassName',
141
+ type: 'JSXOpeningElement',
142
+ },
143
+ ],
144
+ },
145
+ {
146
+ code: '<img src="test.jpg" alt="test" />',
147
+ errors: [
148
+ {
149
+ messageId: 'missingClassName',
150
+ type: 'JSXOpeningElement',
151
+ },
152
+ ],
153
+ },
154
+
155
+ // HTML elements inside React components (should still error)
156
+ {
157
+ code: '<MyComponent><div>Content</div></MyComponent>',
158
+ errors: [
159
+ {
160
+ messageId: 'missingClassName',
161
+ type: 'JSXOpeningElement',
162
+ },
163
+ ],
164
+ },
165
+ {
166
+ code: '<React.StrictMode><div>Content</div></React.StrictMode>',
167
+ errors: [
168
+ {
169
+ messageId: 'missingClassName',
170
+ type: 'JSXOpeningElement',
171
+ },
172
+ ],
173
+ },
174
+ {
175
+ code: '<React.Fragment><span>Content</span></React.Fragment>',
176
+ errors: [
177
+ {
178
+ messageId: 'missingClassName',
179
+ type: 'JSXOpeningElement',
180
+ },
181
+ ],
182
+ },
183
+ {
184
+ code: '<Fragment><p>Content</p></Fragment>',
185
+ errors: [
186
+ {
187
+ messageId: 'missingClassName',
188
+ type: 'JSXOpeningElement',
189
+ },
190
+ ],
191
+ },
192
+ {
193
+ code: '<><div>Content</div></>',
194
+ errors: [
195
+ {
196
+ messageId: 'missingClassName',
197
+ type: 'JSXOpeningElement',
198
+ },
199
+ ],
200
+ },
201
+
202
+ // Multiple errors in one file
203
+ {
204
+ code: `
205
+ <div className="container">
206
+ <span>Missing className</span>
207
+ <p>Also missing className</p>
208
+ </div>
209
+ `,
210
+ errors: [
211
+ {
212
+ messageId: 'missingClassName',
213
+ type: 'JSXOpeningElement',
214
+ },
215
+ {
216
+ messageId: 'missingClassName',
217
+ type: 'JSXOpeningElement',
218
+ },
219
+ ],
220
+ },
221
+
222
+ // Mixed valid/invalid cases
223
+ {
224
+ code: `
225
+ <div className="container">
226
+ <span className="valid">Valid</span>
227
+ <p>Invalid - no className</p>
228
+ <MyComponent>
229
+ <div>Invalid - no className</div>
230
+ </MyComponent>
231
+ </div>
232
+ `,
233
+ errors: [
234
+ {
235
+ messageId: 'missingClassName',
236
+ type: 'JSXOpeningElement',
237
+ },
238
+ {
239
+ messageId: 'missingClassName',
240
+ type: 'JSXOpeningElement',
241
+ },
242
+ ],
243
+ },
244
+
245
+ // Edge cases
246
+ {
247
+ code: '<h1>Heading</h1>',
248
+ errors: [
249
+ {
250
+ messageId: 'missingClassName',
251
+ type: 'JSXOpeningElement',
252
+ },
253
+ ],
254
+ },
255
+ {
256
+ code: '<section>Section</section>',
257
+ errors: [
258
+ {
259
+ messageId: 'missingClassName',
260
+ type: 'JSXOpeningElement',
261
+ },
262
+ ],
263
+ },
264
+ {
265
+ code: '<article>Article</article>',
266
+ errors: [
267
+ {
268
+ messageId: 'missingClassName',
269
+ type: 'JSXOpeningElement',
270
+ },
271
+ ],
272
+ },
273
+ {
274
+ code: '<nav>Navigation</nav>',
275
+ errors: [
276
+ {
277
+ messageId: 'missingClassName',
278
+ type: 'JSXOpeningElement',
279
+ },
280
+ ],
281
+ },
282
+ ],
283
+ });