@pyreon/kinetic 0.11.1 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,388 @@
1
+ import type { VNode } from "@pyreon/core"
2
+ import GroupRenderer from "../kinetic/GroupRenderer"
3
+ import type { KineticConfig } from "../kinetic/types"
4
+
5
+ // Mock rAF for deterministic testing
6
+ let rafCallbacks: (() => void)[] = []
7
+ const originalRaf = globalThis.requestAnimationFrame
8
+ const originalCaf = globalThis.cancelAnimationFrame
9
+
10
+ beforeEach(() => {
11
+ vi.useFakeTimers()
12
+ rafCallbacks = []
13
+
14
+ vi.stubGlobal(
15
+ "requestAnimationFrame",
16
+ vi.fn((cb: () => void) => {
17
+ rafCallbacks.push(cb)
18
+ return rafCallbacks.length
19
+ }),
20
+ )
21
+
22
+ vi.stubGlobal("cancelAnimationFrame", vi.fn())
23
+ })
24
+
25
+ afterEach(() => {
26
+ vi.useRealTimers()
27
+ vi.stubGlobal("requestAnimationFrame", originalRaf)
28
+ vi.stubGlobal("cancelAnimationFrame", originalCaf)
29
+ })
30
+
31
+ const makeConfig = (overrides: Partial<KineticConfig> = {}): KineticConfig => ({
32
+ tag: "div",
33
+ mode: "group",
34
+ enter: "g-enter",
35
+ enterFrom: "g-enter-from",
36
+ enterTo: "g-enter-to",
37
+ leave: "g-leave",
38
+ leaveFrom: "g-leave-from",
39
+ leaveTo: "g-leave-to",
40
+ ...overrides,
41
+ })
42
+
43
+ const makeKeyedChild = (key: string | number, text: string): VNode => ({
44
+ type: "span",
45
+ props: { "data-testid": `child-${key}` },
46
+ children: [text],
47
+ key,
48
+ })
49
+
50
+ describe("GroupRenderer", () => {
51
+ it("returns a VNode wrapping children in config.tag", () => {
52
+ const config = makeConfig()
53
+ const children = [makeKeyedChild("a", "Alpha"), makeKeyedChild("b", "Beta")]
54
+
55
+ const vnode = GroupRenderer({
56
+ config,
57
+ htmlProps: {},
58
+ callbacks: {},
59
+ children,
60
+ })
61
+
62
+ expect(vnode).not.toBeNull()
63
+ expect(vnode?.type).toBe("div")
64
+ })
65
+
66
+ it("uses custom tag from config", () => {
67
+ const config = makeConfig({ tag: "ul" })
68
+ const children = [makeKeyedChild("a", "Alpha")]
69
+
70
+ const vnode = GroupRenderer({
71
+ config,
72
+ htmlProps: {},
73
+ callbacks: {},
74
+ children,
75
+ })
76
+
77
+ expect(vnode?.type).toBe("ul")
78
+ })
79
+
80
+ it("passes htmlProps to the wrapper element", () => {
81
+ const config = makeConfig()
82
+ const children = [makeKeyedChild("a", "Alpha")]
83
+
84
+ const vnode = GroupRenderer({
85
+ config,
86
+ htmlProps: { "data-testid": "group-wrapper", class: "my-group" },
87
+ callbacks: {},
88
+ children,
89
+ })
90
+
91
+ const props = vnode?.props as Record<string, unknown>
92
+ expect(props?.["data-testid"]).toBe("group-wrapper")
93
+ expect(props?.class).toBe("my-group")
94
+ })
95
+
96
+ it("wraps each keyed child in a TransitionItem", () => {
97
+ const config = makeConfig()
98
+ const children = [makeKeyedChild("a", "Alpha"), makeKeyedChild("b", "Beta")]
99
+
100
+ const vnode = GroupRenderer({
101
+ config,
102
+ htmlProps: {},
103
+ callbacks: {},
104
+ children,
105
+ })
106
+
107
+ // The wrapper div should have children that are TransitionItem VNodes
108
+ const wrapperChildren = vnode?.children
109
+ expect(wrapperChildren).toBeDefined()
110
+ const childArray = Array.isArray(wrapperChildren) ? wrapperChildren : [wrapperChildren]
111
+ expect(childArray.length).toBe(2)
112
+
113
+ // Each child should be a TransitionItem (function component)
114
+ for (const child of childArray) {
115
+ const childVNode = child as VNode
116
+ expect(typeof childVNode.type).toBe("function")
117
+ }
118
+ })
119
+
120
+ it("sets appear=true for newly added children (non-initial)", () => {
121
+ const config = makeConfig()
122
+
123
+ // First render with initial children
124
+ const initialChildren = [makeKeyedChild("a", "Alpha")]
125
+ GroupRenderer({
126
+ config,
127
+ htmlProps: {},
128
+ callbacks: {},
129
+ children: initialChildren,
130
+ })
131
+
132
+ // Second render with a new child added
133
+ const updatedChildren = [makeKeyedChild("a", "Alpha"), makeKeyedChild("b", "Beta")]
134
+ const vnode = GroupRenderer({
135
+ config,
136
+ htmlProps: {},
137
+ callbacks: {},
138
+ children: updatedChildren,
139
+ })
140
+
141
+ const wrapperChildren = vnode?.children as VNode[]
142
+ expect(wrapperChildren.length).toBe(2)
143
+ })
144
+
145
+ it("passes transition class config to TransitionItem children", () => {
146
+ const config = makeConfig({
147
+ enter: "custom-enter",
148
+ enterFrom: "custom-from",
149
+ enterTo: "custom-to",
150
+ leave: "custom-leave",
151
+ leaveFrom: "custom-lfrom",
152
+ leaveTo: "custom-lto",
153
+ })
154
+ const children = [makeKeyedChild("a", "Alpha")]
155
+
156
+ const vnode = GroupRenderer({
157
+ config,
158
+ htmlProps: {},
159
+ callbacks: {},
160
+ children,
161
+ })
162
+
163
+ const wrapperChildren = vnode?.children as VNode[]
164
+ const transitionItemVNode = wrapperChildren[0] as VNode
165
+ const tiProps = transitionItemVNode.props as Record<string, unknown>
166
+
167
+ expect(tiProps.enter).toBe("custom-enter")
168
+ expect(tiProps.enterFrom).toBe("custom-from")
169
+ expect(tiProps.enterTo).toBe("custom-to")
170
+ expect(tiProps.leave).toBe("custom-leave")
171
+ expect(tiProps.leaveFrom).toBe("custom-lfrom")
172
+ expect(tiProps.leaveTo).toBe("custom-lto")
173
+ })
174
+
175
+ it("passes style transition config to TransitionItem children", () => {
176
+ const config = makeConfig({
177
+ enterStyle: { opacity: 0 },
178
+ enterToStyle: { opacity: 1 },
179
+ enterTransition: "opacity 300ms ease",
180
+ leaveStyle: { opacity: 1 },
181
+ leaveToStyle: { opacity: 0 },
182
+ leaveTransition: "opacity 200ms ease-in",
183
+ })
184
+ const children = [makeKeyedChild("a", "Alpha")]
185
+
186
+ const vnode = GroupRenderer({
187
+ config,
188
+ htmlProps: {},
189
+ callbacks: {},
190
+ children,
191
+ })
192
+
193
+ const wrapperChildren = vnode?.children as VNode[]
194
+ const tiProps = wrapperChildren[0]?.props as Record<string, unknown>
195
+
196
+ expect(tiProps.enterStyle).toEqual({ opacity: 0 })
197
+ expect(tiProps.enterToStyle).toEqual({ opacity: 1 })
198
+ expect(tiProps.enterTransition).toBe("opacity 300ms ease")
199
+ expect(tiProps.leaveStyle).toEqual({ opacity: 1 })
200
+ expect(tiProps.leaveToStyle).toEqual({ opacity: 0 })
201
+ expect(tiProps.leaveTransition).toBe("opacity 200ms ease-in")
202
+ })
203
+
204
+ it("uses effectiveAppear from config when appear prop is not provided", () => {
205
+ const config = makeConfig({ appear: true })
206
+ const children = [makeKeyedChild("a", "Alpha")]
207
+
208
+ const vnode = GroupRenderer({
209
+ config,
210
+ htmlProps: {},
211
+ callbacks: {},
212
+ children,
213
+ })
214
+
215
+ const wrapperChildren = vnode?.children as VNode[]
216
+ const tiProps = wrapperChildren[0]?.props as Record<string, unknown>
217
+
218
+ // Initial children should use effectiveAppear (true)
219
+ expect(tiProps.appear).toBe(true)
220
+ })
221
+
222
+ it("uses effectiveTimeout from config when timeout prop is not provided", () => {
223
+ const config = makeConfig({ timeout: 2000 })
224
+ const children = [makeKeyedChild("a", "Alpha")]
225
+
226
+ const vnode = GroupRenderer({
227
+ config,
228
+ htmlProps: {},
229
+ callbacks: {},
230
+ children,
231
+ })
232
+
233
+ const wrapperChildren = vnode?.children as VNode[]
234
+ const tiProps = wrapperChildren[0]?.props as Record<string, unknown>
235
+
236
+ expect(tiProps.timeout).toBe(2000)
237
+ })
238
+
239
+ it("defaults timeout to 5000 when not provided", () => {
240
+ const config = makeConfig()
241
+ const children = [makeKeyedChild("a", "Alpha")]
242
+
243
+ const vnode = GroupRenderer({
244
+ config,
245
+ htmlProps: {},
246
+ callbacks: {},
247
+ children,
248
+ })
249
+
250
+ const wrapperChildren = vnode?.children as VNode[]
251
+ const tiProps = wrapperChildren[0]?.props as Record<string, unknown>
252
+
253
+ expect(tiProps.timeout).toBe(5000)
254
+ })
255
+
256
+ it("ignores children without keys", () => {
257
+ const config = makeConfig()
258
+ const keyedChild = makeKeyedChild("a", "Alpha")
259
+ const unkeyedChild: VNode = { type: "span", props: {}, children: ["No key"], key: null }
260
+
261
+ const vnode = GroupRenderer({
262
+ config,
263
+ htmlProps: {},
264
+ callbacks: {},
265
+ children: [keyedChild, unkeyedChild],
266
+ })
267
+
268
+ // Only keyed child should be wrapped in TransitionItem
269
+ const wrapperChildren = vnode?.children as VNode[]
270
+ expect(wrapperChildren.length).toBe(1)
271
+ })
272
+
273
+ it("appear prop overrides config.appear", () => {
274
+ const config = makeConfig({ appear: false })
275
+ const children = [makeKeyedChild("a", "Alpha")]
276
+
277
+ const vnode = GroupRenderer({
278
+ config,
279
+ htmlProps: {},
280
+ appear: true,
281
+ callbacks: {},
282
+ children,
283
+ })
284
+
285
+ const wrapperChildren = vnode?.children as VNode[]
286
+ const tiProps = wrapperChildren[0]?.props as Record<string, unknown>
287
+
288
+ expect(tiProps.appear).toBe(true)
289
+ })
290
+
291
+ it("timeout prop overrides config.timeout", () => {
292
+ const config = makeConfig({ timeout: 2000 })
293
+ const children = [makeKeyedChild("a", "Alpha")]
294
+
295
+ const vnode = GroupRenderer({
296
+ config,
297
+ htmlProps: {},
298
+ timeout: 3000,
299
+ callbacks: {},
300
+ children,
301
+ })
302
+
303
+ const wrapperChildren = vnode?.children as VNode[]
304
+ const tiProps = wrapperChildren[0]?.props as Record<string, unknown>
305
+
306
+ expect(tiProps.timeout).toBe(3000)
307
+ })
308
+
309
+ it("handleAfterLeave fires the callbacks.onAfterLeave and updates forceUpdateSignal", () => {
310
+ const onAfterLeave = vi.fn()
311
+ const config = makeConfig()
312
+ const children = [makeKeyedChild("a", "Alpha")]
313
+
314
+ const vnode = GroupRenderer({
315
+ config,
316
+ htmlProps: {},
317
+ callbacks: { onAfterLeave },
318
+ children,
319
+ })
320
+
321
+ // Each TransitionItem child gets an onAfterLeave that calls handleAfterLeave(key)
322
+ const wrapperChildren = vnode?.children as VNode[]
323
+ const tiProps = wrapperChildren[0]?.props as Record<string, unknown>
324
+ const tiAfterLeave = tiProps.onAfterLeave as () => void
325
+
326
+ expect(tiAfterLeave).toBeDefined()
327
+ tiAfterLeave()
328
+
329
+ expect(onAfterLeave).toHaveBeenCalledTimes(1)
330
+ })
331
+
332
+ it("TransitionItem children have show returning true for current children", () => {
333
+ const config = makeConfig()
334
+ const children = [makeKeyedChild("a", "Alpha"), makeKeyedChild("b", "Beta")]
335
+
336
+ const vnode = GroupRenderer({
337
+ config,
338
+ htmlProps: {},
339
+ callbacks: {},
340
+ children,
341
+ })
342
+
343
+ const wrapperChildren = vnode?.children as VNode[]
344
+
345
+ for (const child of wrapperChildren) {
346
+ const tiProps = (child as VNode).props as Record<string, unknown>
347
+ const showFn = tiProps.show as () => boolean
348
+ expect(showFn()).toBe(true)
349
+ }
350
+ })
351
+
352
+ it("initial children use effectiveAppear for appear prop", () => {
353
+ const config = makeConfig({ appear: false })
354
+ const children = [makeKeyedChild("a", "Alpha")]
355
+
356
+ const vnode = GroupRenderer({
357
+ config,
358
+ htmlProps: {},
359
+ callbacks: {},
360
+ children,
361
+ })
362
+
363
+ const wrapperChildren = vnode?.children as VNode[]
364
+ const tiProps = wrapperChildren[0]?.props as Record<string, unknown>
365
+
366
+ // Initial keys use effectiveAppear (false in this case)
367
+ expect(tiProps.appear).toBe(false)
368
+ })
369
+
370
+ it("each TransitionItem child gets the element as its children", () => {
371
+ const config = makeConfig()
372
+ const children = [makeKeyedChild("a", "Alpha")]
373
+
374
+ const vnode = GroupRenderer({
375
+ config,
376
+ htmlProps: {},
377
+ callbacks: {},
378
+ children,
379
+ })
380
+
381
+ const wrapperChildren = vnode?.children as VNode[]
382
+ const tiVNode = wrapperChildren[0] as VNode
383
+
384
+ // The TransitionItem should have the original element as children
385
+ const tiChildren = (tiVNode.props as Record<string, unknown>)?.children ?? tiVNode.children
386
+ expect(tiChildren).toBeDefined()
387
+ })
388
+ })