@ccheever/exact-renderer 0.1.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 (80) hide show
  1. package/package.json +118 -0
  2. package/src/__tests__/adapter-window-state.test.tsx +190 -0
  3. package/src/__tests__/attrs.test.ts +157 -0
  4. package/src/__tests__/classname.test.ts +332 -0
  5. package/src/__tests__/color.test.ts +169 -0
  6. package/src/__tests__/dom-mirror.test.ts +682 -0
  7. package/src/__tests__/dom-shim.test.ts +274 -0
  8. package/src/__tests__/fixtures/SvelteCounter.svelte +7 -0
  9. package/src/__tests__/fixtures/SvelteInput.svelte +8 -0
  10. package/src/__tests__/host-config.test.ts +51 -0
  11. package/src/__tests__/host-ops.test.ts +2234 -0
  12. package/src/__tests__/image-source.test.ts +135 -0
  13. package/src/__tests__/liquid-glass.test.ts +72 -0
  14. package/src/__tests__/multi-root.test.ts +118 -0
  15. package/src/__tests__/native-view-events.test.ts +102 -0
  16. package/src/__tests__/nodes.test.ts +399 -0
  17. package/src/__tests__/normalize.test.ts +576 -0
  18. package/src/__tests__/paragraph-lowering.test.tsx +144 -0
  19. package/src/__tests__/props.test.ts +518 -0
  20. package/src/__tests__/protocol-encoder.test.ts +732 -0
  21. package/src/__tests__/protocol-fixture-bytes.test.ts +41 -0
  22. package/src/__tests__/reconciler.test.tsx +241 -0
  23. package/src/__tests__/svelte-adapter.test.ts +166 -0
  24. package/src/__tests__/svg-source.test.ts +71 -0
  25. package/src/__tests__/tags.test.ts +354 -0
  26. package/src/__tests__/toggle.test.ts +441 -0
  27. package/src/__tests__/transitions.test.ts +106 -0
  28. package/src/__tests__/web-primitives.test.tsx +454 -0
  29. package/src/__tests__/window-hooks.test.tsx +447 -0
  30. package/src/adapter-contract.ts +68 -0
  31. package/src/attrs.ts +596 -0
  32. package/src/classname-contract.ts +87 -0
  33. package/src/classname-resolve.ts +553 -0
  34. package/src/classname-runtime.ts +29 -0
  35. package/src/components.ts +214 -0
  36. package/src/css-variable-context.ts +83 -0
  37. package/src/dom-hydration.ts +160 -0
  38. package/src/dom-mirror.ts +1459 -0
  39. package/src/dom-shim.ts +1736 -0
  40. package/src/group-context.ts +69 -0
  41. package/src/host-config.ts +431 -0
  42. package/src/host-ops.ts +3167 -0
  43. package/src/image-source.native.ts +703 -0
  44. package/src/image-source.ts +554 -0
  45. package/src/index.ts +278 -0
  46. package/src/inspector-runtime.ts +244 -0
  47. package/src/inspector.ts +3570 -0
  48. package/src/jsx-augmentations.ts +54 -0
  49. package/src/keyboard-avoidance.ts +217 -0
  50. package/src/native-primitives.ts +43 -0
  51. package/src/native-view-events.ts +322 -0
  52. package/src/native-view.ts +60 -0
  53. package/src/nodes/index.ts +41 -0
  54. package/src/nodes/node.ts +531 -0
  55. package/src/peer-context.ts +100 -0
  56. package/src/primitives.native.ts +8 -0
  57. package/src/primitives.ts +8 -0
  58. package/src/props/index.ts +14 -0
  59. package/src/props/normalize.ts +816 -0
  60. package/src/protocol/encoder.ts +940 -0
  61. package/src/protocol/index.ts +33 -0
  62. package/src/reconciler.ts +581 -0
  63. package/src/runtime.ts +11 -0
  64. package/src/safe-area.ts +543 -0
  65. package/src/solid.ts +490 -0
  66. package/src/style/color.js +1 -0
  67. package/src/style/color.ts +15 -0
  68. package/src/style/index.js +1 -0
  69. package/src/style/index.ts +22 -0
  70. package/src/style/normalize.js +1 -0
  71. package/src/style/normalize.ts +1426 -0
  72. package/src/svelte.ts +349 -0
  73. package/src/svg-source.ts +222 -0
  74. package/src/tags/index.ts +21 -0
  75. package/src/tags/tag-map.ts +289 -0
  76. package/src/text/paragraph-lowering.ts +310 -0
  77. package/src/types.ts +1175 -0
  78. package/src/vue.ts +535 -0
  79. package/src/web-host.ts +19 -0
  80. package/src/web-primitives.ts +1654 -0
