@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,598 @@
1
+ /**
2
+ * Test runner - executes play functions in browser via Playwright
3
+ *
4
+ * Playwright is an optional dependency. If not installed, the test command
5
+ * will prompt the user to install it.
6
+ */
7
+
8
+ import type {
9
+ TestCase,
10
+ TestResult,
11
+ TestSuite,
12
+ TestRunResult,
13
+ TestError,
14
+ StepResult,
15
+ A11yResult,
16
+ A11yViolation,
17
+ RunnerOptions,
18
+ TestReporter,
19
+ } from './types.js';
20
+ import { groupTestsByComponent } from './discovery.js';
21
+
22
+ // Dynamic playwright types (since it's optional)
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ type Browser = any;
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ type BrowserContext = any;
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ type Page = any;
29
+
30
+ /**
31
+ * Browser pool for parallel test execution
32
+ */
33
+ interface BrowserPool {
34
+ browser: Browser;
35
+ contexts: BrowserContextEntry[];
36
+ }
37
+
38
+ interface BrowserContextEntry {
39
+ id: number;
40
+ context: BrowserContext;
41
+ page: Page;
42
+ busy: boolean;
43
+ }
44
+
45
+ /**
46
+ * Run tests with the given configuration
47
+ */
48
+ export async function runTests(
49
+ testCases: TestCase[],
50
+ options: RunnerOptions,
51
+ reporters: TestReporter[] = []
52
+ ): Promise<TestRunResult> {
53
+ const startTime = new Date();
54
+ const results: TestResult[] = [];
55
+
56
+ // Notify reporters of run start
57
+ for (const reporter of reporters) {
58
+ reporter.onRunStart?.(testCases.length);
59
+ }
60
+
61
+ // Import Playwright dynamically (optional dependency)
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ let playwright: any;
64
+ try {
65
+ playwright = await import('playwright');
66
+ } catch {
67
+ throw new Error(
68
+ 'Playwright is required for running tests. Install it with: npm install -D playwright'
69
+ );
70
+ }
71
+
72
+ // Launch browser
73
+ const browserType = playwright[options.browser];
74
+ const browser = await browserType.launch({
75
+ headless: options.headless,
76
+ });
77
+
78
+ // Create browser pool for parallel execution
79
+ const pool = await createBrowserPool(browser, options.parallel);
80
+
81
+ try {
82
+ // Determine server URL
83
+ const serverUrl = options.serverUrl || `http://localhost:${options.port}`;
84
+
85
+ // Run tests with parallelism
86
+ if (options.parallel > 1) {
87
+ await runTestsParallel(
88
+ testCases,
89
+ pool,
90
+ serverUrl,
91
+ options,
92
+ reporters,
93
+ results
94
+ );
95
+ } else {
96
+ await runTestsSequential(
97
+ testCases,
98
+ pool.contexts[0],
99
+ serverUrl,
100
+ options,
101
+ reporters,
102
+ results
103
+ );
104
+ }
105
+ } finally {
106
+ // Cleanup browser pool
107
+ await cleanupBrowserPool(pool);
108
+ }
109
+
110
+ const endTime = new Date();
111
+
112
+ // Aggregate results into suites
113
+ const suites = aggregateResults(results);
114
+
115
+ const runResult: TestRunResult = {
116
+ suites,
117
+ totalTests: results.length,
118
+ totalPassed: results.filter((r) => r.status === 'passed').length,
119
+ totalFailed: results.filter((r) => r.status === 'failed').length,
120
+ totalSkipped: results.filter((r) => r.status === 'skipped').length,
121
+ totalDuration: endTime.getTime() - startTime.getTime(),
122
+ startTime,
123
+ endTime,
124
+ totalA11yViolations: options.a11y
125
+ ? results.reduce((sum, r) => sum + (r.accessibility?.violations.length || 0), 0)
126
+ : undefined,
127
+ };
128
+
129
+ // Notify reporters of run completion
130
+ for (const reporter of reporters) {
131
+ await reporter.onRunComplete(runResult);
132
+ }
133
+
134
+ return runResult;
135
+ }
136
+
137
+ /**
138
+ * Create a browser pool for parallel execution
139
+ */
140
+ async function createBrowserPool(
141
+ browser: Browser,
142
+ parallelism: number
143
+ ): Promise<BrowserPool> {
144
+ const contexts: BrowserContextEntry[] = [];
145
+
146
+ for (let i = 0; i < parallelism; i++) {
147
+ const context = await browser.newContext({
148
+ viewport: { width: 1280, height: 720 },
149
+ });
150
+ const page = await context.newPage();
151
+ contexts.push({ id: i, context, page, busy: false });
152
+ }
153
+
154
+ return { browser, contexts };
155
+ }
156
+
157
+ /**
158
+ * Cleanup browser pool
159
+ */
160
+ async function cleanupBrowserPool(pool: BrowserPool): Promise<void> {
161
+ for (const entry of pool.contexts) {
162
+ await entry.page.close();
163
+ await entry.context.close();
164
+ }
165
+ await pool.browser.close();
166
+ }
167
+
168
+ /**
169
+ * Run tests sequentially
170
+ */
171
+ async function runTestsSequential(
172
+ testCases: TestCase[],
173
+ ctx: BrowserContextEntry,
174
+ serverUrl: string,
175
+ options: RunnerOptions,
176
+ reporters: TestReporter[],
177
+ results: TestResult[]
178
+ ): Promise<void> {
179
+ for (const testCase of testCases) {
180
+ // Notify reporters of test start
181
+ for (const reporter of reporters) {
182
+ reporter.onTestStart?.(testCase);
183
+ }
184
+
185
+ const result = await runSingleTest(testCase, ctx, serverUrl, options);
186
+ results.push(result);
187
+
188
+ // Notify reporters of test completion
189
+ for (const reporter of reporters) {
190
+ reporter.onTestComplete?.(result);
191
+ }
192
+
193
+ // Bail on first failure if configured
194
+ if (options.bail && result.status === 'failed') {
195
+ break;
196
+ }
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Run tests in parallel
202
+ */
203
+ async function runTestsParallel(
204
+ testCases: TestCase[],
205
+ pool: BrowserPool,
206
+ serverUrl: string,
207
+ options: RunnerOptions,
208
+ reporters: TestReporter[],
209
+ results: TestResult[]
210
+ ): Promise<void> {
211
+ const queue = [...testCases];
212
+ const inFlight: Promise<void>[] = [];
213
+ let bailTriggered = false;
214
+
215
+ const acquireContext = (): BrowserContextEntry | null => {
216
+ const available = pool.contexts.find((c) => !c.busy);
217
+ if (available) {
218
+ available.busy = true;
219
+ return available;
220
+ }
221
+ return null;
222
+ };
223
+
224
+ const releaseContext = (ctx: BrowserContextEntry): void => {
225
+ ctx.busy = false;
226
+ };
227
+
228
+ const runNext = async (): Promise<void> => {
229
+ if (bailTriggered || queue.length === 0) return;
230
+
231
+ const ctx = acquireContext();
232
+ if (!ctx) return;
233
+
234
+ const testCase = queue.shift()!;
235
+
236
+ // Notify reporters of test start
237
+ for (const reporter of reporters) {
238
+ reporter.onTestStart?.(testCase);
239
+ }
240
+
241
+ try {
242
+ const result = await runSingleTest(testCase, ctx, serverUrl, options);
243
+ results.push(result);
244
+
245
+ // Notify reporters of test completion
246
+ for (const reporter of reporters) {
247
+ reporter.onTestComplete?.(result);
248
+ }
249
+
250
+ // Check bail condition
251
+ if (options.bail && result.status === 'failed') {
252
+ bailTriggered = true;
253
+ }
254
+ } finally {
255
+ releaseContext(ctx);
256
+ // Start next test if not bailing
257
+ if (!bailTriggered && queue.length > 0) {
258
+ const nextPromise = runNext();
259
+ inFlight.push(nextPromise);
260
+ }
261
+ }
262
+ };
263
+
264
+ // Start initial batch
265
+ const initialBatch = Math.min(pool.contexts.length, queue.length);
266
+ for (let i = 0; i < initialBatch; i++) {
267
+ inFlight.push(runNext());
268
+ }
269
+
270
+ // Wait for all to complete
271
+ await Promise.all(inFlight);
272
+ }
273
+
274
+ /**
275
+ * Run a single test case
276
+ */
277
+ async function runSingleTest(
278
+ testCase: TestCase,
279
+ ctx: BrowserContextEntry,
280
+ serverUrl: string,
281
+ options: RunnerOptions
282
+ ): Promise<TestResult> {
283
+ const { page } = ctx;
284
+ const startTime = performance.now();
285
+ const steps: StepResult[] = [];
286
+ let error: TestError | undefined;
287
+ let status: TestResult['status'] = 'passed';
288
+ let retryAttempt = 0;
289
+
290
+ // Retry logic
291
+ const maxAttempts = options.retries + 1;
292
+
293
+ while (retryAttempt < maxAttempts) {
294
+ try {
295
+ // Navigate to the variant
296
+ const url = buildVariantUrl(serverUrl, testCase);
297
+ await page.goto(url, { waitUntil: 'networkidle' });
298
+
299
+ // Wait for preview container
300
+ await page.waitForSelector('[data-preview-container="true"]', {
301
+ timeout: options.timeout,
302
+ });
303
+
304
+ // Execute play function in browser context
305
+ const playResult = await executePlayFunction(page, testCase, options.timeout);
306
+
307
+ if (playResult.error) {
308
+ error = playResult.error;
309
+ status = 'failed';
310
+ steps.push(...playResult.steps);
311
+
312
+ // Retry if not last attempt
313
+ if (retryAttempt < maxAttempts - 1) {
314
+ retryAttempt++;
315
+ continue;
316
+ }
317
+ } else {
318
+ status = 'passed';
319
+ steps.push(...playResult.steps);
320
+ }
321
+
322
+ break; // Success, no retry needed
323
+ } catch (e) {
324
+ error = {
325
+ message: e instanceof Error ? e.message : String(e),
326
+ stack: e instanceof Error ? e.stack : undefined,
327
+ };
328
+ status = 'failed';
329
+
330
+ if (retryAttempt < maxAttempts - 1) {
331
+ retryAttempt++;
332
+ continue;
333
+ }
334
+ break;
335
+ }
336
+ }
337
+
338
+ const duration = performance.now() - startTime;
339
+
340
+ // Run accessibility checks if enabled
341
+ let accessibility: A11yResult | undefined;
342
+ if (options.a11y && status === 'passed') {
343
+ accessibility = await runA11yChecks(page);
344
+ }
345
+
346
+ // Capture screenshot for visual regression if enabled
347
+ let screenshotPath: string | undefined;
348
+ if (options.visual) {
349
+ screenshotPath = await captureScreenshot(page, testCase, options.outputDir);
350
+ }
351
+
352
+ return {
353
+ testCase,
354
+ status,
355
+ duration,
356
+ steps,
357
+ error,
358
+ accessibility,
359
+ screenshotPath,
360
+ retryAttempt: retryAttempt > 0 ? retryAttempt : undefined,
361
+ };
362
+ }
363
+
364
+ /**
365
+ * Build URL for a variant
366
+ */
367
+ function buildVariantUrl(serverUrl: string, testCase: TestCase): string {
368
+ const params = new URLSearchParams({
369
+ component: testCase.component,
370
+ variant: testCase.variant,
371
+ isolated: 'true',
372
+ });
373
+ return `${serverUrl}?${params.toString()}`;
374
+ }
375
+
376
+ /**
377
+ * Execute play function in browser context
378
+ */
379
+ async function executePlayFunction(
380
+ page: Page,
381
+ testCase: TestCase,
382
+ timeout: number
383
+ ): Promise<{ steps: StepResult[]; error?: TestError }> {
384
+ const result = await page.evaluate(
385
+ async ({ component, variant, timeout }: { component: string; variant: string; timeout: number }) => {
386
+ // Find the segment definition in the global registry
387
+ const registry = (window as unknown as { __SEGMENTS_REGISTRY__?: Map<string, unknown> })
388
+ .__SEGMENTS_REGISTRY__;
389
+
390
+ if (!registry) {
391
+ return {
392
+ steps: [],
393
+ error: { message: 'Segments registry not found. Make sure the viewer is loaded.' },
394
+ };
395
+ }
396
+
397
+ const segment = registry.get(component) as {
398
+ variants?: Array<{
399
+ name: string;
400
+ play?: (ctx: {
401
+ canvasElement: HTMLElement;
402
+ args: Record<string, unknown>;
403
+ step: (name: string, fn: () => Promise<void>) => Promise<void>;
404
+ }) => Promise<void>;
405
+ }>;
406
+ } | undefined;
407
+
408
+ if (!segment) {
409
+ return {
410
+ steps: [],
411
+ error: { message: `Component "${component}" not found in registry` },
412
+ };
413
+ }
414
+
415
+ const variantDef = segment.variants?.find((v) => v.name === variant);
416
+ if (!variantDef || !variantDef.play) {
417
+ return {
418
+ steps: [],
419
+ error: { message: `Variant "${variant}" not found or has no play function` },
420
+ };
421
+ }
422
+
423
+ // Find the preview container
424
+ const canvasElement = document.querySelector('[data-preview-container="true"]') as HTMLElement;
425
+ if (!canvasElement) {
426
+ return {
427
+ steps: [],
428
+ error: { message: 'Preview container not found' },
429
+ };
430
+ }
431
+
432
+ // Track steps
433
+ const steps: Array<{
434
+ name: string;
435
+ status: 'passed' | 'failed';
436
+ duration: number;
437
+ error?: { message: string };
438
+ }> = [];
439
+
440
+ const step = async (name: string, fn: () => Promise<void>): Promise<void> => {
441
+ const stepStart = performance.now();
442
+ try {
443
+ await fn();
444
+ steps.push({
445
+ name,
446
+ status: 'passed',
447
+ duration: performance.now() - stepStart,
448
+ });
449
+ } catch (e) {
450
+ steps.push({
451
+ name,
452
+ status: 'failed',
453
+ duration: performance.now() - stepStart,
454
+ error: { message: e instanceof Error ? e.message : String(e) },
455
+ });
456
+ throw e;
457
+ }
458
+ };
459
+
460
+ // Execute with timeout
461
+ const timeoutPromise = new Promise<never>((_, reject) => {
462
+ setTimeout(() => reject(new Error(`Test timed out after ${timeout}ms`)), timeout);
463
+ });
464
+
465
+ try {
466
+ await Promise.race([
467
+ variantDef.play({
468
+ canvasElement,
469
+ args: {},
470
+ step,
471
+ }),
472
+ timeoutPromise,
473
+ ]);
474
+
475
+ // If no explicit steps, add one for the whole play function
476
+ if (steps.length === 0) {
477
+ steps.push({
478
+ name: 'Play function',
479
+ status: 'passed',
480
+ duration: 0,
481
+ });
482
+ }
483
+
484
+ return { steps, error: undefined };
485
+ } catch (e) {
486
+ // If no steps failed explicitly, add one
487
+ if (!steps.some((s) => s.status === 'failed')) {
488
+ steps.push({
489
+ name: 'Play function',
490
+ status: 'failed',
491
+ duration: 0,
492
+ error: { message: e instanceof Error ? e.message : String(e) },
493
+ });
494
+ }
495
+
496
+ return {
497
+ steps,
498
+ error: { message: e instanceof Error ? e.message : String(e) },
499
+ };
500
+ }
501
+ },
502
+ { component: testCase.component, variant: testCase.variant, timeout }
503
+ );
504
+
505
+ return result as { steps: StepResult[]; error?: TestError };
506
+ }
507
+
508
+ /**
509
+ * Run accessibility checks using axe-core
510
+ */
511
+ async function runA11yChecks(page: Page): Promise<A11yResult> {
512
+ try {
513
+ // Inject axe-core if not already present
514
+ await page.evaluate(() => {
515
+ if ((window as unknown as { axe?: unknown }).axe) return;
516
+
517
+ return new Promise<void>((resolve, reject) => {
518
+ const script = document.createElement('script');
519
+ script.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.8.2/axe.min.js';
520
+ script.onload = () => resolve();
521
+ script.onerror = () => reject(new Error('Failed to load axe-core'));
522
+ document.head.appendChild(script);
523
+ });
524
+ });
525
+
526
+ // Run axe-core analysis
527
+ const results = await page.evaluate(async () => {
528
+ const axe = (window as unknown as { axe: { run: (el: HTMLElement) => Promise<{ violations: unknown[]; passes: unknown[] }> } }).axe;
529
+ const container = document.querySelector('[data-preview-container="true"]') as HTMLElement;
530
+ if (!container) {
531
+ return { violations: [], passes: 0 };
532
+ }
533
+ const axeResults = await axe.run(container);
534
+ return {
535
+ violations: axeResults.violations,
536
+ passes: axeResults.passes.length,
537
+ };
538
+ });
539
+
540
+ return {
541
+ violations: results.violations as A11yViolation[],
542
+ passes: results.passes,
543
+ };
544
+ } catch {
545
+ return { violations: [], passes: 0 };
546
+ }
547
+ }
548
+
549
+ /**
550
+ * Capture screenshot for visual regression
551
+ */
552
+ async function captureScreenshot(
553
+ page: Page,
554
+ testCase: TestCase,
555
+ outputDir: string
556
+ ): Promise<string> {
557
+ const { mkdir, writeFile } = await import('node:fs/promises');
558
+ const { join } = await import('node:path');
559
+
560
+ const screenshotsDir = join(outputDir, 'screenshots');
561
+ await mkdir(screenshotsDir, { recursive: true });
562
+
563
+ const filename = `${testCase.component}--${testCase.variant}.png`
564
+ .replace(/\s+/g, '-')
565
+ .toLowerCase();
566
+ const filepath = join(screenshotsDir, filename);
567
+
568
+ const container = await page.$('[data-preview-container="true"]');
569
+ if (container) {
570
+ const screenshot = await container.screenshot();
571
+ await writeFile(filepath, screenshot);
572
+ }
573
+
574
+ return filepath;
575
+ }
576
+
577
+ /**
578
+ * Aggregate test results into suites by component
579
+ */
580
+ function aggregateResults(results: TestResult[]): TestSuite[] {
581
+ const grouped = groupTestsByComponent(results.map((r) => r.testCase));
582
+ const suites: TestSuite[] = [];
583
+
584
+ for (const [componentName, testCases] of grouped) {
585
+ const componentResults = results.filter((r) => r.testCase.component === componentName);
586
+
587
+ suites.push({
588
+ name: componentName,
589
+ tests: componentResults,
590
+ duration: componentResults.reduce((sum, r) => sum + r.duration, 0),
591
+ passed: componentResults.filter((r) => r.status === 'passed').length,
592
+ failed: componentResults.filter((r) => r.status === 'failed').length,
593
+ skipped: componentResults.filter((r) => r.status === 'skipped').length,
594
+ });
595
+ }
596
+
597
+ return suites;
598
+ }