@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,190 @@
1
+ /**
2
+ * JSON reporter - machine-readable output
3
+ *
4
+ * Outputs structured JSON for:
5
+ * - AI analysis and processing
6
+ * - Custom dashboards
7
+ * - Integration with other tools
8
+ */
9
+
10
+ import { writeFile, mkdir } from 'node:fs/promises';
11
+ import { dirname } from 'node:path';
12
+ import type { TestRunResult, TestReporter } from '../types.js';
13
+
14
+ export interface JsonReporterOptions {
15
+ /** Output file path */
16
+ outputPath: string;
17
+ /** Pretty print the JSON */
18
+ pretty?: boolean;
19
+ /** Include full step details */
20
+ includeSteps?: boolean;
21
+ /** Include stack traces in errors */
22
+ includeStacks?: boolean;
23
+ }
24
+
25
+ /**
26
+ * JSON report structure
27
+ */
28
+ export interface JsonReport {
29
+ /** Report metadata */
30
+ meta: {
31
+ /** Report version */
32
+ version: string;
33
+ /** Test run start time */
34
+ startTime: string;
35
+ /** Test run end time */
36
+ endTime: string;
37
+ /** Total duration in milliseconds */
38
+ duration: number;
39
+ };
40
+ /** Summary statistics */
41
+ summary: {
42
+ total: number;
43
+ passed: number;
44
+ failed: number;
45
+ skipped: number;
46
+ passRate: number;
47
+ a11yViolations?: number;
48
+ };
49
+ /** Test suites with results */
50
+ suites: Array<{
51
+ name: string;
52
+ tests: number;
53
+ passed: number;
54
+ failed: number;
55
+ skipped: number;
56
+ duration: number;
57
+ results: Array<{
58
+ id: string;
59
+ component: string;
60
+ variant: string;
61
+ status: 'passed' | 'failed' | 'skipped';
62
+ duration: number;
63
+ steps?: Array<{
64
+ name: string;
65
+ status: 'passed' | 'failed' | 'skipped';
66
+ duration: number;
67
+ error?: string;
68
+ }>;
69
+ error?: {
70
+ message: string;
71
+ stack?: string;
72
+ };
73
+ accessibility?: {
74
+ violations: number;
75
+ passes: number;
76
+ details?: Array<{
77
+ id: string;
78
+ impact: string;
79
+ description: string;
80
+ elements: number;
81
+ }>;
82
+ };
83
+ retryAttempt?: number;
84
+ }>;
85
+ }>;
86
+ }
87
+
88
+ /**
89
+ * Create a JSON reporter
90
+ */
91
+ export function createJsonReporter(options: JsonReporterOptions): TestReporter {
92
+ const { outputPath, pretty = true, includeSteps = true, includeStacks = false } = options;
93
+
94
+ return {
95
+ async onRunComplete(result: TestRunResult) {
96
+ const report = generateJsonReport(result, includeSteps, includeStacks);
97
+
98
+ // Ensure output directory exists
99
+ await mkdir(dirname(outputPath), { recursive: true });
100
+
101
+ // Write the JSON file
102
+ const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
103
+ await writeFile(outputPath, json, 'utf-8');
104
+ },
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Generate JSON report from test results
110
+ */
111
+ function generateJsonReport(
112
+ result: TestRunResult,
113
+ includeSteps: boolean,
114
+ includeStacks: boolean
115
+ ): JsonReport {
116
+ const { suites, totalTests, totalPassed, totalFailed, totalSkipped, totalDuration, startTime, endTime } = result;
117
+
118
+ return {
119
+ meta: {
120
+ version: '1.0.0',
121
+ startTime: startTime.toISOString(),
122
+ endTime: endTime.toISOString(),
123
+ duration: totalDuration,
124
+ },
125
+ summary: {
126
+ total: totalTests,
127
+ passed: totalPassed,
128
+ failed: totalFailed,
129
+ skipped: totalSkipped,
130
+ passRate: totalTests > 0 ? Math.round((totalPassed / totalTests) * 100) : 0,
131
+ a11yViolations: result.totalA11yViolations,
132
+ },
133
+ suites: suites.map((suite) => ({
134
+ name: suite.name,
135
+ tests: suite.tests.length,
136
+ passed: suite.passed,
137
+ failed: suite.failed,
138
+ skipped: suite.skipped,
139
+ duration: suite.duration,
140
+ results: suite.tests.map((test) => {
141
+ const resultObj: JsonReport['suites'][0]['results'][0] = {
142
+ id: test.testCase.id,
143
+ component: test.testCase.component,
144
+ variant: test.testCase.variant,
145
+ status: test.status,
146
+ duration: test.duration,
147
+ };
148
+
149
+ // Include steps if requested
150
+ if (includeSteps && test.steps.length > 0) {
151
+ resultObj.steps = test.steps.map((step) => ({
152
+ name: step.name,
153
+ status: step.status,
154
+ duration: step.duration,
155
+ error: step.error?.message,
156
+ }));
157
+ }
158
+
159
+ // Include error if present
160
+ if (test.error) {
161
+ resultObj.error = {
162
+ message: test.error.message,
163
+ stack: includeStacks ? test.error.stack : undefined,
164
+ };
165
+ }
166
+
167
+ // Include accessibility results if present
168
+ if (test.accessibility) {
169
+ resultObj.accessibility = {
170
+ violations: test.accessibility.violations.length,
171
+ passes: test.accessibility.passes,
172
+ details: test.accessibility.violations.map((v) => ({
173
+ id: v.id,
174
+ impact: v.impact,
175
+ description: v.description,
176
+ elements: v.nodes.length,
177
+ })),
178
+ };
179
+ }
180
+
181
+ // Include retry info if applicable
182
+ if (test.retryAttempt) {
183
+ resultObj.retryAttempt = test.retryAttempt;
184
+ }
185
+
186
+ return resultObj;
187
+ }),
188
+ })),
189
+ };
190
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * JUnit XML reporter - CI/CD integration
3
+ *
4
+ * Generates JUnit XML format compatible with:
5
+ * - GitHub Actions (mikepenz/action-junit-report)
6
+ * - Jenkins
7
+ * - GitLab CI
8
+ * - CircleCI
9
+ * - Azure DevOps
10
+ */
11
+
12
+ import { writeFile, mkdir } from 'node:fs/promises';
13
+ import { join, dirname } from 'node:path';
14
+ import type { TestRunResult, TestReporter, TestResult, TestSuite } from '../types.js';
15
+
16
+ export interface JUnitReporterOptions {
17
+ /** Output file path */
18
+ outputPath: string;
19
+ /** Suite name for the root element */
20
+ suiteName?: string;
21
+ /** Include properties in the XML */
22
+ includeProperties?: boolean;
23
+ }
24
+
25
+ /**
26
+ * Create a JUnit XML reporter
27
+ */
28
+ export function createJUnitReporter(options: JUnitReporterOptions): TestReporter {
29
+ const { outputPath, suiteName = 'Segments Tests', includeProperties = true } = options;
30
+
31
+ return {
32
+ async onRunComplete(result: TestRunResult) {
33
+ const xml = generateJUnitXml(result, suiteName, includeProperties);
34
+
35
+ // Ensure output directory exists
36
+ await mkdir(dirname(outputPath), { recursive: true });
37
+
38
+ // Write the XML file
39
+ await writeFile(outputPath, xml, 'utf-8');
40
+ },
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Generate JUnit XML from test results
46
+ */
47
+ function generateJUnitXml(
48
+ result: TestRunResult,
49
+ suiteName: string,
50
+ includeProperties: boolean
51
+ ): string {
52
+ const lines: string[] = [];
53
+
54
+ // XML declaration
55
+ lines.push('<?xml version="1.0" encoding="UTF-8"?>');
56
+
57
+ // Root testsuites element
58
+ lines.push(
59
+ `<testsuites name="${escapeXml(suiteName)}" tests="${result.totalTests}" failures="${result.totalFailed}" skipped="${result.totalSkipped}" time="${(result.totalDuration / 1000).toFixed(3)}">`
60
+ );
61
+
62
+ // Generate each test suite
63
+ for (const suite of result.suites) {
64
+ lines.push(generateTestSuiteXml(suite, includeProperties));
65
+ }
66
+
67
+ lines.push('</testsuites>');
68
+
69
+ return lines.join('\n');
70
+ }
71
+
72
+ /**
73
+ * Generate XML for a single test suite
74
+ */
75
+ function generateTestSuiteXml(suite: TestSuite, includeProperties: boolean): string {
76
+ const lines: string[] = [];
77
+
78
+ // Testsuite element
79
+ lines.push(
80
+ ` <testsuite name="${escapeXml(suite.name)}" tests="${suite.tests.length}" failures="${suite.failed}" skipped="${suite.skipped}" time="${(suite.duration / 1000).toFixed(3)}">`
81
+ );
82
+
83
+ // Properties (optional)
84
+ if (includeProperties) {
85
+ lines.push(' <properties>');
86
+ lines.push(` <property name="component" value="${escapeXml(suite.name)}"/>`);
87
+ lines.push(' </properties>');
88
+ }
89
+
90
+ // Test cases
91
+ for (const test of suite.tests) {
92
+ lines.push(generateTestCaseXml(test));
93
+ }
94
+
95
+ lines.push(' </testsuite>');
96
+
97
+ return lines.join('\n');
98
+ }
99
+
100
+ /**
101
+ * Generate XML for a single test case
102
+ */
103
+ function generateTestCaseXml(result: TestResult): string {
104
+ const { testCase, status, duration, error, steps, accessibility } = result;
105
+ const lines: string[] = [];
106
+
107
+ const testName = testCase.variant;
108
+ const className = testCase.component;
109
+ const time = (duration / 1000).toFixed(3);
110
+
111
+ // Opening tag
112
+ if (status === 'passed' && !accessibility?.violations.length) {
113
+ // Simple passed test - self-closing tag
114
+ lines.push(` <testcase name="${escapeXml(testName)}" classname="${escapeXml(className)}" time="${time}"/>`);
115
+ } else {
116
+ // Test with failure, skip, or a11y info
117
+ lines.push(` <testcase name="${escapeXml(testName)}" classname="${escapeXml(className)}" time="${time}">`);
118
+
119
+ if (status === 'failed') {
120
+ // Failure element
121
+ const message = error?.message || 'Test failed';
122
+ lines.push(` <failure message="${escapeXml(message)}" type="AssertionError">`);
123
+
124
+ // Add detailed failure info
125
+ const details: string[] = [];
126
+
127
+ // Add step failures
128
+ const failedSteps = steps.filter((s) => s.status === 'failed');
129
+ if (failedSteps.length > 0) {
130
+ details.push('Failed steps:');
131
+ for (const step of failedSteps) {
132
+ details.push(` - ${step.name}: ${step.error?.message || 'Failed'}`);
133
+ }
134
+ }
135
+
136
+ // Add stack trace if available
137
+ if (error?.stack) {
138
+ details.push('');
139
+ details.push('Stack trace:');
140
+ details.push(error.stack);
141
+ }
142
+
143
+ if (details.length > 0) {
144
+ lines.push(escapeXml(details.join('\n')));
145
+ }
146
+
147
+ lines.push(' </failure>');
148
+ } else if (status === 'skipped') {
149
+ lines.push(' <skipped/>');
150
+ }
151
+
152
+ // Add system-out for accessibility violations
153
+ if (accessibility?.violations.length) {
154
+ lines.push(' <system-out>');
155
+ lines.push(`Accessibility violations (${accessibility.violations.length}):`);
156
+ for (const violation of accessibility.violations) {
157
+ lines.push(` [${violation.impact}] ${violation.id}: ${violation.description}`);
158
+ lines.push(` Help: ${violation.help}`);
159
+ for (const node of violation.nodes.slice(0, 3)) {
160
+ lines.push(` Element: ${node.html.substring(0, 100)}`);
161
+ }
162
+ if (violation.nodes.length > 3) {
163
+ lines.push(` ... and ${violation.nodes.length - 3} more elements`);
164
+ }
165
+ }
166
+ lines.push(' </system-out>');
167
+ }
168
+
169
+ lines.push(' </testcase>');
170
+ }
171
+
172
+ return lines.join('\n');
173
+ }
174
+
175
+ /**
176
+ * Escape special XML characters
177
+ */
178
+ function escapeXml(str: string): string {
179
+ return str
180
+ .replace(/&/g, '&amp;')
181
+ .replace(/</g, '&lt;')
182
+ .replace(/>/g, '&gt;')
183
+ .replace(/"/g, '&quot;')
184
+ .replace(/'/g, '&apos;')
185
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, ''); // Remove invalid XML characters
186
+ }