@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.
Files changed (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. 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
+ }