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.
- package/dist/index.js +2200 -236
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/skills/ui-consistency-enforcer/SKILL.md +445 -0
- package/skills/ui-consistency-enforcer/references/REGISTRY-ENTRY.md +163 -0
- package/skills/ui-consistency-enforcer/references/RULE-TEMPLATE.ts +253 -0
- package/skills/ui-consistency-enforcer/references/TEST-TEMPLATE.ts +496 -0
|
@@ -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
|
+
*/
|