@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,332 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import type {
4
+ SerializedCompiledFragment,
5
+ SerializedExactStyleSheet,
6
+ } from '../classname-contract.js';
7
+ import {
8
+ __clearExactStylesheets,
9
+ __registerExactStylesheet,
10
+ classNameHasInteractiveConditions,
11
+ classNameHasMediaConditions,
12
+ resolveClassNameStyle,
13
+ setMediaState,
14
+ } from '../classname-resolve.js';
15
+ import {
16
+ resolveClassNameStyle as resolveRuntimeClassNameStyle,
17
+ } from '../classname-runtime.js';
18
+ import { createInstance } from '../host-ops.js';
19
+
20
+ function rgba(r: number, g: number, b: number, a = 255) {
21
+ return { r, g, b, a };
22
+ }
23
+
24
+ function points(value: number) {
25
+ return { type: 'points' as const, value };
26
+ }
27
+
28
+ function fragment(
29
+ order: number,
30
+ style: SerializedCompiledFragment['style'],
31
+ condition: SerializedCompiledFragment['condition'] = null,
32
+ mediaGate: SerializedCompiledFragment['mediaGate'] = null,
33
+ ): SerializedCompiledFragment {
34
+ return {
35
+ order,
36
+ condition,
37
+ mediaGate,
38
+ style,
39
+ };
40
+ }
41
+
42
+ function registerArtifact(
43
+ sourceId: string,
44
+ classes: SerializedExactStyleSheet['classes'],
45
+ warnings: string[] = [],
46
+ ): void {
47
+ __registerExactStylesheet(sourceId, {
48
+ classes,
49
+ warnings,
50
+ });
51
+ }
52
+
53
+ describe('native className runtime resolution', () => {
54
+ beforeEach(() => {
55
+ __clearExactStylesheets();
56
+ setMediaState({
57
+ width: 400,
58
+ height: 800,
59
+ orientation: 'portrait',
60
+ colorScheme: 'light',
61
+ reducedMotion: false,
62
+ });
63
+ });
64
+
65
+ it('publishes registered styles to the lightweight startup resolver', () => {
66
+ expect(resolveRuntimeClassNameStyle('card')).toEqual({});
67
+
68
+ registerArtifact('/tmp/runtime.css', {
69
+ card: [fragment(1, { opacity: 0.75 })],
70
+ });
71
+
72
+ expect(resolveRuntimeClassNameStyle('card')).toEqual({ opacity: 0.75 });
73
+ });
74
+
75
+ it('resolves declarations by stylesheet source order rather than class token order', () => {
76
+ registerArtifact('/tmp/source-order.css', {
77
+ // Match the compiler's rule ordering: lower values are earlier in the
78
+ // stylesheet, higher values are later and therefore win the cascade.
79
+ 'pt-2': [fragment(0, { paddingTop: points(8) })],
80
+ 'p-4': [
81
+ fragment(1, {
82
+ paddingTop: points(16),
83
+ paddingRight: points(16),
84
+ paddingBottom: points(16),
85
+ paddingLeft: points(16),
86
+ }),
87
+ ],
88
+ });
89
+
90
+ const forward = resolveClassNameStyle('p-4 pt-2');
91
+ const reverse = resolveClassNameStyle('pt-2 p-4');
92
+
93
+ expect(forward).toEqual(reverse);
94
+ expect(forward.paddingTop).toEqual(points(16));
95
+ expect(forward.paddingRight).toEqual(points(16));
96
+ expect(forward.paddingBottom).toEqual(points(16));
97
+ expect(forward.paddingLeft).toEqual(points(16));
98
+ });
99
+
100
+ it('applies disabled pseudo-class fragments when disabled is active', () => {
101
+ registerArtifact('/tmp/disabled.css', {
102
+ button: [
103
+ fragment(1, { opacity: 1 }),
104
+ fragment(2, { opacity: 0.4 }, { type: 'pseudo', pseudo: 'disabled' }),
105
+ ],
106
+ });
107
+
108
+ expect(resolveClassNameStyle('button')).toEqual({ opacity: 1 });
109
+ expect(resolveClassNameStyle('button', { props: { disabled: true } })).toEqual({
110
+ opacity: 0.4,
111
+ });
112
+ });
113
+
114
+ it('applies media-gated fragments when viewport state matches', () => {
115
+ registerArtifact('/tmp/responsive.css', {
116
+ item: [
117
+ fragment(1, { flexDirection: 'column' }),
118
+ fragment(2, { flexDirection: 'row' }, {
119
+ type: 'media',
120
+ media: [{ feature: 'min-width', value: 768 }],
121
+ }),
122
+ ],
123
+ });
124
+
125
+ expect(resolveClassNameStyle('item')).toEqual({ flexDirection: 'column' });
126
+
127
+ setMediaState({ width: 1024 });
128
+ expect(resolveClassNameStyle('item')).toEqual({ flexDirection: 'row' });
129
+ });
130
+
131
+ it('supports prefers-color-scheme fragments', () => {
132
+ registerArtifact('/tmp/dark.css', {
133
+ bg: [
134
+ fragment(1, { backgroundColor: rgba(255, 255, 255) }),
135
+ fragment(2, { backgroundColor: rgba(0, 0, 0) }, {
136
+ type: 'media',
137
+ media: [{ feature: 'prefers-color-scheme', value: 'dark' }],
138
+ }),
139
+ ],
140
+ });
141
+
142
+ expect(resolveClassNameStyle('bg').backgroundColor).toEqual(rgba(255, 255, 255));
143
+
144
+ setMediaState({ colorScheme: 'dark' });
145
+ expect(resolveClassNameStyle('bg').backgroundColor).toEqual(rgba(0, 0, 0));
146
+ });
147
+
148
+ it('honors strict greater-than media range operators', () => {
149
+ registerArtifact('/tmp/strict-range.css', {
150
+ base: [
151
+ fragment(1, { flexDirection: 'column' }),
152
+ fragment(2, { flexDirection: 'row' }, {
153
+ type: 'media',
154
+ media: [{ feature: 'gt-width', value: 768 }],
155
+ }),
156
+ ],
157
+ });
158
+
159
+ setMediaState({ width: 768 });
160
+ expect(resolveClassNameStyle('base').flexDirection).toBe('column');
161
+
162
+ setMediaState({ width: 769 });
163
+ expect(resolveClassNameStyle('base').flexDirection).toBe('row');
164
+ });
165
+
166
+ it('applies hover, focus, active, and positional pseudo fragments', () => {
167
+ registerArtifact('/tmp/interactive.css', {
168
+ btn: [
169
+ fragment(1, { backgroundColor: rgba(59, 130, 246) }),
170
+ fragment(2, { backgroundColor: rgba(37, 99, 235) }, { type: 'pseudo', pseudo: 'hover' }),
171
+ fragment(3, { borderColor: rgba(59, 130, 246) }, { type: 'pseudo', pseudo: 'focus' }),
172
+ fragment(4, { borderColor: rgba(29, 78, 216) }, { type: 'pseudo', pseudo: 'active' }),
173
+ ],
174
+ item: [
175
+ fragment(1, { marginTop: points(8) }),
176
+ fragment(2, { marginTop: points(0) }, { type: 'pseudo', pseudo: 'first-child' }),
177
+ fragment(3, { marginBottom: points(0) }, { type: 'pseudo', pseudo: 'last-child' }),
178
+ ],
179
+ });
180
+
181
+ const hovered = resolveClassNameStyle('btn', {
182
+ interaction: { hovered: true, focused: false, active: false, focusVisible: false },
183
+ });
184
+ expect(hovered.backgroundColor).toEqual(rgba(37, 99, 235));
185
+
186
+ const focused = resolveClassNameStyle('btn', {
187
+ interaction: { hovered: false, focused: true, active: false, focusVisible: false },
188
+ });
189
+ expect(focused.borderColor).toEqual(rgba(59, 130, 246));
190
+
191
+ const active = resolveClassNameStyle('btn', {
192
+ interaction: { hovered: false, focused: false, active: true, focusVisible: false },
193
+ });
194
+ expect(active.borderColor).toEqual(rgba(29, 78, 216));
195
+
196
+ const first = resolveClassNameStyle('item', {
197
+ position: { isFirst: true, isLast: false },
198
+ });
199
+ expect(first.marginTop).toEqual(points(0));
200
+
201
+ const last = resolveClassNameStyle('item', {
202
+ position: { isFirst: false, isLast: true },
203
+ });
204
+ expect(last.marginBottom).toEqual(points(0));
205
+ });
206
+
207
+ it('detects interactive and media-conditional fragments', () => {
208
+ registerArtifact('/tmp/detect.css', {
209
+ static: [fragment(1, { textColor: rgba(255, 0, 0) })],
210
+ hoverable: [
211
+ fragment(1, { textColor: rgba(255, 0, 0) }),
212
+ fragment(2, { textColor: rgba(0, 0, 255) }, { type: 'pseudo', pseudo: 'hover' }),
213
+ ],
214
+ responsive: [
215
+ fragment(1, { textColor: rgba(255, 0, 0) }),
216
+ fragment(2, { textColor: rgba(0, 0, 255) }, {
217
+ type: 'media',
218
+ media: [{ feature: 'min-width', value: 768 }],
219
+ }),
220
+ ],
221
+ });
222
+
223
+ expect(classNameHasInteractiveConditions('static')).toBe(false);
224
+ expect(classNameHasInteractiveConditions('hoverable')).toBe(true);
225
+ expect(classNameHasMediaConditions('static')).toBe(false);
226
+ expect(classNameHasMediaConditions('responsive')).toBe(true);
227
+ });
228
+
229
+ it('combines media gates with pseudo conditions', () => {
230
+ registerArtifact('/tmp/media-hover.css', {
231
+ btn: [
232
+ fragment(1, { backgroundColor: rgba(255, 255, 255) }),
233
+ fragment(
234
+ 2,
235
+ { backgroundColor: rgba(59, 130, 246) },
236
+ { type: 'pseudo', pseudo: 'hover' },
237
+ [{ feature: 'min-width', value: 768 }],
238
+ ),
239
+ ],
240
+ });
241
+
242
+ const narrowHover = resolveClassNameStyle('btn', {
243
+ interaction: { hovered: true, focused: false, active: false, focusVisible: false },
244
+ });
245
+ expect(narrowHover.backgroundColor).toEqual(rgba(255, 255, 255));
246
+
247
+ setMediaState({ width: 1024 });
248
+ const wideHover = resolveClassNameStyle('btn', {
249
+ interaction: { hovered: true, focused: false, active: false, focusVisible: false },
250
+ });
251
+ expect(wideHover.backgroundColor).toEqual(rgba(59, 130, 246));
252
+ });
253
+
254
+ it('resolves group and peer modifiers from provided interaction state', () => {
255
+ registerArtifact('/tmp/group-peer.css', {
256
+ child: [fragment(1, { textColor: rgba(0, 0, 0) })],
257
+ 'group-hover:text-blue': [
258
+ fragment(2, { textColor: rgba(59, 130, 246) }, {
259
+ type: 'group',
260
+ groupName: null,
261
+ pseudo: 'hover',
262
+ }),
263
+ ],
264
+ 'peer-focus:ring': [
265
+ fragment(3, { borderColor: rgba(59, 130, 246) }, {
266
+ type: 'peer',
267
+ peerName: null,
268
+ pseudo: 'focus',
269
+ }),
270
+ ],
271
+ });
272
+
273
+ const groupStates = new Map([
274
+ [
275
+ null,
276
+ { hovered: true, focused: false, active: false, focusVisible: false },
277
+ ],
278
+ ]);
279
+
280
+ const grouped = resolveClassNameStyle('child group-hover:text-blue', {
281
+ groupStates,
282
+ });
283
+ expect(grouped.textColor).toEqual(rgba(59, 130, 246));
284
+
285
+ const peerFocused = resolveClassNameStyle('peer-focus:ring', {
286
+ peerStates: {
287
+ getState() {
288
+ return { hovered: false, focused: true, active: false, focusVisible: false };
289
+ },
290
+ },
291
+ });
292
+ expect(peerFocused.borderColor).toEqual(rgba(59, 130, 246));
293
+ });
294
+
295
+ it('preserves normalized numeric style values from precompiled fragments', () => {
296
+ registerArtifact('/tmp/tracking.css', {
297
+ 'tracking-wide': [fragment(1, { letterSpacing: 0.4 })],
298
+ });
299
+
300
+ expect(resolveClassNameStyle('tracking-wide').letterSpacing).toBe(0.4);
301
+ });
302
+
303
+ it('lets inline style override className while preserving non-conflicting class properties', () => {
304
+ registerArtifact('/tmp/card.css', {
305
+ card: [
306
+ fragment(1, {
307
+ paddingTop: points(16),
308
+ paddingRight: points(16),
309
+ paddingBottom: points(16),
310
+ paddingLeft: points(16),
311
+ backgroundColor: rgba(255, 0, 0),
312
+ }),
313
+ ],
314
+ });
315
+
316
+ const instance = createInstance('View', {
317
+ className: 'card',
318
+ style: {
319
+ paddingTop: 4,
320
+ opacity: 0.5,
321
+ },
322
+ });
323
+
324
+ expect(instance.style.flexDirection).toBe('column');
325
+ expect(instance.style.paddingTop).toEqual(points(4));
326
+ expect(instance.style.paddingRight).toEqual(points(16));
327
+ expect(instance.style.paddingBottom).toEqual(points(16));
328
+ expect(instance.style.paddingLeft).toEqual(points(16));
329
+ expect(instance.style.backgroundColor).toEqual(rgba(255, 0, 0));
330
+ expect(instance.style.opacity).toBe(0.5);
331
+ });
332
+ });
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Color Parsing Tests
3
+ *
4
+ * Comprehensive tests for the color parsing utilities.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import { parseColor, isValidColor, colorToHex, colorToRgba } from '../style/color.js';
9
+
10
+ describe('parseColor', () => {
11
+ describe('hex colors', () => {
12
+ it('should parse 3-digit hex colors', () => {
13
+ expect(parseColor('#f00')).toEqual({ r: 255, g: 0, b: 0, a: 255 });
14
+ expect(parseColor('#0f0')).toEqual({ r: 0, g: 255, b: 0, a: 255 });
15
+ expect(parseColor('#00f')).toEqual({ r: 0, g: 0, b: 255, a: 255 });
16
+ expect(parseColor('#fff')).toEqual({ r: 255, g: 255, b: 255, a: 255 });
17
+ expect(parseColor('#000')).toEqual({ r: 0, g: 0, b: 0, a: 255 });
18
+ expect(parseColor('#abc')).toEqual({ r: 170, g: 187, b: 204, a: 255 });
19
+ });
20
+
21
+ it('should parse 6-digit hex colors', () => {
22
+ expect(parseColor('#ff0000')).toEqual({ r: 255, g: 0, b: 0, a: 255 });
23
+ expect(parseColor('#00ff00')).toEqual({ r: 0, g: 255, b: 0, a: 255 });
24
+ expect(parseColor('#0000ff')).toEqual({ r: 0, g: 0, b: 255, a: 255 });
25
+ expect(parseColor('#ffffff')).toEqual({ r: 255, g: 255, b: 255, a: 255 });
26
+ expect(parseColor('#000000')).toEqual({ r: 0, g: 0, b: 0, a: 255 });
27
+ expect(parseColor('#808080')).toEqual({ r: 128, g: 128, b: 128, a: 255 });
28
+ expect(parseColor('#6366f1')).toEqual({ r: 99, g: 102, b: 241, a: 255 });
29
+ });
30
+
31
+ it('should parse 8-digit hex colors with alpha', () => {
32
+ expect(parseColor('#ff000080')).toEqual({ r: 255, g: 0, b: 0, a: 128 });
33
+ expect(parseColor('#00ff00ff')).toEqual({ r: 0, g: 255, b: 0, a: 255 });
34
+ expect(parseColor('#0000ff00')).toEqual({ r: 0, g: 0, b: 255, a: 0 });
35
+ expect(parseColor('#10b98120')).toEqual({ r: 16, g: 185, b: 129, a: 32 });
36
+ });
37
+
38
+ it('should handle uppercase hex', () => {
39
+ expect(parseColor('#FF0000')).toEqual({ r: 255, g: 0, b: 0, a: 255 });
40
+ expect(parseColor('#AABBCC')).toEqual({ r: 170, g: 187, b: 204, a: 255 });
41
+ });
42
+
43
+ it('should handle mixed case hex', () => {
44
+ expect(parseColor('#AaBbCc')).toEqual({ r: 170, g: 187, b: 204, a: 255 });
45
+ });
46
+ });
47
+
48
+ describe('rgb/rgba colors', () => {
49
+ it('should parse rgb() colors', () => {
50
+ expect(parseColor('rgb(255, 0, 0)')).toEqual({ r: 255, g: 0, b: 0, a: 255 });
51
+ expect(parseColor('rgb(0, 255, 0)')).toEqual({ r: 0, g: 255, b: 0, a: 255 });
52
+ expect(parseColor('rgb(0, 0, 255)')).toEqual({ r: 0, g: 0, b: 255, a: 255 });
53
+ expect(parseColor('rgb(128, 128, 128)')).toEqual({ r: 128, g: 128, b: 128, a: 255 });
54
+ });
55
+
56
+ it('should parse rgba() colors', () => {
57
+ expect(parseColor('rgba(255, 0, 0, 0.5)')).toEqual({ r: 255, g: 0, b: 0, a: 128 });
58
+ expect(parseColor('rgba(0, 255, 0, 1)')).toEqual({ r: 0, g: 255, b: 0, a: 255 });
59
+ expect(parseColor('rgba(0, 0, 255, 0)')).toEqual({ r: 0, g: 0, b: 255, a: 0 });
60
+ expect(parseColor('rgba(128, 128, 128, 0.75)')).toEqual({ r: 128, g: 128, b: 128, a: 191 });
61
+ });
62
+
63
+ it('should handle spaces in rgb values', () => {
64
+ expect(parseColor('rgb(255,0,0)')).toEqual({ r: 255, g: 0, b: 0, a: 255 });
65
+ expect(parseColor('rgb( 255 , 0 , 0 )')).toEqual({ r: 255, g: 0, b: 0, a: 255 });
66
+ });
67
+ });
68
+
69
+ describe('named colors', () => {
70
+ it('should parse common named colors', () => {
71
+ expect(parseColor('white')).toEqual({ r: 255, g: 255, b: 255, a: 255 });
72
+ expect(parseColor('black')).toEqual({ r: 0, g: 0, b: 0, a: 255 });
73
+ expect(parseColor('red')).toEqual({ r: 255, g: 0, b: 0, a: 255 });
74
+ expect(parseColor('green')).toEqual({ r: 0, g: 128, b: 0, a: 255 });
75
+ expect(parseColor('blue')).toEqual({ r: 0, g: 0, b: 255, a: 255 });
76
+ expect(parseColor('yellow')).toEqual({ r: 255, g: 255, b: 0, a: 255 });
77
+ expect(parseColor('transparent')).toEqual({ r: 0, g: 0, b: 0, a: 0 });
78
+ });
79
+
80
+ it('should parse extended named colors', () => {
81
+ expect(parseColor('orange')).toEqual({ r: 255, g: 165, b: 0, a: 255 });
82
+ expect(parseColor('pink')).toEqual({ r: 255, g: 192, b: 203, a: 255 });
83
+ expect(parseColor('indigo')).toEqual({ r: 75, g: 0, b: 130, a: 255 });
84
+ expect(parseColor('teal')).toEqual({ r: 0, g: 128, b: 128, a: 255 });
85
+ });
86
+
87
+ it('should be case-insensitive for named colors', () => {
88
+ expect(parseColor('WHITE')).toEqual({ r: 255, g: 255, b: 255, a: 255 });
89
+ expect(parseColor('Black')).toEqual({ r: 0, g: 0, b: 0, a: 255 });
90
+ expect(parseColor('RED')).toEqual({ r: 255, g: 0, b: 0, a: 255 });
91
+ expect(parseColor('TrAnSpArEnT')).toEqual({ r: 0, g: 0, b: 0, a: 0 });
92
+ });
93
+ });
94
+
95
+ describe('edge cases', () => {
96
+ it('should return null for empty string', () => {
97
+ expect(parseColor('')).toBeNull();
98
+ });
99
+
100
+ it('should return null for undefined', () => {
101
+ expect(parseColor(undefined)).toBeNull();
102
+ });
103
+
104
+ it('should return null for null', () => {
105
+ expect(parseColor(null)).toBeNull();
106
+ });
107
+
108
+ it('should return null for unknown colors', () => {
109
+ expect(parseColor('unknowncolor')).toBeNull();
110
+ expect(parseColor('notacolor')).toBeNull();
111
+ });
112
+
113
+ it('should return null for invalid hex formats', () => {
114
+ expect(parseColor('#')).toBeNull();
115
+ expect(parseColor('#f')).toBeNull();
116
+ expect(parseColor('#ff')).toBeNull();
117
+ expect(parseColor('#fffff')).toBeNull();
118
+ expect(parseColor('#fffffffff')).toBeNull();
119
+ expect(parseColor('#gggggg')).toBeNull();
120
+ });
121
+
122
+ it('should handle whitespace', () => {
123
+ expect(parseColor(' #ff0000 ')).toEqual({ r: 255, g: 0, b: 0, a: 255 });
124
+ expect(parseColor(' white ')).toEqual({ r: 255, g: 255, b: 255, a: 255 });
125
+ });
126
+ });
127
+ });
128
+
129
+ describe('isValidColor', () => {
130
+ it('should return true for valid colors', () => {
131
+ expect(isValidColor('#ff0000')).toBe(true);
132
+ expect(isValidColor('rgb(255, 0, 0)')).toBe(true);
133
+ expect(isValidColor('white')).toBe(true);
134
+ });
135
+
136
+ it('should return false for invalid colors', () => {
137
+ expect(isValidColor('notacolor')).toBe(false);
138
+ expect(isValidColor('#gg0000')).toBe(false);
139
+ expect(isValidColor('')).toBe(false);
140
+ });
141
+ });
142
+
143
+ describe('colorToHex', () => {
144
+ it('should convert RGBA to hex without alpha', () => {
145
+ expect(colorToHex({ r: 255, g: 0, b: 0, a: 255 })).toBe('#ff0000');
146
+ expect(colorToHex({ r: 0, g: 255, b: 0, a: 255 })).toBe('#00ff00');
147
+ expect(colorToHex({ r: 0, g: 0, b: 255, a: 255 })).toBe('#0000ff');
148
+ });
149
+
150
+ it('should convert RGBA to hex with alpha when requested', () => {
151
+ expect(colorToHex({ r: 255, g: 0, b: 0, a: 128 }, true)).toBe('#ff000080');
152
+ expect(colorToHex({ r: 0, g: 0, b: 0, a: 0 }, true)).toBe('#00000000');
153
+ });
154
+
155
+ it('should not include alpha when it is 255', () => {
156
+ expect(colorToHex({ r: 255, g: 0, b: 0, a: 255 }, true)).toBe('#ff0000');
157
+ });
158
+ });
159
+
160
+ describe('colorToRgba', () => {
161
+ it('should convert RGBA to rgb() when alpha is 255', () => {
162
+ expect(colorToRgba({ r: 255, g: 0, b: 0, a: 255 })).toBe('rgb(255, 0, 0)');
163
+ });
164
+
165
+ it('should convert RGBA to rgba() when alpha is not 255', () => {
166
+ expect(colorToRgba({ r: 255, g: 0, b: 0, a: 128 })).toBe('rgba(255, 0, 0, 0.50)');
167
+ expect(colorToRgba({ r: 0, g: 0, b: 0, a: 0 })).toBe('rgba(0, 0, 0, 0.00)');
168
+ });
169
+ });