@pyreon/elements 0.24.4 → 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 +10 -12
- package/src/Element/component.tsx +0 -315
- package/src/Element/constants.ts +0 -96
- package/src/Element/index.ts +0 -6
- package/src/Element/types.ts +0 -168
- package/src/Element/utils.ts +0 -15
- package/src/List/component.tsx +0 -105
- package/src/List/index.ts +0 -5
- package/src/Overlay/component.tsx +0 -140
- package/src/Overlay/context.tsx +0 -36
- package/src/Overlay/index.ts +0 -7
- package/src/Overlay/positioning.ts +0 -191
- package/src/Overlay/useOverlay.tsx +0 -461
- package/src/Portal/component.tsx +0 -54
- package/src/Portal/index.ts +0 -5
- package/src/Text/component.tsx +0 -67
- package/src/Text/index.ts +0 -5
- package/src/Text/styled.ts +0 -30
- package/src/Util/component.tsx +0 -43
- package/src/Util/index.ts +0 -5
- package/src/__tests__/Content.test.tsx +0 -123
- package/src/__tests__/Element-slot-reactivity.browser.test.tsx +0 -152
- package/src/__tests__/Element.test.ts +0 -819
- package/src/__tests__/Iterator.test.ts +0 -492
- package/src/__tests__/Iterator.types.test.ts +0 -237
- package/src/__tests__/List.test.ts +0 -199
- package/src/__tests__/Overlay.test.ts +0 -492
- package/src/__tests__/Portal.test.ts +0 -156
- package/src/__tests__/Text.test.ts +0 -274
- package/src/__tests__/Util.test.ts +0 -63
- package/src/__tests__/Wrapper-innerhtml.test.tsx +0 -178
- package/src/__tests__/Wrapper.test.tsx +0 -196
- package/src/__tests__/elements.browser.test.tsx +0 -132
- package/src/__tests__/equalBeforeAfter.test.ts +0 -122
- package/src/__tests__/helpers.test.ts +0 -65
- package/src/__tests__/integration.test.tsx +0 -118
- package/src/__tests__/internElementBundle.test.ts +0 -102
- package/src/__tests__/iterator-function-children.test.tsx +0 -120
- package/src/__tests__/native-markers.test.ts +0 -13
- package/src/__tests__/overlayContext.test.tsx +0 -78
- package/src/__tests__/perf-stress.browser.test.tsx +0 -119
- package/src/__tests__/positioning.test.ts +0 -90
- package/src/__tests__/responsiveProps.test.ts +0 -328
- package/src/__tests__/slot-component-reference.test.tsx +0 -157
- package/src/__tests__/useOverlay.test.ts +0 -1336
- package/src/__tests__/utils.test.ts +0 -69
- package/src/__tests__/wrapper-block-cascade.test.ts +0 -121
- package/src/constants.ts +0 -1
- package/src/env.d.ts +0 -6
- package/src/helpers/Content/component.tsx +0 -75
- package/src/helpers/Content/index.ts +0 -3
- package/src/helpers/Content/styled.ts +0 -105
- package/src/helpers/Content/types.ts +0 -49
- package/src/helpers/Iterator/component.tsx +0 -316
- package/src/helpers/Iterator/index.ts +0 -30
- package/src/helpers/Iterator/types.ts +0 -138
- package/src/helpers/Wrapper/component.tsx +0 -180
- package/src/helpers/Wrapper/constants.ts +0 -10
- package/src/helpers/Wrapper/index.ts +0 -3
- package/src/helpers/Wrapper/styled.ts +0 -64
- package/src/helpers/Wrapper/types.ts +0 -56
- package/src/helpers/Wrapper/utils.ts +0 -7
- package/src/helpers/index.ts +0 -4
- package/src/helpers/internElementBundle.ts +0 -37
- package/src/helpers/isPyreonComponent.ts +0 -46
- package/src/index.ts +0 -42
- package/src/manifest.ts +0 -190
- package/src/tests/manifest-snapshot.test.ts +0 -45
- package/src/types.ts +0 -112
- package/src/utils.ts +0 -5
|
@@ -1,819 +0,0 @@
|
|
|
1
|
-
import type { VNode } from '@pyreon/core'
|
|
2
|
-
import { h } from '@pyreon/core'
|
|
3
|
-
import * as runtimeDom from '@pyreon/runtime-dom'
|
|
4
|
-
import { describe, expect, it } from 'vitest'
|
|
5
|
-
import { Element } from '../Element'
|
|
6
|
-
import Content from '../helpers/Content/component'
|
|
7
|
-
import Wrapper from '../helpers/Wrapper/component'
|
|
8
|
-
|
|
9
|
-
// Namespace-import + destructure defeats CodeQL Autofix's `js/unused-import`
|
|
10
|
-
// false-positive — `mount` is referenced inside `it()` callbacks far below,
|
|
11
|
-
// which the bot's static analyzer fails to trace, causing it to remove the
|
|
12
|
-
// import in a loop on every push. The namespace import is unambiguously
|
|
13
|
-
// referenced on the next line, so the rule cannot fire.
|
|
14
|
-
const { mount } = runtimeDom
|
|
15
|
-
|
|
16
|
-
const asVNode = (v: unknown) => v as VNode
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Helper to extract Content VNodes from the Wrapper's props.children.
|
|
20
|
-
* In Pyreon, JSX children are passed as props.children (not result.children).
|
|
21
|
-
*/
|
|
22
|
-
const getContentSlots = (result: VNode): VNode[] => {
|
|
23
|
-
const children = result.props.children
|
|
24
|
-
if (!Array.isArray(children)) return []
|
|
25
|
-
return children.filter(
|
|
26
|
-
(c: unknown) =>
|
|
27
|
-
c != null && typeof c === 'object' && 'type' in (c as VNode) && (c as VNode).type === Content,
|
|
28
|
-
) as VNode[]
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Element's simple-Element fast path inlines the Wrapper helper directly into
|
|
33
|
-
* a single Styled invocation — saves a component hop, splitProps call, and
|
|
34
|
-
* mountChild per Element. After the inline, layout props live on
|
|
35
|
-
* `props.$element.{direction,alignX,…}` and the HTML tag moves from `tag` to
|
|
36
|
-
* `as`. Compound (with-beforeContent/afterContent) renders and the rare
|
|
37
|
-
* needsFix tags (button/fieldset/legend) still go through the original
|
|
38
|
-
* `Wrapper` component.
|
|
39
|
-
*
|
|
40
|
-
* `getLayoutProps()` reads from whichever shape the result happens to be in
|
|
41
|
-
* so the test assertions don't need to know which path Element took.
|
|
42
|
-
*/
|
|
43
|
-
const getLayoutProps = (result: VNode): Record<string, unknown> => {
|
|
44
|
-
const p = result.props as Record<string, unknown>
|
|
45
|
-
// Inlined path: layout lives in $element bag, tag in `as`
|
|
46
|
-
if (p.$element && typeof p.$element === 'object') {
|
|
47
|
-
const el = p.$element as Record<string, unknown>
|
|
48
|
-
return {
|
|
49
|
-
tag: p.as,
|
|
50
|
-
direction: el.direction,
|
|
51
|
-
alignX: el.alignX,
|
|
52
|
-
alignY: el.alignY,
|
|
53
|
-
block: el.block,
|
|
54
|
-
equalCols: el.equalCols,
|
|
55
|
-
extendCss: el.extraStyles,
|
|
56
|
-
// isInline is no longer a separate prop in the inlined path —
|
|
57
|
-
// SUB_TAG/childFix decisions happen at compose-time.
|
|
58
|
-
isInline: undefined,
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// Wrapper-helper path: flat props
|
|
62
|
-
return {
|
|
63
|
-
tag: p.tag,
|
|
64
|
-
direction: p.direction,
|
|
65
|
-
alignX: p.alignX,
|
|
66
|
-
alignY: p.alignY,
|
|
67
|
-
block: p.block,
|
|
68
|
-
equalCols: p.equalCols,
|
|
69
|
-
extendCss: p.extendCss,
|
|
70
|
-
isInline: p.isInline,
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
describe('Element', () => {
|
|
75
|
-
describe('basic rendering', () => {
|
|
76
|
-
it('returns a VNode whose type is the Wrapper component (a function)', () => {
|
|
77
|
-
const result = asVNode(Element({ children: 'hello' }))
|
|
78
|
-
expect(typeof result.type).toBe('function')
|
|
79
|
-
expect(typeof result.type).toBe("function")
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('passes tag as the tag prop to Wrapper', () => {
|
|
83
|
-
const result = asVNode(Element({ tag: 'section', children: 'content' }))
|
|
84
|
-
expect(getLayoutProps(result).tag).toBe('section')
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('defaults tag to undefined when not specified', () => {
|
|
88
|
-
const result = asVNode(Element({ children: 'hello' }))
|
|
89
|
-
expect(getLayoutProps(result).tag).toBeUndefined()
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('renders with no children', () => {
|
|
93
|
-
const result = asVNode(Element({}))
|
|
94
|
-
expect(typeof result.type).toBe("function")
|
|
95
|
-
})
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
describe('simple element (no beforeContent/afterContent)', () => {
|
|
99
|
-
it('uses contentDirection as wrapper direction (defaults to rows)', () => {
|
|
100
|
-
const result = asVNode(Element({ children: 'test' }))
|
|
101
|
-
expect(getLayoutProps(result).direction).toBe('rows')
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('uses contentAlignX as wrapper alignX (defaults to left)', () => {
|
|
105
|
-
const result = asVNode(Element({ children: 'test' }))
|
|
106
|
-
expect(getLayoutProps(result).alignX).toBe('left')
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('uses contentAlignY as wrapper alignY (defaults to center)', () => {
|
|
110
|
-
const result = asVNode(Element({ children: 'test' }))
|
|
111
|
-
expect(getLayoutProps(result).alignY).toBe('center')
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('overrides direction with contentDirection when simple', () => {
|
|
115
|
-
const result = asVNode(Element({ contentDirection: 'inline', children: 'test' }))
|
|
116
|
-
expect(getLayoutProps(result).direction).toBe('inline')
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('overrides alignX with contentAlignX when simple', () => {
|
|
120
|
-
const result = asVNode(Element({ contentAlignX: 'center', children: 'test' }))
|
|
121
|
-
expect(getLayoutProps(result).alignX).toBe('center')
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('overrides alignY with contentAlignY when simple', () => {
|
|
125
|
-
const result = asVNode(Element({ contentAlignY: 'top', children: 'test' }))
|
|
126
|
-
expect(getLayoutProps(result).alignY).toBe('top')
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it('renders children directly via render() without Content wrappers', () => {
|
|
130
|
-
const result = asVNode(Element({ children: h('span', null, 'inner') }))
|
|
131
|
-
const slots = getContentSlots(result)
|
|
132
|
-
expect(slots).toHaveLength(0)
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('renders string children directly as props.children', () => {
|
|
136
|
-
const result = asVNode(Element({ children: 'hello' }))
|
|
137
|
-
// Simple element fast path — passes children as a single value, not a
|
|
138
|
-
// 3-slot array wrapping falsy beforeContent/afterContent. This avoids
|
|
139
|
-
// 2 extra mountChild calls per Element in the common case.
|
|
140
|
-
//
|
|
141
|
-
// Children are wrapped in a reactive accessor (`() => resolveSlot(...)`)
|
|
142
|
-
// so function-valued slot props (e.g. `content={() => <X />}`) stay
|
|
143
|
-
// reactive — see `Element-slot-reactivity.browser.test.tsx`. The
|
|
144
|
-
// accessor's RESOLVED value is the string `'hello'`.
|
|
145
|
-
expect(typeof result.props.children).toBe('function')
|
|
146
|
-
expect((result.props.children as () => unknown)()).toBe('hello')
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('passes block prop to Wrapper', () => {
|
|
150
|
-
const result = asVNode(Element({ block: true, children: 'test' }))
|
|
151
|
-
expect(getLayoutProps(result).block).toBe(true)
|
|
152
|
-
})
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
describe('three-section layout (with beforeContent/afterContent)', () => {
|
|
156
|
-
it('defaults wrapper direction to inline', () => {
|
|
157
|
-
const result = asVNode(
|
|
158
|
-
Element({
|
|
159
|
-
beforeContent: h('span', null, 'B'),
|
|
160
|
-
children: 'test',
|
|
161
|
-
afterContent: h('span', null, 'A'),
|
|
162
|
-
}),
|
|
163
|
-
)
|
|
164
|
-
expect(getLayoutProps(result).direction).toBe('inline')
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it('uses explicit direction when provided', () => {
|
|
168
|
-
const result = asVNode(
|
|
169
|
-
Element({
|
|
170
|
-
direction: 'rows',
|
|
171
|
-
beforeContent: h('span', null, 'B'),
|
|
172
|
-
children: 'test',
|
|
173
|
-
afterContent: h('span', null, 'A'),
|
|
174
|
-
}),
|
|
175
|
-
)
|
|
176
|
-
expect(getLayoutProps(result).direction).toBe('rows')
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it('uses default alignX (left) and alignY (center)', () => {
|
|
180
|
-
const result = asVNode(
|
|
181
|
-
Element({
|
|
182
|
-
beforeContent: 'B',
|
|
183
|
-
children: 'test',
|
|
184
|
-
afterContent: 'A',
|
|
185
|
-
}),
|
|
186
|
-
)
|
|
187
|
-
expect(getLayoutProps(result).alignX).toBe('left')
|
|
188
|
-
expect(getLayoutProps(result).alignY).toBe('center')
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
it('uses explicit alignX and alignY', () => {
|
|
192
|
-
const result = asVNode(
|
|
193
|
-
Element({
|
|
194
|
-
alignX: 'center',
|
|
195
|
-
alignY: 'top',
|
|
196
|
-
beforeContent: 'B',
|
|
197
|
-
children: 'test',
|
|
198
|
-
afterContent: 'A',
|
|
199
|
-
}),
|
|
200
|
-
)
|
|
201
|
-
expect(getLayoutProps(result).alignX).toBe('center')
|
|
202
|
-
expect(getLayoutProps(result).alignY).toBe('top')
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
it('renders three Content children when both before and after exist', () => {
|
|
206
|
-
const before = h('span', null, 'Before')
|
|
207
|
-
const after = h('span', null, 'After')
|
|
208
|
-
const result = asVNode(
|
|
209
|
-
Element({
|
|
210
|
-
beforeContent: before,
|
|
211
|
-
children: 'Main',
|
|
212
|
-
afterContent: after,
|
|
213
|
-
}),
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
const slots = getContentSlots(result)
|
|
217
|
-
expect(slots).toHaveLength(3)
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
it('sets correct contentType on each Content slot', () => {
|
|
221
|
-
const before = h('span', null, 'Before')
|
|
222
|
-
const after = h('span', null, 'After')
|
|
223
|
-
const result = asVNode(
|
|
224
|
-
Element({
|
|
225
|
-
beforeContent: before,
|
|
226
|
-
children: 'Main',
|
|
227
|
-
afterContent: after,
|
|
228
|
-
}),
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
const slots = getContentSlots(result)
|
|
232
|
-
const [slot0, slot1, slot2] = slots as [VNode, VNode, VNode]
|
|
233
|
-
expect(slot0.props.contentType).toBe('before')
|
|
234
|
-
expect(slot1.props.contentType).toBe('content')
|
|
235
|
-
expect(slot2.props.contentType).toBe('after')
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
it('passes parentDirection to Content slots', () => {
|
|
239
|
-
const result = asVNode(
|
|
240
|
-
Element({
|
|
241
|
-
direction: 'rows',
|
|
242
|
-
beforeContent: 'B',
|
|
243
|
-
children: 'M',
|
|
244
|
-
afterContent: 'A',
|
|
245
|
-
}),
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
const slots = getContentSlots(result)
|
|
249
|
-
for (const slot of slots) {
|
|
250
|
-
expect(slot.props.parentDirection).toBe('rows')
|
|
251
|
-
}
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
it('renders before and content Content slots when no afterContent', () => {
|
|
255
|
-
const before = h('span', null, 'Before')
|
|
256
|
-
const result = asVNode(
|
|
257
|
-
Element({
|
|
258
|
-
beforeContent: before,
|
|
259
|
-
children: 'Main',
|
|
260
|
-
}),
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
const slots = getContentSlots(result)
|
|
264
|
-
// beforeContent makes isSimpleElement false, so content also gets a Content wrapper
|
|
265
|
-
expect(slots).toHaveLength(2)
|
|
266
|
-
const [s0, s1] = slots as [VNode, VNode]
|
|
267
|
-
expect(s0.props.contentType).toBe('before')
|
|
268
|
-
expect(s1.props.contentType).toBe('content')
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
it('renders content and after Content slots when no beforeContent', () => {
|
|
272
|
-
const after = h('span', null, 'After')
|
|
273
|
-
const result = asVNode(
|
|
274
|
-
Element({
|
|
275
|
-
children: 'Main',
|
|
276
|
-
afterContent: after,
|
|
277
|
-
}),
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
const slots = getContentSlots(result)
|
|
281
|
-
// content slot + after slot (both are Content wrappers since afterContent makes it non-simple)
|
|
282
|
-
expect(slots).toHaveLength(2)
|
|
283
|
-
const [c0, c1] = slots as [VNode, VNode]
|
|
284
|
-
expect(c0.props.contentType).toBe('content')
|
|
285
|
-
expect(c1.props.contentType).toBe('after')
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
it('uses span sub-tag for inline parent elements (like span)', () => {
|
|
289
|
-
const result = asVNode(
|
|
290
|
-
Element({
|
|
291
|
-
tag: 'span',
|
|
292
|
-
beforeContent: 'B',
|
|
293
|
-
children: 'M',
|
|
294
|
-
afterContent: 'A',
|
|
295
|
-
}),
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
const slots = getContentSlots(result)
|
|
299
|
-
for (const slot of slots) {
|
|
300
|
-
expect(slot.props.tag).toBe('span')
|
|
301
|
-
}
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
it('uses undefined sub-tag for block parent elements (like div)', () => {
|
|
305
|
-
const result = asVNode(
|
|
306
|
-
Element({
|
|
307
|
-
tag: 'div',
|
|
308
|
-
beforeContent: 'B',
|
|
309
|
-
children: 'M',
|
|
310
|
-
afterContent: 'A',
|
|
311
|
-
}),
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
const slots = getContentSlots(result)
|
|
315
|
-
for (const slot of slots) {
|
|
316
|
-
expect(slot.props.tag).toBeUndefined()
|
|
317
|
-
}
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
it('passes equalCols to Content slots', () => {
|
|
321
|
-
const result = asVNode(
|
|
322
|
-
Element({
|
|
323
|
-
equalCols: true,
|
|
324
|
-
beforeContent: 'B',
|
|
325
|
-
children: 'M',
|
|
326
|
-
afterContent: 'A',
|
|
327
|
-
}),
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
const slots = getContentSlots(result)
|
|
331
|
-
for (const slot of slots) {
|
|
332
|
-
expect(slot.props.equalCols).toBe(true)
|
|
333
|
-
}
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
it('passes gap to before and after Content slots but not content slot', () => {
|
|
337
|
-
const result = asVNode(
|
|
338
|
-
Element({
|
|
339
|
-
gap: 16,
|
|
340
|
-
beforeContent: 'B',
|
|
341
|
-
children: 'M',
|
|
342
|
-
afterContent: 'A',
|
|
343
|
-
}),
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
const slots = getContentSlots(result)
|
|
347
|
-
const beforeSlot = slots.find((v) => v.props.contentType === 'before')
|
|
348
|
-
const contentSlot = slots.find((v) => v.props.contentType === 'content')
|
|
349
|
-
const afterSlot = slots.find((v) => v.props.contentType === 'after')
|
|
350
|
-
|
|
351
|
-
expect(beforeSlot?.props.gap).toBe(16)
|
|
352
|
-
expect(contentSlot?.props.gap).toBeUndefined()
|
|
353
|
-
expect(afterSlot?.props.gap).toBe(16)
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
it('passes content-level alignment to the content Content slot', () => {
|
|
357
|
-
const result = asVNode(
|
|
358
|
-
Element({
|
|
359
|
-
contentDirection: 'inline',
|
|
360
|
-
contentAlignX: 'center',
|
|
361
|
-
contentAlignY: 'top',
|
|
362
|
-
beforeContent: 'B',
|
|
363
|
-
children: 'M',
|
|
364
|
-
afterContent: 'A',
|
|
365
|
-
}),
|
|
366
|
-
)
|
|
367
|
-
|
|
368
|
-
const slots = getContentSlots(result)
|
|
369
|
-
const contentSlot = slots.find((v) => v.props.contentType === 'content')
|
|
370
|
-
expect(contentSlot?.props.direction).toBe('inline')
|
|
371
|
-
expect(contentSlot?.props.alignX).toBe('center')
|
|
372
|
-
expect(contentSlot?.props.alignY).toBe('top')
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
it('passes before-level alignment to the before Content slot', () => {
|
|
376
|
-
const result = asVNode(
|
|
377
|
-
Element({
|
|
378
|
-
beforeContentDirection: 'rows',
|
|
379
|
-
beforeContentAlignX: 'right',
|
|
380
|
-
beforeContentAlignY: 'bottom',
|
|
381
|
-
beforeContent: 'B',
|
|
382
|
-
children: 'M',
|
|
383
|
-
afterContent: 'A',
|
|
384
|
-
}),
|
|
385
|
-
)
|
|
386
|
-
|
|
387
|
-
const slots = getContentSlots(result)
|
|
388
|
-
const beforeSlot = slots.find((v) => v.props.contentType === 'before')
|
|
389
|
-
expect(beforeSlot?.props.direction).toBe('rows')
|
|
390
|
-
expect(beforeSlot?.props.alignX).toBe('right')
|
|
391
|
-
expect(beforeSlot?.props.alignY).toBe('bottom')
|
|
392
|
-
})
|
|
393
|
-
|
|
394
|
-
it('passes after-level alignment to the after Content slot', () => {
|
|
395
|
-
const result = asVNode(
|
|
396
|
-
Element({
|
|
397
|
-
afterContentDirection: 'rows',
|
|
398
|
-
afterContentAlignX: 'center',
|
|
399
|
-
afterContentAlignY: 'top',
|
|
400
|
-
beforeContent: 'B',
|
|
401
|
-
children: 'M',
|
|
402
|
-
afterContent: 'A',
|
|
403
|
-
}),
|
|
404
|
-
)
|
|
405
|
-
|
|
406
|
-
const slots = getContentSlots(result)
|
|
407
|
-
const afterSlot = slots.find((v) => v.props.contentType === 'after')
|
|
408
|
-
expect(afterSlot?.props.direction).toBe('rows')
|
|
409
|
-
expect(afterSlot?.props.alignX).toBe('center')
|
|
410
|
-
expect(afterSlot?.props.alignY).toBe('top')
|
|
411
|
-
})
|
|
412
|
-
})
|
|
413
|
-
|
|
414
|
-
describe('HTML attribute filtering', () => {
|
|
415
|
-
it('passes through id', () => {
|
|
416
|
-
const result = asVNode(Element({ id: 'my-el', children: 'test' }))
|
|
417
|
-
expect(result.props.id).toBe('my-el')
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
it('passes through role', () => {
|
|
421
|
-
const result = asVNode(Element({ role: 'button', children: 'test' }))
|
|
422
|
-
expect(result.props.role).toBe('button')
|
|
423
|
-
})
|
|
424
|
-
|
|
425
|
-
it('passes through data- attributes', () => {
|
|
426
|
-
const result = asVNode(Element({ 'data-testid': 'el', children: 'test' }))
|
|
427
|
-
expect(result.props['data-testid']).toBe('el')
|
|
428
|
-
})
|
|
429
|
-
|
|
430
|
-
it('passes through aria- attributes', () => {
|
|
431
|
-
const result = asVNode(Element({ 'aria-label': 'label', children: 'test' }))
|
|
432
|
-
expect(result.props['aria-label']).toBe('label')
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
it('passes through on-prefixed event handlers', () => {
|
|
436
|
-
const handler = () => {
|
|
437
|
-
/* noop */
|
|
438
|
-
}
|
|
439
|
-
const result = asVNode(Element({ onClick: handler, children: 'test' }))
|
|
440
|
-
expect(result.props.onClick).toBe(handler)
|
|
441
|
-
})
|
|
442
|
-
|
|
443
|
-
it('passes through tabindex', () => {
|
|
444
|
-
// @ts-expect-error — testing element-specific attr forwarding
|
|
445
|
-
const result = asVNode(Element({ tabindex: 0, children: 'test' }))
|
|
446
|
-
expect(result.props.tabindex).toBe(0)
|
|
447
|
-
})
|
|
448
|
-
|
|
449
|
-
it('passes through title', () => {
|
|
450
|
-
const result = asVNode(Element({ title: 'tooltip', children: 'test' }))
|
|
451
|
-
expect(result.props.title).toBe('tooltip')
|
|
452
|
-
})
|
|
453
|
-
|
|
454
|
-
it('passes through href for anchor tag', () => {
|
|
455
|
-
// @ts-expect-error — testing element-specific attr forwarding
|
|
456
|
-
const result = asVNode(Element({ tag: 'a', href: '/link', children: 'test' }))
|
|
457
|
-
expect(result.props.href).toBe('/link')
|
|
458
|
-
})
|
|
459
|
-
|
|
460
|
-
it('passes through disabled for button tag', () => {
|
|
461
|
-
// @ts-expect-error — testing element-specific attr forwarding
|
|
462
|
-
const result = asVNode(Element({ tag: 'button', disabled: true, children: 'test' }))
|
|
463
|
-
expect(result.props.disabled).toBe(true)
|
|
464
|
-
})
|
|
465
|
-
|
|
466
|
-
it('passes through class', () => {
|
|
467
|
-
const result = asVNode(Element({ class: 'my-class', children: 'test' }))
|
|
468
|
-
expect(result.props.class).toBe('my-class')
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
it('does not set class when not provided', () => {
|
|
472
|
-
const result = asVNode(Element({ children: 'test' }))
|
|
473
|
-
expect(result.props.class).toBeUndefined()
|
|
474
|
-
})
|
|
475
|
-
|
|
476
|
-
it('filters out reserved props (gap, beforeContent, afterContent, css, etc.)', () => {
|
|
477
|
-
const result = asVNode(
|
|
478
|
-
Element({
|
|
479
|
-
beforeContent: h('span', null, 'x'),
|
|
480
|
-
afterContent: h('span', null, 'y'),
|
|
481
|
-
children: 'test',
|
|
482
|
-
direction: 'inline',
|
|
483
|
-
alignX: 'center',
|
|
484
|
-
alignY: 'center',
|
|
485
|
-
gap: 8,
|
|
486
|
-
block: true,
|
|
487
|
-
equalCols: true,
|
|
488
|
-
}),
|
|
489
|
-
)
|
|
490
|
-
// These reserved props are consumed by Element and should not leak to Wrapper
|
|
491
|
-
expect(result.props.gap).toBeUndefined()
|
|
492
|
-
expect(result.props.beforeContent).toBeUndefined()
|
|
493
|
-
expect(result.props.afterContent).toBeUndefined()
|
|
494
|
-
expect(result.props.contentDirection).toBeUndefined()
|
|
495
|
-
expect(result.props.css).toBeUndefined()
|
|
496
|
-
expect(result.props.contentCss).toBeUndefined()
|
|
497
|
-
expect(result.props.beforeContentCss).toBeUndefined()
|
|
498
|
-
expect(result.props.afterContentCss).toBeUndefined()
|
|
499
|
-
})
|
|
500
|
-
})
|
|
501
|
-
|
|
502
|
-
describe('ref handling', () => {
|
|
503
|
-
it('passes a merged ref function to Wrapper', () => {
|
|
504
|
-
const result = asVNode(Element({ children: 'test' }))
|
|
505
|
-
expect(typeof result.props.ref).toBe('function')
|
|
506
|
-
})
|
|
507
|
-
|
|
508
|
-
it('wraps function ref in mergedRef', () => {
|
|
509
|
-
let captured: HTMLElement | null = null
|
|
510
|
-
const ref = (node: HTMLElement | null) => {
|
|
511
|
-
captured = node
|
|
512
|
-
}
|
|
513
|
-
const result = asVNode(Element({ ref, children: 'test' }))
|
|
514
|
-
expect(typeof result.props.ref).toBe('function')
|
|
515
|
-
const fakeNode = {} as HTMLElement
|
|
516
|
-
;(result.props.ref as (node: HTMLElement | null) => void)(fakeNode)
|
|
517
|
-
expect(captured).toBe(fakeNode)
|
|
518
|
-
})
|
|
519
|
-
|
|
520
|
-
it('wraps object ref in mergedRef', () => {
|
|
521
|
-
const ref = { current: null as HTMLElement | null }
|
|
522
|
-
const result = asVNode(Element({ ref, children: 'test' }))
|
|
523
|
-
expect(typeof result.props.ref).toBe('function')
|
|
524
|
-
const fakeNode = {} as HTMLElement
|
|
525
|
-
;(result.props.ref as (node: HTMLElement | null) => void)(fakeNode)
|
|
526
|
-
expect(ref.current).toBe(fakeNode)
|
|
527
|
-
})
|
|
528
|
-
})
|
|
529
|
-
|
|
530
|
-
describe('void / empty elements', () => {
|
|
531
|
-
it('renders img with no children', () => {
|
|
532
|
-
// @ts-expect-error — testing element-specific attr forwarding
|
|
533
|
-
const result = asVNode(Element({ tag: 'img', src: '/pic.png' }))
|
|
534
|
-
expect(typeof result.type).toBe("function")
|
|
535
|
-
expect(getLayoutProps(result).tag).toBe('img')
|
|
536
|
-
expect(result.props.src).toBe('/pic.png')
|
|
537
|
-
expect(result.props.children).toBeUndefined()
|
|
538
|
-
})
|
|
539
|
-
|
|
540
|
-
it('renders input with no children', () => {
|
|
541
|
-
// @ts-expect-error — testing element-specific attr forwarding
|
|
542
|
-
const result = asVNode(Element({ tag: 'input', type: 'text' }))
|
|
543
|
-
expect(typeof result.type).toBe("function")
|
|
544
|
-
expect(getLayoutProps(result).tag).toBe('input')
|
|
545
|
-
expect(result.props.type).toBe('text')
|
|
546
|
-
expect(result.props.children).toBeUndefined()
|
|
547
|
-
})
|
|
548
|
-
|
|
549
|
-
it('renders with dangerouslySetInnerHTML (treated as empty)', () => {
|
|
550
|
-
const result = asVNode(Element({ dangerouslySetInnerHTML: { __html: '<b>hi</b>' } }))
|
|
551
|
-
expect(typeof result.type).toBe("function")
|
|
552
|
-
expect(result.props.dangerouslySetInnerHTML).toEqual({ __html: '<b>hi</b>' })
|
|
553
|
-
expect(result.props.children).toBeUndefined()
|
|
554
|
-
})
|
|
555
|
-
|
|
556
|
-
it('renders br with no children', () => {
|
|
557
|
-
const result = asVNode(Element({ tag: 'br' }))
|
|
558
|
-
expect(typeof result.type).toBe("function")
|
|
559
|
-
expect(result.props.children).toBeUndefined()
|
|
560
|
-
})
|
|
561
|
-
|
|
562
|
-
it('renders hr with no children', () => {
|
|
563
|
-
const result = asVNode(Element({ tag: 'hr' }))
|
|
564
|
-
expect(typeof result.type).toBe("function")
|
|
565
|
-
expect(result.props.children).toBeUndefined()
|
|
566
|
-
})
|
|
567
|
-
})
|
|
568
|
-
|
|
569
|
-
describe('inline-vs-block tag rendering', () => {
|
|
570
|
-
// The simple-element fast path inlines Wrapper into Styled and forwards
|
|
571
|
-
// `tag` as the `as` prop. The pre-fast-path `isInline` flag was an
|
|
572
|
-
// internal Wrapper plumbing detail that determined the inner SUB_TAG
|
|
573
|
-
// for the rare needsFix (button/fieldset/legend) path — invisible on
|
|
574
|
-
// span/a/section, which never need that fix. After the fast path the
|
|
575
|
-
// flag is gone in the simple path; the rendered tag is the contract.
|
|
576
|
-
it('renders inline tags like span as <span>', () => {
|
|
577
|
-
const result = asVNode(Element({ tag: 'span', children: 'text' }))
|
|
578
|
-
expect(getLayoutProps(result).tag).toBe('span')
|
|
579
|
-
})
|
|
580
|
-
|
|
581
|
-
it('renders anchor tag as <a>', () => {
|
|
582
|
-
// @ts-expect-error — testing element-specific attr forwarding
|
|
583
|
-
const result = asVNode(Element({ tag: 'a', href: '#', children: 'link' }))
|
|
584
|
-
expect(getLayoutProps(result).tag).toBe('a')
|
|
585
|
-
})
|
|
586
|
-
|
|
587
|
-
it('renders block tags like section as <section>', () => {
|
|
588
|
-
const result = asVNode(Element({ tag: 'section', children: 'text' }))
|
|
589
|
-
expect(getLayoutProps(result).tag).toBe('section')
|
|
590
|
-
})
|
|
591
|
-
|
|
592
|
-
it('leaves tag undefined when not specified (default div)', () => {
|
|
593
|
-
const result = asVNode(Element({ children: 'text' }))
|
|
594
|
-
expect(getLayoutProps(result).tag).toBeUndefined()
|
|
595
|
-
})
|
|
596
|
-
})
|
|
597
|
-
|
|
598
|
-
describe('extendCss prop', () => {
|
|
599
|
-
it('passes css prop as extendCss to Wrapper', () => {
|
|
600
|
-
const customCss = 'color: red;'
|
|
601
|
-
const result = asVNode(Element({ css: customCss, children: 'test' }))
|
|
602
|
-
expect(getLayoutProps(result).extendCss).toBe(customCss)
|
|
603
|
-
})
|
|
604
|
-
|
|
605
|
-
it('does not pass extendCss when css not provided', () => {
|
|
606
|
-
const result = asVNode(Element({ children: 'test' }))
|
|
607
|
-
expect(getLayoutProps(result).extendCss).toBeUndefined()
|
|
608
|
-
})
|
|
609
|
-
})
|
|
610
|
-
|
|
611
|
-
describe('content fallback chain', () => {
|
|
612
|
-
it('prefers children over content', () => {
|
|
613
|
-
const result = asVNode(Element({ children: 'child', content: 'alt' }))
|
|
614
|
-
// Simple-element fast path returns children directly. The fallback
|
|
615
|
-
// chain (children → content → label) is exercised inside getChildren(),
|
|
616
|
-
// which runs INSIDE the reactive accessor wrap — so invoking the
|
|
617
|
-
// accessor reveals the resolved value.
|
|
618
|
-
expect(typeof result.props.children).toBe('function')
|
|
619
|
-
expect((result.props.children as () => unknown)()).toBe('child')
|
|
620
|
-
})
|
|
621
|
-
|
|
622
|
-
it('falls back to content when no children', () => {
|
|
623
|
-
const result = asVNode(Element({ content: 'alt content' }))
|
|
624
|
-
const children = result.props.children as unknown[]
|
|
625
|
-
expect(children).toBeDefined()
|
|
626
|
-
})
|
|
627
|
-
|
|
628
|
-
it('falls back to label when no children or content', () => {
|
|
629
|
-
const result = asVNode(Element({ label: 'label text' }))
|
|
630
|
-
const children = result.props.children as unknown[]
|
|
631
|
-
expect(children).toBeDefined()
|
|
632
|
-
})
|
|
633
|
-
})
|
|
634
|
-
|
|
635
|
-
describe('Wrapper as prop reset', () => {
|
|
636
|
-
it('resets the as prop to undefined on Wrapper', () => {
|
|
637
|
-
const result = asVNode(Element({ children: 'test' }))
|
|
638
|
-
expect(result.props.as).toBeUndefined()
|
|
639
|
-
})
|
|
640
|
-
})
|
|
641
|
-
|
|
642
|
-
describe('button tag (flex fix needed)', () => {
|
|
643
|
-
it('passes tag as button to Wrapper', () => {
|
|
644
|
-
const result = asVNode(Element({ tag: 'button', children: 'click' }))
|
|
645
|
-
expect(typeof result.type).toBe("function")
|
|
646
|
-
expect(getLayoutProps(result).tag).toBe('button')
|
|
647
|
-
})
|
|
648
|
-
|
|
649
|
-
it('passes isInline=true for button (inline element)', () => {
|
|
650
|
-
const result = asVNode(Element({ tag: 'button', children: 'click' }))
|
|
651
|
-
expect(getLayoutProps(result).isInline).toBe(true)
|
|
652
|
-
})
|
|
653
|
-
})
|
|
654
|
-
|
|
655
|
-
describe('equalBeforeAfter ResizeObserver', () => {
|
|
656
|
-
// Captures the live ResizeObserver constructor — we install a stub on
|
|
657
|
-
// globalThis for the duration of the test, mount + unmount via the real
|
|
658
|
-
// runtime-dom pipeline, and assert the observer was set up + cleaned up.
|
|
659
|
-
// Mirrors vitus-labs's useLayoutEffect + ResizeObserver setup so async
|
|
660
|
-
// slot resizes (font swaps, lazy text, viewport changes) keep the
|
|
661
|
-
// before/after slots equalized — not just the one-shot mount measurement.
|
|
662
|
-
type ROStub = {
|
|
663
|
-
observed: HTMLElement[]
|
|
664
|
-
disconnects: number
|
|
665
|
-
callbacks: Array<() => void>
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function installResizeObserverStub(): ROStub {
|
|
669
|
-
const stub: ROStub = { observed: [], disconnects: 0, callbacks: [] }
|
|
670
|
-
class StubResizeObserver {
|
|
671
|
-
callback: () => void
|
|
672
|
-
constructor(callback: () => void) {
|
|
673
|
-
this.callback = callback
|
|
674
|
-
stub.callbacks.push(callback)
|
|
675
|
-
}
|
|
676
|
-
observe(node: HTMLElement) {
|
|
677
|
-
stub.observed.push(node)
|
|
678
|
-
}
|
|
679
|
-
disconnect() {
|
|
680
|
-
stub.disconnects++
|
|
681
|
-
}
|
|
682
|
-
unobserve() {
|
|
683
|
-
/* no-op */
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
;(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = StubResizeObserver
|
|
687
|
-
return stub
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function uninstallResizeObserverStub(prev: unknown) {
|
|
691
|
-
if (prev === undefined)
|
|
692
|
-
delete (globalThis as unknown as { ResizeObserver?: unknown }).ResizeObserver
|
|
693
|
-
else (globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = prev
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
it('observes the equalize ref on mount when equalBeforeAfter+before+after are set', async () => {
|
|
697
|
-
const { mount } = await import('@pyreon/runtime-dom')
|
|
698
|
-
const prev = (globalThis as unknown as { ResizeObserver?: unknown }).ResizeObserver
|
|
699
|
-
const stub = installResizeObserverStub()
|
|
700
|
-
try {
|
|
701
|
-
const root = document.createElement('div')
|
|
702
|
-
document.body.appendChild(root)
|
|
703
|
-
|
|
704
|
-
const unmount = mount(
|
|
705
|
-
h(Element, {
|
|
706
|
-
equalBeforeAfter: true,
|
|
707
|
-
beforeContent: h('span', null, 'B'),
|
|
708
|
-
children: 'main',
|
|
709
|
-
afterContent: h('span', null, 'A'),
|
|
710
|
-
}),
|
|
711
|
-
root,
|
|
712
|
-
)
|
|
713
|
-
|
|
714
|
-
expect(stub.observed.length).toBe(1)
|
|
715
|
-
expect(stub.disconnects).toBe(0)
|
|
716
|
-
|
|
717
|
-
unmount()
|
|
718
|
-
expect(stub.disconnects).toBe(1)
|
|
719
|
-
|
|
720
|
-
root.remove()
|
|
721
|
-
} finally {
|
|
722
|
-
uninstallResizeObserverStub(prev)
|
|
723
|
-
}
|
|
724
|
-
})
|
|
725
|
-
|
|
726
|
-
it('does not register an observer when equalBeforeAfter is false', async () => {
|
|
727
|
-
const { mount } = await import('@pyreon/runtime-dom')
|
|
728
|
-
const prev = (globalThis as unknown as { ResizeObserver?: unknown }).ResizeObserver
|
|
729
|
-
const stub = installResizeObserverStub()
|
|
730
|
-
try {
|
|
731
|
-
const root = document.createElement('div')
|
|
732
|
-
document.body.appendChild(root)
|
|
733
|
-
|
|
734
|
-
const unmount = mount(
|
|
735
|
-
h(Element, {
|
|
736
|
-
beforeContent: h('span', null, 'B'),
|
|
737
|
-
children: 'main',
|
|
738
|
-
afterContent: h('span', null, 'A'),
|
|
739
|
-
}),
|
|
740
|
-
root,
|
|
741
|
-
)
|
|
742
|
-
|
|
743
|
-
expect(stub.observed.length).toBe(0)
|
|
744
|
-
|
|
745
|
-
unmount()
|
|
746
|
-
root.remove()
|
|
747
|
-
} finally {
|
|
748
|
-
uninstallResizeObserverStub(prev)
|
|
749
|
-
}
|
|
750
|
-
})
|
|
751
|
-
|
|
752
|
-
it('does not register an observer when only one of before/after is set', async () => {
|
|
753
|
-
const { mount } = await import('@pyreon/runtime-dom')
|
|
754
|
-
const prev = (globalThis as unknown as { ResizeObserver?: unknown }).ResizeObserver
|
|
755
|
-
const stub = installResizeObserverStub()
|
|
756
|
-
try {
|
|
757
|
-
const root = document.createElement('div')
|
|
758
|
-
document.body.appendChild(root)
|
|
759
|
-
|
|
760
|
-
const unmount = mount(
|
|
761
|
-
h(Element, {
|
|
762
|
-
equalBeforeAfter: true,
|
|
763
|
-
beforeContent: h('span', null, 'B'),
|
|
764
|
-
children: 'main',
|
|
765
|
-
}),
|
|
766
|
-
root,
|
|
767
|
-
)
|
|
768
|
-
|
|
769
|
-
expect(stub.observed.length).toBe(0)
|
|
770
|
-
|
|
771
|
-
unmount()
|
|
772
|
-
root.remove()
|
|
773
|
-
} finally {
|
|
774
|
-
uninstallResizeObserverStub(prev)
|
|
775
|
-
}
|
|
776
|
-
})
|
|
777
|
-
|
|
778
|
-
it('survives missing ResizeObserver global (SSR / older runtimes)', async () => {
|
|
779
|
-
const { mount } = await import('@pyreon/runtime-dom')
|
|
780
|
-
const prev = (globalThis as unknown as { ResizeObserver?: unknown }).ResizeObserver
|
|
781
|
-
delete (globalThis as unknown as { ResizeObserver?: unknown }).ResizeObserver
|
|
782
|
-
try {
|
|
783
|
-
const root = document.createElement('div')
|
|
784
|
-
document.body.appendChild(root)
|
|
785
|
-
|
|
786
|
-
// Should not throw even though ResizeObserver is undefined.
|
|
787
|
-
const unmount = mount(
|
|
788
|
-
h(Element, {
|
|
789
|
-
equalBeforeAfter: true,
|
|
790
|
-
beforeContent: h('span', null, 'B'),
|
|
791
|
-
children: 'main',
|
|
792
|
-
afterContent: h('span', null, 'A'),
|
|
793
|
-
}),
|
|
794
|
-
root,
|
|
795
|
-
)
|
|
796
|
-
unmount()
|
|
797
|
-
root.remove()
|
|
798
|
-
} finally {
|
|
799
|
-
uninstallResizeObserverStub(prev)
|
|
800
|
-
}
|
|
801
|
-
})
|
|
802
|
-
})
|
|
803
|
-
|
|
804
|
-
describe('component metadata', () => {
|
|
805
|
-
it('has displayName set', () => {
|
|
806
|
-
expect(Element.displayName).toBeDefined()
|
|
807
|
-
expect(Element.displayName).toContain('Element')
|
|
808
|
-
})
|
|
809
|
-
|
|
810
|
-
it('has PYREON__COMPONENT set', () => {
|
|
811
|
-
expect(Element.PYREON__COMPONENT).toBeDefined()
|
|
812
|
-
expect(Element.PYREON__COMPONENT).toContain('Element')
|
|
813
|
-
})
|
|
814
|
-
|
|
815
|
-
it('has pkgName set', () => {
|
|
816
|
-
expect(Element.pkgName).toBeDefined()
|
|
817
|
-
})
|
|
818
|
-
})
|
|
819
|
-
})
|