uilint 0.2.0 → 0.2.3

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.
@@ -0,0 +1,496 @@
1
+ /**
2
+ * Tests for: {rule-name}
3
+ *
4
+ * {Description of what this rule tests}
5
+ *
6
+ * Test Organization:
7
+ * - valid: Cases that should NOT trigger errors
8
+ * - invalid: Cases that SHOULD trigger errors
9
+ *
10
+ * Each section is organized by category with comment headers.
11
+ */
12
+
13
+ import { RuleTester } from "@typescript-eslint/rule-tester";
14
+ import { describe, it, afterAll, beforeEach } from "vitest";
15
+ import rule from "./{rule-name}";
16
+ // Uncomment if your rule uses caching (cross-file analysis):
17
+ // import { clearCache } from "../utils/import-graph.js";
18
+
19
+ // Configure RuleTester to use Vitest
20
+ RuleTester.afterAll = afterAll;
21
+ RuleTester.describe = describe;
22
+ RuleTester.it = it;
23
+
24
+ // Create rule tester with JSX support
25
+ const ruleTester = new RuleTester({
26
+ languageOptions: {
27
+ ecmaVersion: 2022,
28
+ sourceType: "module",
29
+ parserOptions: {
30
+ ecmaFeatures: { jsx: true },
31
+ },
32
+ },
33
+ });
34
+
35
+ // Clear cache between tests if rule uses cross-file analysis
36
+ // beforeEach(() => {
37
+ // clearCache();
38
+ // });
39
+
40
+ ruleTester.run("{rule-name}", rule, {
41
+ valid: [
42
+ // ============================================
43
+ // PREFERRED PATTERN - BASIC USAGE
44
+ // ============================================
45
+ {
46
+ name: "uses preferred component correctly",
47
+ code: `
48
+ import { Button } from "@/components/ui/button";
49
+
50
+ export function Page() {
51
+ return <Button>Click me</Button>;
52
+ }
53
+ `,
54
+ },
55
+ {
56
+ name: "uses multiple preferred components",
57
+ code: `
58
+ import { Button } from "@/components/ui/button";
59
+ import { Card } from "@/components/ui/card";
60
+
61
+ export function Page() {
62
+ return (
63
+ <Card>
64
+ <Button>Submit</Button>
65
+ </Card>
66
+ );
67
+ }
68
+ `,
69
+ },
70
+
71
+ // ============================================
72
+ // WITH CONFIGURATION OPTIONS
73
+ // ============================================
74
+ {
75
+ name: "respects custom preferred component",
76
+ code: `
77
+ import { PrimaryButton } from "@/components/buttons";
78
+
79
+ export function Page() {
80
+ return <PrimaryButton>Click</PrimaryButton>;
81
+ }
82
+ `,
83
+ options: [
84
+ {
85
+ preferred: "PrimaryButton",
86
+ importSource: "@/components/buttons",
87
+ elements: ["button"],
88
+ },
89
+ ],
90
+ },
91
+ {
92
+ name: "ignores elements not in target list",
93
+ code: `
94
+ export function Page() {
95
+ return <div><span>Text</span></div>;
96
+ }
97
+ `,
98
+ // Default options only check "button"
99
+ },
100
+
101
+ // ============================================
102
+ // EDGE CASES - SHOULD NOT ERROR
103
+ // ============================================
104
+ {
105
+ name: "component defined in same file (not imported)",
106
+ code: `
107
+ function CustomButton({ children }) {
108
+ return <button className="custom">{children}</button>;
109
+ }
110
+
111
+ export function Page() {
112
+ return <CustomButton>Click</CustomButton>;
113
+ }
114
+ `,
115
+ },
116
+ {
117
+ name: "inside test file (if tests are excluded)",
118
+ code: `
119
+ // Assuming tests are in ignore list
120
+ export function TestComponent() {
121
+ return <button>Test</button>;
122
+ }
123
+ `,
124
+ options: [{ ignore: [".test.", ".spec."] }],
125
+ filename: "Component.test.tsx",
126
+ },
127
+ {
128
+ name: "namespace import usage",
129
+ code: `
130
+ import * as UI from "@/components/ui";
131
+
132
+ export function Page() {
133
+ return <UI.Button>Click</UI.Button>;
134
+ }
135
+ `,
136
+ },
137
+
138
+ // ============================================
139
+ // ALIASED IMPORTS
140
+ // ============================================
141
+ {
142
+ name: "aliased import of preferred component",
143
+ code: `
144
+ import { Button as Btn } from "@/components/ui/button";
145
+
146
+ export function Page() {
147
+ return <Btn>Click</Btn>;
148
+ }
149
+ `,
150
+ },
151
+
152
+ // ============================================
153
+ // DIFFERENT COMPONENT TYPES
154
+ // ============================================
155
+ {
156
+ name: "class component using preferred",
157
+ code: `
158
+ import { Button } from "@/components/ui/button";
159
+
160
+ class Page extends React.Component {
161
+ render() {
162
+ return <Button>Click</Button>;
163
+ }
164
+ }
165
+ `,
166
+ },
167
+ {
168
+ name: "forwardRef component using preferred",
169
+ code: `
170
+ import { Button } from "@/components/ui/button";
171
+ import { forwardRef } from "react";
172
+
173
+ const MyComponent = forwardRef((props, ref) => {
174
+ return <Button ref={ref}>Click</Button>;
175
+ });
176
+ `,
177
+ },
178
+ ],
179
+
180
+ invalid: [
181
+ // ============================================
182
+ // BASIC VIOLATIONS
183
+ // ============================================
184
+ {
185
+ name: "uses native button instead of preferred",
186
+ code: `
187
+ export function Page() {
188
+ return <button>Click me</button>;
189
+ }
190
+ `,
191
+ errors: [
192
+ {
193
+ messageId: "preferComponent",
194
+ data: {
195
+ preferred: "Button",
196
+ source: "@/components/ui/button",
197
+ element: "button",
198
+ },
199
+ },
200
+ ],
201
+ },
202
+ {
203
+ name: "uses native input instead of preferred",
204
+ code: `
205
+ export function Page() {
206
+ return <input type="text" />;
207
+ }
208
+ `,
209
+ options: [
210
+ {
211
+ preferred: "Input",
212
+ importSource: "@/components/ui/input",
213
+ elements: ["input"],
214
+ },
215
+ ],
216
+ errors: [
217
+ {
218
+ messageId: "preferComponent",
219
+ data: {
220
+ preferred: "Input",
221
+ source: "@/components/ui/input",
222
+ element: "input",
223
+ },
224
+ },
225
+ ],
226
+ },
227
+
228
+ // ============================================
229
+ // MULTIPLE VIOLATIONS IN ONE FILE
230
+ // ============================================
231
+ {
232
+ name: "multiple native buttons in same file",
233
+ code: `
234
+ export function Page() {
235
+ return (
236
+ <div>
237
+ <button>First</button>
238
+ <button>Second</button>
239
+ <button>Third</button>
240
+ </div>
241
+ );
242
+ }
243
+ `,
244
+ errors: [
245
+ {
246
+ messageId: "preferComponent",
247
+ data: { preferred: "Button", element: "button" },
248
+ },
249
+ {
250
+ messageId: "preferComponent",
251
+ data: { preferred: "Button", element: "button" },
252
+ },
253
+ {
254
+ messageId: "preferComponent",
255
+ data: { preferred: "Button", element: "button" },
256
+ },
257
+ ],
258
+ },
259
+
260
+ // ============================================
261
+ // MIXED USAGE (some preferred, some native)
262
+ // ============================================
263
+ {
264
+ name: "mixes preferred and native elements",
265
+ code: `
266
+ import { Button } from "@/components/ui/button";
267
+
268
+ export function Page() {
269
+ return (
270
+ <div>
271
+ <Button>Good</Button>
272
+ <button>Bad</button>
273
+ </div>
274
+ );
275
+ }
276
+ `,
277
+ errors: [
278
+ {
279
+ messageId: "preferComponent",
280
+ data: { preferred: "Button", element: "button" },
281
+ },
282
+ ],
283
+ },
284
+
285
+ // ============================================
286
+ // DIFFERENT COMPONENT PATTERNS
287
+ // ============================================
288
+ {
289
+ name: "arrow function component with violation",
290
+ code: `
291
+ const Page = () => {
292
+ return <button>Click</button>;
293
+ };
294
+
295
+ export default Page;
296
+ `,
297
+ errors: [
298
+ {
299
+ messageId: "preferComponent",
300
+ },
301
+ ],
302
+ },
303
+ {
304
+ name: "class component with violation",
305
+ code: `
306
+ class Page extends React.Component {
307
+ render() {
308
+ return <button>Click</button>;
309
+ }
310
+ }
311
+ `,
312
+ errors: [
313
+ {
314
+ messageId: "preferComponent",
315
+ },
316
+ ],
317
+ },
318
+
319
+ // ============================================
320
+ // NESTED ELEMENTS
321
+ // ============================================
322
+ {
323
+ name: "native button nested in other components",
324
+ code: `
325
+ import { Card } from "@/components/ui/card";
326
+
327
+ export function Page() {
328
+ return (
329
+ <Card>
330
+ <div>
331
+ <button>Nested</button>
332
+ </div>
333
+ </Card>
334
+ );
335
+ }
336
+ `,
337
+ errors: [
338
+ {
339
+ messageId: "preferComponent",
340
+ },
341
+ ],
342
+ },
343
+
344
+ // ============================================
345
+ // WITH CUSTOM OPTIONS
346
+ // ============================================
347
+ {
348
+ name: "violates with custom element list",
349
+ code: `
350
+ export function Form() {
351
+ return (
352
+ <form>
353
+ <input type="text" />
354
+ <textarea />
355
+ <select><option>A</option></select>
356
+ </form>
357
+ );
358
+ }
359
+ `,
360
+ options: [
361
+ {
362
+ preferred: "FormControl",
363
+ importSource: "@/components/forms",
364
+ elements: ["input", "textarea", "select"],
365
+ },
366
+ ],
367
+ errors: [
368
+ { messageId: "preferComponent", data: { element: "input" } },
369
+ { messageId: "preferComponent", data: { element: "textarea" } },
370
+ { messageId: "preferComponent", data: { element: "select" } },
371
+ ],
372
+ },
373
+
374
+ // ============================================
375
+ // REAL-WORLD PATTERNS
376
+ // ============================================
377
+ {
378
+ name: "form with native submit button",
379
+ code: `
380
+ import { Input } from "@/components/ui/input";
381
+
382
+ export function LoginForm() {
383
+ return (
384
+ <form>
385
+ <Input type="email" placeholder="Email" />
386
+ <Input type="password" placeholder="Password" />
387
+ <button type="submit">Login</button>
388
+ </form>
389
+ );
390
+ }
391
+ `,
392
+ errors: [
393
+ {
394
+ messageId: "preferComponent",
395
+ data: { preferred: "Button", element: "button" },
396
+ },
397
+ ],
398
+ },
399
+ {
400
+ name: "modal with native close button",
401
+ code: `
402
+ import { Dialog } from "@/components/ui/dialog";
403
+
404
+ export function Modal() {
405
+ return (
406
+ <Dialog>
407
+ <h2>Title</h2>
408
+ <p>Content</p>
409
+ <button onClick={close}>Close</button>
410
+ </Dialog>
411
+ );
412
+ }
413
+ `,
414
+ errors: [
415
+ {
416
+ messageId: "preferComponent",
417
+ },
418
+ ],
419
+ },
420
+
421
+ // ============================================
422
+ // EDGE CASES THAT SHOULD STILL ERROR
423
+ // ============================================
424
+ {
425
+ name: "button with many attributes still errors",
426
+ code: `
427
+ export function Page() {
428
+ return (
429
+ <button
430
+ type="button"
431
+ onClick={handleClick}
432
+ className="custom-class"
433
+ disabled={isDisabled}
434
+ aria-label="Action"
435
+ >
436
+ Click
437
+ </button>
438
+ );
439
+ }
440
+ `,
441
+ errors: [
442
+ {
443
+ messageId: "preferComponent",
444
+ },
445
+ ],
446
+ },
447
+ {
448
+ name: "self-closing button errors",
449
+ code: `
450
+ export function Page() {
451
+ return <button />;
452
+ }
453
+ `,
454
+ errors: [
455
+ {
456
+ messageId: "preferComponent",
457
+ },
458
+ ],
459
+ },
460
+ ],
461
+ });
462
+
463
+ // ============================================
464
+ // ADDITIONAL TEST PATTERNS (for reference)
465
+ // ============================================
466
+
467
+ /*
468
+ * Testing with specific error locations:
469
+ *
470
+ * {
471
+ * name: "error at specific location",
472
+ * code: `line1\nline2\n<button>here</button>`,
473
+ * errors: [{
474
+ * messageId: "preferComponent",
475
+ * line: 3,
476
+ * column: 1,
477
+ * }],
478
+ * }
479
+ *
480
+ * Testing with autofix (if rule supports fixing):
481
+ *
482
+ * {
483
+ * name: "autofixes native to preferred",
484
+ * code: `<button>Click</button>`,
485
+ * output: `<Button>Click</Button>`,
486
+ * errors: [{ messageId: "preferComponent" }],
487
+ * }
488
+ *
489
+ * Testing error count without specific messages:
490
+ *
491
+ * {
492
+ * name: "reports 3 errors",
493
+ * code: `...`,
494
+ * errors: 3, // Just count, no details
495
+ * }
496
+ */