@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,383 +0,0 @@
1
- import { computed, signal } from '@pyreon/reactivity'
2
- import { _bindDirect, _bindText, _clearTplCache, _tpl, _tplCacheSize } from '../template'
3
-
4
- // ─── _bindText ──────────────────────────────────────────────────────────────
5
-
6
- describe('_bindText', () => {
7
- test('fast path: signal source sets text and updates reactively', () => {
8
- const s = signal('hello')
9
- const node = document.createTextNode('')
10
-
11
- const dispose = _bindText(s, node)
12
- expect(node.data).toBe('hello')
13
-
14
- s.set('world')
15
- expect(node.data).toBe('world')
16
-
17
- dispose()
18
- })
19
-
20
- test('fast path: computed source sets text and updates reactively', () => {
21
- const s = signal(2)
22
- const doubled = computed(() => s() * 2)
23
- const node = document.createTextNode('')
24
-
25
- const dispose = _bindText(doubled, node)
26
- expect(node.data).toBe('4')
27
-
28
- s.set(5)
29
- expect(node.data).toBe('10')
30
-
31
- dispose()
32
- })
33
-
34
- test('fallback: plain function source uses renderEffect', () => {
35
- const s = signal('initial')
36
- // Plain function — no .direct property
37
- const getter = () => s()
38
- const node = document.createTextNode('')
39
-
40
- const dispose = _bindText(getter as unknown as Parameters<typeof _bindText>[0], node)
41
- expect(node.data).toBe('initial')
42
-
43
- s.set('updated')
44
- expect(node.data).toBe('updated')
45
-
46
- dispose()
47
- })
48
-
49
- test('disposal stops updates for signal source', () => {
50
- const s = signal('a')
51
- const node = document.createTextNode('')
52
-
53
- const dispose = _bindText(s, node)
54
- expect(node.data).toBe('a')
55
-
56
- dispose()
57
-
58
- s.set('b')
59
- expect(node.data).toBe('a')
60
- })
61
-
62
- test('disposal stops updates for computed source', () => {
63
- const s = signal(1)
64
- const c = computed(() => s() + 10)
65
- const node = document.createTextNode('')
66
-
67
- const dispose = _bindText(c, node)
68
- expect(node.data).toBe('11')
69
-
70
- dispose()
71
-
72
- s.set(2)
73
- expect(node.data).toBe('11')
74
- })
75
-
76
- test('disposal stops updates for plain function source', () => {
77
- const s = signal('x')
78
- const getter = () => s()
79
- const node = document.createTextNode('')
80
-
81
- const dispose = _bindText(getter as unknown as Parameters<typeof _bindText>[0], node)
82
- expect(node.data).toBe('x')
83
-
84
- dispose()
85
-
86
- s.set('y')
87
- expect(node.data).toBe('x')
88
- })
89
-
90
- test('null value renders as empty string', () => {
91
- const s = signal<string | null>('text')
92
- const node = document.createTextNode('')
93
-
94
- const dispose = _bindText(s, node)
95
- expect(node.data).toBe('text')
96
-
97
- s.set(null)
98
- expect(node.data).toBe('')
99
-
100
- dispose()
101
- })
102
-
103
- test('false value renders as empty string', () => {
104
- const s = signal<string | false>('text')
105
- const node = document.createTextNode('')
106
-
107
- const dispose = _bindText(s, node)
108
- s.set(false)
109
- expect(node.data).toBe('')
110
-
111
- dispose()
112
- })
113
-
114
- test('undefined value renders as empty string', () => {
115
- const s = signal<string | undefined>('text')
116
- const node = document.createTextNode('')
117
-
118
- const dispose = _bindText(s, node)
119
- s.set(undefined)
120
- expect(node.data).toBe('')
121
-
122
- dispose()
123
- })
124
-
125
- test('skips DOM write when value unchanged (fast path)', () => {
126
- const s = signal('same')
127
- const node = document.createTextNode('')
128
-
129
- const dispose = _bindText(s, node)
130
- expect(node.data).toBe('same')
131
-
132
- // Set same value — should skip the DOM write (next !== node.data is false)
133
- s.set('same')
134
- expect(node.data).toBe('same')
135
-
136
- dispose()
137
- })
138
-
139
- test('fallback path: null/false/undefined → empty string', () => {
140
- const s = signal<string | null | false | undefined>('text')
141
- const getter = () => s()
142
- const node = document.createTextNode('')
143
-
144
- const dispose = _bindText(getter as unknown as Parameters<typeof _bindText>[0], node)
145
- expect(node.data).toBe('text')
146
-
147
- s.set(null)
148
- expect(node.data).toBe('')
149
-
150
- s.set(false)
151
- expect(node.data).toBe('')
152
-
153
- s.set(undefined)
154
- expect(node.data).toBe('')
155
-
156
- s.set('restored')
157
- expect(node.data).toBe('restored')
158
-
159
- dispose()
160
- })
161
-
162
- test('fallback path: skips DOM write when value unchanged', () => {
163
- const s = signal('x')
164
- const getter = () => s()
165
- const node = document.createTextNode('')
166
-
167
- const dispose = _bindText(getter as unknown as Parameters<typeof _bindText>[0], node)
168
- expect(node.data).toBe('x')
169
-
170
- s.set('x') // same value — skip
171
- expect(node.data).toBe('x')
172
-
173
- dispose()
174
- })
175
-
176
- test('number coercion via String()', () => {
177
- const s = signal<number>(42)
178
- const node = document.createTextNode('')
179
-
180
- const dispose = _bindText(s, node)
181
- expect(node.data).toBe('42')
182
-
183
- s.set(0)
184
- expect(node.data).toBe('0')
185
-
186
- dispose()
187
- })
188
- })
189
-
190
- // ─── _bindDirect ────────────────────────────────────────────────────────────
191
-
192
- describe('_bindDirect', () => {
193
- test('fast path: signal source calls updater immediately and on change', () => {
194
- const s = signal('red')
195
- const el = document.createElement('div')
196
-
197
- const dispose = _bindDirect(s, (v) => {
198
- el.className = String(v)
199
- })
200
-
201
- expect(el.className).toBe('red')
202
-
203
- s.set('blue')
204
- expect(el.className).toBe('blue')
205
-
206
- dispose()
207
- })
208
-
209
- test('fallback: plain function source uses renderEffect', () => {
210
- const s = signal(10)
211
- const getter = () => s()
212
- const el = document.createElement('div')
213
-
214
- const dispose = _bindDirect(getter as unknown as Parameters<typeof _bindDirect>[0], (v) => {
215
- el.style.width = `${v}px`
216
- })
217
-
218
- expect(el.style.width).toBe('10px')
219
-
220
- s.set(20)
221
- expect(el.style.width).toBe('20px')
222
-
223
- dispose()
224
- })
225
-
226
- test('disposal stops updates for signal source', () => {
227
- const s = signal('a')
228
- const el = document.createElement('div')
229
-
230
- const dispose = _bindDirect(s, (v) => {
231
- el.setAttribute('data-val', String(v))
232
- })
233
-
234
- expect(el.getAttribute('data-val')).toBe('a')
235
-
236
- dispose()
237
-
238
- s.set('b')
239
- expect(el.getAttribute('data-val')).toBe('a')
240
- })
241
-
242
- test('disposal stops updates for plain function source', () => {
243
- const s = signal(1)
244
- const getter = () => s()
245
- const el = document.createElement('div')
246
-
247
- const dispose = _bindDirect(getter as unknown as Parameters<typeof _bindDirect>[0], (v) => {
248
- el.setAttribute('data-num', String(v))
249
- })
250
-
251
- expect(el.getAttribute('data-num')).toBe('1')
252
-
253
- dispose()
254
-
255
- s.set(2)
256
- expect(el.getAttribute('data-num')).toBe('1')
257
- })
258
- })
259
-
260
- // ─── _mountSlot ────────────────────────────────────────────────────────────
261
-
262
- describe('_mountSlot', async () => {
263
- const { _mountSlot } = await import('../template')
264
- const { h } = await import('@pyreon/core')
265
-
266
- test('mounts a string child as text', () => {
267
- const parent = document.createElement('div')
268
- const placeholder = document.createComment('')
269
- parent.appendChild(placeholder)
270
-
271
- _mountSlot('hello', parent, placeholder)
272
- expect(parent.textContent).toBe('hello')
273
- })
274
-
275
- test('mounts a VNode child as DOM element', () => {
276
- const parent = document.createElement('div')
277
- const placeholder = document.createComment('')
278
- parent.appendChild(placeholder)
279
-
280
- const vnode = h('span', { class: 'test' }, 'content')
281
- _mountSlot(vnode, parent, placeholder)
282
- expect(parent.querySelector('span')).not.toBeNull()
283
- expect(parent.querySelector('span')?.textContent).toBe('content')
284
- expect(parent.querySelector('span')?.className).toBe('test')
285
- })
286
-
287
- test('mounts an array of children', () => {
288
- const parent = document.createElement('div')
289
- const placeholder = document.createComment('')
290
- parent.appendChild(placeholder)
291
-
292
- _mountSlot(['first', ' ', 'second'], parent, placeholder)
293
- expect(parent.textContent).toBe('first second')
294
- })
295
-
296
- test('handles null/undefined children', () => {
297
- const parent = document.createElement('div')
298
- const placeholder = document.createComment('')
299
- parent.appendChild(placeholder)
300
-
301
- _mountSlot(null, parent, placeholder)
302
- expect(parent.childNodes.length).toBe(0)
303
- })
304
-
305
- test('handles false/true children', () => {
306
- const parent = document.createElement('div')
307
- const placeholder = document.createComment('')
308
- parent.appendChild(placeholder)
309
-
310
- _mountSlot(false, parent, placeholder)
311
- expect(parent.childNodes.length).toBe(0)
312
- })
313
- })
314
-
315
- // ─── Audit bug #5: _tplCache LRU eviction ───────────────────────────────────
316
- //
317
- // The cache is an LRU-bounded Map keyed on the HTML string. Typed JSX
318
- // produces a small bounded set of unique HTML strings — most apps stay in
319
- // the dozens-to-hundreds. But an app that constructs JSX from user input
320
- // or compiles many large dynamic templates could grow this unbounded
321
- // pre-fix. The cap at 1024 entries keeps memory predictable while being
322
- // generous enough that no realistic codebase hits it.
323
-
324
- describe('_tpl cache — LRU eviction (audit bug #5)', () => {
325
- const TPL_CACHE_MAX = 1024
326
-
327
- test('cache stays bounded when more than MAX unique templates are emitted', () => {
328
- _clearTplCache()
329
- const noBind = (): null => null
330
-
331
- // Emit 1.5x the cap of unique templates — without LRU bound, cache
332
- // would grow to 1536 entries.
333
- const overshoot = Math.floor(TPL_CACHE_MAX * 1.5)
334
- for (let i = 0; i < overshoot; i++) {
335
- _tpl(`<div data-i="${i}">${i}</div>`, noBind)
336
- }
337
-
338
- expect(_tplCacheSize()).toBeLessThanOrEqual(TPL_CACHE_MAX)
339
- expect(_tplCacheSize()).toBeGreaterThan(0)
340
- })
341
-
342
- test('eviction is oldest-first; recently-touched entries survive', () => {
343
- _clearTplCache()
344
- const noBind = (): null => null
345
-
346
- // Fill the cache to the cap.
347
- const baseHtml = (i: number): string => `<span data-i="${i}">${i}</span>`
348
- for (let i = 0; i < TPL_CACHE_MAX; i++) _tpl(baseHtml(i), noBind)
349
- expect(_tplCacheSize()).toBe(TPL_CACHE_MAX)
350
-
351
- // Touch entry 0 (the oldest). Map insertion-order semantics mean a
352
- // re-insert after delete moves it to the most-recent position.
353
- _tpl(baseHtml(0), noBind)
354
-
355
- // Add ONE new entry — the OLDEST untouched entry should evict, NOT entry 0.
356
- _tpl('<p>brand-new</p>', noBind)
357
-
358
- expect(_tplCacheSize()).toBe(TPL_CACHE_MAX)
359
-
360
- // Touch entry 0 again — if eviction policy were broken and entry 0
361
- // had been evicted, this re-creates it. We need a way to assert it
362
- // was retained. Approach: count cache misses by checking size delta.
363
- // Adding the brand-new entry above evicted ONE; the cache stayed at
364
- // cap. If we now add 1 more brand-new entry without re-using existing
365
- // keys, size stays at cap. If we re-touch entry 0, size also stays at
366
- // cap (already cached). The assertion: re-emitting entry 0 must NOT
367
- // grow the cache (cache hit, not miss).
368
- const sizeBeforeReHit = _tplCacheSize()
369
- _tpl(baseHtml(0), noBind)
370
- expect(_tplCacheSize()).toBe(sizeBeforeReHit) // re-emit was a hit
371
- })
372
-
373
- test('repeated emit of same template produces ONE cached entry', () => {
374
- _clearTplCache()
375
- const noBind = (): null => null
376
-
377
- for (let i = 0; i < 100; i++) {
378
- _tpl('<div class="static"></div>', noBind)
379
- }
380
-
381
- expect(_tplCacheSize()).toBe(1)
382
- })
383
- })
@@ -1,126 +0,0 @@
1
- import type { ComponentFn } from '@pyreon/core'
2
- import { h } from '@pyreon/core'
3
- import { signal } from '@pyreon/reactivity'
4
- import { Transition as _Transition, mount } from '../index'
5
-
6
- const Transition = _Transition as unknown as ComponentFn<Record<string, unknown>>
7
-
8
- function container(): HTMLElement {
9
- const el = document.createElement('div')
10
- document.body.appendChild(el)
11
- return el
12
- }
13
-
14
- // Regression for the 5s safety-timer leak in applyEnter / applyLeave.
15
- // Before the fix, when transitionend fired normally the 5s setTimeout was
16
- // never cleared and would re-invoke `done()` later, firing onAfterEnter /
17
- // onAfterLeave twice and leaking closures over the element.
18
- describe('Transition — safety-timer leak (regression)', () => {
19
- beforeEach(() => {
20
- vi.useFakeTimers()
21
- })
22
- afterEach(() => {
23
- vi.useRealTimers()
24
- })
25
-
26
- test('onAfterEnter fires exactly once when transitionend fires before 5s safety timeout', async () => {
27
- const el = container()
28
- const show = signal(false)
29
- const onAfterEnter = vi.fn()
30
-
31
- mount(
32
- h(
33
- Transition,
34
- { name: 'fade', show: () => show(), onAfterEnter },
35
- h('div', { class: 'enter-leak' }, 'x'),
36
- ),
37
- el,
38
- )
39
-
40
- show.set(true)
41
- await vi.advanceTimersByTimeAsync(20)
42
-
43
- const target = el.querySelector('.enter-leak') as HTMLElement
44
- expect(target).not.toBeNull()
45
-
46
- target.dispatchEvent(new Event('transitionend'))
47
- expect(onAfterEnter).toHaveBeenCalledTimes(1)
48
-
49
- // Advance past the safety timeout. With the leak, done() would fire again.
50
- await vi.advanceTimersByTimeAsync(6000)
51
- expect(onAfterEnter).toHaveBeenCalledTimes(1)
52
- })
53
-
54
- test('onAfterLeave fires exactly once when transitionend fires before 5s safety timeout', async () => {
55
- const el = container()
56
- const show = signal(true)
57
- const onAfterLeave = vi.fn()
58
-
59
- mount(
60
- h(
61
- Transition,
62
- { name: 'fade', show: () => show(), onAfterLeave },
63
- h('div', { class: 'leave-leak' }, 'x'),
64
- ),
65
- el,
66
- )
67
- await vi.advanceTimersByTimeAsync(20)
68
-
69
- show.set(false)
70
- await vi.advanceTimersByTimeAsync(20)
71
-
72
- const target = el.querySelector('.leave-leak') as HTMLElement
73
- expect(target).not.toBeNull()
74
-
75
- target.dispatchEvent(new Event('transitionend'))
76
- expect(onAfterLeave).toHaveBeenCalledTimes(1)
77
-
78
- await vi.advanceTimersByTimeAsync(6000)
79
- expect(onAfterLeave).toHaveBeenCalledTimes(1)
80
- })
81
-
82
- // Regression: component unmount during an in-flight transition used to
83
- // leave the 5s safety timer running. onAfterEnter / onAfterLeave would
84
- // fire on a detached element up to 5 seconds after unmount.
85
- test('onAfterEnter does NOT fire after component unmount during enter transition', async () => {
86
- const el = container()
87
- const show = signal(false)
88
- const onAfterEnter = vi.fn()
89
- const dispose = mount(
90
- h(
91
- Transition,
92
- { name: 'fade', show: () => show(), onAfterEnter },
93
- h('div', { class: 'unmount-enter' }, 'x'),
94
- ),
95
- el,
96
- )
97
-
98
- show.set(true)
99
- await vi.advanceTimersByTimeAsync(20)
100
- // Mid-transition — unmount without firing transitionend.
101
- dispose()
102
- await vi.advanceTimersByTimeAsync(6000)
103
- expect(onAfterEnter).not.toHaveBeenCalled()
104
- })
105
-
106
- test('onAfterLeave does NOT fire after component unmount during leave transition', async () => {
107
- const el = container()
108
- const show = signal(true)
109
- const onAfterLeave = vi.fn()
110
- const dispose = mount(
111
- h(
112
- Transition,
113
- { name: 'fade', show: () => show(), onAfterLeave },
114
- h('div', { class: 'unmount-leave' }, 'x'),
115
- ),
116
- el,
117
- )
118
- await vi.advanceTimersByTimeAsync(20)
119
-
120
- show.set(false)
121
- await vi.advanceTimersByTimeAsync(20)
122
- dispose()
123
- await vi.advanceTimersByTimeAsync(6000)
124
- expect(onAfterLeave).not.toHaveBeenCalled()
125
- })
126
- })