@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,274 +0,0 @@
|
|
|
1
|
-
import type { VNode } from '@pyreon/core'
|
|
2
|
-
import { h } from '@pyreon/core'
|
|
3
|
-
import { describe, expect, it } from 'vitest'
|
|
4
|
-
import { Text } from '../Text'
|
|
5
|
-
|
|
6
|
-
const asVNode = (v: unknown) => v as VNode
|
|
7
|
-
|
|
8
|
-
describe('Text', () => {
|
|
9
|
-
describe('static properties', () => {
|
|
10
|
-
it('has isText set to true', () => {
|
|
11
|
-
expect(Text.isText).toBe(true)
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it('has correct displayName', () => {
|
|
15
|
-
expect(Text.displayName).toBe('@pyreon/elements/Text')
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('has correct pkgName', () => {
|
|
19
|
-
expect(Text.pkgName).toBe('@pyreon/elements')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('has correct PYREON__COMPONENT', () => {
|
|
23
|
-
expect(Text.PYREON__COMPONENT).toBe('@pyreon/elements/Text')
|
|
24
|
-
})
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
describe('default rendering', () => {
|
|
28
|
-
it('returns a VNode whose type is the Styled component (a function)', () => {
|
|
29
|
-
const result = asVNode(Text({ children: 'Hello' }))
|
|
30
|
-
expect(typeof result.type).toBe('function')
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('does not set as prop when no tag or paragraph provided', () => {
|
|
34
|
-
const result = asVNode(Text({ children: 'Hello' }))
|
|
35
|
-
expect(result.props.as).toBeUndefined()
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('renders children in output', () => {
|
|
39
|
-
const result = asVNode(Text({ children: 'Hello' }))
|
|
40
|
-
expect(result.props.children).toBe('Hello')
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('passes $text prop with extraStyles', () => {
|
|
44
|
-
const result = asVNode(Text({ children: 'Hello' }))
|
|
45
|
-
expect(result.props.$text).toEqual({ extraStyles: undefined })
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('renders null content when no children or label', () => {
|
|
49
|
-
const result = asVNode(Text({}))
|
|
50
|
-
expect(result.props.children).toBeUndefined()
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
describe('content fallback chain', () => {
|
|
55
|
-
it('renders children as content', () => {
|
|
56
|
-
const result = asVNode(Text({ children: 'child content' }))
|
|
57
|
-
expect(result.props.children).toBe('child content')
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('renders label when children not provided', () => {
|
|
61
|
-
const result = asVNode(Text({ label: 'label text' }))
|
|
62
|
-
expect(result.props.children).toBe('label text')
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('prefers children over label', () => {
|
|
66
|
-
const result = asVNode(Text({ children: 'child', label: 'label' }))
|
|
67
|
-
expect(result.props.children).toBe('child')
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('renders VNode children', () => {
|
|
71
|
-
const child = h('strong', null, 'bold')
|
|
72
|
-
const result = asVNode(Text({ children: child }))
|
|
73
|
-
expect(result.props.children).toBe(child)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('renders number label', () => {
|
|
77
|
-
const result = asVNode(Text({ label: 42 }))
|
|
78
|
-
expect(result.props.children).toBe(42)
|
|
79
|
-
})
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
describe('tag prop', () => {
|
|
83
|
-
it('sets as prop to h1', () => {
|
|
84
|
-
const result = asVNode(Text({ tag: 'h1', children: 'Heading' }))
|
|
85
|
-
expect(result.props.as).toBe('h1')
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it('sets as prop to h2', () => {
|
|
89
|
-
const result = asVNode(Text({ tag: 'h2', children: 'Sub heading' }))
|
|
90
|
-
expect(result.props.as).toBe('h2')
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('sets as prop to strong', () => {
|
|
94
|
-
const result = asVNode(Text({ tag: 'strong', children: 'Bold' }))
|
|
95
|
-
expect(result.props.as).toBe('strong')
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('sets as prop to em', () => {
|
|
99
|
-
const result = asVNode(Text({ tag: 'em', children: 'Italic' }))
|
|
100
|
-
expect(result.props.as).toBe('em')
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('does not forward tag as a prop', () => {
|
|
104
|
-
const result = asVNode(Text({ tag: 'h1', children: 'Heading' }))
|
|
105
|
-
expect(result.props.tag).toBeUndefined()
|
|
106
|
-
})
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
describe('paragraph prop', () => {
|
|
110
|
-
it('sets as prop to p when paragraph is true', () => {
|
|
111
|
-
const result = asVNode(Text({ paragraph: true, children: 'Paragraph text' }))
|
|
112
|
-
expect(result.props.as).toBe('p')
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('does not set as prop when paragraph is false', () => {
|
|
116
|
-
const result = asVNode(Text({ paragraph: false, children: 'Inline text' }))
|
|
117
|
-
expect(result.props.as).toBeUndefined()
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('paragraph is overridden when tag is also set (tag wins because paragraph branch is skipped)', () => {
|
|
121
|
-
// When paragraph is true, finalTag = 'p'
|
|
122
|
-
// When tag is also set, the else branch is not reached, so paragraph wins
|
|
123
|
-
const result = asVNode(Text({ tag: 'h1', paragraph: true, children: 'Heading' }))
|
|
124
|
-
expect(result.props.as).toBe('p')
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('uses tag when paragraph is false', () => {
|
|
128
|
-
const result = asVNode(Text({ tag: 'h1', paragraph: false, children: 'Heading' }))
|
|
129
|
-
expect(result.props.as).toBe('h1')
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('does not forward paragraph as a prop', () => {
|
|
133
|
-
const result = asVNode(Text({ paragraph: true, children: 'text' }))
|
|
134
|
-
expect(result.props.paragraph).toBeUndefined()
|
|
135
|
-
})
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
describe('css prop', () => {
|
|
139
|
-
it('passes css value through $text.extraStyles', () => {
|
|
140
|
-
const customCss = 'color: red;'
|
|
141
|
-
const result = asVNode(Text({ css: customCss, children: 'styled' }))
|
|
142
|
-
expect(result.props.$text).toEqual({ extraStyles: customCss })
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
it('passes undefined extraStyles when no css prop', () => {
|
|
146
|
-
const result = asVNode(Text({ children: 'plain' }))
|
|
147
|
-
expect(result.props.$text).toEqual({ extraStyles: undefined })
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('does not forward css as a direct prop', () => {
|
|
151
|
-
const result = asVNode(Text({ css: 'color: blue;', children: 'text' }))
|
|
152
|
-
expect(result.props.css).toBeUndefined()
|
|
153
|
-
})
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
describe('HTML attribute forwarding', () => {
|
|
157
|
-
it('passes through id', () => {
|
|
158
|
-
const result = asVNode(Text({ id: 'text-id', children: 'text' }))
|
|
159
|
-
expect(result.props.id).toBe('text-id')
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('passes through role', () => {
|
|
163
|
-
const result = asVNode(Text({ role: 'heading', children: 'text' }))
|
|
164
|
-
expect(result.props.role).toBe('heading')
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it('passes through title', () => {
|
|
168
|
-
const result = asVNode(Text({ title: 'tooltip', children: 'text' }))
|
|
169
|
-
expect(result.props.title).toBe('tooltip')
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
it('passes through data- attributes', () => {
|
|
173
|
-
const result = asVNode(Text({ 'data-testid': 'txt', children: 'text' }))
|
|
174
|
-
expect(result.props['data-testid']).toBe('txt')
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
it('passes through aria- attributes', () => {
|
|
178
|
-
const result = asVNode(Text({ 'aria-label': 'label', children: 'text' }))
|
|
179
|
-
expect(result.props['aria-label']).toBe('label')
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('passes through on-prefixed event handlers', () => {
|
|
183
|
-
const handler = () => undefined
|
|
184
|
-
const result = asVNode(Text({ onClick: handler, children: 'text' }))
|
|
185
|
-
expect(result.props.onClick).toBe(handler)
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
it('passes through ref', () => {
|
|
189
|
-
const ref = { current: null }
|
|
190
|
-
const result = asVNode(Text({ ref, children: 'text' }))
|
|
191
|
-
expect(result.props.ref).toBe(ref)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
it('passes through class', () => {
|
|
195
|
-
const result = asVNode(Text({ class: 'title', children: 'text' }))
|
|
196
|
-
expect(result.props.class).toBe('title')
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
it('passes through style', () => {
|
|
200
|
-
const result = asVNode(Text({ style: 'color: red;', children: 'text' }))
|
|
201
|
-
expect(result.props.style).toBe('color: red;')
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
it('does not set class when not provided', () => {
|
|
205
|
-
const result = asVNode(Text({ children: 'text' }))
|
|
206
|
-
expect(result.props.class).toBeUndefined()
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
it('does not set style when not provided', () => {
|
|
210
|
-
const result = asVNode(Text({ children: 'text' }))
|
|
211
|
-
expect(result.props.style).toBeUndefined()
|
|
212
|
-
})
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
describe('reserved props are consumed and not forwarded', () => {
|
|
216
|
-
it('does not forward paragraph, label, tag, or css', () => {
|
|
217
|
-
const result = asVNode(
|
|
218
|
-
Text({
|
|
219
|
-
paragraph: true,
|
|
220
|
-
label: 'lbl',
|
|
221
|
-
children: 'text',
|
|
222
|
-
tag: 'h1',
|
|
223
|
-
css: 'font-size: 2rem;',
|
|
224
|
-
}),
|
|
225
|
-
)
|
|
226
|
-
expect(result.props.paragraph).toBeUndefined()
|
|
227
|
-
expect(result.props.label).toBeUndefined()
|
|
228
|
-
expect(result.props.tag).toBeUndefined()
|
|
229
|
-
expect(result.props.css).toBeUndefined()
|
|
230
|
-
})
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
describe('combined props', () => {
|
|
234
|
-
it('renders with paragraph, css, class, and data attribute together', () => {
|
|
235
|
-
const result = asVNode(
|
|
236
|
-
Text({
|
|
237
|
-
paragraph: true,
|
|
238
|
-
css: 'margin: 0;',
|
|
239
|
-
class: 'intro',
|
|
240
|
-
'data-testid': 'intro-text',
|
|
241
|
-
children: 'Hello world',
|
|
242
|
-
}),
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
expect(typeof result.type).toBe('function')
|
|
246
|
-
expect(result.props.as).toBe('p')
|
|
247
|
-
expect(result.props.$text).toEqual({ extraStyles: 'margin: 0;' })
|
|
248
|
-
expect(result.props.class).toBe('intro')
|
|
249
|
-
expect(result.props['data-testid']).toBe('intro-text')
|
|
250
|
-
expect(result.props.children).toBe('Hello world')
|
|
251
|
-
// Reserved props not forwarded
|
|
252
|
-
expect(result.props.paragraph).toBeUndefined()
|
|
253
|
-
expect(result.props.css).toBeUndefined()
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
it('renders with tag, ref, and event handler together', () => {
|
|
257
|
-
const ref = { current: null }
|
|
258
|
-
const handler = () => undefined
|
|
259
|
-
const result = asVNode(
|
|
260
|
-
Text({
|
|
261
|
-
tag: 'h2',
|
|
262
|
-
ref,
|
|
263
|
-
onClick: handler,
|
|
264
|
-
children: 'Subtitle',
|
|
265
|
-
}),
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
expect(result.props.as).toBe('h2')
|
|
269
|
-
expect(result.props.ref).toBe(ref)
|
|
270
|
-
expect(result.props.onClick).toBe(handler)
|
|
271
|
-
expect(result.props.children).toBe('Subtitle')
|
|
272
|
-
})
|
|
273
|
-
})
|
|
274
|
-
})
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import type { VNodeChild } from '@pyreon/core'
|
|
2
|
-
import { h } from '@pyreon/core'
|
|
3
|
-
import { describe, expect, it } from 'vitest'
|
|
4
|
-
import Util from '../Util/component'
|
|
5
|
-
|
|
6
|
-
describe('Util', () => {
|
|
7
|
-
describe('statics', () => {
|
|
8
|
-
it('has displayName', () => {
|
|
9
|
-
expect(Util.displayName).toBe('@pyreon/elements/Util')
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
it('has pkgName', () => {
|
|
13
|
-
expect(Util.pkgName).toBe('@pyreon/elements')
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('has PYREON__COMPONENT', () => {
|
|
17
|
-
expect(Util.PYREON__COMPONENT).toBe('@pyreon/elements/Util')
|
|
18
|
-
})
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
describe('className injection', () => {
|
|
22
|
-
it('calls render with className prop for string className', () => {
|
|
23
|
-
const child = h('div', { 'data-testid': 'child' }, 'Content')
|
|
24
|
-
const result = Util({ children: child, className: 'my-class' }) as VNodeChild
|
|
25
|
-
// render() should merge className into the child
|
|
26
|
-
expect(result).toBeDefined()
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('joins array className into space-separated string', () => {
|
|
30
|
-
const child = h('div', { 'data-testid': 'child' }, 'Content')
|
|
31
|
-
const result = Util({ children: child, className: ['cls-a', 'cls-b'] }) as VNodeChild
|
|
32
|
-
expect(result).toBeDefined()
|
|
33
|
-
})
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
describe('style injection', () => {
|
|
37
|
-
it('calls render with style prop for style object', () => {
|
|
38
|
-
const child = h('div', { 'data-testid': 'child' }, 'Content')
|
|
39
|
-
const result = Util({ children: child, style: { color: 'red' } }) as VNodeChild
|
|
40
|
-
expect(result).toBeDefined()
|
|
41
|
-
})
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
describe('both className and style', () => {
|
|
45
|
-
it('passes both className and style to render', () => {
|
|
46
|
-
const child = h('div', { 'data-testid': 'child' }, 'Content')
|
|
47
|
-
const result = Util({
|
|
48
|
-
children: child,
|
|
49
|
-
className: 'my-class',
|
|
50
|
-
style: { color: 'blue' },
|
|
51
|
-
}) as VNodeChild
|
|
52
|
-
expect(result).toBeDefined()
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
describe('no-op when no props', () => {
|
|
57
|
-
it('renders children without modification when no className or style', () => {
|
|
58
|
-
const child = h('div', { 'data-testid': 'child' }, 'Content')
|
|
59
|
-
const result = Util({ children: child }) as VNodeChild
|
|
60
|
-
expect(result).toBeDefined()
|
|
61
|
-
})
|
|
62
|
-
})
|
|
63
|
-
})
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression: Wrapper used to silently drop `dangerouslySetInnerHTML`.
|
|
3
|
-
*
|
|
4
|
-
* Bug shape: `OWN_KEYS` listed `'dangerouslySetInnerHTML'`, so `splitProps`
|
|
5
|
-
* moved it into `own`. The Styled JSX call only spread `...commonProps`
|
|
6
|
-
* (built from `rest`) and never re-attached `own.dangerouslySetInnerHTML`.
|
|
7
|
-
* Both runtimes (`runtime-server` and `runtime-dom`) support the prop —
|
|
8
|
-
* the data was lost between Wrapper and the renderer.
|
|
9
|
-
*
|
|
10
|
-
* Two test layers:
|
|
11
|
-
*
|
|
12
|
-
* 1. **Mock-vnode tests** (this file's first describe block) — fast
|
|
13
|
-
* structural assertions against the vnode tree Wrapper returns. Catches
|
|
14
|
-
* the prop drop at the API surface where it originally happened.
|
|
15
|
-
*
|
|
16
|
-
* 2. **Real-h() mount tests** (second describe block) — uses real `h()` +
|
|
17
|
-
* `mount()` to exercise the full Element → Wrapper → Styled → DOM
|
|
18
|
-
* pipeline. Catches the prop drop wherever it might occur along the
|
|
19
|
-
* chain (Wrapper, Element, rocketstyle attrs HOC, runtime-dom prop
|
|
20
|
-
* application). This is the "safety net" pattern from
|
|
21
|
-
* .claude/rules/test-environment-parity.md — mock-vnode tests bypass
|
|
22
|
-
* the HOC + mount pipeline and CAN miss bugs that surface only when
|
|
23
|
-
* the real `h()` + mount path runs, exactly like PR #197's silent
|
|
24
|
-
* metadata drop. Always have both.
|
|
25
|
-
*/
|
|
26
|
-
import { h, type VNode } from '@pyreon/core'
|
|
27
|
-
import { mount } from '@pyreon/runtime-dom'
|
|
28
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
29
|
-
|
|
30
|
-
vi.mock('~/utils', () => ({
|
|
31
|
-
IS_DEVELOPMENT: false,
|
|
32
|
-
}))
|
|
33
|
-
|
|
34
|
-
import Wrapper from '../helpers/Wrapper/component'
|
|
35
|
-
import { Element } from '../Element'
|
|
36
|
-
|
|
37
|
-
const asVNode = (v: unknown) => v as VNode
|
|
38
|
-
|
|
39
|
-
describe('Wrapper — dangerouslySetInnerHTML forwarding (mock-vnode)', () => {
|
|
40
|
-
it('forwards dangerouslySetInnerHTML to the rendered Styled vnode (non-needsFix path)', () => {
|
|
41
|
-
const html = { __html: '<svg>x</svg>' }
|
|
42
|
-
const result = asVNode(
|
|
43
|
-
Wrapper({
|
|
44
|
-
tag: 'div',
|
|
45
|
-
dangerouslySetInnerHTML: html,
|
|
46
|
-
}),
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
// Bug-shape assertion: the prop must reach the rendered vnode.
|
|
50
|
-
// Pre-fix this is `undefined` → SVG is silently dropped.
|
|
51
|
-
expect(result.props.dangerouslySetInnerHTML).toBe(html)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('drops children when dangerouslySetInnerHTML is present (mutually exclusive)', () => {
|
|
55
|
-
const html = { __html: '<svg>x</svg>' }
|
|
56
|
-
const result = asVNode(
|
|
57
|
-
Wrapper({
|
|
58
|
-
tag: 'div',
|
|
59
|
-
dangerouslySetInnerHTML: html,
|
|
60
|
-
// children would conflict — innerHTML wins.
|
|
61
|
-
children: 'should be dropped',
|
|
62
|
-
}),
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
// children slot must not coexist with innerHTML — runtime-server's
|
|
66
|
-
// and runtime-dom's prop pipeline both treat them as inner-content
|
|
67
|
-
// sources, and emitting both would result in either a malformed
|
|
68
|
-
// tree or innerHTML being overwritten by the children mount.
|
|
69
|
-
expect(result.children).toEqual([])
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('forwards dangerouslySetInnerHTML on the needsFix path (button/fieldset/legend)', () => {
|
|
73
|
-
// button/fieldset/legend take the two-layer flex fix path. innerHTML
|
|
74
|
-
// belongs on the inner styled node (where the actual content goes),
|
|
75
|
-
// NOT on the outer wrapper.
|
|
76
|
-
const html = { __html: '<span>label</span>' }
|
|
77
|
-
const result = asVNode(
|
|
78
|
-
Wrapper({
|
|
79
|
-
tag: 'button',
|
|
80
|
-
dangerouslySetInnerHTML: html,
|
|
81
|
-
}),
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
// The `needsFix` branch should NOT trigger when innerHTML is set
|
|
85
|
-
// (innerHTML replaces all children, including the inner flex-fix
|
|
86
|
-
// layer). The simplest correct behavior: bypass needsFix when
|
|
87
|
-
// innerHTML is present and forward the prop on the single Styled.
|
|
88
|
-
expect(result.props.dangerouslySetInnerHTML).toBe(html)
|
|
89
|
-
})
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
// Real-h() mount tests — parallel coverage that runs the full pipeline.
|
|
93
|
-
// Element uses Wrapper internally; mounting Element with
|
|
94
|
-
// `dangerouslySetInnerHTML` exercises every layer the bug could surface
|
|
95
|
-
// at: Element's split → Wrapper → Styled → runtime-dom's prop application.
|
|
96
|
-
// happy-dom is the test environment; `dangerouslySetInnerHTML` translates
|
|
97
|
-
// to `el.innerHTML = ...` which happy-dom handles natively.
|
|
98
|
-
describe('Wrapper — dangerouslySetInnerHTML forwarding (real h() + mount)', () => {
|
|
99
|
-
it('Element with dangerouslySetInnerHTML actually injects HTML into the DOM (non-needsFix tag)', () => {
|
|
100
|
-
const root = document.createElement('div')
|
|
101
|
-
document.body.appendChild(root)
|
|
102
|
-
|
|
103
|
-
const unmount = mount(
|
|
104
|
-
h(Element, {
|
|
105
|
-
tag: 'div',
|
|
106
|
-
'data-testid': 'innerhtml-host',
|
|
107
|
-
dangerouslySetInnerHTML: { __html: '<svg data-marker="real-h-svg">x</svg>' },
|
|
108
|
-
}),
|
|
109
|
-
root,
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
// The structural assertion: SVG element exists in the rendered DOM.
|
|
113
|
-
// Pre-fix Wrapper dropped the prop → no SVG → null query result.
|
|
114
|
-
const svg = root.querySelector('[data-marker="real-h-svg"]')
|
|
115
|
-
expect(svg).not.toBeNull()
|
|
116
|
-
expect(svg?.tagName.toLowerCase()).toBe('svg')
|
|
117
|
-
|
|
118
|
-
unmount()
|
|
119
|
-
root.remove()
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('Element with dangerouslySetInnerHTML on a needsFix tag (button) still injects HTML', () => {
|
|
123
|
-
// button is a needsFix tag (two-layer flex fix). The Wrapper branch
|
|
124
|
-
// must bypass the two-layer fix when innerHTML is present, OR forward
|
|
125
|
-
// innerHTML to the right layer. Either way the rendered DOM must
|
|
126
|
-
// contain the user-supplied HTML.
|
|
127
|
-
const root = document.createElement('div')
|
|
128
|
-
document.body.appendChild(root)
|
|
129
|
-
|
|
130
|
-
const unmount = mount(
|
|
131
|
-
h(Element, {
|
|
132
|
-
tag: 'button',
|
|
133
|
-
'data-testid': 'innerhtml-button',
|
|
134
|
-
dangerouslySetInnerHTML: { __html: '<span data-marker="real-h-button-label">click me</span>' },
|
|
135
|
-
}),
|
|
136
|
-
root,
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
const span = root.querySelector('[data-marker="real-h-button-label"]')
|
|
140
|
-
expect(span).not.toBeNull()
|
|
141
|
-
expect(span?.textContent).toBe('click me')
|
|
142
|
-
|
|
143
|
-
unmount()
|
|
144
|
-
root.remove()
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('children passed alongside dangerouslySetInnerHTML are dropped (innerHTML wins)', () => {
|
|
148
|
-
// Bug shape: if Wrapper's `own.children` leaks into the rendered vnode
|
|
149
|
-
// alongside `dangerouslySetInnerHTML`, runtime-dom would either mount
|
|
150
|
-
// children INTO the innerHTML-populated element (overwriting), or land
|
|
151
|
-
// both side-by-side. The contract is that innerHTML wins and children
|
|
152
|
-
// are dropped. Verifying at the DOM level catches both failure shapes.
|
|
153
|
-
const root = document.createElement('div')
|
|
154
|
-
document.body.appendChild(root)
|
|
155
|
-
|
|
156
|
-
const unmount = mount(
|
|
157
|
-
h(
|
|
158
|
-
Element,
|
|
159
|
-
{
|
|
160
|
-
tag: 'div',
|
|
161
|
-
'data-testid': 'innerhtml-with-children',
|
|
162
|
-
dangerouslySetInnerHTML: { __html: '<i data-marker="real-h-winner">html wins</i>' },
|
|
163
|
-
},
|
|
164
|
-
'this child text should NOT appear',
|
|
165
|
-
),
|
|
166
|
-
root,
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
const host = root.querySelector('[data-testid="innerhtml-with-children"]')!
|
|
170
|
-
expect(host.querySelector('[data-marker="real-h-winner"]')).not.toBeNull()
|
|
171
|
-
// The child string must not appear anywhere in the rendered host.
|
|
172
|
-
expect(host.textContent).not.toContain('this child text should NOT appear')
|
|
173
|
-
expect(host.textContent).toContain('html wins')
|
|
174
|
-
|
|
175
|
-
unmount()
|
|
176
|
-
root.remove()
|
|
177
|
-
})
|
|
178
|
-
})
|