@metrostar/comet-mcp 1.3.2 → 1.3.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.
@@ -1,1564 +0,0 @@
1
- import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { getFriendlyDirectoryName, getComponentsFromPackage, getComponentDetails, extractJSDocDescription, extractProps, extractTypes, findComponentFile, findComponentDirectory, fetchUrl, parseSitemapForUrls, extractUSWDSContent, determineContentType, } from './utils';
3
- import fs from 'fs';
4
- import path from 'path';
5
- // Mock fs and path modules
6
- vi.mock('fs');
7
- vi.mock('path');
8
- const mockedFs = vi.mocked(fs);
9
- const mockedPath = vi.mocked(path);
10
- describe('MCP Utils', () => {
11
- beforeEach(() => {
12
- vi.clearAllMocks();
13
- // Mock path.join to return predictable paths
14
- mockedPath.join.mockImplementation((...args) => args.join('/'));
15
- // Mock process.cwd() to return a consistent value for testing
16
- vi.spyOn(process, 'cwd').mockReturnValue('/mock/project/root');
17
- });
18
- afterEach(() => {
19
- vi.restoreAllMocks();
20
- });
21
- test('getFriendlyDirectoryName should convert PascalCase to kebab-case', () => {
22
- expect(getFriendlyDirectoryName('MyComponent')).toBe('my-component');
23
- expect(getFriendlyDirectoryName('SimpleButton')).toBe('simple-button');
24
- expect(getFriendlyDirectoryName('DataTable')).toBe('data-table');
25
- });
26
- describe('getComponentsFromPackage', () => {
27
- test('should extract components from comet-uswds package', () => {
28
- // Mock file system
29
- mockedFs.existsSync.mockReturnValue(true);
30
- mockedFs.readFileSync.mockReturnValue(`
31
- export { default as Accordion, AccordionItem } from './accordion';
32
- export type { AccordionItemProps } from './accordion';
33
- export { default as Alert } from './alert';
34
- export { default as Button } from './button';
35
- export { default as Card, CardFooter, CardBody } from './card';
36
- `);
37
- const components = getComponentsFromPackage('@metrostar/comet-uswds');
38
- expect(components).toEqual([
39
- 'Accordion',
40
- 'AccordionItem',
41
- 'Alert',
42
- 'Button',
43
- 'Card',
44
- 'CardBody',
45
- 'CardFooter',
46
- ]);
47
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/mock/project/root/node_modules/@metrostar/comet-uswds');
48
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/mock/project/root/node_modules/@metrostar/comet-uswds/dist/index.d.ts');
49
- });
50
- test('should extract components from comet-extras package', () => {
51
- mockedFs.existsSync.mockReturnValue(true);
52
- mockedFs.readFileSync.mockReturnValue(`
53
- export { default as DataTable } from './data-table';
54
- export { default as Spinner } from './spinner';
55
- export { default as Tabs, TabPanel } from './tabs';
56
- `);
57
- const components = getComponentsFromPackage('@metrostar/comet-extras');
58
- expect(components).toEqual(['DataTable', 'Spinner', 'TabPanel', 'Tabs']);
59
- });
60
- test('should extract components from comet-data-viz package', () => {
61
- mockedFs.existsSync.mockReturnValue(true);
62
- mockedFs.readFileSync.mockReturnValue(`
63
- export { default as BarGraph } from './bar-graph';
64
- export { default as LineGraph } from './line-graph';
65
- export { default as PieChart } from './pie-chart';
66
- `);
67
- const components = getComponentsFromPackage('@metrostar/comet-data-viz');
68
- expect(components).toEqual(['BarGraph', 'LineGraph', 'PieChart']);
69
- });
70
- test('should return empty array when package directory does not exist', () => {
71
- mockedFs.existsSync.mockReturnValueOnce(false);
72
- const components = getComponentsFromPackage('@metrostar/comet-nonexistent');
73
- expect(components).toEqual([]);
74
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/mock/project/root/node_modules/@metrostar/comet-nonexistent');
75
- });
76
- test('should return empty array when components index file does not exist', () => {
77
- mockedFs.existsSync.mockReturnValueOnce(true).mockReturnValueOnce(false);
78
- const components = getComponentsFromPackage('@metrostar/comet-uswds');
79
- expect(components).toEqual([]);
80
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/mock/project/root/node_modules/@metrostar/comet-uswds');
81
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/mock/project/root/node_modules/@metrostar/comet-uswds/dist/index.d.ts');
82
- });
83
- test('should handle readFileSync errors gracefully', () => {
84
- mockedFs.existsSync.mockReturnValue(true);
85
- mockedFs.readFileSync.mockImplementation(() => {
86
- throw new Error('File read error');
87
- });
88
- const components = getComponentsFromPackage('@metrostar/comet-uswds');
89
- expect(components).toEqual([]);
90
- });
91
- test('should handle complex export patterns', () => {
92
- mockedFs.existsSync.mockReturnValue(true);
93
- mockedFs.readFileSync.mockReturnValue(`
94
- export { default as Button } from './button';
95
- export { default as ButtonGroup } from './button-group';
96
- export { default as Card, CardFooter, CardBody, CardHeader, CardMedia } from './card';
97
- export type { CardProps } from './card';
98
- export { default as Checkbox, CheckboxGroup } from './checkbox';
99
- export type { CheckboxData } from './checkbox';
100
- `);
101
- const components = getComponentsFromPackage('@metrostar/comet-uswds');
102
- expect(components).toEqual([
103
- 'Button',
104
- 'ButtonGroup',
105
- 'Card',
106
- 'CardBody',
107
- 'CardFooter',
108
- 'CardHeader',
109
- 'CardMedia',
110
- 'Checkbox',
111
- 'CheckboxGroup',
112
- ]);
113
- });
114
- test('should remove duplicates and sort components', () => {
115
- mockedFs.existsSync.mockReturnValue(true);
116
- mockedFs.readFileSync.mockReturnValue(`
117
- export { default as Button } from './button';
118
- export { default as Alert } from './alert';
119
- export { default as Button } from './button-duplicate';
120
- export { default as Card } from './card';
121
- `);
122
- const components = getComponentsFromPackage('@metrostar/comet-uswds');
123
- expect(components).toEqual(['Alert', 'Button', 'Card']);
124
- });
125
- test('should use PROJECT_ROOT environment variable when available', () => {
126
- const originalEnv = process.env.PROJECT_ROOT;
127
- const customRoot = '/custom/root';
128
- process.env.PROJECT_ROOT = customRoot;
129
- mockedFs.existsSync.mockReturnValue(true);
130
- mockedFs.readFileSync.mockReturnValue(`
131
- export { default as Button } from './button';
132
- `);
133
- getComponentsFromPackage('@metrostar/comet-uswds');
134
- expect(mockedPath.join).toHaveBeenCalledWith(`${customRoot}/node_modules/@metrostar/comet-uswds`, 'dist', 'index.d.ts');
135
- process.env.PROJECT_ROOT = originalEnv;
136
- });
137
- });
138
- describe('extractJSDocDescription', () => {
139
- test('should extract description from single-line JSDoc comment', () => {
140
- const content = `
141
- /**
142
- * This is a button component that renders a clickable element
143
- */
144
- const Button = () => {
145
- return <button>Click me</button>;
146
- };
147
- `;
148
- const result = extractJSDocDescription(content);
149
- expect(result).toBe('This is a button component that renders a clickable element');
150
- });
151
- test('should extract description from multi-line JSDoc comment with only first line', () => {
152
- const content = `
153
- /**
154
- * A versatile card component for displaying content
155
- * @param title The title of the card
156
- * @param children The content to display in the card
157
- */
158
- const Card = ({ title, children }) => {
159
- return <div className="card">{children}</div>;
160
- };
161
- `;
162
- const result = extractJSDocDescription(content);
163
- expect(result).toBe('A versatile card component for displaying content');
164
- });
165
- test('should handle JSDoc comment with extra whitespace', () => {
166
- const content = `
167
- /**
168
- * This is a component with extra whitespace
169
- */
170
- const Component = () => {
171
- return <div>Content</div>;
172
- };
173
- `;
174
- const result = extractJSDocDescription(content);
175
- expect(result).toBe('This is a component with extra whitespace');
176
- });
177
- test('should return undefined when no JSDoc comment is found', () => {
178
- const content = `
179
- // This is a regular comment
180
- const Button = () => {
181
- return <button>Click me</button>;
182
- };
183
- `;
184
- const result = extractJSDocDescription(content);
185
- expect(result).toBeUndefined();
186
- });
187
- test('should return undefined when JSDoc comment is malformed', () => {
188
- const content = `
189
- /*
190
- * This is not a proper JSDoc comment
191
- */
192
- const Button = () => {
193
- return <button>Click me</button>;
194
- };
195
- `;
196
- const result = extractJSDocDescription(content);
197
- expect(result).toBeUndefined();
198
- });
199
- test('should handle JSDoc comment with no description line', () => {
200
- const content = `
201
- /**
202
- * @param props The component props
203
- */
204
- const Button = (props) => {
205
- return <button>Click me</button>;
206
- };
207
- `;
208
- const result = extractJSDocDescription(content);
209
- expect(result).toBeUndefined();
210
- });
211
- test('should extract description from JSDoc comment with different asterisk formatting', () => {
212
- const content = `
213
- /**
214
- * Alert component for displaying important messages
215
- */
216
- const Alert = () => {
217
- return <div className="alert">Alert message</div>;
218
- };
219
- `;
220
- const result = extractJSDocDescription(content);
221
- expect(result).toBe('Alert component for displaying important messages');
222
- });
223
- test('should handle multiple JSDoc comments and extract from the first one', () => {
224
- const content = `
225
- /**
226
- * First component description
227
- */
228
- const FirstComponent = () => {
229
- return <div>First</div>;
230
- };
231
-
232
- /**
233
- * Second component description
234
- */
235
- const SecondComponent = () => {
236
- return <div>Second</div>;
237
- };
238
- `;
239
- const result = extractJSDocDescription(content);
240
- expect(result).toBe('First component description');
241
- });
242
- test('should handle empty content', () => {
243
- const result = extractJSDocDescription('');
244
- expect(result).toBeUndefined();
245
- });
246
- test('should handle content with only whitespace', () => {
247
- const result = extractJSDocDescription(' \n \t ');
248
- expect(result).toBeUndefined();
249
- });
250
- });
251
- describe('extractProps', () => {
252
- test('should extract props from a simple interface', () => {
253
- const content = `
254
- interface ButtonProps {
255
- label: string;
256
- onClick: () => void;
257
- disabled?: boolean;
258
- }
259
- `;
260
- const result = extractProps(content);
261
- expect(result).toEqual(['disabled', 'label', 'onClick']);
262
- });
263
- test('should extract props from multiple interfaces', () => {
264
- const content = `
265
- interface CardProps {
266
- title: string;
267
- children: React.ReactNode;
268
- }
269
-
270
- interface ButtonProps {
271
- label: string;
272
- variant?: 'primary' | 'secondary';
273
- }
274
- `;
275
- const result = extractProps(content);
276
- expect(result).toEqual(['children', 'label', 'title', 'variant']);
277
- });
278
- test('should handle props with different types', () => {
279
- const content = `
280
- interface ComponentProps {
281
- id: string;
282
- count: number;
283
- isVisible: boolean;
284
- items: string[];
285
- }
286
- `;
287
- const result = extractProps(content);
288
- expect(result).toEqual(['count', 'id', 'isVisible', 'items']);
289
- });
290
- test('should handle optional props with question mark', () => {
291
- const content = `
292
- interface AlertProps {
293
- message: string;
294
- type?: 'info' | 'warning' | 'error';
295
- dismissible?: boolean;
296
- onClose?: () => void;
297
- }
298
- `;
299
- const result = extractProps(content);
300
- expect(result).toEqual(['dismissible', 'message', 'onClose', 'type']);
301
- });
302
- test('should handle props with complex types and generics', () => {
303
- const content = `
304
- interface DataTableProps {
305
- data: Array<Record<string, any>>;
306
- columns: Column<T>[];
307
- onRowClick?: (row: T) => void;
308
- renderCell?: (value: any, row: T) => React.ReactNode;
309
- }
310
- `;
311
- const result = extractProps(content);
312
- expect(result).toEqual(['columns', 'data', 'onRowClick', 'renderCell']);
313
- });
314
- test('should ignore non-Props interfaces', () => {
315
- const content = `
316
- interface User {
317
- name: string;
318
- email: string;
319
- }
320
-
321
- interface ButtonProps {
322
- label: string;
323
- onClick: () => void;
324
- }
325
-
326
- interface ApiResponse {
327
- data: any;
328
- success: boolean;
329
- }
330
- `;
331
- const result = extractProps(content);
332
- expect(result).toEqual(['label', 'onClick']);
333
- });
334
- test('should handle interfaces with extends (current limitation)', () => {
335
- const content = `
336
- interface ButtonProps extends BaseProps {
337
- label: string;
338
- variant: 'primary' | 'secondary';
339
- }
340
- `;
341
- // The current regex doesn't handle 'extends' syntax properly
342
- const result = extractProps(content);
343
- expect(result).toEqual([]);
344
- });
345
- test('should handle simple nested interfaces and object properties', () => {
346
- const content = `
347
- interface FormProps {
348
- fields: Array<FieldConfig>;
349
- onSubmit: FormSubmitHandler;
350
- validation?: ValidationConfig;
351
- }
352
- `;
353
- const result = extractProps(content);
354
- expect(result).toEqual(['fields', 'onSubmit', 'validation']);
355
- });
356
- test('should handle props with comments', () => {
357
- const content = `
358
- interface CardProps {
359
- // The title to display
360
- title: string;
361
- /** The content of the card */
362
- children: React.ReactNode;
363
- // Optional footer content
364
- footer?: React.ReactNode;
365
- }
366
- `;
367
- const result = extractProps(content);
368
- expect(result).toEqual(['children', 'footer', 'title']);
369
- });
370
- test('should remove duplicates and sort props', () => {
371
- const content = `
372
- interface FirstProps {
373
- title: string;
374
- count: number;
375
- }
376
-
377
- interface SecondProps {
378
- title: string;
379
- isVisible: boolean;
380
- count: number;
381
- }
382
- `;
383
- const result = extractProps(content);
384
- expect(result).toEqual(['count', 'isVisible', 'title']);
385
- });
386
- test('should handle multiline prop definitions', () => {
387
- const content = `
388
- interface ComponentProps {
389
- longPropertyName:
390
- | 'option1'
391
- | 'option2'
392
- | 'option3';
393
- callback: CallbackFunction;
394
- config: ConfigObject;
395
- }
396
- `;
397
- const result = extractProps(content);
398
- expect(result).toEqual(['callback', 'config', 'longPropertyName']);
399
- });
400
- test('should return empty array when no Props interfaces found', () => {
401
- const content = `
402
- interface User {
403
- name: string;
404
- email: string;
405
- }
406
-
407
- interface Config {
408
- theme: string;
409
- debug: boolean;
410
- }
411
- `;
412
- const result = extractProps(content);
413
- expect(result).toEqual([]);
414
- });
415
- test('should handle empty content', () => {
416
- const result = extractProps('');
417
- expect(result).toEqual([]);
418
- });
419
- test('should handle content with only whitespace', () => {
420
- const result = extractProps(' \n \t ');
421
- expect(result).toEqual([]);
422
- });
423
- test('should handle malformed interfaces gracefully', () => {
424
- const content = `
425
- interface BrokenProps {
426
- validProp: string;
427
- // Missing type annotation
428
- invalidProp;
429
- anotherValidProp: number;
430
- }
431
- `;
432
- const result = extractProps(content);
433
- expect(result).toEqual(['anotherValidProp', 'validProp']);
434
- });
435
- test('should handle interfaces with different naming patterns', () => {
436
- const content = `
437
- interface ComponentProps {
438
- basic: string;
439
- }
440
-
441
- interface MyComponentProps {
442
- advanced: number;
443
- }
444
-
445
- interface Props {
446
- simple: boolean;
447
- }
448
- `;
449
- const result = extractProps(content);
450
- expect(result).toEqual(['advanced', 'basic', 'simple']);
451
- });
452
- test('should extract properties from complex object definitions', () => {
453
- const content = `
454
- interface FormProps {
455
- fields: {
456
- name: string;
457
- type: 'text' | 'email';
458
- required: boolean;
459
- }[];
460
- }
461
- `;
462
- // The current implementation extracts nested properties when the regex matches object definitions
463
- const result = extractProps(content);
464
- expect(result).toEqual(['fields', 'name', 'required', 'type']);
465
- });
466
- });
467
- describe('extractTypes', () => {
468
- test('should extract exported type aliases', () => {
469
- const content = `
470
- export type ButtonVariant = 'primary' | 'secondary' | 'tertiary';
471
- export type Size = 'small' | 'medium' | 'large';
472
- `;
473
- const result = extractTypes(content);
474
- expect(result).toEqual(['ButtonVariant', 'Size']);
475
- });
476
- test('should extract exported interfaces', () => {
477
- const content = `
478
- export interface User {
479
- id: string;
480
- name: string;
481
- }
482
-
483
- export interface Product {
484
- id: number;
485
- title: string;
486
- }
487
- `;
488
- const result = extractTypes(content);
489
- expect(result).toEqual(['Product', 'User']);
490
- });
491
- test('should extract both types and interfaces', () => {
492
- const content = `
493
- export type Theme = 'light' | 'dark';
494
- export interface Config {
495
- theme: Theme;
496
- debug: boolean;
497
- }
498
- export type Status = 'loading' | 'success' | 'error';
499
- `;
500
- const result = extractTypes(content);
501
- expect(result).toEqual(['Config', 'Status', 'Theme']);
502
- });
503
- test('should ignore non-exported types and interfaces', () => {
504
- const content = `
505
- type InternalType = string;
506
- interface InternalInterface {
507
- value: string;
508
- }
509
- export type PublicType = number;
510
- export interface PublicInterface {
511
- data: string;
512
- }
513
- `;
514
- const result = extractTypes(content);
515
- expect(result).toEqual(['PublicInterface', 'PublicType']);
516
- });
517
- test('should handle different export syntax variations', () => {
518
- const content = `
519
- export type SpacedType = string;
520
- export interface TabbedInterface {
521
- value: number;
522
- }
523
- export type NormalType = boolean;
524
- `;
525
- const result = extractTypes(content);
526
- expect(result).toEqual(['NormalType', 'SpacedType', 'TabbedInterface']);
527
- });
528
- test('should handle generic types and interfaces', () => {
529
- const content = `
530
- export type ApiResponse<T> = {
531
- data: T;
532
- success: boolean;
533
- };
534
-
535
- export interface Repository<T, K> {
536
- findById(id: K): T | null;
537
- save(entity: T): void;
538
- }
539
- `;
540
- const result = extractTypes(content);
541
- expect(result).toEqual(['ApiResponse', 'Repository']);
542
- });
543
- test('should handle complex type definitions', () => {
544
- const content = `
545
- export type EventHandler = (event: Event) => void;
546
- export type ComponentProps = {
547
- id: string;
548
- children?: React.ReactNode;
549
- };
550
- export interface FormFieldConfig {
551
- name: string;
552
- type: 'text' | 'email' | 'password';
553
- validation?: ValidationRule[];
554
- }
555
- `;
556
- const result = extractTypes(content);
557
- expect(result).toEqual(['ComponentProps', 'EventHandler', 'FormFieldConfig']);
558
- });
559
- test('should handle types and interfaces with extends', () => {
560
- const content = `
561
- export interface BaseEntity {
562
- id: string;
563
- createdAt: Date;
564
- }
565
-
566
- export interface User extends BaseEntity {
567
- name: string;
568
- email: string;
569
- }
570
-
571
- export type ExtendedConfig = Config & {
572
- version: string;
573
- };
574
- `;
575
- const result = extractTypes(content);
576
- expect(result).toEqual(['BaseEntity', 'ExtendedConfig', 'User']);
577
- });
578
- test('should handle multiline type definitions', () => {
579
- const content = `
580
- export type ComplexUnion =
581
- | 'option1'
582
- | 'option2'
583
- | 'option3';
584
-
585
- export interface MultilineInterface
586
- extends BaseInterface {
587
- property: string;
588
- }
589
- `;
590
- const result = extractTypes(content);
591
- expect(result).toEqual(['ComplexUnion', 'MultilineInterface']);
592
- });
593
- test('should handle types and interfaces with comments', () => {
594
- const content = `
595
- // This is a type comment
596
- export type CommentedType = string;
597
-
598
- /**
599
- * This is an interface comment
600
- */
601
- export interface CommentedInterface {
602
- value: number;
603
- }
604
-
605
- /* Another comment style */
606
- export type AnotherType = boolean;
607
- `;
608
- const result = extractTypes(content);
609
- expect(result).toEqual(['AnotherType', 'CommentedInterface', 'CommentedType']);
610
- });
611
- test('should handle mixed exports with other items', () => {
612
- const content = `
613
- export const CONSTANT = 'value';
614
- export type MyType = string;
615
- export function myFunction() {}
616
- export interface MyInterface {
617
- prop: string;
618
- }
619
- export class MyClass {}
620
- export enum MyEnum {
621
- A, B, C
622
- }
623
- `;
624
- const result = extractTypes(content);
625
- expect(result).toEqual(['MyInterface', 'MyType']);
626
- });
627
- test('should return empty array when no exported types found', () => {
628
- const content = `
629
- const localVar = 'value';
630
- type InternalType = string;
631
- interface InternalInterface {
632
- prop: string;
633
- }
634
- function normalFunction() {}
635
- `;
636
- const result = extractTypes(content);
637
- expect(result).toEqual([]);
638
- });
639
- test('should handle empty content', () => {
640
- const result = extractTypes('');
641
- expect(result).toEqual([]);
642
- });
643
- test('should handle content with only whitespace', () => {
644
- const result = extractTypes(' \n \t ');
645
- expect(result).toEqual([]);
646
- });
647
- test('should handle malformed export statements gracefully', () => {
648
- const content = `
649
- export type ValidType = string;
650
- export type // malformed
651
- export interface ValidInterface {
652
- prop: string;
653
- }
654
- export type AnotherValidType = number;
655
- `;
656
- const result = extractTypes(content);
657
- expect(result).toEqual(['AnotherValidType', 'ValidInterface', 'ValidType']);
658
- });
659
- test('should remove duplicates and sort results', () => {
660
- const content = `
661
- export type TypeB = string;
662
- export interface InterfaceA {
663
- prop: string;
664
- }
665
- export type TypeA = number;
666
- export interface InterfaceB {
667
- prop: number;
668
- }
669
- `;
670
- const result = extractTypes(content);
671
- expect(result).toEqual(['InterfaceA', 'InterfaceB', 'TypeA', 'TypeB']);
672
- });
673
- test('should handle re-exported types', () => {
674
- const content = `
675
- export { type ExistingType } from './other-module';
676
- export type NewType = string;
677
- export { type AnotherType, interface SomeInterface } from './another-module';
678
- `;
679
- const result = extractTypes(content);
680
- // Current implementation only catches direct export type/interface declarations
681
- expect(result).toEqual(['NewType']);
682
- });
683
- });
684
- describe('findComponentFile', () => {
685
- beforeEach(() => {
686
- vi.clearAllMocks();
687
- });
688
- test('should find index.tsx file in local development', () => {
689
- const componentDir = '/path/to/button';
690
- const componentName = 'Button';
691
- mockedFs.existsSync.mockImplementation((filePath) => {
692
- return filePath === '/path/to/button/index.tsx';
693
- });
694
- const result = findComponentFile(componentDir, componentName, true);
695
- expect(result).toBe('/path/to/button/index.tsx');
696
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/button/index.tsx');
697
- });
698
- test('should find component-named file when index.tsx does not exist', () => {
699
- const componentDir = '/path/to/alert';
700
- const componentName = 'Alert';
701
- mockedFs.existsSync.mockImplementation((filePath) => {
702
- return filePath === '/path/to/alert/Alert.tsx';
703
- });
704
- const result = findComponentFile(componentDir, componentName, true);
705
- expect(result).toBe('/path/to/alert/Alert.tsx');
706
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/alert/index.tsx');
707
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/alert/Alert.tsx');
708
- });
709
- test('should find lowercase component file when others do not exist', () => {
710
- const componentDir = '/path/to/card';
711
- const componentName = 'Card';
712
- mockedFs.existsSync.mockImplementation((filePath) => {
713
- return filePath === '/path/to/card/card.tsx';
714
- });
715
- const result = findComponentFile(componentDir, componentName, true);
716
- expect(result).toBe('/path/to/card/card.tsx');
717
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/card/index.tsx');
718
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/card/Card.tsx');
719
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/card/card.tsx');
720
- });
721
- test('should return null when no files exist in local development', () => {
722
- const componentDir = '/path/to/nonexistent';
723
- const componentName = 'NonExistent';
724
- mockedFs.existsSync.mockReturnValue(false);
725
- const result = findComponentFile(componentDir, componentName, true);
726
- expect(result).toBeNull();
727
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/nonexistent/index.tsx');
728
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/nonexistent/NonExistent.tsx');
729
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/nonexistent/nonexistent.tsx');
730
- });
731
- test('should return first matching file when multiple exist', () => {
732
- const componentDir = '/path/to/button';
733
- const componentName = 'Button';
734
- // Mock both index.tsx and Button.tsx to exist
735
- mockedFs.existsSync.mockReturnValue(true);
736
- const result = findComponentFile(componentDir, componentName, true);
737
- // Should return index.tsx since it's checked first
738
- expect(result).toBe('/path/to/button/index.tsx');
739
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/button/index.tsx');
740
- });
741
- test('should return null when not in local development mode', () => {
742
- const componentDir = '/path/to/button';
743
- const componentName = 'Button';
744
- mockedFs.existsSync.mockReturnValue(true);
745
- const result = findComponentFile(componentDir, componentName, false);
746
- expect(result).toBeNull();
747
- expect(mockedFs.existsSync).not.toHaveBeenCalled();
748
- });
749
- test('should handle complex component names correctly', () => {
750
- const componentDir = '/path/to/data-table';
751
- const componentName = 'DataTable';
752
- mockedFs.existsSync.mockImplementation((filePath) => {
753
- return filePath === '/path/to/data-table/datatable.tsx';
754
- });
755
- const result = findComponentFile(componentDir, componentName, true);
756
- expect(result).toBe('/path/to/data-table/datatable.tsx');
757
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/data-table/index.tsx');
758
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/data-table/DataTable.tsx');
759
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/data-table/datatable.tsx');
760
- });
761
- test('should handle single character component names', () => {
762
- const componentDir = '/path/to/x';
763
- const componentName = 'X';
764
- mockedFs.existsSync.mockImplementation((filePath) => {
765
- return filePath === '/path/to/x/X.tsx';
766
- });
767
- const result = findComponentFile(componentDir, componentName, true);
768
- expect(result).toBe('/path/to/x/X.tsx');
769
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/x/index.tsx');
770
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/x/X.tsx');
771
- });
772
- test('should handle empty component directory path', () => {
773
- const componentDir = '';
774
- const componentName = 'Button';
775
- mockedFs.existsSync.mockReturnValue(false);
776
- const result = findComponentFile(componentDir, componentName, true);
777
- expect(result).toBeNull();
778
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/index.tsx');
779
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/Button.tsx');
780
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/button.tsx');
781
- });
782
- test('should handle empty component name', () => {
783
- const componentDir = '/path/to/component';
784
- const componentName = '';
785
- mockedFs.existsSync.mockImplementation((filePath) => {
786
- return filePath === '/path/to/component/index.tsx';
787
- });
788
- const result = findComponentFile(componentDir, componentName, true);
789
- expect(result).toBe('/path/to/component/index.tsx');
790
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/component/index.tsx');
791
- // When index.tsx exists, the function returns early and doesn't check other files
792
- });
793
- test('should handle component names with numbers', () => {
794
- const componentDir = '/path/to/form2';
795
- const componentName = 'Form2';
796
- mockedFs.existsSync.mockImplementation((filePath) => {
797
- return filePath === '/path/to/form2/form2.tsx';
798
- });
799
- const result = findComponentFile(componentDir, componentName, true);
800
- expect(result).toBe('/path/to/form2/form2.tsx');
801
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/form2/index.tsx');
802
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/form2/Form2.tsx');
803
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/form2/form2.tsx');
804
- });
805
- test('should use path.join correctly for cross-platform compatibility', () => {
806
- const componentDir = '/path/to/button';
807
- const componentName = 'Button';
808
- mockedFs.existsSync.mockReturnValue(false);
809
- mockedPath.join.mockImplementation((...args) => args.join('/'));
810
- findComponentFile(componentDir, componentName, true);
811
- expect(mockedPath.join).toHaveBeenCalledWith('/path/to/button', 'index.tsx');
812
- expect(mockedPath.join).toHaveBeenCalledWith('/path/to/button', 'Button.tsx');
813
- expect(mockedPath.join).toHaveBeenCalledWith('/path/to/button', 'button.tsx');
814
- });
815
- });
816
- describe('findComponentDirectory', () => {
817
- beforeEach(() => {
818
- vi.clearAllMocks();
819
- });
820
- test('should find component directory with exact match in local development', () => {
821
- const packagePath = '/path/to/package';
822
- const componentName = 'Button';
823
- mockedFs.existsSync.mockImplementation((filePath) => {
824
- return filePath === '/path/to/package/src/components';
825
- });
826
- mockedFs.readdirSync.mockReturnValue([
827
- { name: 'button', isDirectory: () => true },
828
- { name: 'alert', isDirectory: () => true },
829
- { name: 'card', isDirectory: () => true },
830
- ]);
831
- const result = findComponentDirectory(packagePath, componentName, true);
832
- expect(result).toBe('/path/to/package/src/components/button');
833
- expect(mockedPath.join).toHaveBeenCalledWith(packagePath, 'src', 'components');
834
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/package/src/components');
835
- expect(mockedFs.readdirSync).toHaveBeenCalledWith('/path/to/package/src/components', {
836
- withFileTypes: true,
837
- });
838
- });
839
- test('should find component directory with kebab-case match', () => {
840
- const packagePath = '/path/to/package';
841
- const componentName = 'DataTable';
842
- mockedFs.existsSync.mockReturnValue(true);
843
- mockedFs.readdirSync.mockReturnValue([
844
- { name: 'data-table', isDirectory: () => true },
845
- { name: 'button', isDirectory: () => true },
846
- { name: 'card', isDirectory: () => true },
847
- ]);
848
- const result = findComponentDirectory(packagePath, componentName, true);
849
- expect(result).toBe('/path/to/package/src/components/data-table');
850
- });
851
- test('should find component directory with lowercase match', () => {
852
- const packagePath = '/path/to/package';
853
- const componentName = 'Alert';
854
- mockedFs.existsSync.mockReturnValue(true);
855
- mockedFs.readdirSync.mockReturnValue([
856
- { name: 'alert', isDirectory: () => true },
857
- { name: 'button', isDirectory: () => true },
858
- { name: 'card', isDirectory: () => true },
859
- ]);
860
- const result = findComponentDirectory(packagePath, componentName, true);
861
- expect(result).toBe('/path/to/package/src/components/alert');
862
- });
863
- test('should find component directory with exact case match', () => {
864
- const packagePath = '/path/to/package';
865
- const componentName = 'Card';
866
- mockedFs.existsSync.mockReturnValue(true);
867
- mockedFs.readdirSync.mockReturnValue([
868
- { name: 'Card', isDirectory: () => true },
869
- { name: 'button', isDirectory: () => true },
870
- { name: 'alert', isDirectory: () => true },
871
- ]);
872
- const result = findComponentDirectory(packagePath, componentName, true);
873
- expect(result).toBe('/path/to/package/src/components/Card');
874
- });
875
- test('should prioritize kebab-case over other matches', () => {
876
- const packagePath = '/path/to/package';
877
- const componentName = 'DataTable';
878
- mockedFs.existsSync.mockReturnValue(true);
879
- mockedFs.readdirSync.mockReturnValue([
880
- { name: 'data-table', isDirectory: () => true },
881
- { name: 'datatable', isDirectory: () => true },
882
- { name: 'DataTable', isDirectory: () => true },
883
- ]);
884
- const result = findComponentDirectory(packagePath, componentName, true);
885
- // The find method returns the first match, which should be kebab-case
886
- expect(result).toBe('/path/to/package/src/components/data-table');
887
- });
888
- test('should return null when components directory does not exist', () => {
889
- const packagePath = '/path/to/package';
890
- const componentName = 'Button';
891
- mockedFs.existsSync.mockReturnValue(false);
892
- const result = findComponentDirectory(packagePath, componentName, true);
893
- expect(result).toBeNull();
894
- expect(mockedFs.existsSync).toHaveBeenCalledWith('/path/to/package/src/components');
895
- expect(mockedFs.readdirSync).not.toHaveBeenCalled();
896
- });
897
- test('should return null when no matching directory found', () => {
898
- const packagePath = '/path/to/package';
899
- const componentName = 'NonExistent';
900
- mockedFs.existsSync.mockReturnValue(true);
901
- mockedFs.readdirSync.mockReturnValue([
902
- { name: 'button', isDirectory: () => true },
903
- { name: 'alert', isDirectory: () => true },
904
- { name: 'card', isDirectory: () => true },
905
- ]);
906
- const result = findComponentDirectory(packagePath, componentName, true);
907
- expect(result).toBeNull();
908
- });
909
- test('should filter out non-directories', () => {
910
- const packagePath = '/path/to/package';
911
- const componentName = 'Button';
912
- mockedFs.existsSync.mockReturnValue(true);
913
- mockedFs.readdirSync.mockReturnValue([
914
- { name: 'button', isDirectory: () => true },
915
- { name: 'readme.md', isDirectory: () => false },
916
- { name: 'index.ts', isDirectory: () => false },
917
- { name: 'alert', isDirectory: () => true },
918
- ]);
919
- const result = findComponentDirectory(packagePath, componentName, true);
920
- expect(result).toBe('/path/to/package/src/components/button');
921
- });
922
- test('should return package path when not in local development mode', () => {
923
- const packagePath = '/path/to/node_modules/package';
924
- const componentName = 'Button';
925
- const result = findComponentDirectory(packagePath, componentName, false);
926
- expect(result).toBe(packagePath);
927
- expect(mockedFs.existsSync).not.toHaveBeenCalled();
928
- expect(mockedFs.readdirSync).not.toHaveBeenCalled();
929
- });
930
- test('should handle complex component names with multiple capital letters', () => {
931
- const packagePath = '/path/to/package';
932
- const componentName = 'XMLHttpRequest';
933
- mockedFs.existsSync.mockReturnValue(true);
934
- mockedFs.readdirSync.mockReturnValue([
935
- { name: 'xmlhttp-request', isDirectory: () => true },
936
- { name: 'button', isDirectory: () => true },
937
- ]);
938
- const result = findComponentDirectory(packagePath, componentName, true);
939
- expect(result).toBe('/path/to/package/src/components/xmlhttp-request');
940
- });
941
- test('should handle component names with numbers', () => {
942
- const packagePath = '/path/to/package';
943
- const componentName = 'Form2';
944
- mockedFs.existsSync.mockReturnValue(true);
945
- mockedFs.readdirSync.mockReturnValue([
946
- { name: 'form2', isDirectory: () => true },
947
- { name: 'form', isDirectory: () => true },
948
- ]);
949
- const result = findComponentDirectory(packagePath, componentName, true);
950
- expect(result).toBe('/path/to/package/src/components/form2');
951
- });
952
- test('should handle single character component names', () => {
953
- const packagePath = '/path/to/package';
954
- const componentName = 'X';
955
- mockedFs.existsSync.mockReturnValue(true);
956
- mockedFs.readdirSync.mockReturnValue([
957
- { name: 'x', isDirectory: () => true },
958
- { name: 'button', isDirectory: () => true },
959
- ]);
960
- const result = findComponentDirectory(packagePath, componentName, true);
961
- expect(result).toBe('/path/to/package/src/components/x');
962
- });
963
- test('should handle empty components directory', () => {
964
- const packagePath = '/path/to/package';
965
- const componentName = 'Button';
966
- mockedFs.existsSync.mockReturnValue(true);
967
- mockedFs.readdirSync.mockReturnValue([]);
968
- const result = findComponentDirectory(packagePath, componentName, true);
969
- expect(result).toBeNull();
970
- });
971
- test('should handle directory with only files (no subdirectories)', () => {
972
- const packagePath = '/path/to/package';
973
- const componentName = 'Button';
974
- mockedFs.existsSync.mockReturnValue(true);
975
- mockedFs.readdirSync.mockReturnValue([
976
- { name: 'index.ts', isDirectory: () => false },
977
- { name: 'types.ts', isDirectory: () => false },
978
- { name: 'utils.ts', isDirectory: () => false },
979
- ]);
980
- const result = findComponentDirectory(packagePath, componentName, true);
981
- expect(result).toBeNull();
982
- });
983
- test('should use path.join correctly for cross-platform compatibility', () => {
984
- const packagePath = '/path/to/package';
985
- const componentName = 'Button';
986
- mockedFs.existsSync.mockReturnValue(true);
987
- mockedFs.readdirSync.mockReturnValue([{ name: 'button', isDirectory: () => true }]);
988
- mockedPath.join.mockImplementation((...args) => args.join('/'));
989
- findComponentDirectory(packagePath, componentName, true);
990
- expect(mockedPath.join).toHaveBeenCalledWith(packagePath, 'src', 'components');
991
- expect(mockedPath.join).toHaveBeenCalledWith('/path/to/package/src/components', 'button');
992
- });
993
- test('should handle readdir error gracefully', () => {
994
- const packagePath = '/path/to/package';
995
- const componentName = 'Button';
996
- mockedFs.existsSync.mockReturnValue(true);
997
- mockedFs.readdirSync.mockImplementation(() => {
998
- throw new Error('Permission denied');
999
- });
1000
- expect(() => {
1001
- findComponentDirectory(packagePath, componentName, true);
1002
- }).toThrow('Permission denied');
1003
- });
1004
- test('should handle special characters in component names', () => {
1005
- const packagePath = '/path/to/package';
1006
- const componentName = 'My$Component';
1007
- mockedFs.existsSync.mockReturnValue(true);
1008
- mockedFs.readdirSync.mockReturnValue([
1009
- { name: 'my$component', isDirectory: () => true },
1010
- { name: 'button', isDirectory: () => true },
1011
- ]);
1012
- const result = findComponentDirectory(packagePath, componentName, true);
1013
- expect(result).toBe('/path/to/package/src/components/my$component');
1014
- });
1015
- });
1016
- describe('getComponentDetails', () => {
1017
- test('should return component details when component is found', () => {
1018
- const componentName = 'Button';
1019
- // Set PROJECT_ROOT environment variable
1020
- const originalEnv = process.env.PROJECT_ROOT;
1021
- process.env.PROJECT_ROOT = '/mock/project/root';
1022
- // Mock that the package directory exists
1023
- mockedFs.existsSync.mockReturnValueOnce(true); // package exists
1024
- mockedFs.existsSync.mockReturnValueOnce(true); // index.d.ts exists
1025
- // Mock the package index file content
1026
- mockedFs.readFileSync.mockReturnValueOnce(`
1027
- export { default as Button } from './button';
1028
- export { default as Alert } from './alert';
1029
- `);
1030
- // Mock that component directory doesn't exist in node_modules (so it checks local dev)
1031
- mockedFs.existsSync.mockReturnValueOnce(false); // node_modules path
1032
- mockedFs.existsSync.mockReturnValueOnce(true); // local packages path
1033
- mockedFs.existsSync.mockReturnValueOnce(true); // src/components directory
1034
- // Mock reading the components directory
1035
- mockedFs.readdirSync.mockReturnValueOnce([
1036
- { name: 'button', isDirectory: () => true },
1037
- { name: 'alert', isDirectory: () => true },
1038
- ]);
1039
- // Mock that component file exists and read its content
1040
- mockedFs.existsSync.mockReturnValueOnce(true); // index.tsx exists
1041
- mockedFs.readFileSync.mockReturnValueOnce(`
1042
- /**
1043
- * A reusable button component
1044
- */
1045
- export interface ButtonProps {
1046
- variant?: 'primary' | 'secondary';
1047
- size?: 'small' | 'medium' | 'large';
1048
- disabled?: boolean;
1049
- children: React.ReactNode;
1050
- }
1051
-
1052
- export type ButtonVariant = 'primary' | 'secondary';
1053
-
1054
- const Button: React.FC<ButtonProps> = ({ children, ...props }) => {
1055
- return <button {...props}>{children}</button>;
1056
- };
1057
-
1058
- export default Button;
1059
- `);
1060
- const result = getComponentDetails(componentName);
1061
- expect(result).toEqual({
1062
- name: 'Button',
1063
- package: '@metrostar/comet-uswds',
1064
- filePath: '/mock/project/root/packages/comet-uswds/src/components/button',
1065
- description: 'A reusable button component',
1066
- props: ['children', 'disabled', 'size', 'variant'],
1067
- types: ['ButtonProps', 'ButtonVariant'],
1068
- });
1069
- // Restore environment
1070
- process.env.PROJECT_ROOT = originalEnv;
1071
- });
1072
- test('should return null when component is not found', () => {
1073
- // Mock that packages exist but don't contain the component
1074
- mockedFs.existsSync.mockReturnValue(true);
1075
- mockedFs.readFileSync.mockReturnValue(`
1076
- export { default as Alert } from './alert';
1077
- export { default as Card } from './card';
1078
- `);
1079
- const result = getComponentDetails('NonExistentComponent');
1080
- expect(result).toBeNull();
1081
- });
1082
- });
1083
- describe('fetchUrl', () => {
1084
- beforeEach(() => {
1085
- // Mock the global fetch function
1086
- global.fetch = vi.fn();
1087
- });
1088
- afterEach(() => {
1089
- vi.restoreAllMocks();
1090
- });
1091
- test('should successfully fetch content from a URL', async () => {
1092
- const mockContent = '<html><body>Test content</body></html>';
1093
- const mockResponse = {
1094
- ok: true,
1095
- status: 200,
1096
- text: vi.fn().mockResolvedValue(mockContent),
1097
- };
1098
- vi.mocked(global.fetch).mockResolvedValue(mockResponse);
1099
- const result = await fetchUrl('https://example.com');
1100
- expect(global.fetch).toHaveBeenCalledWith('https://example.com');
1101
- expect(mockResponse.text).toHaveBeenCalled();
1102
- expect(result).toBe(mockContent);
1103
- });
1104
- test('should throw error when response is not ok', async () => {
1105
- const mockResponse = {
1106
- ok: false,
1107
- status: 404,
1108
- text: vi.fn(),
1109
- };
1110
- vi.mocked(global.fetch).mockResolvedValue(mockResponse);
1111
- await expect(fetchUrl('https://example.com/not-found')).rejects.toThrow('HTTP error! status: 404');
1112
- expect(global.fetch).toHaveBeenCalledWith('https://example.com/not-found');
1113
- expect(mockResponse.text).not.toHaveBeenCalled();
1114
- });
1115
- test('should throw error when fetch throws an exception', async () => {
1116
- const fetchError = new Error('Network error');
1117
- vi.mocked(global.fetch).mockRejectedValue(fetchError);
1118
- await expect(fetchUrl('https://example.com')).rejects.toThrow('Network error');
1119
- expect(global.fetch).toHaveBeenCalledWith('https://example.com');
1120
- });
1121
- test('should handle different HTTP status codes', async () => {
1122
- const mockResponse = {
1123
- ok: false,
1124
- status: 500,
1125
- text: vi.fn(),
1126
- };
1127
- vi.mocked(global.fetch).mockResolvedValue(mockResponse);
1128
- await expect(fetchUrl('https://example.com/server-error')).rejects.toThrow('HTTP error! status: 500');
1129
- });
1130
- test('should handle empty response content', async () => {
1131
- const mockResponse = {
1132
- ok: true,
1133
- status: 200,
1134
- text: vi.fn().mockResolvedValue(''),
1135
- };
1136
- vi.mocked(global.fetch).mockResolvedValue(mockResponse);
1137
- const result = await fetchUrl('https://example.com/empty');
1138
- expect(result).toBe('');
1139
- });
1140
- test('should handle response.text() rejection', async () => {
1141
- const mockResponse = {
1142
- ok: true,
1143
- status: 200,
1144
- text: vi.fn().mockRejectedValue(new Error('Failed to read response')),
1145
- };
1146
- vi.mocked(global.fetch).mockResolvedValue(mockResponse);
1147
- await expect(fetchUrl('https://example.com')).rejects.toThrow('Failed to read response');
1148
- });
1149
- });
1150
- describe('parseSitemapForUrls', () => {
1151
- test('should extract URLs that match patterns from sitemap XML', () => {
1152
- const sitemapContent = `<?xml version="1.0" encoding="UTF-8"?>
1153
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1154
- <url>
1155
- <loc>https://example.com/components/button</loc>
1156
- <lastmod>2023-01-01</lastmod>
1157
- </url>
1158
- <url>
1159
- <loc>https://example.com/utilities/spacing</loc>
1160
- <lastmod>2023-01-02</lastmod>
1161
- </url>
1162
- <url>
1163
- <loc>https://example.com/about</loc>
1164
- <lastmod>2023-01-03</lastmod>
1165
- </url>
1166
- </urlset>`;
1167
- const patterns = ['components', 'utilities'];
1168
- const result = parseSitemapForUrls(sitemapContent, patterns);
1169
- expect(result).toEqual([
1170
- 'https://example.com/components/button',
1171
- 'https://example.com/utilities/spacing',
1172
- ]);
1173
- });
1174
- test('should return empty array when no URLs match patterns', () => {
1175
- const sitemapContent = `<?xml version="1.0" encoding="UTF-8"?>
1176
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1177
- <url>
1178
- <loc>https://example.com/about</loc>
1179
- </url>
1180
- <url>
1181
- <loc>https://example.com/contact</loc>
1182
- </url>
1183
- </urlset>`;
1184
- const patterns = ['components', 'utilities'];
1185
- const result = parseSitemapForUrls(sitemapContent, patterns);
1186
- expect(result).toEqual([]);
1187
- });
1188
- test('should handle empty sitemap content', () => {
1189
- const patterns = ['components'];
1190
- const result = parseSitemapForUrls('', patterns);
1191
- expect(result).toEqual([]);
1192
- });
1193
- test('should handle malformed XML gracefully', () => {
1194
- const sitemapContent = 'not valid xml content';
1195
- const patterns = ['components'];
1196
- const result = parseSitemapForUrls(sitemapContent, patterns);
1197
- expect(result).toEqual([]);
1198
- });
1199
- test('should handle sitemap without loc tags', () => {
1200
- const sitemapContent = `<?xml version="1.0" encoding="UTF-8"?>
1201
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1202
- <url>
1203
- <lastmod>2023-01-01</lastmod>
1204
- </url>
1205
- </urlset>`;
1206
- const patterns = ['components'];
1207
- const result = parseSitemapForUrls(sitemapContent, patterns);
1208
- expect(result).toEqual([]);
1209
- });
1210
- test('should handle empty patterns array', () => {
1211
- const sitemapContent = `<?xml version="1.0" encoding="UTF-8"?>
1212
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1213
- <url>
1214
- <loc>https://example.com/components/button</loc>
1215
- </url>
1216
- </urlset>`;
1217
- const patterns = [];
1218
- const result = parseSitemapForUrls(sitemapContent, patterns);
1219
- expect(result).toEqual([]);
1220
- });
1221
- test('should perform case-insensitive pattern matching', () => {
1222
- const sitemapContent = `<?xml version="1.0" encoding="UTF-8"?>
1223
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1224
- <url>
1225
- <loc>https://example.com/components/Button</loc>
1226
- </url>
1227
- <url>
1228
- <loc>https://example.com/utilities/spacing</loc>
1229
- </url>
1230
- </urlset>`;
1231
- const patterns = ['COMPONENTS', 'UTILITIES'];
1232
- const result = parseSitemapForUrls(sitemapContent, patterns);
1233
- expect(result).toEqual([
1234
- 'https://example.com/components/Button',
1235
- 'https://example.com/utilities/spacing',
1236
- ]);
1237
- });
1238
- test('should handle multiple pattern matches for same URL', () => {
1239
- const sitemapContent = `<?xml version="1.0" encoding="UTF-8"?>
1240
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1241
- <url>
1242
- <loc>https://example.com/components/utilities/button-utils</loc>
1243
- </url>
1244
- </urlset>`;
1245
- const patterns = ['components', 'utilities'];
1246
- const result = parseSitemapForUrls(sitemapContent, patterns);
1247
- expect(result).toEqual(['https://example.com/components/utilities/button-utils']);
1248
- });
1249
- test('should handle URLs with special characters', () => {
1250
- const sitemapContent = `<?xml version="1.0" encoding="UTF-8"?>
1251
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1252
- <url>
1253
- <loc>https://example.com/components/button-group?version=1.0&amp;type=primary</loc>
1254
- </url>
1255
- </urlset>`;
1256
- const patterns = ['components'];
1257
- const result = parseSitemapForUrls(sitemapContent, patterns);
1258
- expect(result).toEqual([
1259
- 'https://example.com/components/button-group?version=1.0&amp;type=primary',
1260
- ]);
1261
- });
1262
- test('should handle partial pattern matches', () => {
1263
- const sitemapContent = `<?xml version="1.0" encoding="UTF-8"?>
1264
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1265
- <url>
1266
- <loc>https://example.com/component-library</loc>
1267
- </url>
1268
- <url>
1269
- <loc>https://example.com/utility-classes</loc>
1270
- </url>
1271
- </urlset>`;
1272
- const patterns = ['component', 'utility'];
1273
- const result = parseSitemapForUrls(sitemapContent, patterns);
1274
- expect(result).toEqual([
1275
- 'https://example.com/component-library',
1276
- 'https://example.com/utility-classes',
1277
- ]);
1278
- });
1279
- test('should handle large sitemap with many URLs', () => {
1280
- const urls = Array.from({ length: 100 }, (_, i) => {
1281
- const type = i % 3 === 0 ? 'components' : i % 3 === 1 ? 'utilities' : 'other';
1282
- return ` <url><loc>https://example.com/${type}/item-${i}</loc></url>`;
1283
- }).join('\n');
1284
- const sitemapContent = `<?xml version="1.0" encoding="UTF-8"?>
1285
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1286
- ${urls}
1287
- </urlset>`;
1288
- const patterns = ['components', 'utilities'];
1289
- const result = parseSitemapForUrls(sitemapContent, patterns);
1290
- // Should find approximately 66 URLs (components and utilities, not 'other')
1291
- expect(result.length).toBe(67); // 34 components + 33 utilities
1292
- expect(result.every((url) => url.includes('components') || url.includes('utilities'))).toBe(true);
1293
- });
1294
- });
1295
- describe('determineContentType', () => {
1296
- test('should return "utility" for utility URLs', () => {
1297
- const url = 'https://designsystem.digital.gov/utilities/spacing/';
1298
- expect(determineContentType(url)).toBe('utility');
1299
- });
1300
- test('should return "design-token" for design token URLs', () => {
1301
- const url = 'https://designsystem.digital.gov/design-tokens/color/';
1302
- expect(determineContentType(url)).toBe('design-token');
1303
- });
1304
- test('should return "component" for component URLs', () => {
1305
- const url = 'https://designsystem.digital.gov/components/button/';
1306
- expect(determineContentType(url)).toBe('component');
1307
- });
1308
- test('should return "pattern" for pattern URLs', () => {
1309
- const url = 'https://designsystem.digital.gov/patterns/create-a-user-profile/';
1310
- expect(determineContentType(url)).toBe('pattern');
1311
- });
1312
- test('should return "template" for template URLs', () => {
1313
- const url = 'https://designsystem.digital.gov/templates/landing-page/';
1314
- expect(determineContentType(url)).toBe('template');
1315
- });
1316
- test('should return "other" for unrecognized URLs', () => {
1317
- const url = 'https://designsystem.digital.gov/about/';
1318
- expect(determineContentType(url)).toBe('other');
1319
- });
1320
- test('should handle URLs with multiple matching segments', () => {
1321
- const url = 'https://designsystem.digital.gov/components/utilities/button/';
1322
- expect(determineContentType(url)).toBe('utility'); // First match wins
1323
- });
1324
- });
1325
- describe('extractUSWDSContent', () => {
1326
- test('should extract basic content from HTML with main tag', () => {
1327
- const html = `
1328
- <!DOCTYPE html>
1329
- <html>
1330
- <head>
1331
- <title>Button Component - USWDS</title>
1332
- </head>
1333
- <body>
1334
- <nav>Navigation content</nav>
1335
- <main>
1336
- <h1>Button Component</h1>
1337
- <p>The button component allows users to take actions.</p>
1338
- <code>usa-button</code>
1339
- <p>Available classes: usa-button, usa-button--secondary</p>
1340
- </main>
1341
- <footer>Footer content</footer>
1342
- </body>
1343
- </html>
1344
- `;
1345
- const result = extractUSWDSContent(html, 'https://designsystem.digital.gov/components/button/');
1346
- expect(result.url).toBe('https://designsystem.digital.gov/components/button/');
1347
- expect(result.title).toBe('Button Component - USWDS');
1348
- expect(result.content).toContain('Button Component');
1349
- expect(result.content).toContain('The button component allows users to take actions');
1350
- expect(result.content).not.toContain('Navigation content');
1351
- expect(result.content).not.toContain('Footer content');
1352
- expect(result.codeExamples).toEqual(['usa-button']);
1353
- expect(result.type).toBe('component');
1354
- });
1355
- test('should extract content from HTML with article tag', () => {
1356
- const html = `
1357
- <html>
1358
- <head><title>Spacing Utility</title></head>
1359
- <body>
1360
- <article>
1361
- <h1>Spacing Utilities</h1>
1362
- <p>Use spacing utilities to control margin and padding.</p>
1363
- <code>margin-1</code>
1364
- <code>padding-2</code>
1365
- </article>
1366
- </body>
1367
- </html>
1368
- `;
1369
- const result = extractUSWDSContent(html, 'https://designsystem.digital.gov/utilities/spacing/');
1370
- expect(result.title).toBe('Spacing Utility');
1371
- expect(result.content).toContain('Spacing Utilities');
1372
- expect(result.codeExamples).toEqual(['margin-1', 'padding-2']);
1373
- expect(result.type).toBe('utility');
1374
- });
1375
- test('should extract content from HTML with content class div', () => {
1376
- const html = `
1377
- <html>
1378
- <head><title>Design Tokens</title></head>
1379
- <body>
1380
- <div class="main-content">
1381
- <h1>Color Tokens</h1>
1382
- <p>Color tokens provide consistent color values.</p>
1383
- <code>color-primary</code>
1384
- </div>
1385
- </body>
1386
- </html>
1387
- `;
1388
- const result = extractUSWDSContent(html, 'https://designsystem.digital.gov/design-tokens/color/');
1389
- expect(result.title).toBe('Design Tokens');
1390
- expect(result.content).toContain('Color Tokens');
1391
- expect(result.codeExamples).toEqual(['color-primary']);
1392
- expect(result.type).toBe('design-token');
1393
- });
1394
- test('should fall back to body content when no main containers found', () => {
1395
- const html = `
1396
- <html>
1397
- <head><title>Simple Page</title></head>
1398
- <body>
1399
- <h1>Simple Content</h1>
1400
- <p>This is body content without main containers.</p>
1401
- <code>simple-class</code>
1402
- </body>
1403
- </html>
1404
- `;
1405
- const result = extractUSWDSContent(html, 'https://example.com/other/');
1406
- expect(result.title).toBe('Simple Page');
1407
- expect(result.content).toContain('Simple Content');
1408
- expect(result.codeExamples).toEqual(['simple-class']);
1409
- expect(result.type).toBe('other');
1410
- });
1411
- test('should remove script and style tags', () => {
1412
- const html = `
1413
- <html>
1414
- <head>
1415
- <title>Test Page</title>
1416
- <style>body { margin: 0; }</style>
1417
- </head>
1418
- <body>
1419
- <script>console.log('test');</script>
1420
- <main>
1421
- <h1>Clean Content</h1>
1422
- <p>This content should remain.</p>
1423
- </main>
1424
- <script src="analytics.js"></script>
1425
- </body>
1426
- </html>
1427
- `;
1428
- const result = extractUSWDSContent(html, 'https://example.com/');
1429
- expect(result.content).toContain('Clean Content');
1430
- expect(result.content).not.toContain('console.log');
1431
- expect(result.content).not.toContain('margin: 0');
1432
- expect(result.content).not.toContain('analytics.js');
1433
- });
1434
- test('should handle empty title', () => {
1435
- const html = `
1436
- <html>
1437
- <head></head>
1438
- <body>
1439
- <main>Content without title</main>
1440
- </body>
1441
- </html>
1442
- `;
1443
- const result = extractUSWDSContent(html, 'https://example.com/');
1444
- expect(result.title).toBe('');
1445
- expect(result.content).toContain('Content without title');
1446
- });
1447
- test('should normalize whitespace in title and content', () => {
1448
- const html = `
1449
- <html>
1450
- <head>
1451
- <title> Multiple Spaces Title </title>
1452
- </head>
1453
- <body>
1454
- <main>
1455
- <h1> Header with spaces </h1>
1456
- <p> Text with multiple spaces </p>
1457
- </main>
1458
- </body>
1459
- </html>
1460
- `;
1461
- const result = extractUSWDSContent(html, 'https://example.com/');
1462
- expect(result.title).toBe('Multiple Spaces Title');
1463
- expect(result.content).toMatch(/Header with spaces/);
1464
- expect(result.content).toMatch(/Text with multiple spaces/);
1465
- expect(result.content).not.toMatch(/ {3}/); // No multiple spaces
1466
- });
1467
- test('should extract utilities from content', () => {
1468
- const html = `
1469
- <html>
1470
- <body>
1471
- <main>
1472
- <p>Use margin-top and padding-left for spacing.</p>
1473
- <p>Available classes: .usa-button, text-center, bg-primary.</p>
1474
- <p>Responsive: tablet:margin, desktop:padding</p>
1475
- </main>
1476
- </body>
1477
- </html>
1478
- `;
1479
- const result = extractUSWDSContent(html, 'https://example.com/');
1480
- // Remove console.log and test what actually works
1481
- expect(result.utilities).toContain('usa-button');
1482
- expect(result.utilities).toContain('text-center');
1483
- expect(result.utilities).toContain('bg-primary');
1484
- expect(result.utilities).toContain('margin-top');
1485
- expect(result.utilities).toContain('padding-left');
1486
- });
1487
- test('should limit content length to 2000 characters', () => {
1488
- const longContent = 'a'.repeat(3000);
1489
- const html = `
1490
- <html>
1491
- <body>
1492
- <main>${longContent}</main>
1493
- </body>
1494
- </html>
1495
- `;
1496
- const result = extractUSWDSContent(html, 'https://example.com/');
1497
- expect(result.content.length).toBe(2000);
1498
- expect(result.content).toBe('a'.repeat(2000));
1499
- });
1500
- test('should handle code tags with attributes', () => {
1501
- const html = `
1502
- <html>
1503
- <body>
1504
- <main>
1505
- <code class="language-html">usa-button</code>
1506
- <code data-lang="css">margin-top-1</code>
1507
- <code>simple-code</code>
1508
- </main>
1509
- </body>
1510
- </html>
1511
- `;
1512
- const result = extractUSWDSContent(html, 'https://example.com/');
1513
- expect(result.codeExamples).toEqual(['usa-button', 'margin-top-1', 'simple-code']);
1514
- });
1515
- test('should handle empty HTML', () => {
1516
- const html = '';
1517
- const result = extractUSWDSContent(html, 'https://example.com/');
1518
- expect(result.url).toBe('https://example.com/');
1519
- expect(result.title).toBe('');
1520
- expect(result.content).toBe('');
1521
- expect(result.codeExamples).toEqual([]);
1522
- expect(result.utilities).toEqual([]);
1523
- expect(result.type).toBe('other');
1524
- });
1525
- test('should remove nav, header, and footer content', () => {
1526
- const html = `
1527
- <html>
1528
- <body>
1529
- <header>Header navigation</header>
1530
- <nav>Main navigation</nav>
1531
- <main>
1532
- <h1>Main Content</h1>
1533
- <p>This is the important content.</p>
1534
- </main>
1535
- <footer>Footer links</footer>
1536
- </body>
1537
- </html>
1538
- `;
1539
- const result = extractUSWDSContent(html, 'https://example.com/');
1540
- expect(result.content).toContain('Main Content');
1541
- expect(result.content).toContain('important content');
1542
- expect(result.content).not.toContain('Header navigation');
1543
- expect(result.content).not.toContain('Main navigation');
1544
- expect(result.content).not.toContain('Footer links');
1545
- });
1546
- test('should handle malformed HTML gracefully', () => {
1547
- const html = `
1548
- <html>
1549
- <head><title>Test</title>
1550
- <body>
1551
- <main>
1552
- <p>Content with unclosed tags
1553
- <div>More content
1554
- </main>
1555
- </html>
1556
- `;
1557
- const result = extractUSWDSContent(html, 'https://example.com/');
1558
- expect(result.title).toBe('Test');
1559
- expect(result.content).toContain('Content with unclosed tags');
1560
- expect(result.content).toContain('More content');
1561
- });
1562
- });
1563
- });
1564
- //# sourceMappingURL=utils.test.js.map