onejs-react 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.
@@ -0,0 +1,674 @@
1
+ /**
2
+ * Tests for host-config.ts - the React reconciler implementation
3
+ *
4
+ * Tests cover:
5
+ * - Instance creation (createInstance)
6
+ * - Style property application and cleanup
7
+ * - ClassName management (add/remove/update)
8
+ * - Event handler registration
9
+ * - Component-specific props (text, value, label)
10
+ * - Child management (appendChild, insertBefore, removeChild)
11
+ */
12
+
13
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
14
+ import { hostConfig, type Instance } from '../host-config';
15
+ import { MockVisualElement, MockLength, MockColor, getEventAPI, flushMicrotasks, getCreatedElements } from './mocks';
16
+
17
+ // Helper to extract value from style (handles both raw values and MockLength/MockColor)
18
+ function getStyleValue(style: unknown): unknown {
19
+ if (style instanceof MockLength) return style.value;
20
+ if (style instanceof MockColor) return style;
21
+ return style;
22
+ }
23
+
24
+ describe('host-config', () => {
25
+ describe('createInstance', () => {
26
+ it('creates a VisualElement for ojs-view', () => {
27
+ const instance = hostConfig.createInstance('ojs-view', {}, null as any, null, null);
28
+
29
+ expect(instance).toBeDefined();
30
+ expect(instance.type).toBe('ojs-view');
31
+ expect(instance.element).toBeInstanceOf(MockVisualElement);
32
+ expect(instance.eventHandlers).toBeInstanceOf(Map);
33
+ expect(instance.appliedStyleKeys).toBeInstanceOf(Set);
34
+ });
35
+
36
+ it('creates a Label for ojs-label', () => {
37
+ const instance = hostConfig.createInstance('ojs-label', { text: 'Hello' }, null as any, null, null);
38
+
39
+ expect(instance.type).toBe('ojs-label');
40
+ expect(instance.element.text).toBe('Hello');
41
+ });
42
+
43
+ it('creates a Button for ojs-button', () => {
44
+ const instance = hostConfig.createInstance('ojs-button', { text: 'Click me' }, null as any, null, null);
45
+
46
+ expect(instance.type).toBe('ojs-button');
47
+ expect(instance.element.text).toBe('Click me');
48
+ });
49
+
50
+ it('creates a TextField for ojs-textfield', () => {
51
+ const instance = hostConfig.createInstance('ojs-textfield', { value: 'test' }, null as any, null, null);
52
+
53
+ expect(instance.type).toBe('ojs-textfield');
54
+ expect(instance.element.value).toBe('test');
55
+ });
56
+
57
+ it('creates a Toggle for ojs-toggle', () => {
58
+ const instance = hostConfig.createInstance('ojs-toggle', { value: true, label: 'Enable' }, null as any, null, null);
59
+
60
+ expect(instance.type).toBe('ojs-toggle');
61
+ expect(instance.element.value).toBe(true);
62
+ expect(instance.element.label).toBe('Enable');
63
+ });
64
+
65
+ it('throws for unknown element type', () => {
66
+ expect(() => {
67
+ hostConfig.createInstance('unknown-type', {}, null as any, null, null);
68
+ }).toThrow('Unknown element type: unknown-type');
69
+ });
70
+ });
71
+
72
+ describe('style application', () => {
73
+ it('applies style properties on creation', () => {
74
+ const instance = hostConfig.createInstance(
75
+ 'ojs-view',
76
+ { style: { width: 100, height: 50, backgroundColor: 'red' } },
77
+ null as any,
78
+ null,
79
+ null
80
+ );
81
+
82
+ // Length properties are now wrapped in MockLength
83
+ expect(getStyleValue(instance.element.style.width)).toBe(100);
84
+ expect(getStyleValue(instance.element.style.height)).toBe(50);
85
+ // Color properties are now wrapped in MockColor
86
+ expect(instance.element.style.backgroundColor).toBeInstanceOf(MockColor);
87
+ });
88
+
89
+ it('tracks applied style keys', () => {
90
+ const instance = hostConfig.createInstance(
91
+ 'ojs-view',
92
+ { style: { width: 100, height: 50 } },
93
+ null as any,
94
+ null,
95
+ null
96
+ );
97
+
98
+ expect(instance.appliedStyleKeys.has('width')).toBe(true);
99
+ expect(instance.appliedStyleKeys.has('height')).toBe(true);
100
+ expect(instance.appliedStyleKeys.has('backgroundColor')).toBe(false);
101
+ });
102
+
103
+ it('expands shorthand padding to individual properties', () => {
104
+ const instance = hostConfig.createInstance(
105
+ 'ojs-view',
106
+ { style: { padding: 10 } },
107
+ null as any,
108
+ null,
109
+ null
110
+ );
111
+
112
+ expect(getStyleValue(instance.element.style.paddingTop)).toBe(10);
113
+ expect(getStyleValue(instance.element.style.paddingRight)).toBe(10);
114
+ expect(getStyleValue(instance.element.style.paddingBottom)).toBe(10);
115
+ expect(getStyleValue(instance.element.style.paddingLeft)).toBe(10);
116
+ expect(instance.appliedStyleKeys.has('paddingTop')).toBe(true);
117
+ });
118
+
119
+ it('expands shorthand margin to individual properties', () => {
120
+ const instance = hostConfig.createInstance(
121
+ 'ojs-view',
122
+ { style: { margin: 20 } },
123
+ null as any,
124
+ null,
125
+ null
126
+ );
127
+
128
+ expect(getStyleValue(instance.element.style.marginTop)).toBe(20);
129
+ expect(getStyleValue(instance.element.style.marginRight)).toBe(20);
130
+ expect(getStyleValue(instance.element.style.marginBottom)).toBe(20);
131
+ expect(getStyleValue(instance.element.style.marginLeft)).toBe(20);
132
+ });
133
+
134
+ it('expands shorthand borderRadius', () => {
135
+ const instance = hostConfig.createInstance(
136
+ 'ojs-view',
137
+ { style: { borderRadius: 8 } },
138
+ null as any,
139
+ null,
140
+ null
141
+ );
142
+
143
+ expect(getStyleValue(instance.element.style.borderTopLeftRadius)).toBe(8);
144
+ expect(getStyleValue(instance.element.style.borderTopRightRadius)).toBe(8);
145
+ expect(getStyleValue(instance.element.style.borderBottomRightRadius)).toBe(8);
146
+ expect(getStyleValue(instance.element.style.borderBottomLeftRadius)).toBe(8);
147
+ });
148
+ });
149
+
150
+ describe('style updates (commitUpdate)', () => {
151
+ it('updates style properties', () => {
152
+ const instance = hostConfig.createInstance(
153
+ 'ojs-view',
154
+ { style: { width: 100 } },
155
+ null as any,
156
+ null,
157
+ null
158
+ );
159
+
160
+ // Simulate React calling commitUpdate
161
+ hostConfig.commitUpdate(
162
+ instance,
163
+ 'ojs-view',
164
+ { style: { width: 100 } },
165
+ { style: { width: 200 } },
166
+ null as any
167
+ );
168
+
169
+ expect(getStyleValue(instance.element.style.width)).toBe(200);
170
+ });
171
+
172
+ it('clears removed style properties', () => {
173
+ const instance = hostConfig.createInstance(
174
+ 'ojs-view',
175
+ { style: { width: 100, height: 50, backgroundColor: 'red' } },
176
+ null as any,
177
+ null,
178
+ null
179
+ );
180
+
181
+ // Update: remove width and backgroundColor, keep height
182
+ hostConfig.commitUpdate(
183
+ instance,
184
+ 'ojs-view',
185
+ { style: { width: 100, height: 50, backgroundColor: 'red' } },
186
+ { style: { height: 75 } },
187
+ null as any
188
+ );
189
+
190
+ expect(instance.element.style.width).toBeUndefined();
191
+ expect(instance.element.style.backgroundColor).toBeUndefined();
192
+ expect(getStyleValue(instance.element.style.height)).toBe(75);
193
+ });
194
+
195
+ it('clears expanded shorthand properties when shorthand is removed', () => {
196
+ const instance = hostConfig.createInstance(
197
+ 'ojs-view',
198
+ { style: { padding: 10, width: 100 } },
199
+ null as any,
200
+ null,
201
+ null
202
+ );
203
+
204
+ // Remove padding shorthand
205
+ hostConfig.commitUpdate(
206
+ instance,
207
+ 'ojs-view',
208
+ { style: { padding: 10, width: 100 } },
209
+ { style: { width: 100 } },
210
+ null as any
211
+ );
212
+
213
+ expect(instance.element.style.paddingTop).toBeUndefined();
214
+ expect(instance.element.style.paddingRight).toBeUndefined();
215
+ expect(instance.element.style.paddingBottom).toBeUndefined();
216
+ expect(instance.element.style.paddingLeft).toBeUndefined();
217
+ expect(getStyleValue(instance.element.style.width)).toBe(100);
218
+ });
219
+
220
+ it('clears all styles when style prop becomes undefined', () => {
221
+ const instance = hostConfig.createInstance(
222
+ 'ojs-view',
223
+ { style: { width: 100, height: 50 } },
224
+ null as any,
225
+ null,
226
+ null
227
+ );
228
+
229
+ hostConfig.commitUpdate(
230
+ instance,
231
+ 'ojs-view',
232
+ { style: { width: 100, height: 50 } },
233
+ {},
234
+ null as any
235
+ );
236
+
237
+ expect(instance.element.style.width).toBeUndefined();
238
+ expect(instance.element.style.height).toBeUndefined();
239
+ });
240
+
241
+ it('updates appliedStyleKeys after style change', () => {
242
+ const instance = hostConfig.createInstance(
243
+ 'ojs-view',
244
+ { style: { width: 100 } },
245
+ null as any,
246
+ null,
247
+ null
248
+ );
249
+
250
+ expect(instance.appliedStyleKeys.has('width')).toBe(true);
251
+ expect(instance.appliedStyleKeys.has('height')).toBe(false);
252
+
253
+ hostConfig.commitUpdate(
254
+ instance,
255
+ 'ojs-view',
256
+ { style: { width: 100 } },
257
+ { style: { height: 50 } },
258
+ null as any
259
+ );
260
+
261
+ expect(instance.appliedStyleKeys.has('width')).toBe(false);
262
+ expect(instance.appliedStyleKeys.has('height')).toBe(true);
263
+ });
264
+ });
265
+
266
+ describe('className management', () => {
267
+ it('applies className on creation', () => {
268
+ const instance = hostConfig.createInstance(
269
+ 'ojs-view',
270
+ { className: 'foo bar' },
271
+ null as any,
272
+ null,
273
+ null
274
+ );
275
+
276
+ const el = instance.element as MockVisualElement;
277
+ expect(el.hasClass('foo')).toBe(true);
278
+ expect(el.hasClass('bar')).toBe(true);
279
+ });
280
+
281
+ it('handles multiple spaces in className', () => {
282
+ const instance = hostConfig.createInstance(
283
+ 'ojs-view',
284
+ { className: 'foo bar baz' },
285
+ null as any,
286
+ null,
287
+ null
288
+ );
289
+
290
+ const el = instance.element as MockVisualElement;
291
+ expect(el.hasClass('foo')).toBe(true);
292
+ expect(el.hasClass('bar')).toBe(true);
293
+ expect(el.hasClass('baz')).toBe(true);
294
+ });
295
+
296
+ it('selectively adds new classes on update', () => {
297
+ const instance = hostConfig.createInstance(
298
+ 'ojs-view',
299
+ { className: 'foo bar' },
300
+ null as any,
301
+ null,
302
+ null
303
+ );
304
+
305
+ hostConfig.commitUpdate(
306
+ instance,
307
+ 'ojs-view',
308
+ { className: 'foo bar' },
309
+ { className: 'foo bar baz' },
310
+ null as any
311
+ );
312
+
313
+ const el = instance.element as MockVisualElement;
314
+ expect(el.hasClass('foo')).toBe(true);
315
+ expect(el.hasClass('bar')).toBe(true);
316
+ expect(el.hasClass('baz')).toBe(true);
317
+ });
318
+
319
+ it('selectively removes old classes on update', () => {
320
+ const instance = hostConfig.createInstance(
321
+ 'ojs-view',
322
+ { className: 'foo bar baz' },
323
+ null as any,
324
+ null,
325
+ null
326
+ );
327
+
328
+ hostConfig.commitUpdate(
329
+ instance,
330
+ 'ojs-view',
331
+ { className: 'foo bar baz' },
332
+ { className: 'foo' },
333
+ null as any
334
+ );
335
+
336
+ const el = instance.element as MockVisualElement;
337
+ expect(el.hasClass('foo')).toBe(true);
338
+ expect(el.hasClass('bar')).toBe(false);
339
+ expect(el.hasClass('baz')).toBe(false);
340
+ });
341
+
342
+ it('handles complete className replacement', () => {
343
+ const instance = hostConfig.createInstance(
344
+ 'ojs-view',
345
+ { className: 'old-class' },
346
+ null as any,
347
+ null,
348
+ null
349
+ );
350
+
351
+ hostConfig.commitUpdate(
352
+ instance,
353
+ 'ojs-view',
354
+ { className: 'old-class' },
355
+ { className: 'new-class' },
356
+ null as any
357
+ );
358
+
359
+ const el = instance.element as MockVisualElement;
360
+ expect(el.hasClass('old-class')).toBe(false);
361
+ expect(el.hasClass('new-class')).toBe(true);
362
+ });
363
+
364
+ it('removes all classes when className becomes undefined', () => {
365
+ const instance = hostConfig.createInstance(
366
+ 'ojs-view',
367
+ { className: 'foo bar' },
368
+ null as any,
369
+ null,
370
+ null
371
+ );
372
+
373
+ hostConfig.commitUpdate(
374
+ instance,
375
+ 'ojs-view',
376
+ { className: 'foo bar' },
377
+ {},
378
+ null as any
379
+ );
380
+
381
+ const el = instance.element as MockVisualElement;
382
+ expect(el.hasClass('foo')).toBe(false);
383
+ expect(el.hasClass('bar')).toBe(false);
384
+ });
385
+ });
386
+
387
+ describe('event handlers', () => {
388
+ it('registers onClick handler on creation', () => {
389
+ const handler = vi.fn();
390
+ const instance = hostConfig.createInstance(
391
+ 'ojs-button',
392
+ { onClick: handler },
393
+ null as any,
394
+ null,
395
+ null
396
+ );
397
+
398
+ const eventAPI = getEventAPI();
399
+ expect(eventAPI.addEventListener).toHaveBeenCalledWith(
400
+ instance.element,
401
+ 'click',
402
+ handler
403
+ );
404
+ expect(instance.eventHandlers.get('click')).toBe(handler);
405
+ });
406
+
407
+ it('registers multiple event handlers', () => {
408
+ const onClick = vi.fn();
409
+ const onPointerDown = vi.fn();
410
+ const instance = hostConfig.createInstance(
411
+ 'ojs-view',
412
+ { onClick, onPointerDown },
413
+ null as any,
414
+ null,
415
+ null
416
+ );
417
+
418
+ const eventAPI = getEventAPI();
419
+ expect(eventAPI.addEventListener).toHaveBeenCalledWith(
420
+ instance.element,
421
+ 'click',
422
+ onClick
423
+ );
424
+ expect(eventAPI.addEventListener).toHaveBeenCalledWith(
425
+ instance.element,
426
+ 'pointerdown',
427
+ onPointerDown
428
+ );
429
+ });
430
+
431
+ it('removes event handler on update', () => {
432
+ const handler = vi.fn();
433
+ const instance = hostConfig.createInstance(
434
+ 'ojs-button',
435
+ { onClick: handler },
436
+ null as any,
437
+ null,
438
+ null
439
+ );
440
+
441
+ // Clear mock to track only update calls
442
+ const eventAPI = getEventAPI();
443
+ eventAPI.addEventListener.mockClear();
444
+ eventAPI.removeEventListener.mockClear();
445
+
446
+ hostConfig.commitUpdate(
447
+ instance,
448
+ 'ojs-button',
449
+ { onClick: handler },
450
+ {},
451
+ null as any
452
+ );
453
+
454
+ expect(eventAPI.removeEventListener).toHaveBeenCalledWith(
455
+ instance.element,
456
+ 'click',
457
+ handler
458
+ );
459
+ expect(instance.eventHandlers.has('click')).toBe(false);
460
+ });
461
+
462
+ it('replaces event handler on update', () => {
463
+ const oldHandler = vi.fn();
464
+ const newHandler = vi.fn();
465
+ const instance = hostConfig.createInstance(
466
+ 'ojs-button',
467
+ { onClick: oldHandler },
468
+ null as any,
469
+ null,
470
+ null
471
+ );
472
+
473
+ const eventAPI = getEventAPI();
474
+ eventAPI.addEventListener.mockClear();
475
+ eventAPI.removeEventListener.mockClear();
476
+
477
+ hostConfig.commitUpdate(
478
+ instance,
479
+ 'ojs-button',
480
+ { onClick: oldHandler },
481
+ { onClick: newHandler },
482
+ null as any
483
+ );
484
+
485
+ expect(eventAPI.removeEventListener).toHaveBeenCalledWith(
486
+ instance.element,
487
+ 'click',
488
+ oldHandler
489
+ );
490
+ expect(eventAPI.addEventListener).toHaveBeenCalledWith(
491
+ instance.element,
492
+ 'click',
493
+ newHandler
494
+ );
495
+ expect(instance.eventHandlers.get('click')).toBe(newHandler);
496
+ });
497
+ });
498
+
499
+ describe('child management', () => {
500
+ it('appendChild adds child to parent', () => {
501
+ const parent = hostConfig.createInstance('ojs-view', {}, null as any, null, null);
502
+ const child = hostConfig.createInstance('ojs-label', { text: 'Hello' }, null as any, null, null);
503
+
504
+ hostConfig.appendChild(parent, child);
505
+
506
+ const parentEl = parent.element as MockVisualElement;
507
+ expect(parentEl.children).toContain(child.element);
508
+ });
509
+
510
+ it('appendInitialChild adds child during initial render', () => {
511
+ const parent = hostConfig.createInstance('ojs-view', {}, null as any, null, null);
512
+ const child = hostConfig.createInstance('ojs-label', {}, null as any, null, null);
513
+
514
+ hostConfig.appendInitialChild(parent, child);
515
+
516
+ const parentEl = parent.element as MockVisualElement;
517
+ expect(parentEl.children).toContain(child.element);
518
+ });
519
+
520
+ it('insertBefore inserts child at correct position', () => {
521
+ const parent = hostConfig.createInstance('ojs-view', {}, null as any, null, null);
522
+ const child1 = hostConfig.createInstance('ojs-label', { text: '1' }, null as any, null, null);
523
+ const child2 = hostConfig.createInstance('ojs-label', { text: '2' }, null as any, null, null);
524
+ const child3 = hostConfig.createInstance('ojs-label', { text: '3' }, null as any, null, null);
525
+
526
+ hostConfig.appendChild(parent, child1);
527
+ hostConfig.appendChild(parent, child3);
528
+ hostConfig.insertBefore(parent, child2, child3);
529
+
530
+ const parentEl = parent.element as MockVisualElement;
531
+ expect(parentEl.children[0]).toBe(child1.element);
532
+ expect(parentEl.children[1]).toBe(child2.element);
533
+ expect(parentEl.children[2]).toBe(child3.element);
534
+ });
535
+
536
+ it('removeChild removes child from parent', () => {
537
+ const parent = hostConfig.createInstance('ojs-view', {}, null as any, null, null);
538
+ const child = hostConfig.createInstance('ojs-label', {}, null as any, null, null);
539
+
540
+ hostConfig.appendChild(parent, child);
541
+ hostConfig.removeChild(parent, child);
542
+
543
+ const parentEl = parent.element as MockVisualElement;
544
+ expect(parentEl.children).not.toContain(child.element);
545
+ });
546
+
547
+ it('removeChild cleans up event listeners', () => {
548
+ const handler = vi.fn();
549
+ const parent = hostConfig.createInstance('ojs-view', {}, null as any, null, null);
550
+ const child = hostConfig.createInstance('ojs-button', { onClick: handler }, null as any, null, null);
551
+
552
+ hostConfig.appendChild(parent, child);
553
+
554
+ const eventAPI = getEventAPI();
555
+ eventAPI.removeAllEventListeners.mockClear();
556
+
557
+ hostConfig.removeChild(parent, child);
558
+
559
+ expect(eventAPI.removeAllEventListeners).toHaveBeenCalledWith(child.element);
560
+ });
561
+ });
562
+
563
+ describe('container operations', () => {
564
+ it('appendChildToContainer adds child to container', () => {
565
+ const container = new MockVisualElement('Container');
566
+ const child = hostConfig.createInstance('ojs-view', {}, null as any, null, null);
567
+
568
+ hostConfig.appendChildToContainer(container as any, child);
569
+
570
+ expect(container.children).toContain(child.element);
571
+ });
572
+
573
+ it('removeChildFromContainer removes child and cleans up events', () => {
574
+ const container = new MockVisualElement('Container');
575
+ const handler = vi.fn();
576
+ const child = hostConfig.createInstance('ojs-button', { onClick: handler }, null as any, null, null);
577
+
578
+ hostConfig.appendChildToContainer(container as any, child);
579
+
580
+ const eventAPI = getEventAPI();
581
+ eventAPI.removeAllEventListeners.mockClear();
582
+
583
+ hostConfig.removeChildFromContainer(container as any, child);
584
+
585
+ expect(container.children).not.toContain(child.element);
586
+ expect(eventAPI.removeAllEventListeners).toHaveBeenCalledWith(child.element);
587
+ });
588
+
589
+ it('clearContainer removes all children', () => {
590
+ const container = new MockVisualElement('Container');
591
+ const child1 = hostConfig.createInstance('ojs-view', {}, null as any, null, null);
592
+ const child2 = hostConfig.createInstance('ojs-view', {}, null as any, null, null);
593
+
594
+ hostConfig.appendChildToContainer(container as any, child1);
595
+ hostConfig.appendChildToContainer(container as any, child2);
596
+
597
+ expect(container.childCount).toBe(2);
598
+
599
+ hostConfig.clearContainer(container as any);
600
+
601
+ expect(container.childCount).toBe(0);
602
+ });
603
+ });
604
+
605
+ describe('text instances', () => {
606
+ it('createTextInstance creates a Label with text', () => {
607
+ const textInstance = hostConfig.createTextInstance('Hello World', null as any, null, null);
608
+
609
+ expect(textInstance.type).toBe('text');
610
+ expect(textInstance.element.text).toBe('Hello World');
611
+ expect(textInstance.appliedStyleKeys).toBeInstanceOf(Set);
612
+ });
613
+
614
+ it('commitTextUpdate updates the text', () => {
615
+ const textInstance = hostConfig.createTextInstance('Old text', null as any, null, null);
616
+
617
+ hostConfig.commitTextUpdate(textInstance, 'Old text', 'New text');
618
+
619
+ expect(textInstance.element.text).toBe('New text');
620
+ });
621
+ });
622
+
623
+ describe('visibility', () => {
624
+ it('hideInstance sets display to none', () => {
625
+ const instance = hostConfig.createInstance('ojs-view', {}, null as any, null, null);
626
+
627
+ hostConfig.hideInstance(instance);
628
+
629
+ expect(instance.element.style.display).toBe('none');
630
+ });
631
+
632
+ it('unhideInstance clears display', () => {
633
+ const instance = hostConfig.createInstance('ojs-view', {}, null as any, null, null);
634
+ instance.element.style.display = 'none';
635
+
636
+ hostConfig.unhideInstance(instance, {});
637
+
638
+ expect(instance.element.style.display).toBe('');
639
+ });
640
+ });
641
+
642
+ describe('prepareUpdate', () => {
643
+ it('returns true when props differ', () => {
644
+ const instance = hostConfig.createInstance('ojs-view', { style: { width: 100 } }, null as any, null, null);
645
+
646
+ const result = hostConfig.prepareUpdate(
647
+ instance,
648
+ 'ojs-view',
649
+ { style: { width: 100 } },
650
+ { style: { width: 200 } },
651
+ null as any,
652
+ null
653
+ );
654
+
655
+ expect(result).toBe(true);
656
+ });
657
+
658
+ it('returns null when props are same reference', () => {
659
+ const props = { style: { width: 100 } };
660
+ const instance = hostConfig.createInstance('ojs-view', props, null as any, null, null);
661
+
662
+ const result = hostConfig.prepareUpdate(
663
+ instance,
664
+ 'ojs-view',
665
+ props,
666
+ props,
667
+ null as any,
668
+ null
669
+ );
670
+
671
+ expect(result).toBeNull();
672
+ });
673
+ });
674
+ });