@pyreon/runtime-dom 0.24.5 → 0.24.6

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 (53) hide show
  1. package/package.json +5 -9
  2. package/src/delegate.ts +0 -98
  3. package/src/devtools.ts +0 -339
  4. package/src/env.d.ts +0 -6
  5. package/src/hydrate.ts +0 -450
  6. package/src/hydration-debug.ts +0 -129
  7. package/src/index.ts +0 -83
  8. package/src/keep-alive-entry.ts +0 -3
  9. package/src/keep-alive.ts +0 -83
  10. package/src/manifest.ts +0 -236
  11. package/src/mount.ts +0 -597
  12. package/src/nodes.ts +0 -896
  13. package/src/props.ts +0 -474
  14. package/src/template.ts +0 -523
  15. package/src/tests/callback-ref-unmount.browser.test.ts +0 -62
  16. package/src/tests/callback-ref-unmount.test.ts +0 -52
  17. package/src/tests/compiler-integration.test.tsx +0 -508
  18. package/src/tests/coverage-gaps.test.ts +0 -3183
  19. package/src/tests/coverage.test.ts +0 -1140
  20. package/src/tests/ctx-stack-growth-repro.test.tsx +0 -158
  21. package/src/tests/dev-gate-pattern.test.ts +0 -46
  22. package/src/tests/dev-gate-treeshake.test.ts +0 -256
  23. package/src/tests/error-boundary-stack-leak-repro.test.tsx +0 -133
  24. package/src/tests/fanout-repro.test.tsx +0 -219
  25. package/src/tests/hydration-integration.test.tsx +0 -540
  26. package/src/tests/keyed-array-in-for-batched-toggle.browser.test.ts +0 -140
  27. package/src/tests/lifecycle-integration.test.tsx +0 -342
  28. package/src/tests/lis-prepend.browser.test.ts +0 -99
  29. package/src/tests/manifest-snapshot.test.ts +0 -85
  30. package/src/tests/mount.test.ts +0 -3529
  31. package/src/tests/native-markers.test.ts +0 -19
  32. package/src/tests/props.test.ts +0 -581
  33. package/src/tests/reactive-props.test.ts +0 -270
  34. package/src/tests/real-world-integration.test.tsx +0 -714
  35. package/src/tests/rs-collapse-dyn-h.browser.test.ts +0 -303
  36. package/src/tests/rs-collapse-dyn.browser.test.ts +0 -316
  37. package/src/tests/rs-collapse-h.browser.test.ts +0 -152
  38. package/src/tests/rs-collapse-h.test.ts +0 -237
  39. package/src/tests/rs-collapse.browser.test.ts +0 -128
  40. package/src/tests/runtime-dom.browser.test.ts +0 -409
  41. package/src/tests/setup.ts +0 -3
  42. package/src/tests/show-context.test.ts +0 -270
  43. package/src/tests/show-of-for-batched-toggle.browser.test.ts +0 -122
  44. package/src/tests/ssr-xss-round-trip.browser.test.ts +0 -93
  45. package/src/tests/style-key-removal.browser.test.ts +0 -54
  46. package/src/tests/style-key-removal.test.ts +0 -88
  47. package/src/tests/template.test.ts +0 -383
  48. package/src/tests/transition-timeout-leak.test.ts +0 -126
  49. package/src/tests/transition.test.ts +0 -568
  50. package/src/tests/verified-correct-probes.test.ts +0 -56
  51. package/src/transition-entry.ts +0 -7
  52. package/src/transition-group.ts +0 -350
  53. package/src/transition.ts +0 -245
