@pyreon/elements 0.11.1 → 0.11.3
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 +8 -7
- package/src/Element/component.tsx +211 -0
- package/src/Element/constants.ts +96 -0
- package/src/Element/index.ts +6 -0
- package/src/Element/types.ts +168 -0
- package/src/Element/utils.ts +15 -0
- package/src/List/component.tsx +57 -0
- package/src/List/index.ts +5 -0
- package/src/Overlay/component.tsx +131 -0
- package/src/Overlay/context.tsx +37 -0
- package/src/Overlay/index.ts +7 -0
- package/src/Overlay/useOverlay.tsx +616 -0
- package/src/Portal/component.tsx +41 -0
- package/src/Portal/index.ts +5 -0
- package/src/Text/component.tsx +65 -0
- package/src/Text/index.ts +5 -0
- package/src/Text/styled.ts +30 -0
- package/src/Util/component.tsx +43 -0
- package/src/Util/index.ts +5 -0
- package/src/__tests__/Content.test.tsx +115 -0
- package/src/__tests__/Element.test.ts +604 -0
- package/src/__tests__/Iterator.test.ts +483 -0
- package/src/__tests__/List.test.ts +199 -0
- package/src/__tests__/Overlay.test.ts +485 -0
- package/src/__tests__/Portal.test.ts +82 -0
- package/src/__tests__/Text.test.ts +274 -0
- package/src/__tests__/Util.test.ts +63 -0
- package/src/__tests__/Wrapper.test.tsx +152 -0
- package/src/__tests__/equalBeforeAfter.test.ts +122 -0
- package/src/__tests__/helpers.test.ts +65 -0
- package/src/__tests__/overlayContext.test.tsx +78 -0
- package/src/__tests__/responsiveProps.test.ts +298 -0
- package/src/__tests__/useOverlay.test.ts +1330 -0
- package/src/__tests__/utils.test.ts +69 -0
- package/src/constants.ts +1 -0
- package/src/helpers/Content/component.tsx +51 -0
- package/src/helpers/Content/index.ts +3 -0
- package/src/helpers/Content/styled.ts +105 -0
- package/src/helpers/Content/types.ts +49 -0
- package/src/helpers/Iterator/component.tsx +252 -0
- package/src/helpers/Iterator/index.ts +13 -0
- package/src/helpers/Iterator/types.ts +79 -0
- package/src/helpers/Wrapper/component.tsx +78 -0
- package/src/helpers/Wrapper/constants.ts +10 -0
- package/src/helpers/Wrapper/index.ts +3 -0
- package/src/helpers/Wrapper/styled.ts +69 -0
- package/src/helpers/Wrapper/types.ts +56 -0
- package/src/helpers/Wrapper/utils.ts +7 -0
- package/src/helpers/index.ts +4 -0
- package/src/index.ts +37 -0
- package/src/types.ts +81 -0
- package/src/utils.ts +1 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import type { ComponentFn, VNode, VNodeChild } from "@pyreon/core"
|
|
2
|
+
import { Fragment, h } from "@pyreon/core"
|
|
3
|
+
import { describe, expect, it, vi } from "vitest"
|
|
4
|
+
import Iterator from "../helpers/Iterator/component"
|
|
5
|
+
|
|
6
|
+
const asVNode = (v: unknown) => v as VNode
|
|
7
|
+
|
|
8
|
+
const TextItem: ComponentFn = (props: any) =>
|
|
9
|
+
h("span", { "data-testid": "item", ...props }, props.children)
|
|
10
|
+
|
|
11
|
+
describe("Iterator", () => {
|
|
12
|
+
describe("static properties", () => {
|
|
13
|
+
it("has isIterator flag", () => {
|
|
14
|
+
expect(Iterator.isIterator).toBe(true)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it("has RESERVED_PROPS", () => {
|
|
18
|
+
expect(Iterator.RESERVED_PROPS).toContain("children")
|
|
19
|
+
expect(Iterator.RESERVED_PROPS).toContain("component")
|
|
20
|
+
expect(Iterator.RESERVED_PROPS).toContain("data")
|
|
21
|
+
expect(Iterator.RESERVED_PROPS).toContain("itemKey")
|
|
22
|
+
expect(Iterator.RESERVED_PROPS).toContain("valueName")
|
|
23
|
+
expect(Iterator.RESERVED_PROPS).toContain("itemProps")
|
|
24
|
+
expect(Iterator.RESERVED_PROPS).toContain("wrapComponent")
|
|
25
|
+
expect(Iterator.RESERVED_PROPS).toContain("wrapProps")
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe("children mode", () => {
|
|
30
|
+
it("renders children directly", () => {
|
|
31
|
+
const children = [
|
|
32
|
+
h("span", { "data-testid": "child-1" }, "A"),
|
|
33
|
+
h("span", { "data-testid": "child-2" }, "B"),
|
|
34
|
+
]
|
|
35
|
+
const result = Iterator({ children })
|
|
36
|
+
expect(result).toEqual(children)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("renders single child", () => {
|
|
40
|
+
const child = h("span", { "data-testid": "only" }, "Only")
|
|
41
|
+
const result = Iterator({ children: child })
|
|
42
|
+
expect(result).toBe(child)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("returns null when children is null/undefined", () => {
|
|
46
|
+
const result = Iterator({})
|
|
47
|
+
expect(result).toBeNull()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("renders fragment children", () => {
|
|
51
|
+
const children = [
|
|
52
|
+
h("span", { "data-testid": "frag-1" }, "A"),
|
|
53
|
+
h("span", { "data-testid": "frag-2" }, "B"),
|
|
54
|
+
]
|
|
55
|
+
const result = Iterator({ children })
|
|
56
|
+
expect(result).toEqual(children)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("renders Fragment children with itemProps", () => {
|
|
60
|
+
const itemPropsFn = vi.fn((_item: unknown, extended: any) => ({
|
|
61
|
+
"data-pos": String(extended.position),
|
|
62
|
+
}))
|
|
63
|
+
const fragChildren = [
|
|
64
|
+
h("span", { "data-testid": "frag-a" }, "A"),
|
|
65
|
+
h("span", { "data-testid": "frag-b" }, "B"),
|
|
66
|
+
]
|
|
67
|
+
const fragment = h(Fragment, null, ...fragChildren)
|
|
68
|
+
const result = Iterator({ children: fragment, itemProps: itemPropsFn })
|
|
69
|
+
expect(itemPropsFn).toHaveBeenCalled()
|
|
70
|
+
expect(Array.isArray(result)).toBe(true)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("renders Fragment children with wrapComponent", () => {
|
|
74
|
+
const Wrap: ComponentFn = (props: any) => h("div", { "data-testid": "wrap" }, props.children)
|
|
75
|
+
const fragChildren = [
|
|
76
|
+
h("span", { "data-testid": "frag-a" }, "A"),
|
|
77
|
+
h("span", { "data-testid": "frag-b" }, "B"),
|
|
78
|
+
]
|
|
79
|
+
const fragment = h(Fragment, null, ...fragChildren)
|
|
80
|
+
const result = Iterator({ children: fragment, wrapComponent: Wrap }) as VNodeChild[]
|
|
81
|
+
expect(Array.isArray(result)).toBe(true)
|
|
82
|
+
expect((result as any[]).length).toBe(2)
|
|
83
|
+
// Each item should be wrapped
|
|
84
|
+
const first = asVNode(result[0])
|
|
85
|
+
expect(first.type).toBe(Wrap)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("children take priority over data", () => {
|
|
89
|
+
const child = h("span", { "data-testid": "child" }, "Child wins")
|
|
90
|
+
const result = Iterator({
|
|
91
|
+
children: child,
|
|
92
|
+
component: TextItem,
|
|
93
|
+
data: ["x", "y"],
|
|
94
|
+
})
|
|
95
|
+
expect(result).toBe(child)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe("simple array mode", () => {
|
|
100
|
+
it("renders string array with component", () => {
|
|
101
|
+
const result = Iterator({
|
|
102
|
+
component: TextItem,
|
|
103
|
+
data: ["hello", "world"],
|
|
104
|
+
}) as VNodeChild[]
|
|
105
|
+
expect(Array.isArray(result)).toBe(true)
|
|
106
|
+
expect(result).toHaveLength(2)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it("renders number array with component", () => {
|
|
110
|
+
const result = Iterator({
|
|
111
|
+
component: TextItem,
|
|
112
|
+
data: [1, 2, 3],
|
|
113
|
+
}) as VNodeChild[]
|
|
114
|
+
expect(Array.isArray(result)).toBe(true)
|
|
115
|
+
expect(result).toHaveLength(3)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("filters null/undefined from data", () => {
|
|
119
|
+
const result = Iterator({
|
|
120
|
+
component: TextItem,
|
|
121
|
+
data: ["a", null, "b", undefined],
|
|
122
|
+
}) as VNodeChild[]
|
|
123
|
+
expect(Array.isArray(result)).toBe(true)
|
|
124
|
+
expect(result).toHaveLength(2)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it("returns null for empty array", () => {
|
|
128
|
+
const result = Iterator({ component: TextItem, data: [] })
|
|
129
|
+
expect(result).toBeNull()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it("returns null for all-null array", () => {
|
|
133
|
+
const result = Iterator({ component: TextItem, data: [null, null] })
|
|
134
|
+
expect(result).toBeNull()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it("uses valueName to set prop name", () => {
|
|
138
|
+
const Item: ComponentFn = (props: any) => h("span", { "data-testid": "item" }, props.title)
|
|
139
|
+
const result = Iterator({
|
|
140
|
+
component: Item,
|
|
141
|
+
data: ["hello"],
|
|
142
|
+
valueName: "title",
|
|
143
|
+
}) as VNodeChild[]
|
|
144
|
+
expect(Array.isArray(result)).toBe(true)
|
|
145
|
+
expect(result).toHaveLength(1)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it("defaults valueName to children", () => {
|
|
149
|
+
const result = Iterator({
|
|
150
|
+
component: TextItem,
|
|
151
|
+
data: ["test"],
|
|
152
|
+
}) as VNodeChild[]
|
|
153
|
+
expect(Array.isArray(result)).toBe(true)
|
|
154
|
+
expect(result).toHaveLength(1)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe("object array mode", () => {
|
|
159
|
+
it("renders object array with component", () => {
|
|
160
|
+
const Item: ComponentFn = (props: any) => h("span", { "data-testid": "item" }, props.name)
|
|
161
|
+
const result = Iterator({
|
|
162
|
+
component: Item,
|
|
163
|
+
data: [
|
|
164
|
+
{ id: 1, name: "Alice" },
|
|
165
|
+
{ id: 2, name: "Bob" },
|
|
166
|
+
],
|
|
167
|
+
}) as VNodeChild[]
|
|
168
|
+
expect(Array.isArray(result)).toBe(true)
|
|
169
|
+
expect(result).toHaveLength(2)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it("filters empty objects from data", () => {
|
|
173
|
+
const Item: ComponentFn = (props: any) => h("span", { "data-testid": "item" }, props.name)
|
|
174
|
+
const result = Iterator({
|
|
175
|
+
component: Item,
|
|
176
|
+
data: [{ name: "Alice" }, {}, { name: "Bob" }],
|
|
177
|
+
}) as VNodeChild[]
|
|
178
|
+
expect(Array.isArray(result)).toBe(true)
|
|
179
|
+
expect(result).toHaveLength(2)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it("supports per-item component override", () => {
|
|
183
|
+
const Default: ComponentFn = (props: any) =>
|
|
184
|
+
h("span", { "data-testid": "default" }, props.label)
|
|
185
|
+
const Custom: ComponentFn = (props: any) => h("em", { "data-testid": "custom" }, props.label)
|
|
186
|
+
const result = Iterator({
|
|
187
|
+
component: Default,
|
|
188
|
+
data: [{ label: "one" }, { label: "two", component: Custom }],
|
|
189
|
+
}) as VNodeChild[]
|
|
190
|
+
expect(Array.isArray(result)).toBe(true)
|
|
191
|
+
expect(result).toHaveLength(2)
|
|
192
|
+
// Second item should use Custom component
|
|
193
|
+
const second = asVNode(result[1])
|
|
194
|
+
expect(second.type).toBe(Custom)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it("uses itemKey string to pick key from item", () => {
|
|
198
|
+
const Item: ComponentFn = (props: any) => h("span", { "data-testid": "item" }, props.slug)
|
|
199
|
+
const result = Iterator({
|
|
200
|
+
component: Item,
|
|
201
|
+
data: [{ slug: "a" }, { slug: "b" }],
|
|
202
|
+
itemKey: "slug",
|
|
203
|
+
}) as VNodeChild[]
|
|
204
|
+
expect(Array.isArray(result)).toBe(true)
|
|
205
|
+
expect(result).toHaveLength(2)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it("uses itemKey function for custom keys", () => {
|
|
209
|
+
const keyFn = vi.fn((_item: unknown, index: number) => `custom-${index}`)
|
|
210
|
+
const Item: ComponentFn = (props: any) => h("span", { "data-testid": "item" }, props.name)
|
|
211
|
+
Iterator({
|
|
212
|
+
component: Item,
|
|
213
|
+
data: [{ name: "a" }, { name: "b" }],
|
|
214
|
+
itemKey: keyFn,
|
|
215
|
+
})
|
|
216
|
+
expect(keyFn).toHaveBeenCalledTimes(2)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it("falls back to id/key/itemId for keys", () => {
|
|
220
|
+
const Item: ComponentFn = (props: any) => h("span", { "data-testid": "item" }, props.name)
|
|
221
|
+
const result = Iterator({
|
|
222
|
+
component: Item,
|
|
223
|
+
data: [
|
|
224
|
+
{ id: "x", name: "Alice" },
|
|
225
|
+
{ key: "y", name: "Bob" },
|
|
226
|
+
{ itemId: "z", name: "Charlie" },
|
|
227
|
+
],
|
|
228
|
+
}) as VNodeChild[]
|
|
229
|
+
expect(result).toHaveLength(3)
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
describe("itemProps", () => {
|
|
234
|
+
it("passes static itemProps to items", () => {
|
|
235
|
+
const result = Iterator({
|
|
236
|
+
component: TextItem,
|
|
237
|
+
data: ["hello"],
|
|
238
|
+
itemProps: { extra: "yes" },
|
|
239
|
+
}) as VNodeChild[]
|
|
240
|
+
expect(result).toHaveLength(1)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it("passes itemProps callback with extended props", () => {
|
|
244
|
+
const itemPropsFn = vi.fn((_item: unknown, extended: any) => ({
|
|
245
|
+
pos: extended.position,
|
|
246
|
+
isFirst: extended.first,
|
|
247
|
+
isLast: extended.last,
|
|
248
|
+
}))
|
|
249
|
+
Iterator({
|
|
250
|
+
component: TextItem,
|
|
251
|
+
data: ["a", "b", "c"],
|
|
252
|
+
itemProps: itemPropsFn,
|
|
253
|
+
})
|
|
254
|
+
expect(itemPropsFn).toHaveBeenCalledTimes(3)
|
|
255
|
+
// First call: position 1, first=true, last=false
|
|
256
|
+
const calls = itemPropsFn.mock.calls as unknown[][]
|
|
257
|
+
expect((calls[0] as unknown[])[1]).toMatchObject({
|
|
258
|
+
position: 1,
|
|
259
|
+
first: true,
|
|
260
|
+
last: false,
|
|
261
|
+
})
|
|
262
|
+
// Last call: position 3, first=false, last=true
|
|
263
|
+
expect((calls[2] as unknown[])[1]).toMatchObject({
|
|
264
|
+
position: 3,
|
|
265
|
+
first: false,
|
|
266
|
+
last: true,
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
describe("wrapComponent", () => {
|
|
272
|
+
it("wraps each item with wrapComponent", () => {
|
|
273
|
+
const Wrap: ComponentFn = (props: any) => h("div", { "data-testid": "wrap" }, props.children)
|
|
274
|
+
const result = Iterator({
|
|
275
|
+
component: TextItem,
|
|
276
|
+
data: ["a", "b"],
|
|
277
|
+
wrapComponent: Wrap,
|
|
278
|
+
}) as VNodeChild[]
|
|
279
|
+
expect(result).toHaveLength(2)
|
|
280
|
+
const first = asVNode(result[0])
|
|
281
|
+
expect(first.type).toBe(Wrap)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it("wraps children with wrapComponent", () => {
|
|
285
|
+
const Wrap: ComponentFn = (props: any) => h("div", { "data-testid": "wrap" }, props.children)
|
|
286
|
+
const result = Iterator({
|
|
287
|
+
wrapComponent: Wrap,
|
|
288
|
+
children: [h("span", null, "A"), h("span", null, "B")],
|
|
289
|
+
}) as VNodeChild[]
|
|
290
|
+
expect(result).toHaveLength(2)
|
|
291
|
+
const first = asVNode(result[0])
|
|
292
|
+
expect(first.type).toBe(Wrap)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it("passes wrapProps to wrapComponent", () => {
|
|
296
|
+
const Wrap: ComponentFn = (props: any) =>
|
|
297
|
+
h("div", { "data-testid": "wrap", "data-extra": props.extra }, props.children)
|
|
298
|
+
const result = Iterator({
|
|
299
|
+
component: TextItem,
|
|
300
|
+
data: ["a"],
|
|
301
|
+
wrapComponent: Wrap,
|
|
302
|
+
wrapProps: { extra: "val" },
|
|
303
|
+
}) as VNodeChild[]
|
|
304
|
+
expect(result).toHaveLength(1)
|
|
305
|
+
const first = asVNode(result[0])
|
|
306
|
+
expect(first.type).toBe(Wrap)
|
|
307
|
+
expect(first.props.extra).toBe("val")
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it("passes wrapProps callback with extended props", () => {
|
|
311
|
+
const wrapPropsFn = vi.fn((_item: unknown, extended: any) => ({
|
|
312
|
+
"data-pos": extended.position,
|
|
313
|
+
}))
|
|
314
|
+
const Wrap: ComponentFn = (props: any) =>
|
|
315
|
+
h("div", { "data-testid": "wrap", ...props }, props.children)
|
|
316
|
+
Iterator({
|
|
317
|
+
component: TextItem,
|
|
318
|
+
data: ["a", "b"],
|
|
319
|
+
wrapComponent: Wrap,
|
|
320
|
+
wrapProps: wrapPropsFn,
|
|
321
|
+
})
|
|
322
|
+
expect(wrapPropsFn).toHaveBeenCalledTimes(2)
|
|
323
|
+
const wCalls = wrapPropsFn.mock.calls as unknown[][]
|
|
324
|
+
expect((wCalls[0] as unknown[])[1]).toMatchObject({ position: 1 })
|
|
325
|
+
expect((wCalls[1] as unknown[])[1]).toMatchObject({ position: 2 })
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it("wraps object array items with wrapComponent and wrapProps callback", () => {
|
|
329
|
+
const wrapPropsFn = vi.fn((_item: unknown, extended: any) => ({
|
|
330
|
+
"data-pos": String(extended.position),
|
|
331
|
+
}))
|
|
332
|
+
const Item: ComponentFn = (props: any) => h("span", { "data-testid": "item" }, props.name)
|
|
333
|
+
const Wrap: ComponentFn = (props: any) =>
|
|
334
|
+
h("div", { "data-testid": "wrap", ...props }, props.children)
|
|
335
|
+
const result = Iterator({
|
|
336
|
+
component: Item,
|
|
337
|
+
data: [{ name: "Alice" }, { name: "Bob" }],
|
|
338
|
+
wrapComponent: Wrap,
|
|
339
|
+
wrapProps: wrapPropsFn,
|
|
340
|
+
}) as VNodeChild[]
|
|
341
|
+
expect(result).toHaveLength(2)
|
|
342
|
+
expect(wrapPropsFn).toHaveBeenCalledTimes(2)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it("passes itemProps callback to object array items", () => {
|
|
346
|
+
const itemPropsFn = vi.fn((_item: unknown, extended: any) => ({
|
|
347
|
+
"data-first": String(extended.first),
|
|
348
|
+
}))
|
|
349
|
+
const Item: ComponentFn = (props: any) =>
|
|
350
|
+
h("span", { "data-testid": "item", ...props }, props.name)
|
|
351
|
+
Iterator({
|
|
352
|
+
component: Item,
|
|
353
|
+
data: [{ name: "Alice" }, { name: "Bob" }],
|
|
354
|
+
itemProps: itemPropsFn,
|
|
355
|
+
})
|
|
356
|
+
expect(itemPropsFn).toHaveBeenCalledTimes(2)
|
|
357
|
+
const ipCalls = itemPropsFn.mock.calls as unknown[][]
|
|
358
|
+
expect((ipCalls[0] as unknown[])[1]).toMatchObject({ first: true })
|
|
359
|
+
expect((ipCalls[1] as unknown[])[1]).toMatchObject({ first: false })
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it("skips wrapComponent for items with custom component in object array", () => {
|
|
363
|
+
const Default: ComponentFn = (props: any) =>
|
|
364
|
+
h("span", { "data-testid": "default" }, props.label)
|
|
365
|
+
const Custom: ComponentFn = (props: any) => h("em", { "data-testid": "custom" }, props.label)
|
|
366
|
+
const Wrap: ComponentFn = (props: any) => h("div", { "data-testid": "wrap" }, props.children)
|
|
367
|
+
const result = Iterator({
|
|
368
|
+
component: Default,
|
|
369
|
+
data: [{ label: "one" }, { label: "two", component: Custom }],
|
|
370
|
+
wrapComponent: Wrap,
|
|
371
|
+
}) as VNodeChild[]
|
|
372
|
+
expect(result).toHaveLength(2)
|
|
373
|
+
// First item (default) should be wrapped
|
|
374
|
+
const first = asVNode(result[0])
|
|
375
|
+
expect(first.type).toBe(Wrap)
|
|
376
|
+
// Second item (custom component) should NOT be wrapped
|
|
377
|
+
const second = asVNode(result[1])
|
|
378
|
+
expect(second.type).toBe(Custom)
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
describe("children with itemProps (no wrapComponent)", () => {
|
|
383
|
+
it("injects itemProps into children without wrapping", () => {
|
|
384
|
+
const itemPropsFn = vi.fn(() => ({ "data-injected": "yes" }))
|
|
385
|
+
Iterator({
|
|
386
|
+
itemProps: itemPropsFn,
|
|
387
|
+
children: [
|
|
388
|
+
h("span", { "data-testid": "child-a" }, "A"),
|
|
389
|
+
h("span", { "data-testid": "child-b" }, "B"),
|
|
390
|
+
],
|
|
391
|
+
})
|
|
392
|
+
expect(itemPropsFn).toHaveBeenCalled()
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it("injects itemProps into single child", () => {
|
|
396
|
+
const itemPropsFn = vi.fn(() => ({}))
|
|
397
|
+
Iterator({
|
|
398
|
+
itemProps: itemPropsFn,
|
|
399
|
+
children: h("span", { "data-testid": "only" }, "Only"),
|
|
400
|
+
})
|
|
401
|
+
expect(itemPropsFn).toHaveBeenCalled()
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
describe("children rendering paths", () => {
|
|
406
|
+
it("renders single child without itemProps or wrapComponent (direct passthrough)", () => {
|
|
407
|
+
const child = h("span", { "data-testid": "single" }, "Single")
|
|
408
|
+
const result = Iterator({ children: child })
|
|
409
|
+
expect(result).toBe(child)
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it("renders array children without itemProps or wrapComponent", () => {
|
|
413
|
+
const children = [
|
|
414
|
+
h("span", { "data-testid": "a" }, "A"),
|
|
415
|
+
h("span", { "data-testid": "b" }, "B"),
|
|
416
|
+
]
|
|
417
|
+
const result = Iterator({ children })
|
|
418
|
+
expect(result).toEqual(children)
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it("renders single child with wrapComponent", () => {
|
|
422
|
+
const Wrap: ComponentFn = (props: any) => h("div", { "data-testid": "wrap" }, props.children)
|
|
423
|
+
const result = Iterator({
|
|
424
|
+
wrapComponent: Wrap,
|
|
425
|
+
children: h("span", { "data-testid": "only" }, "Only"),
|
|
426
|
+
})
|
|
427
|
+
const vnode = asVNode(result)
|
|
428
|
+
expect(vnode.type).toBe(Wrap)
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it("renders single child with itemProps function", () => {
|
|
432
|
+
const itemPropsFn = vi.fn((_item: unknown, extended: any) => ({
|
|
433
|
+
"data-pos": String(extended.position),
|
|
434
|
+
}))
|
|
435
|
+
Iterator({
|
|
436
|
+
itemProps: itemPropsFn,
|
|
437
|
+
children: h("span", { "data-testid": "only" }, "Only"),
|
|
438
|
+
})
|
|
439
|
+
expect(itemPropsFn).toHaveBeenCalledTimes(1)
|
|
440
|
+
})
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
describe("edge cases", () => {
|
|
444
|
+
it("returns null when component is missing but data exists", () => {
|
|
445
|
+
const result = Iterator({ data: ["a", "b"] })
|
|
446
|
+
expect(result).toBeNull()
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it("returns null when data is not an array", () => {
|
|
450
|
+
const result = Iterator({
|
|
451
|
+
component: TextItem,
|
|
452
|
+
data: "not-array" as any,
|
|
453
|
+
})
|
|
454
|
+
expect(result).toBeNull()
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it("returns null for mixed simple and object array", () => {
|
|
458
|
+
const result = Iterator({
|
|
459
|
+
component: TextItem,
|
|
460
|
+
data: ["hello", { name: "world" }] as any,
|
|
461
|
+
})
|
|
462
|
+
expect(result).toBeNull()
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it("returns null for unsupported data types in array", () => {
|
|
466
|
+
const result = Iterator({
|
|
467
|
+
component: TextItem,
|
|
468
|
+
data: [true, false] as any,
|
|
469
|
+
})
|
|
470
|
+
expect(result).toBeNull()
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
it("handles itemKey as number (fallback to index)", () => {
|
|
474
|
+
const Item: ComponentFn = (props: any) => h("span", { "data-testid": "item" }, props.name)
|
|
475
|
+
const result = Iterator({
|
|
476
|
+
component: Item,
|
|
477
|
+
data: [{ name: "Alice" }, { name: "Bob" }],
|
|
478
|
+
itemKey: 42 as any,
|
|
479
|
+
}) as VNodeChild[]
|
|
480
|
+
expect(result).toHaveLength(2)
|
|
481
|
+
})
|
|
482
|
+
})
|
|
483
|
+
})
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { ComponentFn, VNode } from "@pyreon/core"
|
|
2
|
+
import { h } from "@pyreon/core"
|
|
3
|
+
import { describe, expect, it } from "vitest"
|
|
4
|
+
import { Element } from "../Element"
|
|
5
|
+
import Iterator from "../helpers/Iterator"
|
|
6
|
+
import { List } from "../List"
|
|
7
|
+
|
|
8
|
+
const asVNode = (v: unknown) => v as VNode
|
|
9
|
+
|
|
10
|
+
describe("List", () => {
|
|
11
|
+
describe("statics", () => {
|
|
12
|
+
it("has correct displayName", () => {
|
|
13
|
+
expect(List.displayName).toBe("@pyreon/elements/List")
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it("has correct pkgName", () => {
|
|
17
|
+
expect(List.pkgName).toBe("@pyreon/elements")
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it("has correct PYREON__COMPONENT", () => {
|
|
21
|
+
expect(List.PYREON__COMPONENT).toBe("@pyreon/elements/List")
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe("rootElement = false (default)", () => {
|
|
26
|
+
it("returns a VNode whose type is Iterator", () => {
|
|
27
|
+
const Comp: ComponentFn = (props: any) => h("span", null, props.children)
|
|
28
|
+
const result = asVNode(
|
|
29
|
+
List({
|
|
30
|
+
data: ["a", "b"],
|
|
31
|
+
component: Comp,
|
|
32
|
+
}),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
expect(result.type).toBe(Iterator)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("passes iterator-related props to the Iterator VNode", () => {
|
|
39
|
+
const Comp: ComponentFn = (props: any) => h("span", null, props.children)
|
|
40
|
+
const data = ["a", "b", "c"]
|
|
41
|
+
const itemKeyFn = (_item: unknown, i: number) => i
|
|
42
|
+
const itemPropsFn = () => ({})
|
|
43
|
+
const WrapComp: ComponentFn = (props: any) => h("div", null, ...props.children)
|
|
44
|
+
|
|
45
|
+
const result = asVNode(
|
|
46
|
+
List({
|
|
47
|
+
data,
|
|
48
|
+
component: Comp,
|
|
49
|
+
itemKey: itemKeyFn,
|
|
50
|
+
itemProps: itemPropsFn,
|
|
51
|
+
valueName: "label",
|
|
52
|
+
wrapComponent: WrapComp,
|
|
53
|
+
wrapProps: { className: "wrap" },
|
|
54
|
+
}),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
expect(result.type).toBe(Iterator)
|
|
58
|
+
expect(result.props.data).toBe(data)
|
|
59
|
+
expect(result.props.component).toBe(Comp)
|
|
60
|
+
expect(result.props.itemKey).toBe(itemKeyFn)
|
|
61
|
+
expect(result.props.itemProps).toBe(itemPropsFn)
|
|
62
|
+
expect(result.props.valueName).toBe("label")
|
|
63
|
+
expect(result.props.wrapComponent).toBe(WrapComp)
|
|
64
|
+
expect(result.props.wrapProps).toEqual({ className: "wrap" })
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("does not pass non-iterator props to Iterator", () => {
|
|
68
|
+
const Comp: ComponentFn = (props: any) => h("span", null, props.children)
|
|
69
|
+
const result = asVNode(
|
|
70
|
+
List({
|
|
71
|
+
data: ["a"],
|
|
72
|
+
component: Comp,
|
|
73
|
+
block: true,
|
|
74
|
+
gap: 8,
|
|
75
|
+
direction: "rows",
|
|
76
|
+
} as any),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
expect(result.type).toBe(Iterator)
|
|
80
|
+
expect(result.props.block).toBeUndefined()
|
|
81
|
+
expect(result.props.gap).toBeUndefined()
|
|
82
|
+
expect(result.props.direction).toBeUndefined()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it("forwards children prop to Iterator", () => {
|
|
86
|
+
const child = h("span", null, "hello")
|
|
87
|
+
const result = asVNode(List({ children: child }))
|
|
88
|
+
|
|
89
|
+
expect(result.type).toBe(Iterator)
|
|
90
|
+
expect(result.props.children).toBe(child)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe("rootElement = true", () => {
|
|
95
|
+
it("returns a VNode whose type is Element", () => {
|
|
96
|
+
const Comp: ComponentFn = (props: any) => h("span", null, props.children)
|
|
97
|
+
const result = asVNode(
|
|
98
|
+
List({
|
|
99
|
+
data: ["a"],
|
|
100
|
+
component: Comp,
|
|
101
|
+
rootElement: true,
|
|
102
|
+
}),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
expect(result.type).toBe(Element)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it("passes layout props to the Element VNode", () => {
|
|
109
|
+
const Comp: ComponentFn = (props: any) => h("span", null, props.children)
|
|
110
|
+
const result = asVNode(
|
|
111
|
+
List({
|
|
112
|
+
data: ["a"],
|
|
113
|
+
component: Comp,
|
|
114
|
+
rootElement: true,
|
|
115
|
+
block: true,
|
|
116
|
+
gap: 8,
|
|
117
|
+
direction: "rows",
|
|
118
|
+
} as any),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
expect(result.type).toBe(Element)
|
|
122
|
+
expect(result.props.block).toBe(true)
|
|
123
|
+
expect(result.props.gap).toBe(8)
|
|
124
|
+
expect(result.props.direction).toBe("rows")
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it("does not pass iterator-reserved props to Element", () => {
|
|
128
|
+
const Comp: ComponentFn = (props: any) => h("span", null, props.children)
|
|
129
|
+
const result = asVNode(
|
|
130
|
+
List({
|
|
131
|
+
data: ["x"],
|
|
132
|
+
component: Comp,
|
|
133
|
+
rootElement: true,
|
|
134
|
+
valueName: "label",
|
|
135
|
+
}),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
expect(result.type).toBe(Element)
|
|
139
|
+
// Iterator-reserved props should not leak to Element
|
|
140
|
+
expect(result.props.data).toBeUndefined()
|
|
141
|
+
expect(result.props.component).toBeUndefined()
|
|
142
|
+
expect(result.props.valueName).toBeUndefined()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it("nests an Iterator VNode as children of Element", () => {
|
|
146
|
+
const Comp: ComponentFn = (props: any) => h("span", null, props.children)
|
|
147
|
+
const data = ["a", "b"]
|
|
148
|
+
const result = asVNode(
|
|
149
|
+
List({
|
|
150
|
+
data,
|
|
151
|
+
component: Comp,
|
|
152
|
+
rootElement: true,
|
|
153
|
+
}),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
expect(result.type).toBe(Element)
|
|
157
|
+
|
|
158
|
+
// The children of Element should contain the Iterator VNode
|
|
159
|
+
const children = result.props.children as unknown
|
|
160
|
+
const iteratorNode = asVNode(Array.isArray(children) ? children[0] : children)
|
|
161
|
+
expect(iteratorNode.type).toBe(Iterator)
|
|
162
|
+
expect(iteratorNode.props.data).toBe(data)
|
|
163
|
+
expect(iteratorNode.props.component).toBe(Comp)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it("forwards ref to Element", () => {
|
|
167
|
+
const Comp: ComponentFn = (props: any) => h("span", null, props.children)
|
|
168
|
+
const refFn = (_node: unknown) => {
|
|
169
|
+
/* noop */
|
|
170
|
+
}
|
|
171
|
+
const result = asVNode(
|
|
172
|
+
List({
|
|
173
|
+
data: ["a"],
|
|
174
|
+
component: Comp,
|
|
175
|
+
rootElement: true,
|
|
176
|
+
ref: refFn,
|
|
177
|
+
} as any),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
expect(result.type).toBe(Element)
|
|
181
|
+
expect(result.props.ref).toBe(refFn)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe("Iterator.RESERVED_PROPS", () => {
|
|
186
|
+
it("contains the expected prop names", () => {
|
|
187
|
+
expect(Iterator.RESERVED_PROPS).toEqual([
|
|
188
|
+
"children",
|
|
189
|
+
"component",
|
|
190
|
+
"wrapComponent",
|
|
191
|
+
"data",
|
|
192
|
+
"itemKey",
|
|
193
|
+
"valueName",
|
|
194
|
+
"itemProps",
|
|
195
|
+
"wrapProps",
|
|
196
|
+
])
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
})
|