@usefragments/core 1.0.0

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 (228) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-243QYRUF.js +2145 -0
  3. package/dist/chunk-243QYRUF.js.map +1 -0
  4. package/dist/chunk-3LLRNCPX.js +111 -0
  5. package/dist/chunk-3LLRNCPX.js.map +1 -0
  6. package/dist/chunk-EIYNNS77.js +48 -0
  7. package/dist/chunk-EIYNNS77.js.map +1 -0
  8. package/dist/chunk-JNBFJ34I.js +326 -0
  9. package/dist/chunk-JNBFJ34I.js.map +1 -0
  10. package/dist/chunk-R3X7UBHU.js +228 -0
  11. package/dist/chunk-R3X7UBHU.js.map +1 -0
  12. package/dist/chunk-SH4KPIYH.js +393 -0
  13. package/dist/chunk-SH4KPIYH.js.map +1 -0
  14. package/dist/chunk-V4VQB57N.js +56 -0
  15. package/dist/chunk-V4VQB57N.js.map +1 -0
  16. package/dist/chunk-X34IA4LR.js +319 -0
  17. package/dist/chunk-X34IA4LR.js.map +1 -0
  18. package/dist/chunk-YF65VYRY.js +493 -0
  19. package/dist/chunk-YF65VYRY.js.map +1 -0
  20. package/dist/codes/index.d.ts +40 -0
  21. package/dist/codes/index.js +23 -0
  22. package/dist/codes/index.js.map +1 -0
  23. package/dist/compiled-types/index.d.ts +5 -0
  24. package/dist/compiled-types/index.js +1 -0
  25. package/dist/compiled-types/index.js.map +1 -0
  26. package/dist/generate/index.d.ts +47 -0
  27. package/dist/generate/index.js +11 -0
  28. package/dist/generate/index.js.map +1 -0
  29. package/dist/governance-BOa3KyiJ.d.ts +3279 -0
  30. package/dist/graph/index.d.ts +66 -0
  31. package/dist/graph/index.js +17 -0
  32. package/dist/graph/index.js.map +1 -0
  33. package/dist/index.d.ts +11388 -0
  34. package/dist/index.js +8711 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/package-identity.d.ts +42 -0
  37. package/dist/package-identity.js +23 -0
  38. package/dist/package-identity.js.map +1 -0
  39. package/dist/preview/index.d.ts +484 -0
  40. package/dist/preview/index.js +1129 -0
  41. package/dist/preview/index.js.map +1 -0
  42. package/dist/react-types.d.ts +22 -0
  43. package/dist/react-types.js +1 -0
  44. package/dist/react-types.js.map +1 -0
  45. package/dist/registry.d.ts +2367 -0
  46. package/dist/registry.js +62 -0
  47. package/dist/registry.js.map +1 -0
  48. package/dist/schemas/index.d.ts +2584 -0
  49. package/dist/schemas/index.js +55 -0
  50. package/dist/schemas/index.js.map +1 -0
  51. package/dist/severity.d.ts +18 -0
  52. package/dist/severity.js +27 -0
  53. package/dist/severity.js.map +1 -0
  54. package/dist/test-utils.d.ts +14 -0
  55. package/dist/test-utils.js +63 -0
  56. package/dist/test-utils.js.map +1 -0
  57. package/dist/topology/index.d.ts +48 -0
  58. package/dist/topology/index.js +7 -0
  59. package/dist/topology/index.js.map +1 -0
  60. package/dist/types-xJ2xyp_G.d.ts +119 -0
  61. package/package.json +120 -0
  62. package/src/__tests__/contract-parser.test.ts +614 -0
  63. package/src/__tests__/dtcg-generators.test.ts +439 -0
  64. package/src/__tests__/dtcg-parser.test.ts +884 -0
  65. package/src/__tests__/preview-runtime.test.tsx +111 -0
  66. package/src/__tests__/schema.test.ts +31 -0
  67. package/src/__tests__/severity.test.ts +34 -0
  68. package/src/__tests__/style-comparison.test.ts +662 -0
  69. package/src/__tests__/tokens-compat.test.ts +51 -0
  70. package/src/agent-format.test.ts +105 -0
  71. package/src/agent-format.ts +141 -0
  72. package/src/bundle.ts +149 -0
  73. package/src/codes/__tests__/codes.test.ts +73 -0
  74. package/src/codes/codes.ts +463 -0
  75. package/src/codes/index.ts +7 -0
  76. package/src/codes/source-finding-bridge.ts +113 -0
  77. package/src/compiled-types/index.ts +293 -0
  78. package/src/component-contract.ts +231 -0
  79. package/src/component-discovery.ts +80 -0
  80. package/src/composition.test.ts +262 -0
  81. package/src/composition.ts +318 -0
  82. package/src/config-paths.test.ts +78 -0
  83. package/src/config-paths.ts +47 -0
  84. package/src/config.ts +18 -0
  85. package/src/conform-prove.test.ts +274 -0
  86. package/src/conform-prove.ts +326 -0
  87. package/src/conform.ts +160 -0
  88. package/src/constants.ts +114 -0
  89. package/src/context.ts +2 -0
  90. package/src/contract/fixture.ts +110 -0
  91. package/src/contract/hash.test.ts +86 -0
  92. package/src/contract/hash.ts +195 -0
  93. package/src/contract/index.ts +43 -0
  94. package/src/contract/preimage.test.ts +254 -0
  95. package/src/contract/preimage.ts +365 -0
  96. package/src/contract/stamp.test.ts +100 -0
  97. package/src/contract/stamp.ts +105 -0
  98. package/src/contract-parser.ts +141 -0
  99. package/src/defineFragment.test.ts +257 -0
  100. package/src/defineFragment.ts +293 -0
  101. package/src/dtcg-generators.ts +324 -0
  102. package/src/dtcg-parser.ts +1 -0
  103. package/src/dtcg.ts +237 -0
  104. package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
  105. package/src/facts/builders.ts +835 -0
  106. package/src/facts/compile.ts +422 -0
  107. package/src/facts/fact-index.ts +394 -0
  108. package/src/facts/facts.test.ts +494 -0
  109. package/src/facts/ids.ts +125 -0
  110. package/src/facts/index.ts +119 -0
  111. package/src/facts/types.ts +614 -0
  112. package/src/figma.ts +263 -0
  113. package/src/fragment-types.ts +214 -0
  114. package/src/generate/generate.test.ts +594 -0
  115. package/src/generate/index.ts +438 -0
  116. package/src/governance-integrity.test.ts +186 -0
  117. package/src/governance-integrity.ts +282 -0
  118. package/src/governance.test.ts +300 -0
  119. package/src/governance.ts +605 -0
  120. package/src/governance.type-test.ts +75 -0
  121. package/src/graph/engine.ts +446 -0
  122. package/src/graph/graph.test.ts +746 -0
  123. package/src/graph/index.ts +32 -0
  124. package/src/graph/serialization.ts +55 -0
  125. package/src/graph/types.ts +178 -0
  126. package/src/index.ts +891 -0
  127. package/src/local-canonical.ts +102 -0
  128. package/src/mcp-snapshot.ts +222 -0
  129. package/src/package-identity-match.ts +33 -0
  130. package/src/package-identity.generated.ts +233 -0
  131. package/src/package-identity.test.ts +99 -0
  132. package/src/package-identity.ts +157 -0
  133. package/src/performance-presets.ts +142 -0
  134. package/src/preview/comparison-artifact.test.ts +228 -0
  135. package/src/preview/comparison-artifact.ts +181 -0
  136. package/src/preview/comparison-context.ts +93 -0
  137. package/src/preview/comparison-evidence.ts +112 -0
  138. package/src/preview/comparison-format.ts +21 -0
  139. package/src/preview/comparison-presentation.ts +187 -0
  140. package/src/preview/comparison-references.test.ts +430 -0
  141. package/src/preview/comparison-references.ts +72 -0
  142. package/src/preview/comparison-score.ts +59 -0
  143. package/src/preview/comparison-styles.ts +197 -0
  144. package/src/preview/comparison.test.ts +559 -0
  145. package/src/preview/comparison.ts +488 -0
  146. package/src/preview/index.ts +10 -0
  147. package/src/preview/types.ts +309 -0
  148. package/src/preview/validation.test.ts +290 -0
  149. package/src/preview/validation.ts +356 -0
  150. package/src/preview-runtime.tsx +144 -0
  151. package/src/raw-html-canonical.test.ts +41 -0
  152. package/src/raw-html-canonical.ts +179 -0
  153. package/src/react-types.ts +21 -0
  154. package/src/registry-install-plan.ts +344 -0
  155. package/src/registry.test.ts +245 -0
  156. package/src/registry.ts +369 -0
  157. package/src/rules/a11y-required-accessible-name.ts +93 -0
  158. package/src/rules/color-math.test.ts +72 -0
  159. package/src/rules/color-math.ts +192 -0
  160. package/src/rules/components-forbidden-prop-value.ts +108 -0
  161. package/src/rules/components-prefer-library.ts +943 -0
  162. package/src/rules/components-unknown-prop.ts +94 -0
  163. package/src/rules/composition-pattern-schema.ts +125 -0
  164. package/src/rules/composition-pattern.test.ts +320 -0
  165. package/src/rules/composition-pattern.ts +271 -0
  166. package/src/rules/emit-gate.test.ts +42 -0
  167. package/src/rules/emit-gate.ts +60 -0
  168. package/src/rules/finding.ts +54 -0
  169. package/src/rules/index.ts +235 -0
  170. package/src/rules/jsx-preferred-component.ts +68 -0
  171. package/src/rules/jsx-preferred-import-path.ts +67 -0
  172. package/src/rules/presets.ts +57 -0
  173. package/src/rules/props-invalid-value.ts +115 -0
  174. package/src/rules/rules.test.ts +3636 -0
  175. package/src/rules/spacing-resolution.ts +184 -0
  176. package/src/rules/styles-no-raw-color.ts +329 -0
  177. package/src/rules/styles-no-raw-dimensions.ts +357 -0
  178. package/src/rules/styles-no-raw-spacing.ts +236 -0
  179. package/src/rules/styles-no-raw-typography.ts +237 -0
  180. package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
  181. package/src/rules/tailwind-arbitrary-color.ts +93 -0
  182. package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
  183. package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
  184. package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
  185. package/src/rules/tailwind-forbidden-palette.ts +107 -0
  186. package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
  187. package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
  188. package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
  189. package/src/rules/tailwind-raw-color-via-token.ts +82 -0
  190. package/src/rules/tailwind-rule-utils.ts +106 -0
  191. package/src/rules/tailwind-unknown-class.test.ts +157 -0
  192. package/src/rules/tailwind-unknown-class.ts +121 -0
  193. package/src/rules/taxonomy.test.ts +238 -0
  194. package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
  195. package/src/rules/tiers.ts +111 -0
  196. package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
  197. package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
  198. package/src/rules/tokens-require-dual-fallback.ts +78 -0
  199. package/src/rules/types.ts +27 -0
  200. package/src/rules/utils.test.ts +110 -0
  201. package/src/rules/utils.ts +599 -0
  202. package/src/schema.ts +427 -0
  203. package/src/schemas/index.ts +275 -0
  204. package/src/schemas/schemas.test.ts +124 -0
  205. package/src/severity.ts +54 -0
  206. package/src/storyAdapter.test.ts +571 -0
  207. package/src/storyAdapter.ts +761 -0
  208. package/src/storyFilters.test.ts +350 -0
  209. package/src/storyFilters.ts +253 -0
  210. package/src/storybook-csf.ts +11 -0
  211. package/src/style-comparison.ts +423 -0
  212. package/src/test-utils.ts +68 -0
  213. package/src/token-parser.ts +2 -0
  214. package/src/token-types.ts +287 -0
  215. package/src/tokens/categories.ts +131 -0
  216. package/src/tokens/color.ts +193 -0
  217. package/src/tokens/design-token-parser.test.ts +159 -0
  218. package/src/tokens/design-token-parser.ts +402 -0
  219. package/src/tokens/dtcg.ts +428 -0
  220. package/src/tokens/index.ts +130 -0
  221. package/src/tokens/lookup.ts +19 -0
  222. package/src/tokens/scss.ts +171 -0
  223. package/src/tokens/tailwind-v4.ts +187 -0
  224. package/src/tokens/types.ts +43 -0
  225. package/src/topology/index.ts +9 -0
  226. package/src/topology/resolve-area.test.ts +174 -0
  227. package/src/topology/resolve-area.ts +215 -0
  228. package/src/types.ts +1057 -0
