@esportsplus/template 0.16.15 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,389 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { EffectSlot } from '../../src/slot/effect';
3
+ import { marker } from '../../src/utilities';
4
+ import type { Element, Renderable } from '../../src/types';
5
+
6
+
7
+ describe('slot/EffectSlot (async)', () => {
8
+ let anchor: Element,
9
+ container: HTMLElement;
10
+
11
+ beforeEach(() => {
12
+ container = document.createElement('div');
13
+ anchor = marker.cloneNode() as unknown as Element;
14
+ container.appendChild(anchor as unknown as Node);
15
+ document.body.appendChild(container);
16
+ });
17
+
18
+ afterEach(() => {
19
+ document.body.removeChild(container);
20
+ });
21
+
22
+ describe('construction', () => {
23
+ it('creates EffectSlot with anchor', () => {
24
+ let slot = new EffectSlot(anchor, async () => 'Hello');
25
+
26
+ expect(slot.anchor).toBe(anchor);
27
+ });
28
+
29
+ it('passes fallback setter to async function', async () => {
30
+ let received = false;
31
+
32
+ new EffectSlot(anchor, async (fallback: any) => {
33
+ received = typeof fallback === 'function';
34
+ return 'done';
35
+ });
36
+
37
+ await vi.waitFor(() => {
38
+ expect(received).toBe(true);
39
+ });
40
+ });
41
+ });
42
+
43
+ describe('fallback rendering', () => {
44
+ it('renders fallback content immediately', async () => {
45
+ let resolve: (v: string) => void;
46
+
47
+ let promise = new Promise<string>((r) => { resolve = r; });
48
+
49
+ new EffectSlot(anchor, async (fallback: any) => {
50
+ fallback('Loading...');
51
+ return promise;
52
+ });
53
+
54
+ await vi.waitFor(() => {
55
+ expect(container.textContent).toContain('Loading...');
56
+ });
57
+
58
+ resolve!('Done');
59
+
60
+ await vi.waitFor(() => {
61
+ expect(container.textContent).toContain('Done');
62
+ expect(container.textContent).not.toContain('Loading...');
63
+ });
64
+ });
65
+
66
+ it('renders fallback as DOM node', async () => {
67
+ let resolve: (v: string) => void;
68
+
69
+ let promise = new Promise<string>((r) => { resolve = r; });
70
+
71
+ let fallbackNode = document.createElement('span');
72
+ fallbackNode.textContent = 'Spinner';
73
+
74
+ new EffectSlot(anchor, async (fallback: any) => {
75
+ fallback(fallbackNode);
76
+ return promise;
77
+ });
78
+
79
+ await vi.waitFor(() => {
80
+ expect(container.querySelector('span')?.textContent).toBe('Spinner');
81
+ });
82
+
83
+ resolve!('Resolved');
84
+
85
+ await vi.waitFor(() => {
86
+ expect(container.querySelector('span')).toBeNull();
87
+ expect(container.textContent).toContain('Resolved');
88
+ });
89
+ });
90
+
91
+ it('updates fallback progressively', async () => {
92
+ let resolve: (v: string) => void;
93
+
94
+ let promise = new Promise<string>((r) => { resolve = r; });
95
+
96
+ new EffectSlot(anchor, async (fallback: any) => {
97
+ fallback('Step 1');
98
+
99
+ await Promise.resolve();
100
+
101
+ fallback('Step 2');
102
+
103
+ return promise;
104
+ });
105
+
106
+ await vi.waitFor(() => {
107
+ expect(container.textContent).toContain('Step 2');
108
+ expect(container.textContent).not.toContain('Step 1');
109
+ });
110
+
111
+ resolve!('Final');
112
+
113
+ await vi.waitFor(() => {
114
+ expect(container.textContent).toContain('Final');
115
+ expect(container.textContent).not.toContain('Step 2');
116
+ });
117
+ });
118
+ });
119
+
120
+ describe('async resolution', () => {
121
+ it('renders resolved value as string', async () => {
122
+ new EffectSlot(anchor, async () => 'Hello World');
123
+
124
+ await vi.waitFor(() => {
125
+ expect(container.textContent).toContain('Hello World');
126
+ });
127
+ });
128
+
129
+ it('renders resolved value as number', async () => {
130
+ new EffectSlot(anchor, async () => 42);
131
+
132
+ await vi.waitFor(() => {
133
+ expect(container.textContent).toContain('42');
134
+ });
135
+ });
136
+
137
+ it('renders resolved DOM node', async () => {
138
+ let div = document.createElement('div');
139
+ div.textContent = 'Resolved Node';
140
+
141
+ new EffectSlot(anchor, async () => div);
142
+
143
+ await vi.waitFor(() => {
144
+ expect(container.querySelector('div')?.textContent).toBe('Resolved Node');
145
+ });
146
+ });
147
+
148
+ it('renders empty string for null', async () => {
149
+ new EffectSlot(anchor, async () => null);
150
+
151
+ await vi.waitFor(() => {
152
+ expect(container.childNodes.length).toBeGreaterThanOrEqual(1);
153
+ });
154
+ });
155
+
156
+ it('renders empty string for false', async () => {
157
+ new EffectSlot(anchor, async () => false);
158
+
159
+ await vi.waitFor(() => {
160
+ expect(container.childNodes.length).toBeGreaterThanOrEqual(1);
161
+ });
162
+ });
163
+ });
164
+
165
+ describe('no fallback', () => {
166
+ it('slot stays empty until resolved', async () => {
167
+ let resolve: (v: string) => void;
168
+
169
+ let promise = new Promise<string>((r) => { resolve = r; });
170
+
171
+ new EffectSlot(anchor, async () => promise);
172
+
173
+ expect(container.textContent).toBe('');
174
+
175
+ resolve!('Now Visible');
176
+
177
+ await vi.waitFor(() => {
178
+ expect(container.textContent).toContain('Now Visible');
179
+ });
180
+ });
181
+ });
182
+
183
+ describe('error handling', () => {
184
+ it('does not crash when async function rejects', async () => {
185
+ new EffectSlot(anchor, async () => {
186
+ throw new Error('async failure');
187
+ });
188
+
189
+ await vi.waitFor(() => {
190
+ expect(container.childNodes.length).toBeGreaterThanOrEqual(1);
191
+ });
192
+ });
193
+
194
+ it('renders fallback even if promise rejects', async () => {
195
+ new EffectSlot(anchor, async (fallback: any) => {
196
+ fallback('Fallback shown');
197
+ throw new Error('rejected');
198
+ });
199
+
200
+ await vi.waitFor(() => {
201
+ expect(container.textContent).toContain('Fallback shown');
202
+ });
203
+ });
204
+ });
205
+
206
+ describe('return value handling', () => {
207
+ it('renders undefined as empty string', async () => {
208
+ new EffectSlot(anchor, async () => undefined);
209
+
210
+ await vi.waitFor(() => {
211
+ let nodes = container.childNodes;
212
+ let found = false;
213
+
214
+ for (let i = 0, n = nodes.length; i < n; i++) {
215
+ if (nodes[i].nodeType === 3) {
216
+ found = true;
217
+ }
218
+ }
219
+
220
+ expect(found).toBe(true);
221
+ });
222
+ });
223
+
224
+ it('renders function return value recursively', async () => {
225
+ new EffectSlot(anchor, async () => () => 'nested');
226
+
227
+ await vi.waitFor(() => {
228
+ expect(container.textContent).toContain('nested');
229
+ });
230
+ });
231
+
232
+ it('renders array of DOM nodes', async () => {
233
+ let span1 = document.createElement('span');
234
+ span1.textContent = 'A';
235
+
236
+ let span2 = document.createElement('span');
237
+ span2.textContent = 'B';
238
+
239
+ new EffectSlot(anchor, async () => [span1, span2]);
240
+
241
+ await vi.waitFor(() => {
242
+ let spans = container.querySelectorAll('span');
243
+
244
+ expect(spans.length).toBe(2);
245
+ expect(spans[0].textContent).toBe('A');
246
+ expect(spans[1].textContent).toBe('B');
247
+ });
248
+ });
249
+
250
+ it('renders array of strings', async () => {
251
+ new EffectSlot(anchor, async () => ['Hello', ' ', 'World']);
252
+
253
+ await vi.waitFor(() => {
254
+ expect(container.textContent).toContain('Hello');
255
+ expect(container.textContent).toContain('World');
256
+ });
257
+ });
258
+ });
259
+
260
+ describe('disposer behavior', () => {
261
+ it('sets disposer to null for async functions', () => {
262
+ let slot = new EffectSlot(anchor, async () => 'test');
263
+
264
+ expect(slot.disposer).toBeNull();
265
+ });
266
+
267
+ it('dispose() is a no-op for async slots', () => {
268
+ let slot = new EffectSlot(anchor, async () => 'test');
269
+
270
+ expect(() => slot.dispose()).not.toThrow();
271
+ });
272
+ });
273
+
274
+ describe('fallback to resolution race', () => {
275
+ it('resolution replaces fallback even when both are DOM nodes', async () => {
276
+ let resolve: (v: Node) => void;
277
+
278
+ let promise = new Promise<Node>((r) => { resolve = r; });
279
+
280
+ let fallbackSpan = document.createElement('span');
281
+ fallbackSpan.className = 'fallback';
282
+ fallbackSpan.textContent = 'Loading';
283
+
284
+ let resolvedDiv = document.createElement('div');
285
+ resolvedDiv.className = 'resolved';
286
+ resolvedDiv.textContent = 'Done';
287
+
288
+ new EffectSlot(anchor, async (fallback: any) => {
289
+ fallback(fallbackSpan);
290
+ return promise;
291
+ });
292
+
293
+ await vi.waitFor(() => {
294
+ expect(container.querySelector('.fallback')).not.toBeNull();
295
+ });
296
+
297
+ resolve!(resolvedDiv);
298
+
299
+ await vi.waitFor(() => {
300
+ expect(container.querySelector('.fallback')).toBeNull();
301
+ expect(container.querySelector('.resolved')?.textContent).toBe('Done');
302
+ });
303
+ });
304
+
305
+ it('resolution replaces fallback when fallback is text and resolution is text', async () => {
306
+ let resolve: (v: string) => void;
307
+
308
+ let promise = new Promise<string>((r) => { resolve = r; });
309
+
310
+ new EffectSlot(anchor, async (fallback: any) => {
311
+ fallback('Loading...');
312
+ return promise;
313
+ });
314
+
315
+ await vi.waitFor(() => {
316
+ expect(container.textContent).toContain('Loading...');
317
+ });
318
+
319
+ resolve!('Complete');
320
+
321
+ await vi.waitFor(() => {
322
+ expect(container.textContent).toContain('Complete');
323
+ expect(container.textContent).not.toContain('Loading...');
324
+ });
325
+ });
326
+
327
+ it('handles immediate resolution without fallback', async () => {
328
+ new EffectSlot(anchor, async () => {
329
+ return 'Instant';
330
+ });
331
+
332
+ await vi.waitFor(() => {
333
+ expect(container.textContent).toContain('Instant');
334
+ });
335
+ });
336
+ });
337
+
338
+ describe('cleanup', () => {
339
+ it('removes previous fallback group when updating', async () => {
340
+ let resolve: (v: string) => void;
341
+
342
+ let promise = new Promise<string>((r) => { resolve = r; });
343
+
344
+ let span1 = document.createElement('span');
345
+ span1.className = 'fallback';
346
+ span1.textContent = 'Loading';
347
+
348
+ new EffectSlot(anchor, async (fallback: any) => {
349
+ fallback(span1);
350
+ return promise;
351
+ });
352
+
353
+ await vi.waitFor(() => {
354
+ expect(container.querySelector('.fallback')).not.toBeNull();
355
+ });
356
+
357
+ resolve!('Done');
358
+
359
+ await vi.waitFor(() => {
360
+ expect(container.querySelector('.fallback')).toBeNull();
361
+ expect(container.textContent).toContain('Done');
362
+ });
363
+ });
364
+
365
+ it('removes previous textnode when updating to DOM node', async () => {
366
+ let resolve: (v: Node) => void;
367
+
368
+ let promise = new Promise<Node>((r) => { resolve = r; });
369
+
370
+ new EffectSlot(anchor, async (fallback: any) => {
371
+ fallback('text fallback');
372
+ return promise;
373
+ });
374
+
375
+ await vi.waitFor(() => {
376
+ expect(container.textContent).toContain('text fallback');
377
+ });
378
+
379
+ let div = document.createElement('div');
380
+ div.textContent = 'replaced';
381
+ resolve!(div);
382
+
383
+ await vi.waitFor(() => {
384
+ expect(container.textContent).not.toContain('text fallback');
385
+ expect(container.querySelector('div')?.textContent).toBe('replaced');
386
+ });
387
+ });
388
+ });
389
+ });
@@ -1,4 +1,5 @@
1
- import { describe, expect, it, beforeEach, vi, afterEach } from 'vitest';
1
+ import { describe, expect, it, beforeEach, afterEach } from 'vitest';
2
+ import { signal, read, write } from '@esportsplus/reactivity';
2
3
  import { EffectSlot } from '../../src/slot/effect';
