@fragments-sdk/cli 0.2.2
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/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +4783 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4FDQSGKX.js +786 -0
- package/dist/chunk-4FDQSGKX.js.map +1 -0
- package/dist/chunk-7H2MMGYG.js +369 -0
- package/dist/chunk-7H2MMGYG.js.map +1 -0
- package/dist/chunk-BSCG3IP7.js +619 -0
- package/dist/chunk-BSCG3IP7.js.map +1 -0
- package/dist/chunk-LY2CFFPY.js +898 -0
- package/dist/chunk-LY2CFFPY.js.map +1 -0
- package/dist/chunk-MUZ6CM66.js +6636 -0
- package/dist/chunk-MUZ6CM66.js.map +1 -0
- package/dist/chunk-OAENNG3G.js +1489 -0
- package/dist/chunk-OAENNG3G.js.map +1 -0
- package/dist/chunk-XHNKNI6J.js +235 -0
- package/dist/chunk-XHNKNI6J.js.map +1 -0
- package/dist/core-DWKLGY4N.js +68 -0
- package/dist/core-DWKLGY4N.js.map +1 -0
- package/dist/generate-4LQNJ7SX.js +249 -0
- package/dist/generate-4LQNJ7SX.js.map +1 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-EMVI47QG.js +416 -0
- package/dist/init-EMVI47QG.js.map +1 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +1117 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/scan-4YPRF7FV.js +12 -0
- package/dist/scan-4YPRF7FV.js.map +1 -0
- package/dist/service-QSZMZJBJ.js +208 -0
- package/dist/service-QSZMZJBJ.js.map +1 -0
- package/dist/static-viewer-MIPGZ4Z7.js +12 -0
- package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
- package/dist/test-SQ5ZHXWU.js +1067 -0
- package/dist/test-SQ5ZHXWU.js.map +1 -0
- package/dist/tokens-HSGMYK64.js +173 -0
- package/dist/tokens-HSGMYK64.js.map +1 -0
- package/dist/viewer-YRF4SQE4.js +11101 -0
- package/dist/viewer-YRF4SQE4.js.map +1 -0
- package/package.json +107 -0
- package/src/ai.ts +266 -0
- package/src/analyze.ts +265 -0
- package/src/bin.ts +916 -0
- package/src/build.ts +248 -0
- package/src/commands/a11y.ts +302 -0
- package/src/commands/add.ts +313 -0
- package/src/commands/audit.ts +195 -0
- package/src/commands/baseline.ts +221 -0
- package/src/commands/build.ts +144 -0
- package/src/commands/compare.ts +337 -0
- package/src/commands/context.ts +107 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/enhance.ts +858 -0
- package/src/commands/generate.ts +391 -0
- package/src/commands/init.ts +531 -0
- package/src/commands/link/figma.ts +645 -0
- package/src/commands/link/index.ts +10 -0
- package/src/commands/link/storybook.ts +267 -0
- package/src/commands/list.ts +49 -0
- package/src/commands/metrics.ts +114 -0
- package/src/commands/reset.ts +242 -0
- package/src/commands/scan.ts +537 -0
- package/src/commands/storygen.ts +207 -0
- package/src/commands/tokens.ts +251 -0
- package/src/commands/validate.ts +93 -0
- package/src/commands/verify.ts +215 -0
- package/src/core/composition.test.ts +262 -0
- package/src/core/composition.ts +255 -0
- package/src/core/config.ts +84 -0
- package/src/core/constants.ts +111 -0
- package/src/core/context.ts +380 -0
- package/src/core/defineSegment.ts +137 -0
- package/src/core/discovery.ts +337 -0
- package/src/core/figma.ts +263 -0
- package/src/core/fragment-types.ts +214 -0
- package/src/core/generators/context.ts +389 -0
- package/src/core/generators/index.ts +23 -0
- package/src/core/generators/registry.ts +364 -0
- package/src/core/generators/typescript-extractor.ts +374 -0
- package/src/core/importAnalyzer.ts +217 -0
- package/src/core/index.ts +149 -0
- package/src/core/loader.ts +155 -0
- package/src/core/node.ts +63 -0
- package/src/core/parser.ts +551 -0
- package/src/core/previewLoader.ts +172 -0
- package/src/core/schema/fragment.schema.json +189 -0
- package/src/core/schema/registry.schema.json +137 -0
- package/src/core/schema.ts +182 -0
- package/src/core/storyAdapter.test.ts +571 -0
- package/src/core/storyAdapter.ts +761 -0
- package/src/core/token-types.ts +287 -0
- package/src/core/types.ts +754 -0
- package/src/diff.ts +323 -0
- package/src/index.ts +43 -0
- package/src/mcp/__tests__/projectFields.test.ts +130 -0
- package/src/mcp/bin.ts +36 -0
- package/src/mcp/index.ts +8 -0
- package/src/mcp/server.ts +1310 -0
- package/src/mcp/utils.ts +54 -0
- package/src/mcp-bin.ts +36 -0
- package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
- package/src/migrate/__tests__/args/args.test.ts +452 -0
- package/src/migrate/__tests__/meta/meta.test.ts +198 -0
- package/src/migrate/__tests__/stories/stories.test.ts +278 -0
- package/src/migrate/__tests__/utils/utils.test.ts +371 -0
- package/src/migrate/__tests__/values/values.test.ts +303 -0
- package/src/migrate/bin.ts +108 -0
- package/src/migrate/converter.ts +658 -0
- package/src/migrate/detect.ts +196 -0
- package/src/migrate/index.ts +45 -0
- package/src/migrate/migrate.ts +163 -0
- package/src/migrate/parser.ts +1136 -0
- package/src/migrate/report.ts +624 -0
- package/src/migrate/types.ts +169 -0
- package/src/screenshot.ts +249 -0
- package/src/service/__tests__/ast-utils.test.ts +426 -0
- package/src/service/__tests__/enhance-scanner.test.ts +200 -0
- package/src/service/__tests__/figma/figma.test.ts +652 -0
- package/src/service/__tests__/metrics-store.test.ts +409 -0
- package/src/service/__tests__/patch-generator.test.ts +186 -0
- package/src/service/__tests__/props-extractor.test.ts +365 -0
- package/src/service/__tests__/token-registry.test.ts +267 -0
- package/src/service/analytics.ts +659 -0
- package/src/service/ast-utils.ts +444 -0
- package/src/service/browser-pool.ts +339 -0
- package/src/service/capture.ts +267 -0
- package/src/service/diff.ts +279 -0
- package/src/service/enhance/aggregator.ts +489 -0
- package/src/service/enhance/cache.ts +275 -0
- package/src/service/enhance/codebase-scanner.ts +357 -0
- package/src/service/enhance/context-generator.ts +529 -0
- package/src/service/enhance/doc-extractor.ts +523 -0
- package/src/service/enhance/index.ts +131 -0
- package/src/service/enhance/props-extractor.ts +665 -0
- package/src/service/enhance/scanner.ts +445 -0
- package/src/service/enhance/storybook-parser.ts +552 -0
- package/src/service/enhance/types.ts +346 -0
- package/src/service/enhance/variant-renderer.ts +479 -0
- package/src/service/figma.ts +1008 -0
- package/src/service/index.ts +249 -0
- package/src/service/metrics-store.ts +333 -0
- package/src/service/patch-generator.ts +349 -0
- package/src/service/report.ts +854 -0
- package/src/service/storage.ts +401 -0
- package/src/service/token-fixes.ts +281 -0
- package/src/service/token-parser.ts +504 -0
- package/src/service/token-registry.ts +721 -0
- package/src/service/utils.ts +172 -0
- package/src/setup.ts +241 -0
- package/src/shared/command-wrapper.ts +81 -0
- package/src/shared/dev-server-client.ts +199 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/segment-loader.ts +59 -0
- package/src/shared/types.ts +147 -0
- package/src/static-viewer.ts +715 -0
- package/src/test/discovery.ts +172 -0
- package/src/test/index.ts +281 -0
- package/src/test/reporters/console.ts +194 -0
- package/src/test/reporters/json.ts +190 -0
- package/src/test/reporters/junit.ts +186 -0
- package/src/test/runner.ts +598 -0
- package/src/test/types.ts +245 -0
- package/src/test/watch.ts +200 -0
- package/src/validators.ts +152 -0
- package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
- package/src/viewer/__tests__/render-utils.test.ts +232 -0
- package/src/viewer/__tests__/style-utils.test.ts +404 -0
- package/src/viewer/bin.ts +86 -0
- package/src/viewer/cli/health.ts +256 -0
- package/src/viewer/cli/index.ts +33 -0
- package/src/viewer/cli/scan.ts +124 -0
- package/src/viewer/cli/utils.ts +174 -0
- package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
- package/src/viewer/components/ActionCapture.tsx +172 -0
- package/src/viewer/components/ActionsPanel.tsx +371 -0
- package/src/viewer/components/App.tsx +638 -0
- package/src/viewer/components/BottomPanel.tsx +224 -0
- package/src/viewer/components/CodePanel.tsx +589 -0
- package/src/viewer/components/CommandPalette.tsx +336 -0
- package/src/viewer/components/ComponentGraph.tsx +394 -0
- package/src/viewer/components/ComponentHeader.tsx +85 -0
- package/src/viewer/components/ContractPanel.tsx +234 -0
- package/src/viewer/components/ErrorBoundary.tsx +85 -0
- package/src/viewer/components/FigmaEmbed.tsx +231 -0
- package/src/viewer/components/FragmentEditor.tsx +485 -0
- package/src/viewer/components/HealthDashboard.tsx +452 -0
- package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
- package/src/viewer/components/Icons.tsx +417 -0
- package/src/viewer/components/InteractionsPanel.tsx +720 -0
- package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
- package/src/viewer/components/IsolatedRender.tsx +111 -0
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
- package/src/viewer/components/LandingPage.tsx +441 -0
- package/src/viewer/components/Layout.tsx +22 -0
- package/src/viewer/components/LeftSidebar.tsx +391 -0
- package/src/viewer/components/MultiViewportPreview.tsx +429 -0
- package/src/viewer/components/PreviewArea.tsx +404 -0
- package/src/viewer/components/PreviewFrameHost.tsx +310 -0
- package/src/viewer/components/PreviewPane.tsx +150 -0
- package/src/viewer/components/PreviewToolbar.tsx +176 -0
- package/src/viewer/components/PropsEditor.tsx +512 -0
- package/src/viewer/components/PropsTable.tsx +98 -0
- package/src/viewer/components/RelationsSection.tsx +57 -0
- package/src/viewer/components/ResizablePanel.tsx +328 -0
- package/src/viewer/components/RightSidebar.tsx +118 -0
- package/src/viewer/components/ScreenshotButton.tsx +90 -0
- package/src/viewer/components/Sidebar.tsx +169 -0
- package/src/viewer/components/SkeletonLoader.tsx +156 -0
- package/src/viewer/components/StoryRenderer.tsx +128 -0
- package/src/viewer/components/ThemeProvider.tsx +96 -0
- package/src/viewer/components/Toast.tsx +67 -0
- package/src/viewer/components/TokenStylePanel.tsx +708 -0
- package/src/viewer/components/UsageSection.tsx +95 -0
- package/src/viewer/components/VariantMatrix.tsx +350 -0
- package/src/viewer/components/VariantRenderer.tsx +131 -0
- package/src/viewer/components/VariantTabs.tsx +84 -0
- package/src/viewer/components/ViewportSelector.tsx +165 -0
- package/src/viewer/components/_future/CreatePage.tsx +836 -0
- package/src/viewer/composition-renderer.ts +381 -0
- package/src/viewer/constants/index.ts +1 -0
- package/src/viewer/constants/ui.ts +185 -0
- package/src/viewer/entry.tsx +299 -0
- package/src/viewer/hooks/index.ts +2 -0
- package/src/viewer/hooks/useA11yCache.ts +383 -0
- package/src/viewer/hooks/useA11yService.ts +498 -0
- package/src/viewer/hooks/useActions.ts +138 -0
- package/src/viewer/hooks/useAppState.ts +124 -0
- package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
- package/src/viewer/hooks/useHmrStatus.ts +109 -0
- package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
- package/src/viewer/hooks/usePreviewBridge.ts +347 -0
- package/src/viewer/hooks/useScrollSpy.ts +78 -0
- package/src/viewer/hooks/useUrlState.ts +330 -0
- package/src/viewer/hooks/useViewSettings.ts +125 -0
- package/src/viewer/index.html +28 -0
- package/src/viewer/index.ts +14 -0
- package/src/viewer/intelligence/healthReport.ts +505 -0
- package/src/viewer/intelligence/styleDrift.ts +340 -0
- package/src/viewer/intelligence/usageScanner.ts +309 -0
- package/src/viewer/jsx-parser.ts +485 -0
- package/src/viewer/postcss.config.js +6 -0
- package/src/viewer/preview-frame-entry.tsx +25 -0
- package/src/viewer/preview-frame.html +109 -0
- package/src/viewer/render-template.html +68 -0
- package/src/viewer/render-utils.ts +170 -0
- package/src/viewer/server.ts +276 -0
- package/src/viewer/style-utils.ts +414 -0
- package/src/viewer/styles/globals.css +355 -0
- package/src/viewer/tailwind.config.js +37 -0
- package/src/viewer/types/a11y.ts +197 -0
- package/src/viewer/utils/a11y-fixes.ts +471 -0
- package/src/viewer/utils/actionExport.ts +372 -0
- package/src/viewer/utils/colorSchemes.ts +201 -0
- package/src/viewer/utils/detectRelationships.ts +256 -0
- package/src/viewer/vite-plugin.ts +2143 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test discovery - finds variants with play functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import type { SegmentsConfig } from '../core/index.js';
|
|
7
|
+
import { discoverSegmentFiles, parseSegmentFile } from '../core/node.js';
|
|
8
|
+
import type { TestCase, DiscoveryOptions } from './types.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Discovered segment with play function metadata
|
|
12
|
+
*/
|
|
13
|
+
interface DiscoveredVariant {
|
|
14
|
+
component: string;
|
|
15
|
+
variant: string;
|
|
16
|
+
tags: string[];
|
|
17
|
+
hasPlayFunction: boolean;
|
|
18
|
+
storyId?: string;
|
|
19
|
+
sourceFile: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Discover all variants with play functions
|
|
24
|
+
*/
|
|
25
|
+
export async function discoverTests(
|
|
26
|
+
config: SegmentsConfig,
|
|
27
|
+
configDir: string,
|
|
28
|
+
options: DiscoveryOptions = {}
|
|
29
|
+
): Promise<TestCase[]> {
|
|
30
|
+
const files = await discoverSegmentFiles(config, configDir);
|
|
31
|
+
const variants: DiscoveredVariant[] = [];
|
|
32
|
+
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
try {
|
|
35
|
+
const content = await readFile(file.absolutePath, 'utf-8');
|
|
36
|
+
const parsed = parseSegmentFile(content, file.relativePath);
|
|
37
|
+
|
|
38
|
+
if (!parsed.meta.name) continue;
|
|
39
|
+
|
|
40
|
+
// Filter by component name if specified
|
|
41
|
+
if (options.component) {
|
|
42
|
+
const componentMatch = parsed.meta.name.toLowerCase().includes(options.component.toLowerCase());
|
|
43
|
+
if (!componentMatch) continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Extract tags from meta for the component
|
|
47
|
+
const componentTags = parsed.meta.tags || [];
|
|
48
|
+
|
|
49
|
+
for (const variant of parsed.variants) {
|
|
50
|
+
// Check if variant has play function by scanning source code
|
|
51
|
+
const hasPlay = hasPlayFunctionInSource(content, variant.name);
|
|
52
|
+
|
|
53
|
+
if (!hasPlay) continue;
|
|
54
|
+
|
|
55
|
+
// Filter by tags if specified (use component-level tags since variant tags aren't in parsed data)
|
|
56
|
+
if (options.tags && options.tags.length > 0) {
|
|
57
|
+
const hasMatchingTag = options.tags.some((tag: string) =>
|
|
58
|
+
componentTags.some((ct: string) => ct.toLowerCase().includes(tag.toLowerCase()))
|
|
59
|
+
);
|
|
60
|
+
if (!hasMatchingTag) continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Filter by grep pattern if specified
|
|
64
|
+
if (options.grep) {
|
|
65
|
+
const pattern = new RegExp(options.grep, 'i');
|
|
66
|
+
const matchesName = pattern.test(variant.name);
|
|
67
|
+
const matchesComponent = pattern.test(parsed.meta.name);
|
|
68
|
+
if (!matchesName && !matchesComponent) continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Exclude pattern
|
|
72
|
+
if (options.exclude) {
|
|
73
|
+
const pattern = new RegExp(options.exclude, 'i');
|
|
74
|
+
const excludeName = pattern.test(variant.name);
|
|
75
|
+
const excludeComponent = pattern.test(parsed.meta.name);
|
|
76
|
+
if (excludeName || excludeComponent) continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
variants.push({
|
|
80
|
+
component: parsed.meta.name,
|
|
81
|
+
variant: variant.name,
|
|
82
|
+
tags: componentTags,
|
|
83
|
+
hasPlayFunction: true,
|
|
84
|
+
storyId: undefined, // Not available from static parsing
|
|
85
|
+
sourceFile: file.relativePath,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// Skip files that can't be parsed
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Convert to test cases
|
|
95
|
+
return variants.map((v) => ({
|
|
96
|
+
id: `${v.component}--${v.variant}`.replace(/\s+/g, '-').toLowerCase(),
|
|
97
|
+
component: v.component,
|
|
98
|
+
variant: v.variant,
|
|
99
|
+
tags: v.tags,
|
|
100
|
+
play: null as unknown as TestCase['play'], // Play function loaded at runtime in browser
|
|
101
|
+
sourceFile: v.sourceFile,
|
|
102
|
+
storyId: v.storyId,
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if a variant has a play function by scanning the source code
|
|
108
|
+
*/
|
|
109
|
+
function hasPlayFunctionInSource(content: string, variantName: string): boolean {
|
|
110
|
+
// Look for play function patterns in the source
|
|
111
|
+
// Pattern 1: play: async ({ ... }) => { ... }
|
|
112
|
+
// Pattern 2: play: async function ({ ... }) { ... }
|
|
113
|
+
// Pattern 3: play: playFunction (reference)
|
|
114
|
+
|
|
115
|
+
// Normalize variant name for matching (handle Default, Primary, etc.)
|
|
116
|
+
const patterns = [
|
|
117
|
+
// Export const VariantName = { ... play: ... }
|
|
118
|
+
new RegExp(`export\\s+const\\s+${escapeRegExp(variantName)}\\s*=\\s*\\{[^}]*\\bplay\\s*:`, 's'),
|
|
119
|
+
// variants: [{ name: 'VariantName', ... play: ... }]
|
|
120
|
+
new RegExp(`name\\s*:\\s*['"\`]${escapeRegExp(variantName)}['"\`][^}]*\\bplay\\s*:`, 's'),
|
|
121
|
+
// In case the variant has play in its definition object
|
|
122
|
+
new RegExp(`['"\`]${escapeRegExp(variantName)}['"\`]\\s*:\\s*\\{[^}]*\\bplay\\s*:`, 's'),
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
return patterns.some(pattern => pattern.test(content));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Escape special regex characters
|
|
130
|
+
*/
|
|
131
|
+
function escapeRegExp(string: string): string {
|
|
132
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Group test cases by component
|
|
137
|
+
*/
|
|
138
|
+
export function groupTestsByComponent(testCases: TestCase[]): Map<string, TestCase[]> {
|
|
139
|
+
const groups = new Map<string, TestCase[]>();
|
|
140
|
+
|
|
141
|
+
for (const test of testCases) {
|
|
142
|
+
const existing = groups.get(test.component) || [];
|
|
143
|
+
existing.push(test);
|
|
144
|
+
groups.set(test.component, existing);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return groups;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get unique tags from all test cases
|
|
152
|
+
*/
|
|
153
|
+
export function getUniqueTags(testCases: TestCase[]): string[] {
|
|
154
|
+
const tags = new Set<string>();
|
|
155
|
+
for (const test of testCases) {
|
|
156
|
+
for (const tag of test.tags) {
|
|
157
|
+
tags.add(tag);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return Array.from(tags).sort();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get unique components from all test cases
|
|
165
|
+
*/
|
|
166
|
+
export function getUniqueComponents(testCases: TestCase[]): string[] {
|
|
167
|
+
const components = new Set<string>();
|
|
168
|
+
for (const test of testCases) {
|
|
169
|
+
components.add(test.component);
|
|
170
|
+
}
|
|
171
|
+
return Array.from(components).sort();
|
|
172
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main test command - orchestrates test discovery, execution, and reporting
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve, join } from 'node:path';
|
|
6
|
+
import { mkdir } from 'node:fs/promises';
|
|
7
|
+
import pc from 'picocolors';
|
|
8
|
+
import type { SegmentsConfig } from '../core/index.js';
|
|
9
|
+
import type {
|
|
10
|
+
TestConfig,
|
|
11
|
+
DiscoveryOptions,
|
|
12
|
+
RunnerOptions,
|
|
13
|
+
TestReporter,
|
|
14
|
+
TestRunResult,
|
|
15
|
+
} from './types.js';
|
|
16
|
+
import { discoverTests, getUniqueComponents, getUniqueTags } from './discovery.js';
|
|
17
|
+
import { runTests } from './runner.js';
|
|
18
|
+
import { createConsoleReporter } from './reporters/console.js';
|
|
19
|
+
import { createJUnitReporter } from './reporters/junit.js';
|
|
20
|
+
import { createJsonReporter } from './reporters/json.js';
|
|
21
|
+
import { startWatchMode } from './watch.js';
|
|
22
|
+
|
|
23
|
+
export interface TestCommandOptions {
|
|
24
|
+
// Discovery options
|
|
25
|
+
component?: string;
|
|
26
|
+
tags?: string;
|
|
27
|
+
grep?: string;
|
|
28
|
+
exclude?: string;
|
|
29
|
+
|
|
30
|
+
// Execution options
|
|
31
|
+
parallel?: number;
|
|
32
|
+
timeout?: number;
|
|
33
|
+
retries?: number;
|
|
34
|
+
bail?: boolean;
|
|
35
|
+
browser?: 'chromium' | 'firefox' | 'webkit';
|
|
36
|
+
headless?: boolean;
|
|
37
|
+
|
|
38
|
+
// Feature flags
|
|
39
|
+
a11y?: boolean;
|
|
40
|
+
visual?: boolean;
|
|
41
|
+
updateSnapshots?: boolean;
|
|
42
|
+
watch?: boolean;
|
|
43
|
+
|
|
44
|
+
// Output options
|
|
45
|
+
reporters?: string;
|
|
46
|
+
output?: string;
|
|
47
|
+
|
|
48
|
+
// Server options
|
|
49
|
+
serverUrl?: string;
|
|
50
|
+
port?: number;
|
|
51
|
+
|
|
52
|
+
// CI mode
|
|
53
|
+
ci?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Default options
|
|
58
|
+
*/
|
|
59
|
+
const DEFAULT_OPTIONS = {
|
|
60
|
+
parallel: 4,
|
|
61
|
+
timeout: 30000,
|
|
62
|
+
retries: 0,
|
|
63
|
+
bail: false,
|
|
64
|
+
browser: 'chromium' as const,
|
|
65
|
+
headless: true,
|
|
66
|
+
a11y: false,
|
|
67
|
+
visual: false,
|
|
68
|
+
updateSnapshots: false,
|
|
69
|
+
watch: false,
|
|
70
|
+
reporters: 'console',
|
|
71
|
+
output: './test-results',
|
|
72
|
+
port: 6006,
|
|
73
|
+
ci: false,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Run the test command
|
|
78
|
+
*/
|
|
79
|
+
export async function runTestCommand(
|
|
80
|
+
config: SegmentsConfig,
|
|
81
|
+
configDir: string,
|
|
82
|
+
options: TestCommandOptions
|
|
83
|
+
): Promise<number> {
|
|
84
|
+
// Merge with defaults
|
|
85
|
+
const opts = {
|
|
86
|
+
...DEFAULT_OPTIONS,
|
|
87
|
+
...options,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Parse discovery options
|
|
91
|
+
const discoveryOptions: DiscoveryOptions = {
|
|
92
|
+
component: opts.component,
|
|
93
|
+
tags: opts.tags ? opts.tags.split(',').map((t) => t.trim()) : undefined,
|
|
94
|
+
grep: opts.grep,
|
|
95
|
+
exclude: opts.exclude,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Parse runner options
|
|
99
|
+
const runnerOptions: RunnerOptions = {
|
|
100
|
+
parallel: opts.parallel,
|
|
101
|
+
timeout: opts.timeout,
|
|
102
|
+
retries: opts.retries,
|
|
103
|
+
bail: opts.bail,
|
|
104
|
+
a11y: opts.a11y,
|
|
105
|
+
visual: opts.visual,
|
|
106
|
+
updateSnapshots: opts.updateSnapshots,
|
|
107
|
+
outputDir: resolve(configDir, opts.output),
|
|
108
|
+
browser: opts.browser,
|
|
109
|
+
headless: opts.headless,
|
|
110
|
+
serverUrl: opts.serverUrl,
|
|
111
|
+
port: opts.port,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// In CI mode, adjust settings
|
|
115
|
+
if (opts.ci) {
|
|
116
|
+
runnerOptions.headless = true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Discover tests
|
|
120
|
+
console.log(pc.cyan('\nDiscovering tests...'));
|
|
121
|
+
const testCases = await discoverTests(config, configDir, discoveryOptions);
|
|
122
|
+
|
|
123
|
+
if (testCases.length === 0) {
|
|
124
|
+
console.log(pc.yellow('No tests with play functions found'));
|
|
125
|
+
|
|
126
|
+
// Show helpful info about what was searched
|
|
127
|
+
const components = getUniqueComponents(testCases);
|
|
128
|
+
const tags = getUniqueTags(testCases);
|
|
129
|
+
|
|
130
|
+
if (discoveryOptions.component) {
|
|
131
|
+
console.log(pc.dim(` Filtered by component: ${discoveryOptions.component}`));
|
|
132
|
+
}
|
|
133
|
+
if (discoveryOptions.tags?.length) {
|
|
134
|
+
console.log(pc.dim(` Filtered by tags: ${discoveryOptions.tags.join(', ')}`));
|
|
135
|
+
}
|
|
136
|
+
if (discoveryOptions.grep) {
|
|
137
|
+
console.log(pc.dim(` Filtered by pattern: ${discoveryOptions.grep}`));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log();
|
|
141
|
+
console.log(pc.dim('Add play functions to your segment variants to enable testing.'));
|
|
142
|
+
console.log();
|
|
143
|
+
|
|
144
|
+
return opts.ci ? 0 : 0; // Not a failure if no tests
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(pc.dim(`Found ${testCases.length} test(s)`));
|
|
148
|
+
|
|
149
|
+
// Create output directory
|
|
150
|
+
await mkdir(runnerOptions.outputDir, { recursive: true });
|
|
151
|
+
|
|
152
|
+
// Create reporters
|
|
153
|
+
const reporters = createReporters(opts.reporters, runnerOptions.outputDir, opts.ci);
|
|
154
|
+
|
|
155
|
+
// Watch mode
|
|
156
|
+
if (opts.watch && !opts.ci) {
|
|
157
|
+
await startWatchMode(config, configDir, runnerOptions, reporters);
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Run tests
|
|
162
|
+
let result: TestRunResult;
|
|
163
|
+
try {
|
|
164
|
+
result = await runTests(testCases, runnerOptions, reporters);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error(pc.red('Error running tests:'), error);
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Return exit code based on results
|
|
171
|
+
if (opts.ci && result.totalFailed > 0) {
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result.totalFailed > 0 ? 1 : 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Create reporters based on configuration
|
|
180
|
+
*/
|
|
181
|
+
function createReporters(
|
|
182
|
+
reporterNames: string,
|
|
183
|
+
outputDir: string,
|
|
184
|
+
ci: boolean
|
|
185
|
+
): TestReporter[] {
|
|
186
|
+
const reporters: TestReporter[] = [];
|
|
187
|
+
const names = reporterNames.split(',').map((n) => n.trim().toLowerCase());
|
|
188
|
+
|
|
189
|
+
for (const name of names) {
|
|
190
|
+
switch (name) {
|
|
191
|
+
case 'console':
|
|
192
|
+
reporters.push(
|
|
193
|
+
createConsoleReporter({
|
|
194
|
+
verbose: !ci,
|
|
195
|
+
showTiming: true,
|
|
196
|
+
})
|
|
197
|
+
);
|
|
198
|
+
break;
|
|
199
|
+
|
|
200
|
+
case 'junit':
|
|
201
|
+
reporters.push(
|
|
202
|
+
createJUnitReporter({
|
|
203
|
+
outputPath: join(outputDir, 'junit.xml'),
|
|
204
|
+
suiteName: 'Segments Tests',
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
break;
|
|
208
|
+
|
|
209
|
+
case 'json':
|
|
210
|
+
reporters.push(
|
|
211
|
+
createJsonReporter({
|
|
212
|
+
outputPath: join(outputDir, 'results.json'),
|
|
213
|
+
pretty: true,
|
|
214
|
+
includeSteps: true,
|
|
215
|
+
includeStacks: !ci,
|
|
216
|
+
})
|
|
217
|
+
);
|
|
218
|
+
break;
|
|
219
|
+
|
|
220
|
+
default:
|
|
221
|
+
console.warn(pc.yellow(`Unknown reporter: ${name}`));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Always include console reporter if not explicitly specified
|
|
226
|
+
if (!names.includes('console') && reporters.length === 0) {
|
|
227
|
+
reporters.push(createConsoleReporter({ verbose: !ci }));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return reporters;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* List available tests without running them
|
|
235
|
+
*/
|
|
236
|
+
export async function listTests(
|
|
237
|
+
config: SegmentsConfig,
|
|
238
|
+
configDir: string,
|
|
239
|
+
options: Pick<TestCommandOptions, 'component' | 'tags' | 'grep' | 'exclude'>
|
|
240
|
+
): Promise<void> {
|
|
241
|
+
const discoveryOptions: DiscoveryOptions = {
|
|
242
|
+
component: options.component,
|
|
243
|
+
tags: options.tags ? options.tags.split(',').map((t) => t.trim()) : undefined,
|
|
244
|
+
grep: options.grep,
|
|
245
|
+
exclude: options.exclude,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const testCases = await discoverTests(config, configDir, discoveryOptions);
|
|
249
|
+
|
|
250
|
+
console.log();
|
|
251
|
+
console.log(pc.cyan(pc.bold('Available Tests')));
|
|
252
|
+
console.log();
|
|
253
|
+
|
|
254
|
+
if (testCases.length === 0) {
|
|
255
|
+
console.log(pc.yellow('No tests with play functions found'));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Group by component
|
|
260
|
+
const byComponent = new Map<string, typeof testCases>();
|
|
261
|
+
for (const test of testCases) {
|
|
262
|
+
const existing = byComponent.get(test.component) || [];
|
|
263
|
+
existing.push(test);
|
|
264
|
+
byComponent.set(test.component, existing);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const [component, tests] of byComponent) {
|
|
268
|
+
console.log(pc.bold(component));
|
|
269
|
+
for (const test of tests) {
|
|
270
|
+
const tags = test.tags.length > 0 ? pc.dim(` [${test.tags.join(', ')}]`) : '';
|
|
271
|
+
console.log(` ${pc.green('›')} ${test.variant}${tags}`);
|
|
272
|
+
}
|
|
273
|
+
console.log();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log(pc.dim(`Total: ${testCases.length} test(s) in ${byComponent.size} component(s)`));
|
|
277
|
+
console.log();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Re-export types
|
|
281
|
+
export type { TestCase, TestResult, TestRunResult, TestSuite, TestReporter } from './types.js';
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console reporter - pretty terminal output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
import type { TestCase, TestResult, TestRunResult, TestReporter } from '../types.js';
|
|
7
|
+
|
|
8
|
+
export interface ConsoleReporterOptions {
|
|
9
|
+
/** Show verbose output including all steps */
|
|
10
|
+
verbose?: boolean;
|
|
11
|
+
/** Show timing for each test */
|
|
12
|
+
showTiming?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a console reporter
|
|
17
|
+
*/
|
|
18
|
+
export function createConsoleReporter(options: ConsoleReporterOptions = {}): TestReporter {
|
|
19
|
+
const { verbose = false, showTiming = true } = options;
|
|
20
|
+
|
|
21
|
+
let startTime: number;
|
|
22
|
+
let testCount: number;
|
|
23
|
+
let currentIndex: number;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
onRunStart(count: number) {
|
|
27
|
+
testCount = count;
|
|
28
|
+
currentIndex = 0;
|
|
29
|
+
startTime = Date.now();
|
|
30
|
+
|
|
31
|
+
console.log();
|
|
32
|
+
console.log(pc.cyan(pc.bold('Segments Test Runner')));
|
|
33
|
+
console.log(pc.dim(`Running ${count} test${count === 1 ? '' : 's'}...`));
|
|
34
|
+
console.log();
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
onTestStart(testCase: TestCase) {
|
|
38
|
+
currentIndex++;
|
|
39
|
+
if (verbose) {
|
|
40
|
+
const progress = pc.dim(`[${currentIndex}/${testCount}]`);
|
|
41
|
+
console.log(`${progress} ${pc.dim('Running')} ${testCase.component} › ${testCase.variant}`);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
onTestComplete(result: TestResult) {
|
|
46
|
+
const { testCase, status, duration, steps, error, accessibility } = result;
|
|
47
|
+
const timing = showTiming ? pc.dim(` (${formatDuration(duration)})`) : '';
|
|
48
|
+
|
|
49
|
+
const statusIcon = getStatusIcon(status);
|
|
50
|
+
const testName = `${testCase.component} › ${testCase.variant}`;
|
|
51
|
+
|
|
52
|
+
if (status === 'passed') {
|
|
53
|
+
console.log(` ${statusIcon} ${testName}${timing}`);
|
|
54
|
+
|
|
55
|
+
// Show a11y results if present
|
|
56
|
+
if (accessibility && accessibility.violations.length > 0) {
|
|
57
|
+
console.log(
|
|
58
|
+
pc.yellow(` ⚠ ${accessibility.violations.length} accessibility violation(s)`)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
} else if (status === 'failed') {
|
|
62
|
+
console.log(` ${statusIcon} ${pc.red(testName)}${timing}`);
|
|
63
|
+
|
|
64
|
+
// Show error message
|
|
65
|
+
if (error) {
|
|
66
|
+
console.log(pc.red(` ${error.message}`));
|
|
67
|
+
if (verbose && error.stack) {
|
|
68
|
+
console.log(pc.dim(` ${error.stack.split('\n').slice(1, 4).join('\n ')}`));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Show failed steps
|
|
73
|
+
if (verbose || steps.some((s) => s.status === 'failed')) {
|
|
74
|
+
for (const step of steps) {
|
|
75
|
+
if (step.status === 'failed') {
|
|
76
|
+
console.log(pc.red(` ✗ ${step.name}`));
|
|
77
|
+
if (step.error) {
|
|
78
|
+
console.log(pc.red(` ${step.error.message}`));
|
|
79
|
+
}
|
|
80
|
+
} else if (verbose && step.status === 'passed') {
|
|
81
|
+
console.log(pc.dim(` ✓ ${step.name}`));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Show retry info
|
|
87
|
+
if (result.retryAttempt) {
|
|
88
|
+
console.log(pc.dim(` (failed after ${result.retryAttempt + 1} attempts)`));
|
|
89
|
+
}
|
|
90
|
+
} else if (status === 'skipped') {
|
|
91
|
+
console.log(` ${statusIcon} ${pc.dim(testName)} ${pc.dim('(skipped)')}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Show all steps in verbose mode
|
|
95
|
+
if (verbose && status === 'passed' && steps.length > 0) {
|
|
96
|
+
for (const step of steps) {
|
|
97
|
+
const stepTiming = showTiming ? pc.dim(` ${formatDuration(step.duration)}`) : '';
|
|
98
|
+
console.log(pc.dim(` ✓ ${step.name}${stepTiming}`));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
onRunComplete(result: TestRunResult) {
|
|
104
|
+
const { totalTests, totalPassed, totalFailed, totalSkipped, totalDuration, suites } = result;
|
|
105
|
+
|
|
106
|
+
console.log();
|
|
107
|
+
console.log(pc.dim('─'.repeat(50)));
|
|
108
|
+
console.log();
|
|
109
|
+
|
|
110
|
+
// Summary by suite
|
|
111
|
+
if (suites.length > 1) {
|
|
112
|
+
console.log(pc.bold('Test Suites:'));
|
|
113
|
+
for (const suite of suites) {
|
|
114
|
+
const statusIcon = suite.failed > 0 ? pc.red('✗') : pc.green('✓');
|
|
115
|
+
const failedText = suite.failed > 0 ? pc.red(`${suite.failed} failed, `) : '';
|
|
116
|
+
const passedText = suite.passed > 0 ? pc.green(`${suite.passed} passed`) : '';
|
|
117
|
+
const skippedText = suite.skipped > 0 ? pc.dim(`, ${suite.skipped} skipped`) : '';
|
|
118
|
+
console.log(` ${statusIcon} ${suite.name} (${failedText}${passedText}${skippedText})`);
|
|
119
|
+
}
|
|
120
|
+
console.log();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Overall summary
|
|
124
|
+
console.log(pc.bold('Tests:'));
|
|
125
|
+
const parts: string[] = [];
|
|
126
|
+
|
|
127
|
+
if (totalFailed > 0) {
|
|
128
|
+
parts.push(pc.red(`${totalFailed} failed`));
|
|
129
|
+
}
|
|
130
|
+
if (totalPassed > 0) {
|
|
131
|
+
parts.push(pc.green(`${totalPassed} passed`));
|
|
132
|
+
}
|
|
133
|
+
if (totalSkipped > 0) {
|
|
134
|
+
parts.push(pc.dim(`${totalSkipped} skipped`));
|
|
135
|
+
}
|
|
136
|
+
parts.push(`${totalTests} total`);
|
|
137
|
+
|
|
138
|
+
console.log(` ${parts.join(', ')}`);
|
|
139
|
+
console.log(` ${pc.dim('Time:')} ${formatDuration(totalDuration)}`);
|
|
140
|
+
|
|
141
|
+
// A11y summary if applicable
|
|
142
|
+
if (result.totalA11yViolations !== undefined) {
|
|
143
|
+
if (result.totalA11yViolations > 0) {
|
|
144
|
+
console.log(
|
|
145
|
+
` ${pc.yellow('Accessibility:')} ${result.totalA11yViolations} violation(s)`
|
|
146
|
+
);
|
|
147
|
+
} else {
|
|
148
|
+
console.log(` ${pc.green('Accessibility:')} All checks passed`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log();
|
|
153
|
+
|
|
154
|
+
// Final status
|
|
155
|
+
if (totalFailed > 0) {
|
|
156
|
+
console.log(pc.red(pc.bold('Test run failed')));
|
|
157
|
+
} else {
|
|
158
|
+
console.log(pc.green(pc.bold('Test run passed')));
|
|
159
|
+
}
|
|
160
|
+
console.log();
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get status icon with color
|
|
167
|
+
*/
|
|
168
|
+
function getStatusIcon(status: TestResult['status']): string {
|
|
169
|
+
switch (status) {
|
|
170
|
+
case 'passed':
|
|
171
|
+
return pc.green('✓');
|
|
172
|
+
case 'failed':
|
|
173
|
+
return pc.red('✗');
|
|
174
|
+
case 'skipped':
|
|
175
|
+
return pc.dim('○');
|
|
176
|
+
default:
|
|
177
|
+
return ' ';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Format duration in human-readable form
|
|
183
|
+
*/
|
|
184
|
+
function formatDuration(ms: number): string {
|
|
185
|
+
if (ms < 1000) {
|
|
186
|
+
return `${Math.round(ms)}ms`;
|
|
187
|
+
}
|
|
188
|
+
if (ms < 60000) {
|
|
189
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
190
|
+
}
|
|
191
|
+
const minutes = Math.floor(ms / 60000);
|
|
192
|
+
const seconds = ((ms % 60000) / 1000).toFixed(1);
|
|
193
|
+
return `${minutes}m ${seconds}s`;
|
|
194
|
+
}
|