@@ -0,0 +1,576 @@
1
+ /**
2
+ * Style Normalization Tests
3
+ *
4
+ * Comprehensive tests for the style normalization utilities.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import {
9
+ parseDimension,
10
+ parseFontWeight,
11
+ parseFontVariantNumeric,
12
+ extractTransforms,
13
+ normalizeTransition,
14
+ normalizeStyle,
15
+ transitionsEqual,
16
+ stylesEqual,
17
+ } from '../style/normalize.js';
18
+
19
+ describe('parseDimension', () => {
20
+ describe('number input', () => {
21
+ it('should convert numbers to points', () => {
22
+ expect(parseDimension(100)).toEqual({ type: 'points', value: 100 });
23
+ expect(parseDimension(0)).toEqual({ type: 'points', value: 0 });
24
+ expect(parseDimension(50.5)).toEqual({ type: 'points', value: 50.5 });
25
+ });
26
+
27
+ it('should handle negative numbers', () => {
28
+ expect(parseDimension(-10)).toEqual({ type: 'points', value: -10 });
29
+ });
30
+
31
+ it('should return undefined for non-finite numbers', () => {
32
+ expect(parseDimension(Infinity)).toBeUndefined();
33
+ expect(parseDimension(NaN)).toBeUndefined();
34
+ });
35
+ });
36
+
37
+ describe('string input', () => {
38
+ it('should parse "auto"', () => {
39
+ expect(parseDimension('auto')).toEqual({ type: 'auto' });
40
+ });
41
+
42
+ it('should parse percentage strings', () => {
43
+ expect(parseDimension('50%')).toEqual({ type: 'percent', value: 50 });
44
+ expect(parseDimension('100%')).toEqual({ type: 'percent', value: 100 });
45
+ expect(parseDimension('0%')).toEqual({ type: 'percent', value: 0 });
46
+ });
47
+
48
+ it('should parse px strings', () => {
49
+ expect(parseDimension('100px')).toEqual({ type: 'points', value: 100 });
50
+ expect(parseDimension('50.5px')).toEqual({ type: 'points', value: 50.5 });
51
+ });
52
+
53
+ it('should parse bare number strings as points', () => {
54
+ expect(parseDimension('100')).toEqual({ type: 'points', value: 100 });
55
+ expect(parseDimension('50.5')).toEqual({ type: 'points', value: 50.5 });
56
+ });
57
+
58
+ it('should handle whitespace', () => {
59
+ expect(parseDimension(' 100 ')).toEqual({ type: 'points', value: 100 });
60
+ expect(parseDimension(' auto ')).toEqual({ type: 'auto' });
61
+ });
62
+
63
+ it('should return undefined for invalid strings', () => {
64
+ expect(parseDimension('invalid')).toBeUndefined();
65
+ expect(parseDimension('abc%')).toBeUndefined();
66
+ });
67
+ });
68
+
69
+ describe('object input', () => {
70
+ it('should pass through valid dimension objects', () => {
71
+ expect(parseDimension({ type: 'auto' })).toEqual({ type: 'auto' });
72
+ expect(parseDimension({ type: 'points', value: 100 })).toEqual({ type: 'points', value: 100 });
73
+ expect(parseDimension({ type: 'percent', value: 50 })).toEqual({ type: 'percent', value: 50 });
74
+ });
75
+ });
76
+
77
+ describe('undefined/null input', () => {
78
+ it('should return undefined for undefined', () => {
79
+ expect(parseDimension(undefined)).toBeUndefined();
80
+ });
81
+
82
+ it('should return undefined for null', () => {
83
+ expect(parseDimension(null as any)).toBeUndefined();
84
+ });
85
+ });
86
+ });
87
+
88
+ describe('parseFontWeight', () => {
89
+ it('should parse numeric weights', () => {
90
+ expect(parseFontWeight(400)).toBe(400);
91
+ expect(parseFontWeight(700)).toBe(700);
92
+ expect(parseFontWeight(100)).toBe(100);
93
+ expect(parseFontWeight(900)).toBe(900);
94
+ });
95
+
96
+ it('should parse string keywords', () => {
97
+ expect(parseFontWeight('normal')).toBe(400);
98
+ expect(parseFontWeight('bold')).toBe(700);
99
+ });
100
+
101
+ it('should parse string numbers', () => {
102
+ expect(parseFontWeight('400')).toBe(400);
103
+ expect(parseFontWeight('700')).toBe(700);
104
+ });
105
+
106
+ it('should return undefined for invalid values', () => {
107
+ expect(parseFontWeight(50)).toBeUndefined();
108
+ expect(parseFontWeight(1000)).toBeUndefined();
109
+ expect(parseFontWeight('invalid')).toBeUndefined();
110
+ });
111
+
112
+ it('should return undefined for undefined/null', () => {
113
+ expect(parseFontWeight(undefined)).toBeUndefined();
114
+ expect(parseFontWeight(null as any)).toBeUndefined();
115
+ });
116
+ });
117
+
118
+ describe('parseFontVariantNumeric', () => {
119
+ it('encodes combinable numeric variants into a bitmask', () => {
120
+ expect(parseFontVariantNumeric('tabular-nums slashed-zero')).toBe(0x11);
121
+ });
122
+
123
+ it('uses last-token-wins semantics for conflicting spacing values', () => {
124
+ expect(parseFontVariantNumeric('tabular-nums proportional-nums')).toBe(0x02);
125
+ expect(parseFontVariantNumeric('proportional-nums tabular-nums')).toBe(0x01);
126
+ });
127
+
128
+ it('uses last-token-wins semantics for conflicting number-case values', () => {
129
+ expect(parseFontVariantNumeric('oldstyle-nums lining-nums')).toBe(0x04);
130
+ expect(parseFontVariantNumeric('lining-nums oldstyle-nums')).toBe(0x08);
131
+ });
132
+
133
+ it('treats normal as a reset', () => {
134
+ expect(parseFontVariantNumeric('tabular-nums normal slashed-zero')).toBe(0x10);
135
+ });
136
+ });
137
+
138
+ describe('extractTransforms', () => {
139
+ it('should extract translateX', () => {
140
+ const result = extractTransforms([{ translateX: 10 }]);
141
+ expect(result.x).toBe(10);
142
+ });
143
+
144
+ it('should extract translateY', () => {
145
+ const result = extractTransforms([{ translateY: 20 }]);
146
+ expect(result.y).toBe(20);
147
+ });
148
+
149
+ it('should extract scale', () => {
150
+ const result = extractTransforms([{ scale: 2 }]);
151
+ expect(result.scale).toBe(2);
152
+ });
153
+
154
+ it('should extract rotate in degrees', () => {
155
+ const result = extractTransforms([{ rotate: '90deg' }]);
156
+ expect(result.rotate).toBeCloseTo(Math.PI / 2);
157
+ });
158
+
159
+ it('should extract rotate in radians', () => {
160
+ const result = extractTransforms([{ rotate: '1.57rad' }]);
161
+ expect(result.rotate).toBeCloseTo(1.57);
162
+ });
163
+
164
+ it('should assume degrees if no unit', () => {
165
+ const result = extractTransforms([{ rotate: '180' }]);
166
+ expect(result.rotate).toBeCloseTo(Math.PI);
167
+ });
168
+
169
+ it('should handle multiple transforms', () => {
170
+ const result = extractTransforms([
171
+ { translateX: 10 },
172
+ { translateY: 20 },
173
+ { scale: 2 },
174
+ { rotate: '45deg' },
175
+ ]);
176
+ expect(result.x).toBe(10);
177
+ expect(result.y).toBe(20);
178
+ expect(result.scale).toBe(2);
179
+ expect(result.rotate).toBeCloseTo(Math.PI / 4);
180
+ });
181
+
182
+ it('should return empty object for undefined/null', () => {
183
+ expect(extractTransforms(undefined)).toEqual({});
184
+ expect(extractTransforms(null as any)).toEqual({});
185
+ });
186
+ });
187
+
188
+ describe('normalizeStyle', () => {
189
+ describe('dimensions', () => {
190
+ it('should normalize width and height', () => {
191
+ const result = normalizeStyle({ width: 100, height: 200 });
192
+ expect(result.width).toEqual({ type: 'points', value: 100 });
193
+ expect(result.height).toEqual({ type: 'points', value: 200 });
194
+ });
195
+
196
+ it('should normalize min/max dimensions', () => {
197
+ const result = normalizeStyle({
198
+ minWidth: 50,
199
+ maxWidth: 500,
200
+ minHeight: 100,
201
+ maxHeight: 1000,
202
+ });
203
+ expect(result.minWidth).toEqual({ type: 'points', value: 50 });
204
+ expect(result.maxWidth).toEqual({ type: 'points', value: 500 });
205
+ expect(result.minHeight).toEqual({ type: 'points', value: 100 });
206
+ expect(result.maxHeight).toEqual({ type: 'points', value: 1000 });
207
+ });
208
+ });
209
+
210
+ describe('padding', () => {
211
+ it('should normalize padding shorthand', () => {
212
+ const result = normalizeStyle({ padding: 10 });
213
+ expect(result.paddingTop).toEqual({ type: 'points', value: 10 });
214
+ expect(result.paddingRight).toEqual({ type: 'points', value: 10 });
215
+ expect(result.paddingBottom).toEqual({ type: 'points', value: 10 });
216
+ expect(result.paddingLeft).toEqual({ type: 'points', value: 10 });
217
+ });
218
+
219
+ it('should normalize paddingVertical/paddingHorizontal', () => {
220
+ const result = normalizeStyle({ paddingVertical: 10, paddingHorizontal: 20 });
221
+ expect(result.paddingTop).toEqual({ type: 'points', value: 10 });
222
+ expect(result.paddingBottom).toEqual({ type: 'points', value: 10 });
223
+ expect(result.paddingRight).toEqual({ type: 'points', value: 20 });
224
+ expect(result.paddingLeft).toEqual({ type: 'points', value: 20 });
225
+ });
226
+
227
+ it('should let specific padding override shorthand', () => {
228
+ const result = normalizeStyle({ padding: 10, paddingTop: 20 });
229
+ expect(result.paddingTop).toEqual({ type: 'points', value: 20 });
230
+ expect(result.paddingRight).toEqual({ type: 'points', value: 10 });
231
+ });
232
+ });
233
+
234
+ describe('margin', () => {
235
+ it('should normalize margin shorthand', () => {
236
+ const result = normalizeStyle({ margin: 10 });
237
+ expect(result.marginTop).toEqual({ type: 'points', value: 10 });
238
+ expect(result.marginRight).toEqual({ type: 'points', value: 10 });
239
+ expect(result.marginBottom).toEqual({ type: 'points', value: 10 });
240
+ expect(result.marginLeft).toEqual({ type: 'points', value: 10 });
241
+ });
242
+ });
243
+
244
+ describe('direction and logical properties', () => {
245
+ it('normalizes explicit direction and writingMode', () => {
246
+ const result = normalizeStyle(
247
+ { direction: 'rtl', writingMode: 'vertical-rl' },
248
+ 'row',
249
+ { includeInheritedValues: true },
250
+ );
251
+
252
+ expect(result.direction).toBe('rtl');
253
+ expect(result.writingMode).toBe('vertical-rl');
254
+ });
255
+
256
+ it('resolves logical spacing and inset props in ltr', () => {
257
+ const result = normalizeStyle(
258
+ {
259
+ marginStart: 8,
260
+ paddingEnd: 12,
261
+ start: 4,
262
+ },
263
+ 'row',
264
+ { resolvedDirection: 'ltr' },
265
+ );
266
+
267
+ expect(result.marginLeft).toEqual({ type: 'points', value: 8 });
268
+ expect(result.paddingRight).toEqual({ type: 'points', value: 12 });
269
+ expect(result.left).toEqual({ type: 'points', value: 4 });
270
+ });
271
+
272
+ it('resolves logical spacing and inset props in rtl', () => {
273
+ const result = normalizeStyle(
274
+ {
275
+ marginStart: 8,
276
+ paddingEnd: 12,
277
+ end: 4,
278
+ },
279
+ 'row',
280
+ { resolvedDirection: 'rtl' },
281
+ );
282
+
283
+ expect(result.marginRight).toEqual({ type: 'points', value: 8 });
284
+ expect(result.paddingLeft).toEqual({ type: 'points', value: 12 });
285
+ expect(result.left).toEqual({ type: 'points', value: 4 });
286
+ });
287
+
288
+ it('resolves inline logical props against vertical writing modes', () => {
289
+ const result = normalizeStyle(
290
+ {
291
+ writingMode: 'vertical-rl',
292
+ paddingInlineStart: 10,
293
+ marginInlineEnd: 5,
294
+ insetInlineStart: 7,
295
+ },
296
+ 'row',
297
+ {
298
+ resolvedDirection: 'ltr',
299
+ resolvedWritingMode: 'vertical-rl',
300
+ },
301
+ );
302
+
303
+ expect(result.paddingTop).toEqual({ type: 'points', value: 10 });
304
+ expect(result.marginBottom).toEqual({ type: 'points', value: 5 });
305
+ expect(result.top).toEqual({ type: 'points', value: 7 });
306
+ expect(result.paddingLeft).toBeUndefined();
307
+ expect(result.marginRight).toBeUndefined();
308
+ expect(result.left).toBeUndefined();
309
+ });
310
+
311
+ it('lets physical props override logical props', () => {
312
+ const result = normalizeStyle(
313
+ {
314
+ marginStart: 8,
315
+ marginLeft: 20,
316
+ paddingEnd: 12,
317
+ paddingRight: 16,
318
+ },
319
+ 'row',
320
+ { resolvedDirection: 'ltr' },
321
+ );
322
+
323
+ expect(result.marginLeft).toEqual({ type: 'points', value: 20 });
324
+ expect(result.paddingRight).toEqual({ type: 'points', value: 16 });
325
+ });
326
+ });
327
+
328
+ describe('flexbox', () => {
329
+ it('should normalize flex shorthand', () => {
330
+ const result = normalizeStyle({ flex: 1 });
331
+ expect(result.flexGrow).toBe(1);
332
+ expect(result.flexShrink).toBe(1);
333
+ });
334
+
335
+ it('should let specific flex props override shorthand', () => {
336
+ const result = normalizeStyle({ flex: 1, flexGrow: 2, flexShrink: 0 });
337
+ expect(result.flexGrow).toBe(2);
338
+ expect(result.flexShrink).toBe(0);
339
+ });
340
+
341
+ it('should normalize flexDirection', () => {
342
+ const result = normalizeStyle({ flexDirection: 'column' });
343
+ expect(result.flexDirection).toBe('column');
344
+ });
345
+
346
+ it('should apply default flexDirection for RN style', () => {
347
+ const result = normalizeStyle({}, 'column');
348
+ expect(result.flexDirection).toBe('column');
349
+ });
350
+
351
+ it('should not include flexDirection for web default', () => {
352
+ const result = normalizeStyle({}, 'row');
353
+ expect(result.flexDirection).toBeUndefined();
354
+ });
355
+
356
+ it('should normalize gap', () => {
357
+ const result = normalizeStyle({ gap: 10 });
358
+ expect(result.rowGap).toBe(10);
359
+ expect(result.columnGap).toBe(10);
360
+ });
361
+ });
362
+
363
+ describe('appearance', () => {
364
+ it('should normalize backgroundColor', () => {
365
+ const result = normalizeStyle({ backgroundColor: '#ff0000' });
366
+ expect(result.backgroundColor).toEqual({ r: 255, g: 0, b: 0, a: 255 });
367
+ });
368
+
369
+ it('should clamp opacity to 0-1', () => {
370
+ expect(normalizeStyle({ opacity: -0.5 }).opacity).toBe(0);
371
+ expect(normalizeStyle({ opacity: 0.5 }).opacity).toBe(0.5);
372
+ expect(normalizeStyle({ opacity: 1.5 }).opacity).toBe(1);
373
+ });
374
+
375
+ it('should normalize borderRadius', () => {
376
+ const result = normalizeStyle({ borderRadius: 10 });
377
+ expect(result.borderRadius).toBe(10);
378
+ });
379
+
380
+ it('should normalize borderRadius to non-negative', () => {
381
+ const result = normalizeStyle({ borderRadius: -5 });
382
+ expect(result.borderRadius).toBe(0);
383
+ });
384
+
385
+ it('should normalize boxShadow into canonical shadow properties', () => {
386
+ const result = normalizeStyle({ boxShadow: '0px 6px 18px rgba(44, 36, 22, 0.14)' });
387
+ expect(result.shadowColor).toEqual({ r: 44, g: 36, b: 22, a: 255 });
388
+ expect(result.shadowOpacity).toBeCloseTo(0.14);
389
+ expect(result.shadowOffsetX).toBe(0);
390
+ expect(result.shadowOffsetY).toBe(6);
391
+ expect(result.shadowRadius).toBe(18);
392
+ });
393
+
394
+ it('should let explicit shadow props override boxShadow values', () => {
395
+ const result = normalizeStyle({
396
+ boxShadow: '0px 6px 18px rgba(44, 36, 22, 0.14)',
397
+ shadowColor: '#5ad1ab',
398
+ shadowOffset: { width: 2, height: 8 },
399
+ shadowRadius: 12,
400
+ shadowOpacity: 0.2,
401
+ });
402
+ expect(result.shadowColor).toEqual({ r: 90, g: 209, b: 171, a: 255 });
403
+ expect(result.shadowOpacity).toBe(0.2);
404
+ expect(result.shadowOffsetX).toBe(2);
405
+ expect(result.shadowOffsetY).toBe(8);
406
+ expect(result.shadowRadius).toBe(12);
407
+ });
408
+ });
409
+
410
+ describe('typography', () => {
411
+ it('should normalize fontSize', () => {
412
+ const result = normalizeStyle({ fontSize: 16 });
413
+ expect(result.fontSize).toBe(16);
414
+ });
415
+
416
+ it('should normalize color to textColor', () => {
417
+ const result = normalizeStyle({ color: '#ffffff' });
418
+ expect(result.textColor).toEqual({ r: 255, g: 255, b: 255, a: 255 });
419
+ });
420
+
421
+ it('should normalize fontWeight string', () => {
422
+ const result = normalizeStyle({ fontWeight: 'bold' });
423
+ expect(result.fontWeight).toBe(700);
424
+ });
425
+
426
+ it('should normalize built-in font families', () => {
427
+ const result = normalizeStyle({ fontFamily: 'ui-monospace' });
428
+ expect(result.fontFamily).toBe(2);
429
+ });
430
+
431
+ it('should preserve textAlign auto for RTL support', () => {
432
+ const result = normalizeStyle({ textAlign: 'auto' });
433
+ expect(result.textAlign).toBe('auto');
434
+ });
435
+
436
+ it('should preserve italic fontStyle', () => {
437
+ const result = normalizeStyle({ fontStyle: 'italic' });
438
+ expect(result.fontStyle).toBe('italic');
439
+ });
440
+
441
+ it('should normalize fontVariantNumeric to a protocol bitmask', () => {
442
+ const result = normalizeStyle({ fontVariantNumeric: 'tabular-nums slashed-zero' });
443
+ expect(result.fontVariantNumeric).toBe(0x11);
444
+ });
445
+
446
+ it('should preserve textDecorationLine values', () => {
447
+ const result = normalizeStyle({ textDecorationLine: 'underline line-through' });
448
+ expect(result.textDecorationLine).toBe('underline line-through');
449
+ });
450
+ });
451
+
452
+ describe('empty style', () => {
453
+ it('should return empty object for undefined', () => {
454
+ const result = normalizeStyle(undefined);
455
+ expect(result).toEqual({});
456
+ });
457
+
458
+ it('should return empty object for empty style', () => {
459
+ const result = normalizeStyle({});
460
+ expect(result).toEqual({});
461
+ });
462
+ });
463
+ });
464
+
465
+ describe('normalizeTransition', () => {
466
+ it('should normalize object timing transitions', () => {
467
+ expect(
468
+ normalizeTransition({
469
+ opacity: { type: 'timing', duration: 180, easing: 'easeOut' },
470
+ })
471
+ ).toEqual({
472
+ opacity: {
473
+ type: 'timing',
474
+ duration: 180,
475
+ delay: 0,
476
+ easing: 'easeOut',
477
+ respectsReducedMotion: true,
478
+ },
479
+ });
480
+ });
481
+
482
+ it('should normalize object spring transitions', () => {
483
+ expect(
484
+ normalizeTransition({
485
+ transform: { type: 'spring', damping: 18, stiffness: 240, mass: 1.2, velocity: 0.3, delay: 20 },
486
+ })
487
+ ).toEqual({
488
+ transform: {
489
+ type: 'spring',
490
+ duration: undefined,
491
+ delay: 20,
492
+ damping: 18,
493
+ stiffness: 240,
494
+ mass: 1.2,
495
+ velocity: 0.3,
496
+ respectsReducedMotion: true,
497
+ },
498
+ });
499
+ });
500
+
501
+ it('should normalize string shorthand transitions', () => {
502
+ expect(normalizeTransition('opacity 180ms ease-out, transform 220ms spring(damping: 16, stiffness: 220)')).toEqual({
503
+ opacity: {
504
+ type: 'timing',
505
+ duration: 180,
506
+ delay: 0,
507
+ easing: 'easeOut',
508
+ respectsReducedMotion: true,
509
+ },
510
+ transform: {
511
+ type: 'spring',
512
+ duration: 220,
513
+ delay: 0,
514
+ damping: 16,
515
+ stiffness: 220,
516
+ mass: 1,
517
+ velocity: 0,
518
+ respectsReducedMotion: true,
519
+ },
520
+ });
521
+ });
522
+
523
+ it('should ignore invalid entries and none shorthand', () => {
524
+ expect(normalizeTransition('none')).toEqual({});
525
+ expect(normalizeTransition('opacity nope, width 100ms linear')).toEqual({});
526
+ });
527
+ });
528
+
529
+ describe('transitionsEqual', () => {
530
+ it('should treat equivalent timing maps as equal', () => {
531
+ const a = normalizeTransition({
532
+ opacity: { type: 'timing', duration: 180, easing: 'easeOut' },
533
+ });
534
+ const b = normalizeTransition('opacity 180ms ease-out');
535
+ expect(transitionsEqual(a, b)).toBe(true);
536
+ });
537
+
538
+ it('should detect transition differences', () => {
539
+ const a = normalizeTransition('opacity 180ms ease-out');
540
+ const b = normalizeTransition('opacity 200ms ease-out');
541
+ expect(transitionsEqual(a, b)).toBe(false);
542
+ });
543
+ });
544
+
545
+ describe('stylesEqual', () => {
546
+ it('should return true for identical objects', () => {
547
+ const style = { width: { type: 'points' as const, value: 100 } };
548
+ expect(stylesEqual(style, style)).toBe(true);
549
+ });
550
+
551
+ it('should return true for equal styles', () => {
552
+ const a = { width: { type: 'points' as const, value: 100 } };
553
+ const b = { width: { type: 'points' as const, value: 100 } };
554
+ expect(stylesEqual(a, b)).toBe(true);
555
+ });
556
+
557
+ it('should return false for different styles', () => {
558
+ const a = { width: { type: 'points' as const, value: 100 } };
559
+ const b = { width: { type: 'points' as const, value: 200 } };
560
+ expect(stylesEqual(a, b)).toBe(false);
561
+ });
562
+
563
+ it('should return false for different key counts', () => {
564
+ const a = { width: { type: 'points' as const, value: 100 } };
565
+ const b = { width: { type: 'points' as const, value: 100 }, height: { type: 'points' as const, value: 100 } };
566
+ expect(stylesEqual(a, b)).toBe(false);
567
+ });
568
+
569
+ it('should compare RGBA colors correctly', () => {
570
+ const a = { backgroundColor: { r: 255, g: 0, b: 0, a: 255 } };
571
+ const b = { backgroundColor: { r: 255, g: 0, b: 0, a: 255 } };
572
+ const c = { backgroundColor: { r: 0, g: 255, b: 0, a: 255 } };
573
+ expect(stylesEqual(a, b)).toBe(true);
574
+ expect(stylesEqual(a, c)).toBe(false);
575
+ });
576
+ });