@@ -0,0 +1,559 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ comparePreviewToDesign,
4
+ buildPreviewComparisonArtifactPayload,
5
+ buildPreviewDesignComparisonEvidence,
6
+ buildPreviewDesignReferenceFromReport,
7
+ formatPreviewComparisonFinding,
8
+ getPreviewComparisonSignals,
9
+ getPreviewComparisonRegionOverlays,
10
+ resolvePreviewComparisonThresholds,
11
+ } from "./index";
12
+
13
+ describe("preview design comparison", () => {
14
+ it("passes when image, dimensions, and styles are within tolerance", () => {
15
+ const report = comparePreviewToDesign({
16
+ design: {
17
+ kind: "figma",
18
+ label: "Button / Primary",
19
+ nodeUrl: "https://www.figma.com/design/abc/System?node-id=1%3A2",
20
+ size: { width: 120, height: 40 },
21
+ styles: {
22
+ backgroundColor: "#006adc",
23
+ borderRadius: "8px",
24
+ fontSize: "14px",
25
+ },
26
+ },
27
+ preview: {
28
+ kind: "preview",
29
+ label: "Button / primary",
30
+ componentKey: "Button",
31
+ variantId: "primary",
32
+ size: { width: 120.5, height: 40 },
33
+ styles: {
34
+ backgroundColor: "rgb(0, 106, 220)",
35
+ borderRadius: "8px",
36
+ fontSize: "14px",
37
+ },
38
+ },
39
+ imageDiff: {
40
+ diffPercentage: 0.2,
41
+ changedRegions: [],
42
+ diffImageUrl: "/diffs/button-primary.png",
43
+ },
44
+ });
45
+
46
+ expect(report.status).toBe("pass");
47
+ expect(report.score).toBeGreaterThanOrEqual(90);
48
+ expect(report.findings).toEqual([]);
49
+ expect(report.image?.match).toBe(true);
50
+ expect(report.image?.diffImageUrl).toBe("/diffs/button-primary.png");
51
+ expect(report.dimensions?.match).toBe(true);
52
+ expect(report.styles?.match).toBe(true);
53
+ expect(getPreviewComparisonSignals(report)).toEqual([
54
+ {
55
+ id: "image",
56
+ label: "Image diff",
57
+ status: "pass",
58
+ value: "0.2% changed",
59
+ detail: "0 changed regions; threshold 1%.",
60
+ },
61
+ {
62
+ id: "dimensions",
63
+ label: "Size",
64
+ status: "pass",
65
+ value: "+0.5 x 0 px",
66
+ detail: "120 x 40 design, 120.5 x 40 preview; tolerance +/-1px.",
67
+ },
68
+ {
69
+ id: "styles",
70
+ label: "Styles",
71
+ status: "pass",
72
+ value: "3/3 matched",
73
+ detail: "All checked computed styles are within tolerance.",
74
+ },
75
+ ]);
76
+ });
77
+
78
+ it("fails when visual diff exceeds threshold and explains changed regions", () => {
79
+ const report = comparePreviewToDesign({
80
+ design: {
81
+ kind: "figma",
82
+ label: "Card",
83
+ size: { width: 320, height: 220 },
84
+ },
85
+ preview: {
86
+ kind: "preview",
87
+ label: "Card",
88
+ componentKey: "Card",
89
+ size: { width: 336, height: 220 },
90
+ },
91
+ imageDiff: {
92
+ diffPercentage: 4.75,
93
+ changedRegions: [{ x: 24, y: 16, width: 120, height: 48 }],
94
+ },
95
+ thresholds: {
96
+ imageDiffPercentage: 1,
97
+ },
98
+ });
99
+
100
+ expect(report.status).toBe("fail");
101
+ expect(report.image?.match).toBe(false);
102
+ expect(report.dimensions?.match).toBe(false);
103
+ expect(report.findings).toMatchObject([
104
+ {
105
+ code: "image-diff-threshold-exceeded",
106
+ severity: "error",
107
+ bounds: { x: 24, y: 16, width: 120, height: 48 },
108
+ },
109
+ {
110
+ code: "dimension-mismatch",
111
+ severity: "warning",
112
+ },
113
+ ]);
114
+ expect(getPreviewComparisonSignals(report)).toMatchObject([
115
+ {
116
+ id: "image",
117
+ status: "fail",
118
+ value: "4.75% changed",
119
+ },
120
+ {
121
+ id: "dimensions",
122
+ status: "review",
123
+ value: "+16 x 0 px",
124
+ },
125
+ {
126
+ id: "styles",
127
+ status: "missing",
128
+ value: "No styles",
129
+ },
130
+ ]);
131
+ expect(getPreviewComparisonRegionOverlays(report)).toEqual([
132
+ {
133
+ x: 24,
134
+ y: 16,
135
+ width: 120,
136
+ height: 48,
137
+ label: "Changed region x 24 y 16 width 120 height 48",
138
+ leftPercent: 7.14,
139
+ topPercent: 7.27,
140
+ widthPercent: 35.71,
141
+ heightPercent: 21.82,
142
+ },
143
+ ]);
144
+ });
145
+
146
+ it("clips direct comparison overlays to the comparison source", () => {
147
+ const report = comparePreviewToDesign({
148
+ design: {
149
+ kind: "figma",
150
+ label: "Card",
151
+ size: { width: 100, height: 80 },
152
+ },
153
+ preview: {
154
+ kind: "preview",
155
+ label: "Card",
156
+ componentKey: "Card",
157
+ size: { width: 100, height: 80 },
158
+ },
159
+ imageDiff: {
160
+ diffPercentage: 0.2,
161
+ changedRegions: [
162
+ { x: 90, y: 70, width: 20, height: 20 },
163
+ { x: 110, y: 10, width: 10, height: 10 },
164
+ ],
165
+ },
166
+ });
167
+
168
+ expect(getPreviewComparisonRegionOverlays(report)).toEqual([
169
+ {
170
+ x: 90,
171
+ y: 70,
172
+ width: 20,
173
+ height: 20,
174
+ label: "Changed region x 90 y 70 width 20 height 20",
175
+ leftPercent: 90,
176
+ topPercent: 87.5,
177
+ widthPercent: 10,
178
+ heightPercent: 12.5,
179
+ },
180
+ ]);
181
+ });
182
+
183
+ it("compares style values with color and numeric tolerance", () => {
184
+ const report = comparePreviewToDesign({
185
+ design: {
186
+ kind: "figma",
187
+ label: "Input",
188
+ styles: {
189
+ backgroundColor: "#fff8",
190
+ borderColor: "#d0d5dd",
191
+ borderRadius: "6px",
192
+ padding: "12px",
193
+ },
194
+ },
195
+ preview: {
196
+ kind: "preview",
197
+ label: "Input",
198
+ componentKey: "Input",
199
+ styles: {
200
+ backgroundColor: "rgb(255 255 255 / 53%)",
201
+ borderColor: "rgb(210, 215, 222)",
202
+ borderRadius: "9px",
203
+ padding: "12.5px",
204
+ },
205
+ },
206
+ });
207
+
208
+ expect(report.status).toBe("review");
209
+ expect(report.styles?.properties).toEqual([
210
+ {
211
+ property: "backgroundColor",
212
+ design: "#fff8",
213
+ preview: "rgb(255 255 255 / 53%)",
214
+ match: true,
215
+ },
216
+ {
217
+ property: "borderColor",
218
+ design: "#d0d5dd",
219
+ preview: "rgb(210, 215, 222)",
220
+ match: true,
221
+ },
222
+ {
223
+ property: "borderRadius",
224
+ design: "6px",
225
+ preview: "9px",
226
+ match: false,
227
+ },
228
+ {
229
+ property: "padding",
230
+ design: "12px",
231
+ preview: "12.5px",
232
+ match: true,
233
+ },
234
+ ]);
235
+ });
236
+
237
+ it("marks reports incomplete when adapters provide no comparison signals", () => {
238
+ const report = comparePreviewToDesign({
239
+ design: {
240
+ kind: "figma",
241
+ label: "Empty",
242
+ },
243
+ preview: {
244
+ kind: "preview",
245
+ label: "Empty",
246
+ componentKey: "Empty",
247
+ },
248
+ });
249
+
250
+ expect(report.status).toBe("incomplete");
251
+ expect(report.score).toBeNull();
252
+ expect(report.findings).toMatchObject([
253
+ {
254
+ code: "missing-comparison-signal",
255
+ severity: "warning",
256
+ },
257
+ ]);
258
+ });
259
+
260
+ it("builds manifest-ready comparison evidence from a live report", () => {
261
+ const report = comparePreviewToDesign({
262
+ design: {
263
+ kind: "figma",
264
+ label: "Button / Primary",
265
+ fileKey: "file123",
266
+ nodeId: "1:2",
267
+ nodeUrl: "https://www.figma.com/design/file123/System?node-id=1%3A2",
268
+ imageDataUrl: "data:image/png;base64,figma",
269
+ size: { width: 120, height: 40 },
270
+ styles: {
271
+ backgroundColor: "#006adc",
272
+ ignored: undefined,
273
+ },
274
+ },
275
+ preview: {
276
+ kind: "preview",
277
+ label: "Button / Primary",
278
+ componentKey: "Button",
279
+ variantId: "primary",
280
+ theme: "light",
281
+ viewport: { width: 120, height: 40 },
282
+ imageDataUrl: "data:image/png;base64,preview",
283
+ size: { width: 120, height: 40 },
284
+ styles: {
285
+ backgroundColor: "rgb(0, 106, 220)",
286
+ },
287
+ capturedAt: "2026-05-24T00:00:00.000Z",
288
+ },
289
+ imageDiff: {
290
+ diffPercentage: 0.2,
291
+ diffImageDataUrl: "data:image/png;base64,diff",
292
+ diffPixelCount: 96,
293
+ totalPixels: 4800,
294
+ changedRegions: [{ x: 10, y: 8, width: 20, height: 12 }],
295
+ },
296
+ });
297
+
298
+ expect(buildPreviewDesignComparisonEvidence(report)).toEqual({
299
+ state: {
300
+ variantId: "primary",
301
+ theme: "light",
302
+ viewport: { width: 120, height: 40 },
303
+ },
304
+ previewImageUrl: "data:image/png;base64,preview",
305
+ previewSize: { width: 120, height: 40 },
306
+ designStyles: {
307
+ backgroundColor: "#006adc",
308
+ },
309
+ previewStyles: {
310
+ backgroundColor: "rgb(0, 106, 220)",
311
+ },
312
+ diffImageUrl: "data:image/png;base64,diff",
313
+ diffPercentage: 0.2,
314
+ diffPixelCount: 96,
315
+ totalPixels: 4800,
316
+ changedRegions: [{ x: 10, y: 8, width: 20, height: 12 }],
317
+ capturedAt: "2026-05-24T00:00:00.000Z",
318
+ });
319
+ expect(buildPreviewDesignReferenceFromReport(report)).toEqual({
320
+ provider: "figma",
321
+ label: "Button / Primary",
322
+ nodeUrl: "https://www.figma.com/design/file123/System?node-id=1%3A2",
323
+ fileKey: "file123",
324
+ nodeId: "1:2",
325
+ imageUrl: "data:image/png;base64,figma",
326
+ size: { width: 120, height: 40 },
327
+ comparison: buildPreviewDesignComparisonEvidence(report),
328
+ });
329
+
330
+ expect(
331
+ buildPreviewComparisonArtifactPayload({
332
+ report,
333
+ assets: {
334
+ rendered: "Button-rendered.png",
335
+ figma: "Button-figma.png",
336
+ diff: "Button-diff.png",
337
+ },
338
+ })
339
+ ).toMatchObject({
340
+ target: {
341
+ componentKey: "Button",
342
+ variantId: "primary",
343
+ designReferenceScope: "variant",
344
+ },
345
+ report,
346
+ designReference: {
347
+ imageUrl: "Button-figma.png",
348
+ comparison: {
349
+ previewImageUrl: "Button-rendered.png",
350
+ diffImageUrl: "Button-diff.png",
351
+ },
352
+ },
353
+ comparisonEvidence: {
354
+ previewImageUrl: "Button-rendered.png",
355
+ diffImageUrl: "Button-diff.png",
356
+ },
357
+ });
358
+ });
359
+
360
+ it("does not persist stale comparison context as reusable evidence", () => {
361
+ const report = comparePreviewToDesign({
362
+ design: {
363
+ kind: "figma",
364
+ label: "Card",
365
+ size: { width: 320, height: 220 },
366
+ },
367
+ preview: {
368
+ kind: "preview",
369
+ label: "Card",
370
+ componentKey: "Card",
371
+ size: { width: 320, height: 220 },
372
+ },
373
+ context: {
374
+ scope: "component",
375
+ captured: { theme: "light" },
376
+ active: { theme: "dark" },
377
+ stale: true,
378
+ staleReasons: ["theme"],
379
+ },
380
+ });
381
+
382
+ expect(buildPreviewDesignComparisonEvidence(report)).toBeNull();
383
+ expect(buildPreviewDesignReferenceFromReport(report)).toEqual({
384
+ provider: "figma",
385
+ label: "Card",
386
+ size: { width: 320, height: 220 },
387
+ });
388
+ expect(
389
+ buildPreviewComparisonArtifactPayload({
390
+ report,
391
+ designReference: {
392
+ provider: "figma",
393
+ label: "Card",
394
+ comparison: {
395
+ diffPercentage: 0.1,
396
+ previewImageUrl: "stale-rendered.png",
397
+ },
398
+ },
399
+ })
400
+ ).toEqual({
401
+ target: {
402
+ componentKey: "Card",
403
+ designReferenceScope: "component",
404
+ },
405
+ report,
406
+ designReference: {
407
+ provider: "figma",
408
+ label: "Card",
409
+ },
410
+ });
411
+ });
412
+
413
+ it("formats comparison findings for terminal and machine surfaces", () => {
414
+ expect(
415
+ formatPreviewComparisonFinding({
416
+ code: "style-mismatch",
417
+ severity: "warning",
418
+ message: "borderRadius differs between Figma and the preview.",
419
+ property: "borderRadius",
420
+ })
421
+ ).toBe("WARNING: borderRadius: borderRadius differs between Figma and the preview.");
422
+ });
423
+
424
+ it("normalizes invalid comparison thresholds before scoring", () => {
425
+ const thresholds = resolvePreviewComparisonThresholds({
426
+ imageDiffPercentage: -1,
427
+ dimensionPx: Number.NaN,
428
+ numericPx: Number.NEGATIVE_INFINITY,
429
+ colorChannel: 300,
430
+ alpha: 2,
431
+ });
432
+
433
+ expect(thresholds).toEqual({
434
+ imageDiffPercentage: 1,
435
+ dimensionPx: 1,
436
+ numericPx: 1,
437
+ colorChannel: 5,
438
+ alpha: 0.05,
439
+ });
440
+
441
+ const report = comparePreviewToDesign({
442
+ design: {
443
+ kind: "figma",
444
+ label: "Badge",
445
+ size: { width: 80, height: 24 },
446
+ },
447
+ preview: {
448
+ kind: "preview",
449
+ label: "Badge",
450
+ componentKey: "Badge",
451
+ size: { width: 80, height: 24 },
452
+ },
453
+ imageDiff: {
454
+ diffPercentage: 0.5,
455
+ },
456
+ thresholds: {
457
+ imageDiffPercentage: -1,
458
+ dimensionPx: Number.NaN,
459
+ },
460
+ });
461
+
462
+ expect(report.thresholds.imageDiffPercentage).toBe(1);
463
+ expect(report.thresholds.dimensionPx).toBe(1);
464
+ expect(report.status).toBe("pass");
465
+ });
466
+
467
+ it("preserves valid zero thresholds for strict comparisons", () => {
468
+ expect(
469
+ resolvePreviewComparisonThresholds({
470
+ imageDiffPercentage: 0,
471
+ dimensionPx: 0,
472
+ numericPx: 0,
473
+ colorChannel: 0,
474
+ alpha: 0,
475
+ })
476
+ ).toEqual({
477
+ imageDiffPercentage: 0,
478
+ dimensionPx: 0,
479
+ numericPx: 0,
480
+ colorChannel: 0,
481
+ alpha: 0,
482
+ });
483
+ });
484
+
485
+ it("skips invalid runtime comparison signals before scoring", () => {
486
+ const report = comparePreviewToDesign({
487
+ design: {
488
+ kind: "figma",
489
+ label: "Runtime input",
490
+ size: { width: 120, height: 40 },
491
+ },
492
+ preview: {
493
+ kind: "preview",
494
+ label: "Runtime input",
495
+ componentKey: "RuntimeInput",
496
+ size: { width: 120, height: 40 },
497
+ },
498
+ imageDiff: {
499
+ diffPercentage: Number.NaN,
500
+ },
501
+ });
502
+
503
+ expect(report.image).toBeUndefined();
504
+ expect(report.dimensions?.match).toBe(true);
505
+ expect(report.status).toBe("review");
506
+ expect(report.findings).toMatchObject([
507
+ {
508
+ code: "invalid-comparison-signal",
509
+ severity: "warning",
510
+ },
511
+ ]);
512
+ });
513
+
514
+ it("omits invalid optional image evidence from runtime reports", () => {
515
+ const report = comparePreviewToDesign({
516
+ design: {
517
+ kind: "figma",
518
+ label: "Runtime image",
519
+ size: { width: 120, height: 40 },
520
+ },
521
+ preview: {
522
+ kind: "preview",
523
+ label: "Runtime image",
524
+ componentKey: "RuntimeImage",
525
+ size: { width: 120, height: 40 },
526
+ },
527
+ imageDiff: {
528
+ diffPercentage: 0.2,
529
+ diffPixelCount: 5000,
530
+ totalPixels: 4800,
531
+ changedRegions: [
532
+ { x: 4, y: 4, width: 12, height: 8 },
533
+ { x: -1, y: 0, width: 12, height: 8 },
534
+ ],
535
+ },
536
+ });
537
+
538
+ expect(report.image).toMatchObject({
539
+ diffPercentage: 0.2,
540
+ changedRegions: [{ x: 4, y: 4, width: 12, height: 8 }],
541
+ });
542
+ expect(report.image?.diffPixelCount).toBeUndefined();
543
+ expect(report.image?.totalPixels).toBeUndefined();
544
+ expect(report.status).toBe("review");
545
+ expect(report.findings).toEqual(
546
+ expect.arrayContaining([
547
+ expect.objectContaining({
548
+ code: "invalid-comparison-signal",
549
+ message:
550
+ "Image diff pixel counts must be whole numbers with changed pixels less than or equal to total pixels.",
551
+ }),
552
+ expect.objectContaining({
553
+ code: "invalid-comparison-signal",
554
+ message: "Changed regions must use finite non-negative coordinates and positive sizes.",
555
+ }),
556
+ ])
557
+ );
558
+ });
559
+ });