@@ -1,568 +0,0 @@
1
- import type { ComponentFn } from '@pyreon/core'
2
- import { h } from '@pyreon/core'
3
- import { signal } from '@pyreon/reactivity'
4
- import {
5
- KeepAlive as _KeepAlive,
6
- Transition as _Transition,
7
- TransitionGroup as _TransitionGroup,
8
- mount,
9
- } from '../index'
10
-
11
- const Transition = _Transition as unknown as ComponentFn<Record<string, unknown>>
12
- const TransitionGroup = _TransitionGroup as unknown as ComponentFn<Record<string, unknown>>
13
- const KeepAlive = _KeepAlive as unknown as ComponentFn<Record<string, unknown>>
14
-
15
- function container(): HTMLElement {
16
- const el = document.createElement('div')
17
- document.body.appendChild(el)
18
- return el
19
- }
20
-
21
- // ─── Transition ──────────────────────────────────────────────────────────────
22
-
23
- describe('Transition', () => {
24
- test('renders child when show is true', () => {
25
- const el = container()
26
- const show = signal(true)
27
- mount(
28
- h(Transition, { name: 'fade', show: () => show() }, h('div', { class: 'child' }, 'hello')),
29
- el,
30
- )
31
- expect(el.querySelector('.child')?.textContent).toBe('hello')
32
- })
33
-
34
- test('does not render child when show is false initially', () => {
35
- const el = container()
36
- const show = signal(false)
37
- mount(
38
- h(Transition, { name: 'fade', show: () => show() }, h('div', { class: 'child' }, 'hello')),
39
- el,
40
- )
41
- expect(el.querySelector('.child')).toBeNull()
42
- })
43
-
44
- test("uses default name 'pyreon' when no name provided", () => {
45
- const el = container()
46
- const show = signal(true)
47
- mount(h(Transition, { show: () => show() }, h('div', { class: 'child' }, 'content')), el)
48
- expect(el.querySelector('.child')?.textContent).toBe('content')
49
- })
50
-
51
- test('applies enter classes when show transitions from false to true', async () => {
52
- const el = container()
53
- const show = signal(false)
54
- mount(
55
- h(Transition, { name: 'fade', show: () => show() }, h('div', { class: 'target' }, 'text')),
56
- el,
57
- )
58
- expect(el.querySelector('.target')).toBeNull()
59
-
60
- show.set(true)
61
- // Wait for microtask (queueMicrotask in handleVisibilityChange)
62
- await new Promise<void>((r) => setTimeout(r, 20))
63
-
64
- const target = el.querySelector('.target') as HTMLElement
65
- expect(target).not.toBeNull()
66
- // After the enter animation starts, the element should have enter classes
67
- // Classes will be in transition — at minimum the element should exist
68
- expect(target.textContent).toBe('text')
69
- })
70
-
71
- test('applies custom enter/leave class overrides', async () => {
72
- const el = container()
73
- const show = signal(true)
74
- mount(
75
- h(
76
- Transition,
77
- {
78
- show: () => show(),
79
- enterFrom: 'my-enter-from',
80
- enterActive: 'my-enter-active',
81
- enterTo: 'my-enter-to',
82
- leaveFrom: 'my-leave-from',
83
- leaveActive: 'my-leave-active',
84
- leaveTo: 'my-leave-to',
85
- },
86
- h('div', { class: 'custom-target' }, 'custom'),
87
- ),
88
- el,
89
- )
90
- expect(el.querySelector('.custom-target')).not.toBeNull()
91
- })
92
-
93
- test('calls lifecycle callbacks on enter', async () => {
94
- const el = container()
95
- const show = signal(false)
96
- const onBeforeEnter = vi.fn()
97
- const onAfterEnter = vi.fn()
98
-
99
- mount(
100
- h(
101
- Transition,
102
- {
103
- name: 'fade',
104
- show: () => show(),
105
- onBeforeEnter,
106
- onAfterEnter,
107
- },
108
- h('div', { class: 'lifecycle' }, 'enter'),
109
- ),
110
- el,
111
- )
112
-
113
- show.set(true)
114
- await new Promise<void>((r) => setTimeout(r, 20))
115
- expect(onBeforeEnter).toHaveBeenCalled()
116
-
117
- // Trigger the transitionend to complete the enter
118
- const target = el.querySelector('.lifecycle') as HTMLElement
119
- if (target) {
120
- target.dispatchEvent(new Event('transitionend'))
121
- // Poll for the assertion instead of fixed sleep. Fixed setTimeout
122
- // is structurally flaky on shared CI runners: scheduling latency
123
- // between dispatchEvent's callback queue and the next tick can
124
- // exceed any reasonable fixed wait (we tried 10ms then 50ms, both
125
- // flaked). `vi.waitFor` polls every 10ms up to the timeout, so it
126
- // settles as soon as the assertion holds while still bounding the
127
- // worst case.
128
- //
129
- // Timeout raised 2000 → 8000: under the full 60+-package parallel
130
- // CI `Test` job, event-loop starvation can delay the Transition's
131
- // completion callback past 2s (it was reproducibly flaking this
132
- // single test there while passing deterministically in isolation).
133
- // The runtime itself bounds Transition completion at a documented
134
- // 5s fallback (CLAUDE.md), so a test asserting that completion must
135
- // allow ≥5s + CI-scheduling margin. The poll still settles
136
- // immediately once `onAfterEnter` fires — this only widens the
137
- // worst-case ceiling, it does not slow the happy path.
138
- await vi.waitFor(() => expect(onAfterEnter).toHaveBeenCalled(), {
139
- timeout: 8000,
140
- })
141
- }
142
- })
143
-
144
- test('calls lifecycle callbacks on leave', async () => {
145
- const el = container()
146
- const show = signal(true)
147
- const onBeforeLeave = vi.fn()
148
- const onAfterLeave = vi.fn()
149
-
150
- mount(
151
- h(
152
- Transition,
153
- {
154
- name: 'fade',
155
- show: () => show(),
156
- onBeforeLeave,
157
- onAfterLeave,
158
- },
159
- h('div', { class: 'leave-target' }, 'leave'),
160
- ),
161
- el,
162
- )
163
-
164
- // Initial render
165
- await new Promise<void>((r) => setTimeout(r, 10))
166
-
167
- show.set(false)
168
- await new Promise<void>((r) => setTimeout(r, 20))
169
- expect(onBeforeLeave).toHaveBeenCalled()
170
-
171
- // Trigger transitionend to complete leave
172
- const target = el.querySelector('.leave-target') as HTMLElement
173
- if (target) {
174
- target.dispatchEvent(new Event('transitionend'))
175
- await new Promise<void>((r) => setTimeout(r, 20))
176
- expect(onAfterLeave).toHaveBeenCalled()
177
- }
178
- })
179
-
180
- test('appear option triggers enter animation on initial mount', async () => {
181
- const el = container()
182
- const show = signal(true)
183
- const onBeforeEnter = vi.fn()
184
-
185
- mount(
186
- h(
187
- Transition,
188
- {
189
- name: 'fade',
190
- show: () => show(),
191
- appear: true,
192
- onBeforeEnter,
193
- },
194
- h('div', { class: 'appear-target' }, 'appear'),
195
- ),
196
- el,
197
- )
198
-
199
- await new Promise<void>((r) => setTimeout(r, 20))
200
- expect(onBeforeEnter).toHaveBeenCalled()
201
- })
202
-
203
- test('warns when child is a component (not a DOM element)', () => {
204
- const el = container()
205
- const show = signal(true)
206
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
207
- const ChildComp = () => h('div', null, 'comp-child')
208
-
209
- mount(h(Transition, { name: 'fade', show: () => show() }, h(ChildComp, null)), el)
210
-
211
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Transition child is a component'))
212
- warnSpy.mockRestore()
213
- })
214
-
215
- test('handles null/undefined children gracefully', () => {
216
- const el = container()
217
- const show = signal(true)
218
- // No children
219
- expect(() => mount(h(Transition, { name: 'fade', show: () => show() }), el)).not.toThrow()
220
- })
221
-
222
- test('cancels pending leave when re-entering', async () => {
223
- const el = container()
224
- const show = signal(true)
225
-
226
- mount(
227
- h(
228
- Transition,
229
- { name: 'fade', show: () => show() },
230
- h('div', { class: 'cancel-test' }, 'toggle'),
231
- ),
232
- el,
233
- )
234
-
235
- await new Promise<void>((r) => setTimeout(r, 10))
236
-
237
- // Start leave
238
- show.set(false)
239
- await new Promise<void>((r) => setTimeout(r, 10))
240
-
241
- // Re-enter before leave animation completes
242
- show.set(true)
243
- await new Promise<void>((r) => setTimeout(r, 30))
244
-
245
- // Element should be visible again
246
- const target = el.querySelector('.cancel-test')
247
- expect(target).not.toBeNull()
248
- })
249
-
250
- test('handles animationend event (CSS animations)', async () => {
251
- const el = container()
252
- const show = signal(false)
253
- const onAfterEnter = vi.fn()
254
-
255
- mount(
256
- h(
257
- Transition,
258
- { name: 'anim', show: () => show(), onAfterEnter },
259
- h('div', { class: 'anim-target' }, 'anim'),
260
- ),
261
- el,
262
- )
263
-
264
- show.set(true)
265
- await new Promise<void>((r) => setTimeout(r, 30))
266
-
267
- const target = el.querySelector('.anim-target') as HTMLElement
268
- if (target) {
269
- // Fire animationend instead of transitionend
270
- target.dispatchEvent(new Event('animationend'))
271
- await new Promise<void>((r) => setTimeout(r, 10))
272
- expect(onAfterEnter).toHaveBeenCalled()
273
- }
274
- })
275
- })
276
-
277
- // ─── TransitionGroup ─────────────────────────────────────────────────────────
278
-
279
- describe('TransitionGroup', () => {
280
- test('renders items inside a wrapper element', async () => {
281
- const el = container()
282
- const items = signal([{ id: 1 }, { id: 2 }, { id: 3 }])
283
-
284
- mount(
285
- h(TransitionGroup, {
286
- tag: 'ul',
287
- name: 'list',
288
- items: () => items(),
289
- keyFn: (item: { id: number }) => item.id,
290
- render: (item: { id: number }) => h('li', null, `item-${item.id}`),
291
- }),
292
- el,
293
- )
294
-
295
- await new Promise<void>((r) => setTimeout(r, 50))
296
- const lis = el.querySelectorAll('li')
297
- expect(lis.length).toBe(3)
298
- expect(lis[0]?.textContent).toBe('item-1')
299
- })
300
-
301
- test("uses default tag 'div' and name 'pyreon'", async () => {
302
- const el = container()
303
- const items = signal([{ id: 1 }])
304
-
305
- mount(
306
- h(TransitionGroup, {
307
- items: () => items(),
308
- keyFn: (item: { id: number }) => item.id,
309
- render: (item: { id: number }) => h('span', null, `s-${item.id}`),
310
- }),
311
- el,
312
- )
313
-
314
- await new Promise<void>((r) => setTimeout(r, 50))
315
- expect(el.querySelector('div')).not.toBeNull()
316
- expect(el.querySelector('span')?.textContent).toBe('s-1')
317
- })
318
-
319
- test('handles item additions', async () => {
320
- const el = container()
321
- const items = signal([{ id: 1 }])
322
-
323
- mount(
324
- h(TransitionGroup, {
325
- tag: 'div',
326
- name: 'list',
327
- items: () => items(),
328
- keyFn: (item: { id: number }) => item.id,
329
- render: (item: { id: number }) => h('span', null, `item-${item.id}`),
330
- }),
331
- el,
332
- )
333
-
334
- await new Promise<void>((r) => setTimeout(r, 50))
335
- expect(el.querySelectorAll('span').length).toBe(1)
336
-
337
- items.set([{ id: 1 }, { id: 2 }])
338
- await new Promise<void>((r) => setTimeout(r, 50))
339
- expect(el.querySelectorAll('span').length).toBe(2)
340
- })
341
-
342
- test('handles item removals with leave animation', async () => {
343
- const el = container()
344
- const items = signal([{ id: 1 }, { id: 2 }])
345
-
346
- mount(
347
- h(TransitionGroup, {
348
- tag: 'div',
349
- name: 'list',
350
- items: () => items(),
351
- keyFn: (item: { id: number }) => item.id,
352
- render: (item: { id: number }) => h('span', null, `item-${item.id}`),
353
- }),
354
- el,
355
- )
356
-
357
- await new Promise<void>((r) => setTimeout(r, 50))
358
- expect(el.querySelectorAll('span').length).toBe(2)
359
-
360
- items.set([{ id: 1 }])
361
- await new Promise<void>((r) => setTimeout(r, 10))
362
-
363
- // The removed item gets leave animation classes.
364
- // After transitionend it would be removed. Simulate that.
365
- const spans = el.querySelectorAll('span')
366
- for (const span of spans) {
367
- span.dispatchEvent(new Event('transitionend'))
368
- }
369
- await new Promise<void>((r) => setTimeout(r, 50))
370
- })
371
-
372
- test('calls lifecycle callbacks on enter/leave', async () => {
373
- const el = container()
374
- const items = signal([{ id: 1 }])
375
- const onBeforeEnter = vi.fn()
376
- const onAfterEnter = vi.fn()
377
- const onBeforeLeave = vi.fn()
378
-
379
- mount(
380
- h(TransitionGroup, {
381
- tag: 'div',
382
- name: 'list',
383
- items: () => items(),
384
- keyFn: (item: { id: number }) => item.id,
385
- render: (item: { id: number }) => h('span', null, `item-${item.id}`),
386
- onBeforeEnter,
387
- onAfterEnter,
388
- onBeforeLeave,
389
- }),
390
- el,
391
- )
392
-
393
- // Wait for initial mount
394
- await new Promise<void>((r) => setTimeout(r, 50))
395
-
396
- // Add an item to trigger enter animation
397
- items.set([{ id: 1 }, { id: 2 }])
398
- await new Promise<void>((r) => setTimeout(r, 50))
399
- expect(onBeforeEnter).toHaveBeenCalled()
400
-
401
- // Trigger transitionend on the new item to fire onAfterEnter
402
- const spans = el.querySelectorAll('span')
403
- const newSpan = spans[spans.length - 1]
404
- if (newSpan) {
405
- newSpan.dispatchEvent(new Event('transitionend'))
406
- await new Promise<void>((r) => setTimeout(r, 10))
407
- expect(onAfterEnter).toHaveBeenCalled()
408
- }
409
-
410
- // Remove item to trigger leave
411
- items.set([{ id: 1 }])
412
- await new Promise<void>((r) => setTimeout(r, 10))
413
- expect(onBeforeLeave).toHaveBeenCalled()
414
- })
415
-
416
- test('appear option animates items on initial mount', async () => {
417
- const el = container()
418
- const items = signal([{ id: 1 }])
419
- const onBeforeEnter = vi.fn()
420
-
421
- mount(
422
- h(TransitionGroup, {
423
- tag: 'div',
424
- name: 'list',
425
- appear: true,
426
- items: () => items(),
427
- keyFn: (item: { id: number }) => item.id,
428
- render: (item: { id: number }) => h('span', null, `item-${item.id}`),
429
- onBeforeEnter,
430
- }),
431
- el,
432
- )
433
-
434
- await new Promise<void>((r) => setTimeout(r, 50))
435
- expect(onBeforeEnter).toHaveBeenCalled()
436
- })
437
-
438
- test('supports custom class overrides', async () => {
439
- const el = container()
440
- const items = signal([{ id: 1 }])
441
-
442
- mount(
443
- h(TransitionGroup, {
444
- tag: 'div',
445
- items: () => items(),
446
- keyFn: (item: { id: number }) => item.id,
447
- render: (item: { id: number }) => h('span', null, `item-${item.id}`),
448
- enterFrom: 'custom-enter-from',
449
- enterActive: 'custom-enter-active',
450
- enterTo: 'custom-enter-to',
451
- leaveFrom: 'custom-leave-from',
452
- leaveActive: 'custom-leave-active',
453
- leaveTo: 'custom-leave-to',
454
- moveClass: 'custom-move',
455
- }),
456
- el,
457
- )
458
-
459
- await new Promise<void>((r) => setTimeout(r, 50))
460
- expect(el.querySelector('span')).not.toBeNull()
461
- })
462
- })
463
-
464
- // ─── KeepAlive ───────────────────────────────────────────────────────────────
465
-
466
- describe('KeepAlive', () => {
467
- test('renders children when active is true', async () => {
468
- const el = container()
469
- const active = signal(true)
470
-
471
- mount(h(KeepAlive, { active: () => active() }, h('span', { class: 'kept' }, 'alive')), el)
472
-
473
- await new Promise<void>((r) => setTimeout(r, 50))
474
- const kept = el.querySelector('.kept')
475
- expect(kept?.textContent).toBe('alive')
476
- })
477
-
478
- test('hides children but keeps them mounted when active is false', async () => {
479
- const el = container()
480
- const active = signal(true)
481
-
482
- mount(h(KeepAlive, { active: () => active() }, h('span', { class: 'kept' }, 'alive')), el)
483
-
484
- await new Promise<void>((r) => setTimeout(r, 50))
485
-
486
- active.set(false)
487
- await new Promise<void>((r) => setTimeout(r, 20))
488
-
489
- // The container div should have display: none, but the child should still be in DOM
490
- const wrapperDiv = el.querySelector('[style]') as HTMLElement
491
- if (wrapperDiv) {
492
- expect(wrapperDiv.style.display).toBe('none')
493
- }
494
- // Child should still exist in the DOM (kept alive)
495
- expect(el.querySelector('.kept')).not.toBeNull()
496
- })
497
-
498
- test('restores display when re-activated', async () => {
499
- const el = container()
500
- const active = signal(true)
501
-
502
- mount(h(KeepAlive, { active: () => active() }, h('span', { class: 'kept' }, 'alive')), el)
503
-
504
- await new Promise<void>((r) => setTimeout(r, 50))
505
- active.set(false)
506
- await new Promise<void>((r) => setTimeout(r, 20))
507
- active.set(true)
508
- await new Promise<void>((r) => setTimeout(r, 20))
509
-
510
- // The container's display should be restored (empty string = visible)
511
- const wrapperDivs = el.querySelectorAll('div')
512
- let foundVisible = false
513
- for (const div of wrapperDivs) {
514
- if (div.style.display === '' || div.style.display === 'contents') {
515
- foundVisible = true
516
- }
517
- }
518
- expect(foundVisible).toBe(true)
519
- })
520
-
521
- test('defaults to active=true when no active prop provided', async () => {
522
- const el = container()
523
-
524
- mount(h(KeepAlive, {}, h('span', { class: 'default' }, 'default')), el)
525
-
526
- await new Promise<void>((r) => setTimeout(r, 50))
527
- expect(el.querySelector('.default')?.textContent).toBe('default')
528
- })
529
-
530
- test('mounts children only once (not re-created on toggle)', async () => {
531
- const el = container()
532
- const active = signal(true)
533
- let mountCount = 0
534
- const Counter = () => {
535
- mountCount++
536
- return h('span', null, 'counter')
537
- }
538
-
539
- mount(h(KeepAlive, { active: () => active() }, h(Counter, null)), el)
540
-
541
- await new Promise<void>((r) => setTimeout(r, 50))
542
- expect(mountCount).toBe(1)
543
-
544
- active.set(false)
545
- await new Promise<void>((r) => setTimeout(r, 20))
546
- active.set(true)
547
- await new Promise<void>((r) => setTimeout(r, 20))
548
-
549
- // Component should NOT be re-created
550
- expect(mountCount).toBe(1)
551
- })
552
-
553
- test('uses display: contents wrapper for transparent layout', async () => {
554
- const el = container()
555
- mount(h(KeepAlive, {}, h('span', null, 'child')), el)
556
-
557
- await new Promise<void>((r) => setTimeout(r, 20))
558
- // KeepAlive renders a div with style="display: contents"
559
- const wrapper = el.querySelector('div')
560
- expect(wrapper).not.toBeNull()
561
- })
562
-
563
- test('handles null children gracefully', async () => {
564
- const el = container()
565
- expect(() => mount(h(KeepAlive, {}), el)).not.toThrow()
566
- await new Promise<void>((r) => setTimeout(r, 50))
567
- })
568
- })
@@ -1,56 +0,0 @@
1
- import { Fragment, h } from '@pyreon/core'
2
- import { mount } from '../index'
3
-
4
- // Lock-in tests for behaviors PR #235 investigated and claimed
5
- // "verified correct". Without code assertions the prose claims
6
- // could silently regress.
7
-
8
- describe('Fragment + key — key is inert', () => {
9
- let container: HTMLDivElement
10
-
11
- beforeEach(() => {
12
- container = document.createElement('div')
13
- document.body.appendChild(container)
14
- })
15
-
16
- afterEach(() => {
17
- container.remove()
18
- })
19
-
20
- it('Fragment with a key renders its children inline (key does not reconcile)', () => {
21
- mount(
22
- h(
23
- Fragment,
24
- { key: 'x' },
25
- h('span', { id: 'a' }, 'a'),
26
- h('span', { id: 'b' }, 'b'),
27
- ),
28
- container,
29
- )
30
-
31
- // Both children are present at the top level of the container —
32
- // no extra wrapper, key didn't alter the structure.
33
- expect(container.querySelector('#a')?.textContent).toBe('a')
34
- expect(container.querySelector('#b')?.textContent).toBe('b')
35
- expect(container.children).toHaveLength(2)
36
- })
37
-
38
- it('Fragment without a key renders identically', () => {
39
- mount(
40
- h(Fragment, null, h('span', { id: 'a' }, 'a'), h('span', { id: 'b' }, 'b')),
41
- container,
42
- )
43
- expect(container.children).toHaveLength(2)
44
- expect(container.querySelector('#a')?.textContent).toBe('a')
45
- expect(container.querySelector('#b')?.textContent).toBe('b')
46
- })
47
- })
48
-
49
- describe('Suspense fast-resolve — fallback-first streaming contract', () => {
50
- // This is a SERVER behavior, not a browser one. The PR-#235 claim was:
51
- // renderToStream always emits fallback first, then swap — even if the
52
- // async child resolves synchronously. Synchronous callers should use
53
- // renderToString. That contract is already locked in by the existing
54
- // streaming integration tests; no additional coverage needed here.
55
- it.skip('locked in by renderToStream integration tests — see runtime-server/src/tests/ssr.test.ts', () => {})
56
- })
@@ -1,7 +0,0 @@
1
- // Subpath entry for @pyreon/runtime-dom/transition
2
- // Apps that don't use animations can avoid importing this entirely.
3
-
4
- export type { TransitionProps } from './transition'
5
- export { Transition } from './transition'
6
- export type { TransitionGroupProps } from './transition-group'
7
- export { TransitionGroup } from './transition-group'