@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,232 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ serializeValue,
4
+ serializePropsToJsx,
5
+ findSegmentByName,
6
+ getAvailableComponents,
7
+ generateRenderScript,
8
+ } from "../render-utils.js";
9
+
10
+ describe("serializeValue", () => {
11
+ it("serializes null", () => {
12
+ expect(serializeValue(null)).toBe("null");
13
+ });
14
+
15
+ it("serializes undefined", () => {
16
+ expect(serializeValue(undefined)).toBe("undefined");
17
+ });
18
+
19
+ it("serializes strings with proper escaping", () => {
20
+ expect(serializeValue("hello")).toBe('"hello"');
21
+ expect(serializeValue("hello world")).toBe('"hello world"');
22
+ expect(serializeValue('with "quotes"')).toBe('"with \\"quotes\\""');
23
+ });
24
+
25
+ it("serializes numbers", () => {
26
+ expect(serializeValue(42)).toBe("42");
27
+ expect(serializeValue(3.14)).toBe("3.14");
28
+ expect(serializeValue(-10)).toBe("-10");
29
+ expect(serializeValue(0)).toBe("0");
30
+ });
31
+
32
+ it("serializes booleans", () => {
33
+ expect(serializeValue(true)).toBe("true");
34
+ expect(serializeValue(false)).toBe("false");
35
+ });
36
+
37
+ it("serializes arrays", () => {
38
+ expect(serializeValue([])).toBe("[]");
39
+ expect(serializeValue([1, 2, 3])).toBe("[1, 2, 3]");
40
+ expect(serializeValue(["a", "b"])).toBe('["a", "b"]');
41
+ expect(serializeValue([true, null, "x"])).toBe('[true, null, "x"]');
42
+ });
43
+
44
+ it("serializes objects", () => {
45
+ expect(serializeValue({})).toBe("{}");
46
+ expect(serializeValue({ a: 1 })).toBe('{"a": 1}');
47
+ expect(serializeValue({ name: "test", value: 42 })).toBe(
48
+ '{"name": "test", "value": 42}'
49
+ );
50
+ });
51
+
52
+ it("serializes nested structures", () => {
53
+ expect(serializeValue({ items: [1, 2], nested: { deep: true } })).toBe(
54
+ '{"items": [1, 2], "nested": {"deep": true}}'
55
+ );
56
+ });
57
+
58
+ it("returns undefined for functions", () => {
59
+ expect(serializeValue(() => {})).toBe("undefined");
60
+ });
61
+ });
62
+
63
+ describe("serializePropsToJsx", () => {
64
+ it("serializes empty props", () => {
65
+ expect(serializePropsToJsx({})).toBe("");
66
+ });
67
+
68
+ it("serializes string props as JSX attributes", () => {
69
+ expect(serializePropsToJsx({ variant: "primary" })).toBe(
70
+ 'variant="primary"'
71
+ );
72
+ expect(serializePropsToJsx({ label: "Click me" })).toBe('label="Click me"');
73
+ });
74
+
75
+ it("serializes boolean props with JSX expression syntax", () => {
76
+ expect(serializePropsToJsx({ disabled: true })).toBe("disabled={true}");
77
+ expect(serializePropsToJsx({ checked: false })).toBe("checked={false}");
78
+ });
79
+
80
+ it("serializes number props with JSX expression syntax", () => {
81
+ expect(serializePropsToJsx({ count: 5 })).toBe("count={5}");
82
+ expect(serializePropsToJsx({ size: 24.5 })).toBe("size={24.5}");
83
+ });
84
+
85
+ it("serializes multiple props", () => {
86
+ const result = serializePropsToJsx({
87
+ variant: "danger",
88
+ disabled: false,
89
+ size: 16,
90
+ });
91
+ expect(result).toBe('variant="danger" disabled={false} size={16}');
92
+ });
93
+
94
+ it("filters out undefined props", () => {
95
+ expect(
96
+ serializePropsToJsx({
97
+ variant: "primary",
98
+ disabled: undefined,
99
+ label: "test",
100
+ })
101
+ ).toBe('variant="primary" label="test"');
102
+ });
103
+
104
+ it("serializes object props", () => {
105
+ expect(serializePropsToJsx({ style: { color: "red" } })).toBe(
106
+ 'style={{"color": "red"}}'
107
+ );
108
+ });
109
+
110
+ it("serializes array props", () => {
111
+ expect(serializePropsToJsx({ items: [1, 2, 3] })).toBe("items={[1, 2, 3]}");
112
+ });
113
+ });
114
+
115
+ describe("findSegmentByName", () => {
116
+ const mockSegments = [
117
+ { path: "Button.segment.tsx", segment: { meta: { name: "Button" } } },
118
+ { path: "Card.segment.tsx", segment: { meta: { name: "Card" } } },
119
+ { path: "Alert.segment.tsx", segment: { meta: { name: "Alert" } } },
120
+ ];
121
+
122
+ it("finds segment by exact name", () => {
123
+ const result = findSegmentByName("Button", mockSegments);
124
+ expect(result).toEqual({ name: "Button", path: "Button.segment.tsx" });
125
+ });
126
+
127
+ it("finds segment case-insensitively", () => {
128
+ expect(findSegmentByName("button", mockSegments)).toEqual({
129
+ name: "Button",
130
+ path: "Button.segment.tsx",
131
+ });
132
+ expect(findSegmentByName("BUTTON", mockSegments)).toEqual({
133
+ name: "Button",
134
+ path: "Button.segment.tsx",
135
+ });
136
+ expect(findSegmentByName("BuTtOn", mockSegments)).toEqual({
137
+ name: "Button",
138
+ path: "Button.segment.tsx",
139
+ });
140
+ });
141
+
142
+ it("returns null for non-existent component", () => {
143
+ expect(findSegmentByName("NonExistent", mockSegments)).toBeNull();
144
+ expect(findSegmentByName("", mockSegments)).toBeNull();
145
+ });
146
+
147
+ it("handles empty segments array", () => {
148
+ expect(findSegmentByName("Button", [])).toBeNull();
149
+ });
150
+ });
151
+
152
+ describe("getAvailableComponents", () => {
153
+ it("returns sorted list of component names", () => {
154
+ const segments = [
155
+ { segment: { meta: { name: "Zebra" } } },
156
+ { segment: { meta: { name: "Alert" } } },
157
+ { segment: { meta: { name: "Button" } } },
158
+ ];
159
+ expect(getAvailableComponents(segments)).toEqual([
160
+ "Alert",
161
+ "Button",
162
+ "Zebra",
163
+ ]);
164
+ });
165
+
166
+ it("returns empty array for empty segments", () => {
167
+ expect(getAvailableComponents([])).toEqual([]);
168
+ });
169
+ });
170
+
171
+ describe("generateRenderScript", () => {
172
+ it("generates script with correct import path", () => {
173
+ const script = generateRenderScript("/path/to/Button.segment.tsx", "Button", {});
174
+ expect(script).toContain('import("/path/to/Button.segment.tsx")');
175
+ });
176
+
177
+ it("generates script with empty props object when no props", () => {
178
+ const script = generateRenderScript("/path/to/Button.tsx", "Button", {});
179
+ expect(script).toContain("React.createElement(Component, {})");
180
+ });
181
+
182
+ it("generates script with props object", () => {
183
+ const script = generateRenderScript("/path/to/Button.tsx", "Button", {
184
+ variant: "danger",
185
+ disabled: true,
186
+ });
187
+ expect(script).toContain(
188
+ 'React.createElement(Component, {"variant":"danger","disabled":true})'
189
+ );
190
+ });
191
+
192
+ it("handles children prop specially", () => {
193
+ const script = generateRenderScript("/path/to/Button.tsx", "Button", {
194
+ variant: "primary",
195
+ children: "Click me",
196
+ });
197
+ // Children should be passed as third argument, not in props object
198
+ expect(script).toContain(
199
+ 'React.createElement(Component, {"variant":"primary"}, "Click me")'
200
+ );
201
+ // Children should NOT be in the props object
202
+ expect(script).not.toContain('"children":"Click me"');
203
+ });
204
+
205
+ it("handles children with other props", () => {
206
+ const script = generateRenderScript("/path/to/Alert.tsx", "Alert", {
207
+ severity: "warning",
208
+ children: "Warning message",
209
+ dismissible: true,
210
+ });
211
+ expect(script).toContain(
212
+ 'React.createElement(Component, {"severity":"warning","dismissible":true}, "Warning message")'
213
+ );
214
+ });
215
+
216
+ it("includes React and ReactDOM imports", () => {
217
+ const script = generateRenderScript("/path/to/Button.tsx", "Button", {});
218
+ expect(script).toContain('import React from "react"');
219
+ expect(script).toContain('import { createRoot } from "react-dom/client"');
220
+ });
221
+
222
+ it("includes render ready signal", () => {
223
+ const script = generateRenderScript("/path/to/Button.tsx", "Button", {});
224
+ expect(script).toContain("window.__RENDER_READY__ = true");
225
+ });
226
+
227
+ it("includes error handling", () => {
228
+ const script = generateRenderScript("/path/to/Button.tsx", "Button", {});
229
+ expect(script).toContain("window.__RENDER_ERROR__");
230
+ expect(script).toContain('class="render-error"');
231
+ });
232
+ });
@@ -0,0 +1,404 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ compareStyles,
4
+ compareStyleValue,
5
+ normalizeStyleValue,
6
+ compareColors,
7
+ parseColor,
8
+ compareNumericValues,
9
+ } from "../style-utils.js";
10
+
11
+ describe("parseColor", () => {
12
+ describe("hex colors", () => {
13
+ it("parses 6-digit hex color", () => {
14
+ expect(parseColor("#ff0000")).toEqual({ r: 255, g: 0, b: 0 });
15
+ });
16
+
17
+ it("parses lowercase hex color", () => {
18
+ expect(parseColor("#00ff00")).toEqual({ r: 0, g: 255, b: 0 });
19
+ });
20
+
21
+ it("parses mixed case hex color", () => {
22
+ expect(parseColor("#0000FF")).toEqual({ r: 0, g: 0, b: 255 });
23
+ });
24
+
25
+ it("parses hex color with values 0-255", () => {
26
+ expect(parseColor("#336699")).toEqual({ r: 51, g: 102, b: 153 });
27
+ });
28
+
29
+ it("returns null for 3-digit hex color", () => {
30
+ expect(parseColor("#f00")).toBeNull();
31
+ });
32
+ });
33
+
34
+ describe("rgb/rgba colors", () => {
35
+ it("parses rgb color", () => {
36
+ expect(parseColor("rgb(255, 0, 0)")).toEqual({ r: 255, g: 0, b: 0, a: 1 });
37
+ });
38
+
39
+ it("parses rgba color with alpha", () => {
40
+ expect(parseColor("rgba(255, 0, 0, 0.5)")).toEqual({
41
+ r: 255,
42
+ g: 0,
43
+ b: 0,
44
+ a: 0.5,
45
+ });
46
+ });
47
+
48
+ it("parses rgba color with alpha 0", () => {
49
+ expect(parseColor("rgba(0, 0, 0, 0)")).toEqual({
50
+ r: 0,
51
+ g: 0,
52
+ b: 0,
53
+ a: 0,
54
+ });
55
+ });
56
+
57
+ it("parses rgba color with alpha 1", () => {
58
+ expect(parseColor("rgba(100, 150, 200, 1)")).toEqual({
59
+ r: 100,
60
+ g: 150,
61
+ b: 200,
62
+ a: 1,
63
+ });
64
+ });
65
+
66
+ it("handles extra whitespace", () => {
67
+ expect(parseColor("rgb( 255 , 128 , 64 )")).toEqual({
68
+ r: 255,
69
+ g: 128,
70
+ b: 64,
71
+ a: 1,
72
+ });
73
+ });
74
+ });
75
+
76
+ describe("invalid colors", () => {
77
+ it("returns null for color keywords", () => {
78
+ expect(parseColor("red")).toBeNull();
79
+ expect(parseColor("transparent")).toBeNull();
80
+ });
81
+
82
+ it("returns null for hsl colors", () => {
83
+ expect(parseColor("hsl(0, 100%, 50%)")).toBeNull();
84
+ });
85
+
86
+ it("returns null for empty string", () => {
87
+ expect(parseColor("")).toBeNull();
88
+ });
89
+ });
90
+ });
91
+
92
+ describe("compareColors", () => {
93
+ it("matches identical hex colors", () => {
94
+ expect(compareColors("#ff0000", "#ff0000", 0)).toBe(true);
95
+ });
96
+
97
+ it("matches colors within tolerance", () => {
98
+ expect(compareColors("#ff0000", "#fe0000", 5)).toBe(true);
99
+ expect(compareColors("rgb(255, 0, 0)", "rgb(252, 0, 0)", 5)).toBe(true);
100
+ });
101
+
102
+ it("fails colors outside tolerance", () => {
103
+ expect(compareColors("#ff0000", "#f00000", 5)).toBe(false);
104
+ expect(compareColors("rgb(255, 0, 0)", "rgb(240, 0, 0)", 5)).toBe(false);
105
+ });
106
+
107
+ it("matches hex and rgba representations of same color", () => {
108
+ expect(compareColors("#ff0000", "rgba(255, 0, 0, 1)", 0)).toBe(true);
109
+ });
110
+
111
+ it("matches colors with similar alpha", () => {
112
+ expect(compareColors("rgba(255, 0, 0, 0.5)", "rgba(255, 0, 0, 0.52)", 5)).toBe(
113
+ true
114
+ );
115
+ });
116
+
117
+ it("fails colors with different alpha", () => {
118
+ expect(compareColors("rgba(255, 0, 0, 0.5)", "rgba(255, 0, 0, 0.7)", 5)).toBe(
119
+ false
120
+ );
121
+ });
122
+
123
+ it("falls back to string comparison for unparseable colors", () => {
124
+ expect(compareColors("red", "red", 0)).toBe(true);
125
+ expect(compareColors("red", "blue", 0)).toBe(false);
126
+ });
127
+ });
128
+
129
+ describe("compareNumericValues", () => {
130
+ it("matches identical values", () => {
131
+ expect(compareNumericValues("10px", "10px", 0)).toBe(true);
132
+ });
133
+
134
+ it("matches values within tolerance", () => {
135
+ expect(compareNumericValues("10px", "11px", 1)).toBe(true);
136
+ expect(compareNumericValues("10px", "9px", 1)).toBe(true);
137
+ });
138
+
139
+ it("fails values outside tolerance", () => {
140
+ expect(compareNumericValues("10px", "12px", 1)).toBe(false);
141
+ });
142
+
143
+ it("handles values without units", () => {
144
+ expect(compareNumericValues("10", "11", 1)).toBe(true);
145
+ });
146
+
147
+ it("handles decimal values", () => {
148
+ expect(compareNumericValues("10.5px", "11px", 1)).toBe(true);
149
+ expect(compareNumericValues("10.5px", "12px", 1)).toBe(false);
150
+ });
151
+
152
+ it("falls back to string comparison for non-numeric values", () => {
153
+ expect(compareNumericValues("auto", "auto", 0)).toBe(true);
154
+ expect(compareNumericValues("auto", "none", 0)).toBe(false);
155
+ });
156
+ });
157
+
158
+ describe("normalizeStyleValue", () => {
159
+ it("trims whitespace", () => {
160
+ expect(normalizeStyleValue("any", " 10px ")).toBe("10px");
161
+ });
162
+
163
+ it("collapses multiple spaces", () => {
164
+ expect(normalizeStyleValue("any", "10px 20px")).toBe("10px 20px");
165
+ });
166
+
167
+ it('normalizes "none" boxShadow to empty string', () => {
168
+ expect(normalizeStyleValue("boxShadow", "none")).toBe("");
169
+ });
170
+
171
+ it('does not normalize "none" for other properties', () => {
172
+ expect(normalizeStyleValue("display", "none")).toBe("none");
173
+ });
174
+
175
+ it("normalizes transparent color to 'transparent'", () => {
176
+ expect(normalizeStyleValue("backgroundColor", "rgba(0, 0, 0, 0)")).toBe(
177
+ "transparent"
178
+ );
179
+ expect(normalizeStyleValue("borderColor", "rgba( 0 , 0 , 0 , 0 )")).toBe(
180
+ "transparent"
181
+ );
182
+ });
183
+ });
184
+
185
+ describe("compareStyleValue", () => {
186
+ describe("exact match", () => {
187
+ it("matches identical values", () => {
188
+ expect(compareStyleValue("fontFamily", "Inter", "Inter")).toBe(true);
189
+ });
190
+
191
+ it("matches after normalization", () => {
192
+ expect(compareStyleValue("fontFamily", " Inter ", "Inter")).toBe(true);
193
+ });
194
+ });
195
+
196
+ describe("color properties", () => {
197
+ it("uses color comparison for backgroundColor", () => {
198
+ expect(
199
+ compareStyleValue("backgroundColor", "#ff0000", "rgb(255, 0, 0)")
200
+ ).toBe(true);
201
+ });
202
+
203
+ it("uses color comparison for borderColor", () => {
204
+ expect(
205
+ compareStyleValue("borderColor", "#00ff00", "rgb(0, 255, 0)")
206
+ ).toBe(true);
207
+ });
208
+
209
+ it("allows tolerance in color comparison", () => {
210
+ expect(
211
+ compareStyleValue("backgroundColor", "#ff0000", "rgb(252, 0, 0)")
212
+ ).toBe(true);
213
+ });
214
+ });
215
+
216
+ describe("numeric properties", () => {
217
+ it("uses numeric comparison for borderWidth", () => {
218
+ expect(compareStyleValue("borderWidth", "1px", "2px")).toBe(true);
219
+ expect(compareStyleValue("borderWidth", "1px", "3px")).toBe(false);
220
+ });
221
+
222
+ it("uses numeric comparison for borderRadius", () => {
223
+ expect(compareStyleValue("borderRadius", "8px", "9px")).toBe(true);
224
+ });
225
+
226
+ it("uses numeric comparison for fontSize", () => {
227
+ expect(compareStyleValue("fontSize", "16px", "17px")).toBe(true);
228
+ });
229
+
230
+ it("uses numeric comparison for padding", () => {
231
+ expect(compareStyleValue("padding", "10px", "11px")).toBe(true);
232
+ });
233
+
234
+ it("uses numeric comparison for gap", () => {
235
+ expect(compareStyleValue("gap", "16px", "17px")).toBe(true);
236
+ });
237
+ });
238
+
239
+ describe("other properties", () => {
240
+ it("uses strict comparison for fontFamily", () => {
241
+ expect(compareStyleValue("fontFamily", "Inter", "Arial")).toBe(false);
242
+ });
243
+
244
+ it("uses strict comparison for fontWeight", () => {
245
+ expect(compareStyleValue("fontWeight", "400", "500")).toBe(false);
246
+ });
247
+
248
+ it("uses strict comparison for textAlign", () => {
249
+ expect(compareStyleValue("textAlign", "left", "center")).toBe(false);
250
+ });
251
+ });
252
+ });
253
+
254
+ describe("compareStyles", () => {
255
+ it("returns match true when all styles match", () => {
256
+ const figmaStyles = {
257
+ backgroundColor: "#ff0000",
258
+ fontSize: "16px",
259
+ };
260
+ const renderedStyles = {
261
+ backgroundColor: "rgb(255, 0, 0)",
262
+ fontSize: "16px",
263
+ };
264
+
265
+ const result = compareStyles(figmaStyles, renderedStyles);
266
+ expect(result.match).toBe(true);
267
+ expect(result.properties).toHaveLength(2);
268
+ expect(result.properties.every((p) => p.match)).toBe(true);
269
+ });
270
+
271
+ it("returns match false when some styles differ", () => {
272
+ const figmaStyles = {
273
+ backgroundColor: "#ff0000",
274
+ fontSize: "16px",
275
+ };
276
+ const renderedStyles = {
277
+ backgroundColor: "rgb(0, 255, 0)",
278
+ fontSize: "16px",
279
+ };
280
+
281
+ const result = compareStyles(figmaStyles, renderedStyles);
282
+ expect(result.match).toBe(false);
283
+ expect(result.properties.find((p) => p.property === "backgroundColor")?.match).toBe(
284
+ false
285
+ );
286
+ expect(result.properties.find((p) => p.property === "fontSize")?.match).toBe(
287
+ true
288
+ );
289
+ });
290
+
291
+ it("only compares properties present in figmaStyles", () => {
292
+ const figmaStyles = {
293
+ backgroundColor: "#ff0000",
294
+ };
295
+ const renderedStyles = {
296
+ backgroundColor: "rgb(255, 0, 0)",
297
+ fontSize: "16px",
298
+ fontFamily: "Inter",
299
+ };
300
+
301
+ const result = compareStyles(figmaStyles, renderedStyles);
302
+ expect(result.properties).toHaveLength(1);
303
+ expect(result.properties[0].property).toBe("backgroundColor");
304
+ });
305
+
306
+ it("marks missing rendered styles as not matching", () => {
307
+ const figmaStyles = {
308
+ backgroundColor: "#ff0000",
309
+ borderRadius: "8px",
310
+ };
311
+ const renderedStyles = {
312
+ backgroundColor: "rgb(255, 0, 0)",
313
+ };
314
+
315
+ const result = compareStyles(figmaStyles, renderedStyles);
316
+ const borderRadius = result.properties.find(
317
+ (p) => p.property === "borderRadius"
318
+ );
319
+ expect(borderRadius?.rendered).toBe("(not set)");
320
+ expect(borderRadius?.match).toBe(false);
321
+ });
322
+
323
+ it("returns cleaned figmaStyles object", () => {
324
+ const figmaStyles = {
325
+ backgroundColor: "#ff0000",
326
+ fontSize: undefined,
327
+ fontFamily: "Inter",
328
+ };
329
+ const renderedStyles = {
330
+ backgroundColor: "rgb(255, 0, 0)",
331
+ fontFamily: "Inter",
332
+ };
333
+
334
+ const result = compareStyles(figmaStyles, renderedStyles);
335
+ expect(result.figmaStyles).toEqual({
336
+ backgroundColor: "#ff0000",
337
+ fontFamily: "Inter",
338
+ });
339
+ expect(result.figmaStyles.fontSize).toBeUndefined();
340
+ });
341
+
342
+ it("preserves renderedStyles in output", () => {
343
+ const figmaStyles = {
344
+ backgroundColor: "#ff0000",
345
+ };
346
+ const renderedStyles = {
347
+ backgroundColor: "rgb(255, 0, 0)",
348
+ fontSize: "16px",
349
+ };
350
+
351
+ const result = compareStyles(figmaStyles, renderedStyles);
352
+ expect(result.renderedStyles).toBe(renderedStyles);
353
+ });
354
+
355
+ it("handles empty figmaStyles", () => {
356
+ const figmaStyles = {};
357
+ const renderedStyles = {
358
+ backgroundColor: "rgb(255, 0, 0)",
359
+ };
360
+
361
+ const result = compareStyles(figmaStyles, renderedStyles);
362
+ expect(result.match).toBe(true);
363
+ expect(result.properties).toHaveLength(0);
364
+ });
365
+
366
+ it("compares all standard properties when present", () => {
367
+ const figmaStyles = {
368
+ backgroundColor: "#ff0000",
369
+ borderColor: "#000000",
370
+ borderWidth: "1px",
371
+ borderRadius: "4px",
372
+ fontFamily: "Inter",
373
+ fontSize: "16px",
374
+ fontWeight: "400",
375
+ lineHeight: "24px",
376
+ letterSpacing: "-0.5px",
377
+ textAlign: "left",
378
+ boxShadow: "0px 4px 8px rgba(0, 0, 0, 0.25)",
379
+ padding: "16px",
380
+ gap: "8px",
381
+ opacity: "0.9",
382
+ };
383
+ const renderedStyles = {
384
+ backgroundColor: "rgb(255, 0, 0)",
385
+ borderColor: "rgb(0, 0, 0)",
386
+ borderWidth: "1px",
387
+ borderRadius: "4px",
388
+ fontFamily: "Inter",
389
+ fontSize: "16px",
390
+ fontWeight: "400",
391
+ lineHeight: "24px",
392
+ letterSpacing: "-0.5px",
393
+ textAlign: "left",
394
+ boxShadow: "0px 4px 8px rgba(0, 0, 0, 0.25)",
395
+ padding: "16px",
396
+ gap: "8px",
397
+ opacity: "0.9",
398
+ };
399
+
400
+ const result = compareStyles(figmaStyles, renderedStyles);
401
+ expect(result.match).toBe(true);
402
+ expect(result.properties).toHaveLength(14);
403
+ });
404
+ });