@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,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
|
-
})
|
package/src/transition-entry.ts
DELETED
|
@@ -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'
|