@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.
- package/package.json +5 -9
- package/src/delegate.ts +0 -98
- package/src/devtools.ts +0 -339
- package/src/env.d.ts +0 -6
- package/src/hydrate.ts +0 -450
- package/src/hydration-debug.ts +0 -129
- package/src/index.ts +0 -83
- package/src/keep-alive-entry.ts +0 -3
- package/src/keep-alive.ts +0 -83
- package/src/manifest.ts +0 -236
- package/src/mount.ts +0 -597
- package/src/nodes.ts +0 -896
- package/src/props.ts +0 -474
- package/src/template.ts +0 -523
- package/src/tests/callback-ref-unmount.browser.test.ts +0 -62
- package/src/tests/callback-ref-unmount.test.ts +0 -52
- package/src/tests/compiler-integration.test.tsx +0 -508
- package/src/tests/coverage-gaps.test.ts +0 -3183
- package/src/tests/coverage.test.ts +0 -1140
- package/src/tests/ctx-stack-growth-repro.test.tsx +0 -158
- package/src/tests/dev-gate-pattern.test.ts +0 -46
- package/src/tests/dev-gate-treeshake.test.ts +0 -256
- package/src/tests/error-boundary-stack-leak-repro.test.tsx +0 -133
- package/src/tests/fanout-repro.test.tsx +0 -219
- package/src/tests/hydration-integration.test.tsx +0 -540
- package/src/tests/keyed-array-in-for-batched-toggle.browser.test.ts +0 -140
- package/src/tests/lifecycle-integration.test.tsx +0 -342
- package/src/tests/lis-prepend.browser.test.ts +0 -99
- package/src/tests/manifest-snapshot.test.ts +0 -85
- package/src/tests/mount.test.ts +0 -3529
- package/src/tests/native-markers.test.ts +0 -19
- package/src/tests/props.test.ts +0 -581
- package/src/tests/reactive-props.test.ts +0 -270
- package/src/tests/real-world-integration.test.tsx +0 -714
- package/src/tests/rs-collapse-dyn-h.browser.test.ts +0 -303
- package/src/tests/rs-collapse-dyn.browser.test.ts +0 -316
- package/src/tests/rs-collapse-h.browser.test.ts +0 -152
- package/src/tests/rs-collapse-h.test.ts +0 -237
- package/src/tests/rs-collapse.browser.test.ts +0 -128
- package/src/tests/runtime-dom.browser.test.ts +0 -409
- package/src/tests/setup.ts +0 -3
- package/src/tests/show-context.test.ts +0 -270
- package/src/tests/show-of-for-batched-toggle.browser.test.ts +0 -122
- package/src/tests/ssr-xss-round-trip.browser.test.ts +0 -93
- package/src/tests/style-key-removal.browser.test.ts +0 -54
- package/src/tests/style-key-removal.test.ts +0 -88
- package/src/tests/template.test.ts +0 -383
- package/src/tests/transition-timeout-leak.test.ts +0 -126
- package/src/tests/transition.test.ts +0 -568
- package/src/tests/verified-correct-probes.test.ts +0 -56
- package/src/transition-entry.ts +0 -7
- package/src/transition-group.ts +0 -350
- 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
|
-
})
|