@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.
Files changed (70) hide show
  1. package/package.json +10 -12
  2. package/src/Element/component.tsx +0 -315
  3. package/src/Element/constants.ts +0 -96
  4. package/src/Element/index.ts +0 -6
  5. package/src/Element/types.ts +0 -168
  6. package/src/Element/utils.ts +0 -15
  7. package/src/List/component.tsx +0 -105
  8. package/src/List/index.ts +0 -5
  9. package/src/Overlay/component.tsx +0 -140
  10. package/src/Overlay/context.tsx +0 -36
  11. package/src/Overlay/index.ts +0 -7
  12. package/src/Overlay/positioning.ts +0 -191
  13. package/src/Overlay/useOverlay.tsx +0 -461
  14. package/src/Portal/component.tsx +0 -54
  15. package/src/Portal/index.ts +0 -5
  16. package/src/Text/component.tsx +0 -67
  17. package/src/Text/index.ts +0 -5
  18. package/src/Text/styled.ts +0 -30
  19. package/src/Util/component.tsx +0 -43
  20. package/src/Util/index.ts +0 -5
  21. package/src/__tests__/Content.test.tsx +0 -123
  22. package/src/__tests__/Element-slot-reactivity.browser.test.tsx +0 -152
  23. package/src/__tests__/Element.test.ts +0 -819
  24. package/src/__tests__/Iterator.test.ts +0 -492
  25. package/src/__tests__/Iterator.types.test.ts +0 -237
  26. package/src/__tests__/List.test.ts +0 -199
  27. package/src/__tests__/Overlay.test.ts +0 -492
  28. package/src/__tests__/Portal.test.ts +0 -156
  29. package/src/__tests__/Text.test.ts +0 -274
  30. package/src/__tests__/Util.test.ts +0 -63
  31. package/src/__tests__/Wrapper-innerhtml.test.tsx +0 -178
  32. package/src/__tests__/Wrapper.test.tsx +0 -196
  33. package/src/__tests__/elements.browser.test.tsx +0 -132
  34. package/src/__tests__/equalBeforeAfter.test.ts +0 -122
  35. package/src/__tests__/helpers.test.ts +0 -65
  36. package/src/__tests__/integration.test.tsx +0 -118
  37. package/src/__tests__/internElementBundle.test.ts +0 -102
  38. package/src/__tests__/iterator-function-children.test.tsx +0 -120
  39. package/src/__tests__/native-markers.test.ts +0 -13
  40. package/src/__tests__/overlayContext.test.tsx +0 -78
  41. package/src/__tests__/perf-stress.browser.test.tsx +0 -119
  42. package/src/__tests__/positioning.test.ts +0 -90
  43. package/src/__tests__/responsiveProps.test.ts +0 -328
  44. package/src/__tests__/slot-component-reference.test.tsx +0 -157
  45. package/src/__tests__/useOverlay.test.ts +0 -1336
  46. package/src/__tests__/utils.test.ts +0 -69
  47. package/src/__tests__/wrapper-block-cascade.test.ts +0 -121
  48. package/src/constants.ts +0 -1
  49. package/src/env.d.ts +0 -6
  50. package/src/helpers/Content/component.tsx +0 -75
  51. package/src/helpers/Content/index.ts +0 -3
  52. package/src/helpers/Content/styled.ts +0 -105
  53. package/src/helpers/Content/types.ts +0 -49
  54. package/src/helpers/Iterator/component.tsx +0 -316
  55. package/src/helpers/Iterator/index.ts +0 -30
  56. package/src/helpers/Iterator/types.ts +0 -138
  57. package/src/helpers/Wrapper/component.tsx +0 -180
  58. package/src/helpers/Wrapper/constants.ts +0 -10
  59. package/src/helpers/Wrapper/index.ts +0 -3
  60. package/src/helpers/Wrapper/styled.ts +0 -64
  61. package/src/helpers/Wrapper/types.ts +0 -56
  62. package/src/helpers/Wrapper/utils.ts +0 -7
  63. package/src/helpers/index.ts +0 -4
  64. package/src/helpers/internElementBundle.ts +0 -37
  65. package/src/helpers/isPyreonComponent.ts +0 -46
  66. package/src/index.ts +0 -42
  67. package/src/manifest.ts +0 -190
  68. package/src/tests/manifest-snapshot.test.ts +0 -45
  69. package/src/types.ts +0 -112
  70. package/src/utils.ts +0 -5