3
4
  import { marker } from '../../src/utilities';
4
5
  import type { Element } from '../../src/types';
@@ -226,6 +227,92 @@ describe('slot/EffectSlot', () => {
226
227
  });
227
228
  });
228
229
 
230
+ describe('RAF scheduled updates', () => {
231
+ it('batches subsequent reactive updates via RAF', async () => {
232
+ let s = signal('first');
233
+
234
+ new EffectSlot(anchor, () => read(s));
235
+
236
+ expect(container.textContent).toContain('first');
237
+
238
+ write(s, 'second');
239
+
240
+ await new Promise(resolve => requestAnimationFrame(resolve));
241
+
242
+ expect(container.textContent).toContain('second');
243
+ });
244
+
245
+ it('coalesces rapid reactive updates into one RAF', async () => {
246
+ let s = signal('a');
247
+
248
+ new EffectSlot(anchor, () => read(s));
249
+
250
+ expect(container.textContent).toContain('a');
251
+
252
+ write(s, 'b');
253
+ write(s, 'c');
254
+ write(s, 'd');
255
+
256
+ await new Promise(resolve => requestAnimationFrame(resolve));
257
+
258
+ expect(container.textContent).toContain('d');
259
+ expect(container.textContent).not.toContain('b');
260
+ });
261
+ });
262
+
263
+ describe('dispose with group content', () => {
264
+ it('dispose with group (complex content) removes group nodes', () => {
265
+ let frag = document.createDocumentFragment(),
266
+ span1 = document.createElement('span'),
267
+ span2 = document.createElement('span');
268
+
269
+ span1.textContent = 'A';
270
+ span2.textContent = 'B';
271
+ frag.appendChild(span1);
272
+ frag.appendChild(span2);
273
+
274
+ let slot = new EffectSlot(anchor, (dispose) => frag);
275
+
276
+ expect(container.querySelector('span')).not.toBeNull();
277
+ expect(slot.group).not.toBeNull();
278
+ expect(slot.textnode).toBeNull();
279
+
280
+ slot.dispose();
281
+
282
+ expect(container.querySelectorAll('span').length).toBe(0);
283
+ });
284
+
285
+ it('dispose with textnode removes text and anchor', () => {
286
+ let slot = new EffectSlot(anchor, (dispose) => 'Hello text');
287
+
288
+ expect(container.textContent).toContain('Hello text');
289
+ expect(slot.textnode).not.toBeNull();
290
+
291
+ slot.dispose();
292
+
293
+ expect(container.textContent).not.toContain('Hello text');
294
+ });
295
+ });
296
+
297
+ describe('textnode reconnection', () => {
298
+ it('reattaches disconnected textnode on update', () => {
299
+ let slot = new EffectSlot(anchor, () => 'Hello');
300
+
301
+ expect(slot.textnode?.isConnected).toBe(true);
302
+
303
+ // Manually remove textnode from DOM
304
+ slot.textnode!.parentNode!.removeChild(slot.textnode!);
305
+
306
+ expect(slot.textnode?.isConnected).toBe(false);
307
+
308
+ // Direct update should reattach
309
+ slot.update('Updated');
310
+
311
+ expect(slot.textnode?.isConnected).toBe(true);
312
+ expect(slot.textnode?.nodeValue).toBe('Updated');
313
+ });
314
+ });
315
+
229
316
  describe('edge cases', () => {
230
317
  it('handles empty string', () => {
231
318
  new EffectSlot(anchor, () => '');