@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,604 @@
|
|
|
1
|
+
import type { VNode } from "@pyreon/core"
|
|
2
|
+
import { h } from "@pyreon/core"
|
|
3
|
+
import { describe, expect, it } from "vitest"
|
|
4
|
+
import { Element } from "../Element"
|
|
5
|
+
import Content from "../helpers/Content/component"
|
|
6
|
+
import Wrapper from "../helpers/Wrapper/component"
|
|
7
|
+
|
|
8
|
+
const asVNode = (v: unknown) => v as VNode
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Helper to extract Content VNodes from the Wrapper's props.children.
|
|
12
|
+
* In Pyreon, JSX children are passed as props.children (not result.children).
|
|
13
|
+
*/
|
|
14
|
+
const getContentSlots = (result: VNode): VNode[] => {
|
|
15
|
+
const children = result.props.children
|
|
16
|
+
if (!Array.isArray(children)) return []
|
|
17
|
+
return children.filter(
|
|
18
|
+
(c: unknown) =>
|
|
19
|
+
c != null && typeof c === "object" && "type" in (c as VNode) && (c as VNode).type === Content,
|
|
20
|
+
) as VNode[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("Element", () => {
|
|
24
|
+
describe("basic rendering", () => {
|
|
25
|
+
it("returns a VNode whose type is the Wrapper component (a function)", () => {
|
|
26
|
+
const result = asVNode(Element({ children: "hello" }))
|
|
27
|
+
expect(typeof result.type).toBe("function")
|
|
28
|
+
expect(result.type).toBe(Wrapper)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("passes tag as the tag prop to Wrapper", () => {
|
|
32
|
+
const result = asVNode(Element({ tag: "section", children: "content" }))
|
|
33
|
+
expect(result.props.tag).toBe("section")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("defaults tag to undefined when not specified", () => {
|
|
37
|
+
const result = asVNode(Element({ children: "hello" }))
|
|
38
|
+
expect(result.props.tag).toBeUndefined()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("renders with no children", () => {
|
|
42
|
+
const result = asVNode(Element({}))
|
|
43
|
+
expect(result.type).toBe(Wrapper)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe("simple element (no beforeContent/afterContent)", () => {
|
|
48
|
+
it("uses contentDirection as wrapper direction (defaults to rows)", () => {
|
|
49
|
+
const result = asVNode(Element({ children: "test" }))
|
|
50
|
+
expect(result.props.direction).toBe("rows")
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("uses contentAlignX as wrapper alignX (defaults to left)", () => {
|
|
54
|
+
const result = asVNode(Element({ children: "test" }))
|
|
55
|
+
expect(result.props.alignX).toBe("left")
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("uses contentAlignY as wrapper alignY (defaults to center)", () => {
|
|
59
|
+
const result = asVNode(Element({ children: "test" }))
|
|
60
|
+
expect(result.props.alignY).toBe("center")
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("overrides direction with contentDirection when simple", () => {
|
|
64
|
+
const result = asVNode(Element({ contentDirection: "inline", children: "test" }))
|
|
65
|
+
expect(result.props.direction).toBe("inline")
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("overrides alignX with contentAlignX when simple", () => {
|
|
69
|
+
const result = asVNode(Element({ contentAlignX: "center", children: "test" }))
|
|
70
|
+
expect(result.props.alignX).toBe("center")
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("overrides alignY with contentAlignY when simple", () => {
|
|
74
|
+
const result = asVNode(Element({ contentAlignY: "top", children: "test" }))
|
|
75
|
+
expect(result.props.alignY).toBe("top")
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("renders children directly via render() without Content wrappers", () => {
|
|
79
|
+
const result = asVNode(Element({ children: h("span", null, "inner") }))
|
|
80
|
+
const slots = getContentSlots(result)
|
|
81
|
+
expect(slots).toHaveLength(0)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("renders string children in props.children array", () => {
|
|
85
|
+
const result = asVNode(Element({ children: "hello" }))
|
|
86
|
+
const children = result.props.children as unknown[]
|
|
87
|
+
// Simple element renders: [falsy beforeContent, render(CHILDREN), falsy afterContent]
|
|
88
|
+
expect(children).toBeDefined()
|
|
89
|
+
expect(Array.isArray(children)).toBe(true)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("passes block prop to Wrapper", () => {
|
|
93
|
+
const result = asVNode(Element({ block: true, children: "test" }))
|
|
94
|
+
expect(result.props.block).toBe(true)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe("three-section layout (with beforeContent/afterContent)", () => {
|
|
99
|
+
it("defaults wrapper direction to inline", () => {
|
|
100
|
+
const result = asVNode(
|
|
101
|
+
Element({
|
|
102
|
+
beforeContent: h("span", null, "B"),
|
|
103
|
+
children: "test",
|
|
104
|
+
afterContent: h("span", null, "A"),
|
|
105
|
+
}),
|
|
106
|
+
)
|
|
107
|
+
expect(result.props.direction).toBe("inline")
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it("uses explicit direction when provided", () => {
|
|
111
|
+
const result = asVNode(
|
|
112
|
+
Element({
|
|
113
|
+
direction: "rows",
|
|
114
|
+
beforeContent: h("span", null, "B"),
|
|
115
|
+
children: "test",
|
|
116
|
+
afterContent: h("span", null, "A"),
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
119
|
+
expect(result.props.direction).toBe("rows")
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it("uses default alignX (left) and alignY (center)", () => {
|
|
123
|
+
const result = asVNode(
|
|
124
|
+
Element({
|
|
125
|
+
beforeContent: "B",
|
|
126
|
+
children: "test",
|
|
127
|
+
afterContent: "A",
|
|
128
|
+
}),
|
|
129
|
+
)
|
|
130
|
+
expect(result.props.alignX).toBe("left")
|
|
131
|
+
expect(result.props.alignY).toBe("center")
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it("uses explicit alignX and alignY", () => {
|
|
135
|
+
const result = asVNode(
|
|
136
|
+
Element({
|
|
137
|
+
alignX: "center",
|
|
138
|
+
alignY: "top",
|
|
139
|
+
beforeContent: "B",
|
|
140
|
+
children: "test",
|
|
141
|
+
afterContent: "A",
|
|
142
|
+
}),
|
|
143
|
+
)
|
|
144
|
+
expect(result.props.alignX).toBe("center")
|
|
145
|
+
expect(result.props.alignY).toBe("top")
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it("renders three Content children when both before and after exist", () => {
|
|
149
|
+
const before = h("span", null, "Before")
|
|
150
|
+
const after = h("span", null, "After")
|
|
151
|
+
const result = asVNode(
|
|
152
|
+
Element({
|
|
153
|
+
beforeContent: before,
|
|
154
|
+
children: "Main",
|
|
155
|
+
afterContent: after,
|
|
156
|
+
}),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
const slots = getContentSlots(result)
|
|
160
|
+
expect(slots).toHaveLength(3)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it("sets correct contentType on each Content slot", () => {
|
|
164
|
+
const before = h("span", null, "Before")
|
|
165
|
+
const after = h("span", null, "After")
|
|
166
|
+
const result = asVNode(
|
|
167
|
+
Element({
|
|
168
|
+
beforeContent: before,
|
|
169
|
+
children: "Main",
|
|
170
|
+
afterContent: after,
|
|
171
|
+
}),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
const slots = getContentSlots(result)
|
|
175
|
+
const [slot0, slot1, slot2] = slots as [VNode, VNode, VNode]
|
|
176
|
+
expect(slot0.props.contentType).toBe("before")
|
|
177
|
+
expect(slot1.props.contentType).toBe("content")
|
|
178
|
+
expect(slot2.props.contentType).toBe("after")
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it("passes parentDirection to Content slots", () => {
|
|
182
|
+
const result = asVNode(
|
|
183
|
+
Element({
|
|
184
|
+
direction: "rows",
|
|
185
|
+
beforeContent: "B",
|
|
186
|
+
children: "M",
|
|
187
|
+
afterContent: "A",
|
|
188
|
+
}),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
const slots = getContentSlots(result)
|
|
192
|
+
for (const slot of slots) {
|
|
193
|
+
expect(slot.props.parentDirection).toBe("rows")
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it("renders before and content Content slots when no afterContent", () => {
|
|
198
|
+
const before = h("span", null, "Before")
|
|
199
|
+
const result = asVNode(
|
|
200
|
+
Element({
|
|
201
|
+
beforeContent: before,
|
|
202
|
+
children: "Main",
|
|
203
|
+
}),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
const slots = getContentSlots(result)
|
|
207
|
+
// beforeContent makes isSimpleElement false, so content also gets a Content wrapper
|
|
208
|
+
expect(slots).toHaveLength(2)
|
|
209
|
+
const [s0, s1] = slots as [VNode, VNode]
|
|
210
|
+
expect(s0.props.contentType).toBe("before")
|
|
211
|
+
expect(s1.props.contentType).toBe("content")
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it("renders content and after Content slots when no beforeContent", () => {
|
|
215
|
+
const after = h("span", null, "After")
|
|
216
|
+
const result = asVNode(
|
|
217
|
+
Element({
|
|
218
|
+
children: "Main",
|
|
219
|
+
afterContent: after,
|
|
220
|
+
}),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
const slots = getContentSlots(result)
|
|
224
|
+
// content slot + after slot (both are Content wrappers since afterContent makes it non-simple)
|
|
225
|
+
expect(slots).toHaveLength(2)
|
|
226
|
+
const [c0, c1] = slots as [VNode, VNode]
|
|
227
|
+
expect(c0.props.contentType).toBe("content")
|
|
228
|
+
expect(c1.props.contentType).toBe("after")
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it("uses span sub-tag for inline parent elements (like span)", () => {
|
|
232
|
+
const result = asVNode(
|
|
233
|
+
Element({
|
|
234
|
+
tag: "span",
|
|
235
|
+
beforeContent: "B",
|
|
236
|
+
children: "M",
|
|
237
|
+
afterContent: "A",
|
|
238
|
+
}),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
const slots = getContentSlots(result)
|
|
242
|
+
for (const slot of slots) {
|
|
243
|
+
expect(slot.props.tag).toBe("span")
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it("uses undefined sub-tag for block parent elements (like div)", () => {
|
|
248
|
+
const result = asVNode(
|
|
249
|
+
Element({
|
|
250
|
+
tag: "div",
|
|
251
|
+
beforeContent: "B",
|
|
252
|
+
children: "M",
|
|
253
|
+
afterContent: "A",
|
|
254
|
+
}),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
const slots = getContentSlots(result)
|
|
258
|
+
for (const slot of slots) {
|
|
259
|
+
expect(slot.props.tag).toBeUndefined()
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it("passes equalCols to Content slots", () => {
|
|
264
|
+
const result = asVNode(
|
|
265
|
+
Element({
|
|
266
|
+
equalCols: true,
|
|
267
|
+
beforeContent: "B",
|
|
268
|
+
children: "M",
|
|
269
|
+
afterContent: "A",
|
|
270
|
+
}),
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
const slots = getContentSlots(result)
|
|
274
|
+
for (const slot of slots) {
|
|
275
|
+
expect(slot.props.equalCols).toBe(true)
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it("passes gap to before and after Content slots but not content slot", () => {
|
|
280
|
+
const result = asVNode(
|
|
281
|
+
Element({
|
|
282
|
+
gap: 16,
|
|
283
|
+
beforeContent: "B",
|
|
284
|
+
children: "M",
|
|
285
|
+
afterContent: "A",
|
|
286
|
+
}),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
const slots = getContentSlots(result)
|
|
290
|
+
const beforeSlot = slots.find((v) => v.props.contentType === "before")
|
|
291
|
+
const contentSlot = slots.find((v) => v.props.contentType === "content")
|
|
292
|
+
const afterSlot = slots.find((v) => v.props.contentType === "after")
|
|
293
|
+
|
|
294
|
+
expect(beforeSlot?.props.gap).toBe(16)
|
|
295
|
+
expect(contentSlot?.props.gap).toBeUndefined()
|
|
296
|
+
expect(afterSlot?.props.gap).toBe(16)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it("passes content-level alignment to the content Content slot", () => {
|
|
300
|
+
const result = asVNode(
|
|
301
|
+
Element({
|
|
302
|
+
contentDirection: "inline",
|
|
303
|
+
contentAlignX: "center",
|
|
304
|
+
contentAlignY: "top",
|
|
305
|
+
beforeContent: "B",
|
|
306
|
+
children: "M",
|
|
307
|
+
afterContent: "A",
|
|
308
|
+
}),
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
const slots = getContentSlots(result)
|
|
312
|
+
const contentSlot = slots.find((v) => v.props.contentType === "content")
|
|
313
|
+
expect(contentSlot?.props.direction).toBe("inline")
|
|
314
|
+
expect(contentSlot?.props.alignX).toBe("center")
|
|
315
|
+
expect(contentSlot?.props.alignY).toBe("top")
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it("passes before-level alignment to the before Content slot", () => {
|
|
319
|
+
const result = asVNode(
|
|
320
|
+
Element({
|
|
321
|
+
beforeContentDirection: "rows",
|
|
322
|
+
beforeContentAlignX: "right",
|
|
323
|
+
beforeContentAlignY: "bottom",
|
|
324
|
+
beforeContent: "B",
|
|
325
|
+
children: "M",
|
|
326
|
+
afterContent: "A",
|
|
327
|
+
}),
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
const slots = getContentSlots(result)
|
|
331
|
+
const beforeSlot = slots.find((v) => v.props.contentType === "before")
|
|
332
|
+
expect(beforeSlot?.props.direction).toBe("rows")
|
|
333
|
+
expect(beforeSlot?.props.alignX).toBe("right")
|
|
334
|
+
expect(beforeSlot?.props.alignY).toBe("bottom")
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
it("passes after-level alignment to the after Content slot", () => {
|
|
338
|
+
const result = asVNode(
|
|
339
|
+
Element({
|
|
340
|
+
afterContentDirection: "rows",
|
|
341
|
+
afterContentAlignX: "center",
|
|
342
|
+
afterContentAlignY: "top",
|
|
343
|
+
beforeContent: "B",
|
|
344
|
+
children: "M",
|
|
345
|
+
afterContent: "A",
|
|
346
|
+
}),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
const slots = getContentSlots(result)
|
|
350
|
+
const afterSlot = slots.find((v) => v.props.contentType === "after")
|
|
351
|
+
expect(afterSlot?.props.direction).toBe("rows")
|
|
352
|
+
expect(afterSlot?.props.alignX).toBe("center")
|
|
353
|
+
expect(afterSlot?.props.alignY).toBe("top")
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
describe("HTML attribute filtering", () => {
|
|
358
|
+
it("passes through id", () => {
|
|
359
|
+
const result = asVNode(Element({ id: "my-el", children: "test" }))
|
|
360
|
+
expect(result.props.id).toBe("my-el")
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it("passes through role", () => {
|
|
364
|
+
const result = asVNode(Element({ role: "button", children: "test" }))
|
|
365
|
+
expect(result.props.role).toBe("button")
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it("passes through data- attributes", () => {
|
|
369
|
+
const result = asVNode(Element({ "data-testid": "el", children: "test" }))
|
|
370
|
+
expect(result.props["data-testid"]).toBe("el")
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it("passes through aria- attributes", () => {
|
|
374
|
+
const result = asVNode(Element({ "aria-label": "label", children: "test" }))
|
|
375
|
+
expect(result.props["aria-label"]).toBe("label")
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it("passes through on-prefixed event handlers", () => {
|
|
379
|
+
const handler = () => {
|
|
380
|
+
/* noop */
|
|
381
|
+
}
|
|
382
|
+
const result = asVNode(Element({ onClick: handler, children: "test" }))
|
|
383
|
+
expect(result.props.onClick).toBe(handler)
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it("passes through tabindex", () => {
|
|
387
|
+
// @ts-expect-error — testing element-specific attr forwarding
|
|
388
|
+
const result = asVNode(Element({ tabindex: 0, children: "test" }))
|
|
389
|
+
expect(result.props.tabindex).toBe(0)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it("passes through title", () => {
|
|
393
|
+
const result = asVNode(Element({ title: "tooltip", children: "test" }))
|
|
394
|
+
expect(result.props.title).toBe("tooltip")
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
it("passes through href for anchor tag", () => {
|
|
398
|
+
// @ts-expect-error — testing element-specific attr forwarding
|
|
399
|
+
const result = asVNode(Element({ tag: "a", href: "/link", children: "test" }))
|
|
400
|
+
expect(result.props.href).toBe("/link")
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it("passes through disabled for button tag", () => {
|
|
404
|
+
// @ts-expect-error — testing element-specific attr forwarding
|
|
405
|
+
const result = asVNode(Element({ tag: "button", disabled: true, children: "test" }))
|
|
406
|
+
expect(result.props.disabled).toBe(true)
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it("passes through class", () => {
|
|
410
|
+
const result = asVNode(Element({ class: "my-class", children: "test" }))
|
|
411
|
+
expect(result.props.class).toBe("my-class")
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it("does not set class when not provided", () => {
|
|
415
|
+
const result = asVNode(Element({ children: "test" }))
|
|
416
|
+
expect(result.props.class).toBeUndefined()
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
it("filters out reserved props (gap, beforeContent, afterContent, css, etc.)", () => {
|
|
420
|
+
const result = asVNode(
|
|
421
|
+
Element({
|
|
422
|
+
beforeContent: h("span", null, "x"),
|
|
423
|
+
afterContent: h("span", null, "y"),
|
|
424
|
+
children: "test",
|
|
425
|
+
direction: "inline",
|
|
426
|
+
alignX: "center",
|
|
427
|
+
alignY: "center",
|
|
428
|
+
gap: 8,
|
|
429
|
+
block: true,
|
|
430
|
+
equalCols: true,
|
|
431
|
+
}),
|
|
432
|
+
)
|
|
433
|
+
// These reserved props are consumed by Element and should not leak to Wrapper
|
|
434
|
+
expect(result.props.gap).toBeUndefined()
|
|
435
|
+
expect(result.props.beforeContent).toBeUndefined()
|
|
436
|
+
expect(result.props.afterContent).toBeUndefined()
|
|
437
|
+
expect(result.props.contentDirection).toBeUndefined()
|
|
438
|
+
expect(result.props.css).toBeUndefined()
|
|
439
|
+
expect(result.props.contentCss).toBeUndefined()
|
|
440
|
+
expect(result.props.beforeContentCss).toBeUndefined()
|
|
441
|
+
expect(result.props.afterContentCss).toBeUndefined()
|
|
442
|
+
})
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
describe("ref handling", () => {
|
|
446
|
+
it("passes a merged ref function to Wrapper", () => {
|
|
447
|
+
const result = asVNode(Element({ children: "test" }))
|
|
448
|
+
expect(typeof result.props.ref).toBe("function")
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it("wraps function ref in mergedRef", () => {
|
|
452
|
+
let captured: HTMLElement | null = null
|
|
453
|
+
const ref = (node: HTMLElement | null) => {
|
|
454
|
+
captured = node
|
|
455
|
+
}
|
|
456
|
+
const result = asVNode(Element({ ref, children: "test" }))
|
|
457
|
+
expect(typeof result.props.ref).toBe("function")
|
|
458
|
+
const fakeNode = {} as HTMLElement
|
|
459
|
+
;(result.props.ref as (node: HTMLElement | null) => void)(fakeNode)
|
|
460
|
+
expect(captured).toBe(fakeNode)
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
it("wraps object ref in mergedRef", () => {
|
|
464
|
+
const ref = { current: null as HTMLElement | null }
|
|
465
|
+
const result = asVNode(Element({ ref, children: "test" }))
|
|
466
|
+
expect(typeof result.props.ref).toBe("function")
|
|
467
|
+
const fakeNode = {} as HTMLElement
|
|
468
|
+
;(result.props.ref as (node: HTMLElement | null) => void)(fakeNode)
|
|
469
|
+
expect(ref.current).toBe(fakeNode)
|
|
470
|
+
})
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
describe("void / empty elements", () => {
|
|
474
|
+
it("renders img with no children", () => {
|
|
475
|
+
// @ts-expect-error — testing element-specific attr forwarding
|
|
476
|
+
const result = asVNode(Element({ tag: "img", src: "/pic.png" }))
|
|
477
|
+
expect(result.type).toBe(Wrapper)
|
|
478
|
+
expect(result.props.tag).toBe("img")
|
|
479
|
+
expect(result.props.src).toBe("/pic.png")
|
|
480
|
+
expect(result.props.children).toBeUndefined()
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
it("renders input with no children", () => {
|
|
484
|
+
// @ts-expect-error — testing element-specific attr forwarding
|
|
485
|
+
const result = asVNode(Element({ tag: "input", type: "text" }))
|
|
486
|
+
expect(result.type).toBe(Wrapper)
|
|
487
|
+
expect(result.props.tag).toBe("input")
|
|
488
|
+
expect(result.props.type).toBe("text")
|
|
489
|
+
expect(result.props.children).toBeUndefined()
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it("renders with dangerouslySetInnerHTML (treated as empty)", () => {
|
|
493
|
+
const result = asVNode(Element({ dangerouslySetInnerHTML: { __html: "<b>hi</b>" } }))
|
|
494
|
+
expect(result.type).toBe(Wrapper)
|
|
495
|
+
expect(result.props.dangerouslySetInnerHTML).toEqual({ __html: "<b>hi</b>" })
|
|
496
|
+
expect(result.props.children).toBeUndefined()
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
it("renders br with no children", () => {
|
|
500
|
+
const result = asVNode(Element({ tag: "br" }))
|
|
501
|
+
expect(result.type).toBe(Wrapper)
|
|
502
|
+
expect(result.props.children).toBeUndefined()
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
it("renders hr with no children", () => {
|
|
506
|
+
const result = asVNode(Element({ tag: "hr" }))
|
|
507
|
+
expect(result.type).toBe(Wrapper)
|
|
508
|
+
expect(result.props.children).toBeUndefined()
|
|
509
|
+
})
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
describe("isInline flag for Wrapper", () => {
|
|
513
|
+
it("passes isInline=true for inline tags like span", () => {
|
|
514
|
+
const result = asVNode(Element({ tag: "span", children: "text" }))
|
|
515
|
+
expect(result.props.isInline).toBe(true)
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
it("passes isInline=true for anchor tag", () => {
|
|
519
|
+
// @ts-expect-error — testing element-specific attr forwarding
|
|
520
|
+
const result = asVNode(Element({ tag: "a", href: "#", children: "link" }))
|
|
521
|
+
expect(result.props.isInline).toBe(true)
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
it("passes isInline=false for block tags like section", () => {
|
|
525
|
+
const result = asVNode(Element({ tag: "section", children: "text" }))
|
|
526
|
+
expect(result.props.isInline).toBe(false)
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
it("passes isInline=false when tag is undefined (default)", () => {
|
|
530
|
+
const result = asVNode(Element({ children: "text" }))
|
|
531
|
+
expect(result.props.isInline).toBe(false)
|
|
532
|
+
})
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
describe("extendCss prop", () => {
|
|
536
|
+
it("passes css prop as extendCss to Wrapper", () => {
|
|
537
|
+
const customCss = "color: red;"
|
|
538
|
+
const result = asVNode(Element({ css: customCss, children: "test" }))
|
|
539
|
+
expect(result.props.extendCss).toBe(customCss)
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
it("does not pass extendCss when css not provided", () => {
|
|
543
|
+
const result = asVNode(Element({ children: "test" }))
|
|
544
|
+
expect(result.props.extendCss).toBeUndefined()
|
|
545
|
+
})
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
describe("content fallback chain", () => {
|
|
549
|
+
it("prefers children over content", () => {
|
|
550
|
+
const result = asVNode(Element({ children: "child", content: "alt" }))
|
|
551
|
+
const children = result.props.children as unknown[]
|
|
552
|
+
expect(children).toBeDefined()
|
|
553
|
+
expect(Array.isArray(children)).toBe(true)
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
it("falls back to content when no children", () => {
|
|
557
|
+
const result = asVNode(Element({ content: "alt content" }))
|
|
558
|
+
const children = result.props.children as unknown[]
|
|
559
|
+
expect(children).toBeDefined()
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
it("falls back to label when no children or content", () => {
|
|
563
|
+
const result = asVNode(Element({ label: "label text" }))
|
|
564
|
+
const children = result.props.children as unknown[]
|
|
565
|
+
expect(children).toBeDefined()
|
|
566
|
+
})
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
describe("Wrapper as prop reset", () => {
|
|
570
|
+
it("resets the as prop to undefined on Wrapper", () => {
|
|
571
|
+
const result = asVNode(Element({ children: "test" }))
|
|
572
|
+
expect(result.props.as).toBeUndefined()
|
|
573
|
+
})
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
describe("button tag (flex fix needed)", () => {
|
|
577
|
+
it("passes tag as button to Wrapper", () => {
|
|
578
|
+
const result = asVNode(Element({ tag: "button", children: "click" }))
|
|
579
|
+
expect(result.type).toBe(Wrapper)
|
|
580
|
+
expect(result.props.tag).toBe("button")
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
it("passes isInline=true for button (inline element)", () => {
|
|
584
|
+
const result = asVNode(Element({ tag: "button", children: "click" }))
|
|
585
|
+
expect(result.props.isInline).toBe(true)
|
|
586
|
+
})
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
describe("component metadata", () => {
|
|
590
|
+
it("has displayName set", () => {
|
|
591
|
+
expect(Element.displayName).toBeDefined()
|
|
592
|
+
expect(Element.displayName).toContain("Element")
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
it("has PYREON__COMPONENT set", () => {
|
|
596
|
+
expect(Element.PYREON__COMPONENT).toBeDefined()
|
|
597
|
+
expect(Element.PYREON__COMPONENT).toContain("Element")
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
it("has pkgName set", () => {
|
|
601
|
+
expect(Element.pkgName).toBeDefined()
|
|
602
|
+
})
|
|
603
|
+
})
|
|
604
|
+
})
|