@@ -1,196 +0,0 @@
1
- import type { VNode } from '@pyreon/core'
2
- import { describe, expect, it, vi } from 'vitest'
3
-
4
- // ---------------------------------------------------------------------------
5
- // Mocks
6
- // ---------------------------------------------------------------------------
7
- vi.mock('~/utils', () => ({
8
- IS_DEVELOPMENT: true,
9
- }))
10
-
11
- import Wrapper from '../helpers/Wrapper/component'
12
- import Styled from '../helpers/Wrapper/styled'
13
-
14
- const asVNode = (v: unknown) => v as VNode
15
-
16
- describe('Wrapper component', () => {
17
- describe('normal element (no flex fix needed)', () => {
18
- it('returns a VNode whose type is the Styled component', () => {
19
- const result = asVNode(Wrapper({ tag: 'div' }))
20
- expect(result.type).toBe(Styled)
21
- })
22
-
23
- it('passes normalElement as $element prop with block, direction, alignX, alignY, equalCols, extraStyles', () => {
24
- const result = asVNode(
25
- Wrapper({
26
- tag: 'div',
27
- block: true,
28
- direction: 'inline',
29
- alignX: 'center',
30
- alignY: 'top',
31
- equalCols: false,
32
- extendCss: 'color: red;',
33
- }),
34
- )
35
-
36
- expect(result.props.$element).toEqual({
37
- block: true,
38
- direction: 'inline',
39
- alignX: 'center',
40
- alignY: 'top',
41
- equalCols: false,
42
- extraStyles: 'color: red;',
43
- })
44
- })
45
-
46
- it("passes tag as the 'as' prop", () => {
47
- const result = asVNode(Wrapper({ tag: 'div' }))
48
- expect(result.props.as).toBe('div')
49
- })
50
-
51
- it('passes ref through', () => {
52
- const ref = () => {}
53
- const result = asVNode(Wrapper({ tag: 'div', ref }))
54
- expect(result.props.ref).toBe(ref)
55
- })
56
-
57
- it('passes children through', () => {
58
- const result = asVNode(Wrapper({ tag: 'div', children: 'child content' }))
59
- expect(result.props.children).toBe('child content')
60
- })
61
-
62
- it('adds data-pyr-element in development mode', () => {
63
- const result = asVNode(Wrapper({ tag: 'div' }))
64
- expect(result.props['data-pyr-element']).toBe('Element')
65
- })
66
-
67
- it('spreads extra props', () => {
68
- const result = asVNode(Wrapper({ tag: 'section', id: 'wrapper', role: 'main' } as any))
69
- expect(result.props.id).toBe('wrapper')
70
- expect(result.props.role).toBe('main')
71
- })
72
- })
73
-
74
- describe('flex-fix path (button/fieldset/legend)', () => {
75
- it('renders parent Styled with parentFixElement for button', () => {
76
- const result = asVNode(Wrapper({ tag: 'button', children: 'Click me' }))
77
- expect(result.type).toBe(Styled)
78
- expect((result.props.$element as any).parentFix).toBe(true)
79
- })
80
-
81
- it('parent Styled receives parentFixElement props (block + extraStyles only)', () => {
82
- const result = asVNode(
83
- Wrapper({
84
- tag: 'button',
85
- block: true,
86
- extendCss: 'border: none;',
87
- direction: 'inline',
88
- alignX: 'center',
89
- alignY: 'center',
90
- }),
91
- )
92
-
93
- expect(result.props.$element).toEqual({
94
- parentFix: true,
95
- block: true,
96
- extraStyles: 'border: none;',
97
- })
98
- expect(result.props.as).toBe('button')
99
- })
100
-
101
- it('child Styled receives childFixElement props (direction, alignX, alignY, equalCols)', () => {
102
- const result = asVNode(
103
- Wrapper({
104
- tag: 'fieldset',
105
- direction: 'rows',
106
- alignX: 'left',
107
- alignY: 'bottom',
108
- equalCols: true,
109
- }),
110
- )
111
-
112
- // The child is the first (only) child of the parent
113
- const child = asVNode(result.props.children)
114
- expect(child.type).toBe(Styled)
115
- expect(child.props.$childFix).toBe(true)
116
- expect(child.props.$element).toEqual({
117
- childFix: true,
118
- direction: 'rows',
119
- alignX: 'left',
120
- alignY: 'bottom',
121
- equalCols: true,
122
- })
123
- })
124
-
125
- it("uses 'div' as inner tag by default", () => {
126
- const result = asVNode(Wrapper({ tag: 'button' }))
127
- const child = asVNode(result.props.children)
128
- expect(child.props.as).toBe('div')
129
- })
130
-
131
- it("uses 'span' as inner tag when isInline is true", () => {
132
- const result = asVNode(Wrapper({ tag: 'button', isInline: true }))
133
- const child = asVNode(result.props.children)
134
- expect(child.props.as).toBe('span')
135
- })
136
- })
137
-
138
- describe('dangerouslySetInnerHTML skips fix', () => {
139
- it('renders single Styled without parentFix for button when dangerouslySetInnerHTML is set', () => {
140
- const result = asVNode(
141
- Wrapper({
142
- tag: 'button',
143
- dangerouslySetInnerHTML: { __html: '<b>Bold</b>' },
144
- } as any),
145
- )
146
-
147
- // Should be the normal (non-fix) path
148
- expect(result.type).toBe(Styled)
149
- expect((result.props.$element as any).parentFix).toBeUndefined()
150
- })
151
- })
152
-
153
- // Void HTML elements (hr, input, img, br, …) cannot have children. Element
154
- // already skips passing children to Wrapper for void tags, but the JSX
155
- // `{own.children}` slot still serialized `undefined` into the vnode and
156
- // tripped runtime-dom's void-element warning. Wrapper now drops the slot
157
- // entirely for void tags.
158
- describe('void HTML elements drop the children slot', () => {
159
- const VOID_TAGS = ['hr', 'input', 'img', 'br', 'area', 'base', 'col', 'embed', 'link', 'source', 'track', 'wbr'] as const
160
-
161
- for (const tag of VOID_TAGS) {
162
- it(`omits children for <${tag}>`, () => {
163
- // Cast to any — the Wrapper Props.tag type narrows out void
164
- // elements, but the runtime guard still has to cover the case
165
- // where a void tag reaches Wrapper (e.g. via rocketstyle attrs
166
- // composing `tag: 'hr'` from a callback whose return type is wider).
167
- const result = asVNode(Wrapper({ tag } as any))
168
- expect(result.type).toBe(Styled)
169
- expect(result.props.children).toBeUndefined()
170
- expect(result.children).toEqual([])
171
- })
172
- }
173
-
174
- it('still renders children for non-void tags', () => {
175
- const result = asVNode(Wrapper({ tag: 'div', children: 'kept' }))
176
- expect(result.props.children).toBe('kept')
177
- })
178
-
179
- it('omits children even when caller accidentally passes them to a void tag', () => {
180
- const result = asVNode(Wrapper({ tag: 'hr', children: 'should-not-leak' }))
181
- expect(result.props.children).toBeUndefined()
182
- })
183
-
184
- it('dangerouslySetInnerHTML on a normally-void tag bypasses the void path', () => {
185
- const result = asVNode(
186
- Wrapper({
187
- tag: 'hr',
188
- dangerouslySetInnerHTML: { __html: '<b>x</b>' },
189
- } as any),
190
- )
191
- // dangerouslySetInnerHTML opts out of the void-element guard
192
- // because the tag becomes a custom element / shadow host case.
193
- expect(result.type).toBe(Styled)
194
- })
195
- })
196
- })
@@ -1,132 +0,0 @@
1
- /** @jsxImportSource @pyreon/core */
2
- import { describe, expect, it, vi } from 'vitest'
3
- import { signal } from '@pyreon/reactivity'
4
- import { flush, mountInBrowser } from '@pyreon/test-utils/browser'
5
- import { Element } from '../Element'
6
- import { Portal } from '../Portal'
7
- import { Text } from '../Text'
8
-
9
- describe('@pyreon/elements browser smoke', () => {
10
- it('Element mounts into real DOM with structural rendering', () => {
11
- const { container, unmount } = mountInBrowser(
12
- <Element tag="div" data-id="el"><span>hello</span></Element>,
13
- )
14
- const el = container.querySelector('[data-id="el"]')
15
- expect(el?.tagName.toLowerCase()).toBe('div')
16
- expect(el?.querySelector('span')?.textContent).toBe('hello')
17
- unmount()
18
- })
19
-
20
- it('Element forwards a reactive text child to the DOM', async () => {
21
- const label = signal('hello')
22
- const { container, unmount } = mountInBrowser(
23
- <Element tag="div">
24
- <span data-id="lbl">{() => label()}</span>
25
- </Element>,
26
- )
27
- const lbl = container.querySelector('[data-id="lbl"]')
28
- expect(lbl?.textContent).toBe('hello')
29
- label.set('world')
30
- await flush()
31
- expect(lbl?.textContent).toBe('world')
32
- unmount()
33
- })
34
-
35
- it('Text renders as inline element', () => {
36
- const { container, unmount } = mountInBrowser(<Text tag="span" data-id="t">hi</Text>)
37
- const el = container.querySelector('[data-id="t"]')
38
- expect(el?.tagName.toLowerCase()).toBe('span')
39
- expect(el?.textContent).toBe('hi')
40
- unmount()
41
- })
42
-
43
- it('Portal projects children to document.body by default', () => {
44
- const { unmount } = mountInBrowser(
45
- <Portal>
46
- <div data-portal-id="p">portal-content</div>
47
- </Portal>,
48
- )
49
- const projected = document.querySelector('[data-portal-id="p"]')
50
- expect(projected?.textContent).toBe('portal-content')
51
- unmount()
52
- expect(document.querySelector('[data-portal-id="p"]')).toBeNull()
53
- })
54
-
55
- it('runs in a real browser — Vitest defines `process.env.NODE_ENV !== "production"`', () => {
56
- // Sanity check the test env: dev gates use bundler-agnostic
57
- // `process.env.NODE_ENV !== 'production'`. Every modern bundler
58
- // (incl. Vitest's Vite) replaces this at build time. In a real-browser
59
- // test run the literal lands as `"development" !== "production"` →
60
- // `true`, so dev warnings fire as expected.
61
- expect(process.env.NODE_ENV).not.toBe('production')
62
- })
63
-
64
- // Void HTML elements via Element must not trip runtime-dom's
65
- // "<X> is a void element and cannot have children" warning.
66
- // Wrapper used to leak an `undefined` child into the vnode for void tags.
67
- describe('void HTML element tags', () => {
68
- const voidTags: Array<'hr' | 'br' | 'input' | 'img'> = ['hr', 'br', 'input', 'img']
69
-
70
- for (const tag of voidTags) {
71
- it(`<${tag}> mounts without "void element" console.warn`, () => {
72
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
73
- const { container, unmount } = mountInBrowser(<Element tag={tag} data-id={tag} />)
74
- const el = container.querySelector(`[data-id="${tag}"]`)
75
- expect(el?.tagName.toLowerCase()).toBe(tag)
76
- const voidWarnings = warnSpy.mock.calls.filter((args) =>
77
- typeof args[0] === 'string' && args[0].includes('void element'),
78
- )
79
- expect(voidWarnings).toEqual([])
80
- warnSpy.mockRestore()
81
- unmount()
82
- })
83
- }
84
- })
85
-
86
- // Regression: Wrapper used to silently drop dangerouslySetInnerHTML.
87
- // The unit test asserts Wrapper forwards the prop on the rendered
88
- // vnode; this real-Chromium test asserts the SVG actually appears in
89
- // the DOM after mount — the user-visible bug shape was "renders empty
90
- // <div></div> instead of inlined SVG".
91
- describe('dangerouslySetInnerHTML — real-Chromium DOM proof', () => {
92
- it('inlines an SVG via Element + dangerouslySetInnerHTML', () => {
93
- const { container, unmount } = mountInBrowser(
94
- <Element
95
- tag="div"
96
- data-id="logo"
97
- dangerouslySetInnerHTML={{
98
- __html: '<svg viewBox="0 0 10 10"><rect width="10" height="10" /></svg>',
99
- }}
100
- />,
101
- )
102
- const root = container.querySelector('[data-id="logo"]')
103
- expect(root).toBeTruthy()
104
- // The SVG must actually be in the DOM, not lost between Wrapper and
105
- // the renderer. Pre-fix this assertion failed: container had
106
- // <div data-id="logo"></div> with no children.
107
- const svg = root?.querySelector('svg')
108
- expect(svg).toBeTruthy()
109
- expect(svg?.tagName.toLowerCase()).toBe('svg')
110
- expect(svg?.querySelector('rect')).toBeTruthy()
111
- unmount()
112
- })
113
-
114
- it('inlines markup on a button (needsFix path)', () => {
115
- const { container, unmount } = mountInBrowser(
116
- <Element
117
- tag="button"
118
- data-id="btn"
119
- dangerouslySetInnerHTML={{ __html: '<strong>Save</strong>' }}
120
- />,
121
- )
122
- const btn = container.querySelector('[data-id="btn"]')
123
- expect(btn).toBeTruthy()
124
- // The bold text must reach the DOM. The needsFix gate's existing
125
- // `!own.dangerouslySetInnerHTML` clause skips the two-layer fix,
126
- // so this falls into the !needsFix branch and the fix's
127
- // `if (innerHTML)` path takes over.
128
- expect(btn?.querySelector('strong')?.textContent).toBe('Save')
129
- unmount()
130
- })
131
- })
132
- })
@@ -1,122 +0,0 @@
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
- const getContentSlots = (result: VNode): VNode[] => {
11
- const children = result.props.children
12
- if (!Array.isArray(children)) return []
13
- return children.filter(
14
- (c: unknown) =>
15
- c != null && typeof c === 'object' && 'type' in (c as VNode) && (c as VNode).type === Content,
16
- ) as VNode[]
17
- }
18
-
19
- describe('Element equalBeforeAfter', () => {
20
- it('always passes a ref function to Wrapper', () => {
21
- const result = asVNode(
22
- Element({
23
- equalBeforeAfter: true,
24
- direction: 'inline',
25
- beforeContent: h('span', null, 'Before'),
26
- afterContent: h('span', null, 'After'),
27
- children: 'Main',
28
- }),
29
- )
30
- expect(typeof result.type).toBe("function")
31
- expect(typeof result.props.ref).toBe('function')
32
- })
33
-
34
- it('does not crash without before/after content', () => {
35
- const result = asVNode(Element({ equalBeforeAfter: true, children: 'Main only' }))
36
- expect(typeof result.type).toBe("function")
37
- })
38
-
39
- it('does not crash with only beforeContent', () => {
40
- const result = asVNode(
41
- Element({
42
- equalBeforeAfter: true,
43
- beforeContent: h('span', null, 'Before'),
44
- children: 'Main',
45
- }),
46
- )
47
- expect(typeof result.type).toBe("function")
48
- })
49
-
50
- it('does not crash with only afterContent', () => {
51
- const result = asVNode(
52
- Element({
53
- equalBeforeAfter: true,
54
- afterContent: h('span', null, 'After'),
55
- children: 'Main',
56
- }),
57
- )
58
- expect(typeof result.type).toBe("function")
59
- })
60
-
61
- it('renders three slot children when both before and after exist', () => {
62
- const result = asVNode(
63
- Element({
64
- equalBeforeAfter: true,
65
- direction: 'inline',
66
- beforeContent: h('span', null, 'Short'),
67
- afterContent: h('span', null, 'Longer content'),
68
- children: 'Center',
69
- }),
70
- )
71
- const slots = getContentSlots(result)
72
- expect(slots).toHaveLength(3)
73
- })
74
-
75
- it('merged ref calls external function ref', () => {
76
- let captured: HTMLElement | null = null
77
- const ref = (node: HTMLElement | null) => {
78
- captured = node
79
- }
80
- const result = asVNode(
81
- Element({
82
- equalBeforeAfter: true,
83
- ref,
84
- beforeContent: h('span', null, 'Before'),
85
- afterContent: h('span', null, 'After'),
86
- children: 'Main',
87
- }),
88
- )
89
- const fakeNode = {} as HTMLElement
90
- ;(result.props.ref as (node: HTMLElement | null) => void)(fakeNode)
91
- expect(captured).toBe(fakeNode)
92
- })
93
-
94
- it('merged ref sets object ref current', () => {
95
- const ref = { current: null as HTMLElement | null }
96
- const result = asVNode(
97
- Element({
98
- equalBeforeAfter: true,
99
- ref,
100
- beforeContent: h('span', null, 'Before'),
101
- afterContent: h('span', null, 'After'),
102
- children: 'Main',
103
- }),
104
- )
105
- const fakeNode = {} as HTMLElement
106
- ;(result.props.ref as (node: HTMLElement | null) => void)(fakeNode)
107
- expect(ref.current).toBe(fakeNode)
108
- })
109
-
110
- it('uses inline direction for equalBeforeAfter', () => {
111
- const result = asVNode(
112
- Element({
113
- equalBeforeAfter: true,
114
- direction: 'inline',
115
- beforeContent: h('span', null, 'B'),
116
- afterContent: h('span', null, 'A'),
117
- children: 'Main',
118
- }),
119
- )
120
- expect(result.props.direction).toBe('inline')
121
- })
122
- })
@@ -1,65 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { INLINE_ELEMENTS_FLEX_FIX } from '../helpers/Wrapper/constants'
3
- import { isWebFixNeeded } from '../helpers/Wrapper/utils'
4
-
5
- describe('Wrapper utils', () => {
6
- describe('isWebFixNeeded', () => {
7
- it('returns true for button', () => {
8
- expect(isWebFixNeeded('button')).toBe(true)
9
- })
10
-
11
- it('returns true for fieldset', () => {
12
- expect(isWebFixNeeded('fieldset')).toBe(true)
13
- })
14
-
15
- it('returns true for legend', () => {
16
- expect(isWebFixNeeded('legend')).toBe(true)
17
- })
18
-
19
- it('returns false for div', () => {
20
- expect(isWebFixNeeded('div')).toBe(false)
21
- })
22
-
23
- it('returns false for span', () => {
24
- expect(isWebFixNeeded('span')).toBe(false)
25
- })
26
-
27
- it('returns false for section', () => {
28
- expect(isWebFixNeeded('section')).toBe(false)
29
- })
30
-
31
- it('returns false for undefined', () => {
32
- expect(isWebFixNeeded(undefined)).toBe(false)
33
- })
34
-
35
- it('returns false for empty string', () => {
36
- expect(isWebFixNeeded('')).toBe(false)
37
- })
38
-
39
- it('returns false for input', () => {
40
- expect(isWebFixNeeded('input')).toBe(false)
41
- })
42
-
43
- it('returns false for form', () => {
44
- expect(isWebFixNeeded('form')).toBe(false)
45
- })
46
- })
47
-
48
- describe('INLINE_ELEMENTS_FLEX_FIX', () => {
49
- it('contains button', () => {
50
- expect(INLINE_ELEMENTS_FLEX_FIX.button).toBe(true)
51
- })
52
-
53
- it('contains fieldset', () => {
54
- expect(INLINE_ELEMENTS_FLEX_FIX.fieldset).toBe(true)
55
- })
56
-
57
- it('contains legend', () => {
58
- expect(INLINE_ELEMENTS_FLEX_FIX.legend).toBe(true)
59
- })
60
-
61
- it('only has 3 entries', () => {
62
- expect(Object.keys(INLINE_ELEMENTS_FLEX_FIX)).toHaveLength(3)
63
- })
64
- })
65
- })
@@ -1,118 +0,0 @@
1
- import type { VNode } from '@pyreon/core'
2
- import { describe, expect, it, vi } from 'vitest'
3
-
4
- // Element's simple-Element fast path inlines the Wrapper helper directly into
5
- // a Styled invocation, so layout props move from `result.props.{tag, direction, …}`
6
- // to `result.props.{as, $element.direction, …}`. This helper reads from
7
- // whichever shape the result is in so assertions don't depend on which path.
8
- const getLayoutProps = (result: VNode): Record<string, unknown> => {
9
- const p = result.props as Record<string, unknown>
10
- if (p.$element && typeof p.$element === 'object') {
11
- const el = p.$element as Record<string, unknown>
12
- return {
13
- tag: p.as,
14
- direction: el.direction,
15
- alignX: el.alignX,
16
- alignY: el.alignY,
17
- block: el.block,
18
- equalCols: el.equalCols,
19
- extendCss: el.extraStyles,
20
- isInline: undefined,
21
- }
22
- }
23
- return {
24
- tag: p.tag,
25
- direction: p.direction,
26
- alignX: p.alignX,
27
- alignY: p.alignY,
28
- block: p.block,
29
- equalCols: p.equalCols,
30
- extendCss: p.extendCss,
31
- isInline: p.isInline,
32
- }
33
- }
34
-
35
- // ---------------------------------------------------------------------------
36
- // Mocks — match patterns from existing element tests
37
- // ---------------------------------------------------------------------------
38
- vi.mock('~/utils', () => ({
39
- IS_DEVELOPMENT: true,
40
- }))
41
-
42
- vi.mock('@pyreon/ui-core', async (importOriginal) => {
43
- const actual = (await importOriginal()) as Record<string, unknown>
44
- return {
45
- ...actual,
46
- render: (children: unknown) => children,
47
- }
48
- })
49
-
50
- import { Element } from '../Element'
51
- import Content from '../helpers/Content/component'
52
- import Wrapper from '../helpers/Wrapper/component'
53
-
54
- const asVNode = (v: unknown) => v as VNode
55
-
56
- const getContentSlots = (result: VNode): VNode[] => {
57
- const children = result.props.children
58
- if (!Array.isArray(children)) return []
59
- return children.filter(
60
- (c: unknown) =>
61
- c != null && typeof c === 'object' && 'type' in (c as VNode) && (c as VNode).type === Content,
62
- ) as VNode[]
63
- }
64
-
65
- // ─── Integration tests ───────────────────────────────────────────────────────
66
-
67
- describe('Element integration', () => {
68
- it('renders with content prop producing Wrapper VNode', () => {
69
- const result = asVNode(Element({ content: 'hello world', children: undefined }))
70
- expect(typeof result.type).toBe("function")
71
- const children = result.props.children
72
- expect(children).toBeDefined()
73
- })
74
-
75
- it('three-section layout: beforeContent + content + afterContent', () => {
76
- const result = asVNode(
77
- Element({
78
- beforeContent: 'icon',
79
- content: 'main text',
80
- afterContent: 'arrow',
81
- children: undefined,
82
- }),
83
- )
84
- expect(typeof result.type).toBe("function")
85
- const slots = getContentSlots(result)
86
- // With before/after content, there should be Content wrapper VNodes
87
- expect(slots.length).toBeGreaterThanOrEqual(2)
88
- })
89
-
90
- it('direction and alignX props pass through to Wrapper', () => {
91
- const result = asVNode(
92
- Element({
93
- direction: 'inline',
94
- alignX: 'center',
95
- children: 'test',
96
- }),
97
- )
98
-
99
- // When beforeContent/afterContent are absent, direction falls through
100
- // to wrapper level and contentDirection/contentAlignX take effect
101
- expect(typeof result.type).toBe("function")
102
- // The wrapper receives alignment props
103
- expect(result.props).toBeDefined()
104
- })
105
-
106
- it('contentDirection overrides default direction on wrapper for simple element', () => {
107
- const result = asVNode(
108
- Element({
109
- contentDirection: 'inline',
110
- contentAlignX: 'center',
111
- children: 'test',
112
- }),
113
- )
114
- // Simple element (no before/after) uses contentDirection as wrapper direction
115
- expect(getLayoutProps(result).direction).toBe('inline')
116
- expect(getLayoutProps(result).alignX).toBe('center')
117
- })
118
- })