@pyreon/compiler 0.24.5 → 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 +11 -13
- package/src/defer-inline.ts +0 -686
- package/src/event-names.ts +0 -65
- package/src/index.ts +0 -61
- package/src/island-audit.ts +0 -675
- package/src/jsx.ts +0 -2792
- package/src/load-native.ts +0 -156
- package/src/lpih.ts +0 -270
- package/src/manifest.ts +0 -280
- package/src/project-scanner.ts +0 -214
- package/src/pyreon-intercept.ts +0 -1029
- package/src/react-intercept.ts +0 -1217
- package/src/reactivity-lens.ts +0 -190
- package/src/ssg-audit.ts +0 -513
- package/src/test-audit.ts +0 -435
- package/src/tests/backend-parity-r7-r9.test.ts +0 -91
- package/src/tests/backend-prop-derived-callback-divergence.test.ts +0 -74
- package/src/tests/collapse-bail-census.test.ts +0 -330
- package/src/tests/collapse-key-source-hygiene.test.ts +0 -88
- package/src/tests/component-child-no-wrap.test.ts +0 -204
- package/src/tests/defer-inline.test.ts +0 -387
- package/src/tests/depth-stress.test.ts +0 -16
- package/src/tests/detector-tag-consistency.test.ts +0 -101
- package/src/tests/dynamic-collapse-detector.test.ts +0 -164
- package/src/tests/dynamic-collapse-emit.test.ts +0 -192
- package/src/tests/dynamic-collapse-scan.test.ts +0 -111
- package/src/tests/element-valued-const-child.test.ts +0 -61
- package/src/tests/falsy-child-characterization.test.ts +0 -48
- package/src/tests/island-audit.test.ts +0 -524
- package/src/tests/jsx.test.ts +0 -2908
- package/src/tests/load-native.test.ts +0 -53
- package/src/tests/lpih.test.ts +0 -404
- package/src/tests/malformed-input-resilience.test.ts +0 -50
- package/src/tests/manifest-snapshot.test.ts +0 -55
- package/src/tests/native-equivalence.test.ts +0 -924
- package/src/tests/partial-collapse-detector.test.ts +0 -121
- package/src/tests/partial-collapse-emit.test.ts +0 -104
- package/src/tests/partial-collapse-robustness.test.ts +0 -53
- package/src/tests/project-scanner.test.ts +0 -269
- package/src/tests/prop-derived-shadow.test.ts +0 -96
- package/src/tests/pure-call-reactive-args.test.ts +0 -50
- package/src/tests/pyreon-intercept.test.ts +0 -816
- package/src/tests/r13-callback-stmt-equivalence.test.ts +0 -58
- package/src/tests/r14-ssr-mode-parity.test.ts +0 -51
- package/src/tests/r15-elemconst-propderived.test.ts +0 -47
- package/src/tests/r19-defer-inline-robust.test.ts +0 -54
- package/src/tests/r20-backend-equivalence-sweep.test.ts +0 -50
- package/src/tests/react-intercept.test.ts +0 -1104
- package/src/tests/reactivity-lens.test.ts +0 -170
- package/src/tests/rocketstyle-collapse.test.ts +0 -208
- package/src/tests/runtime/control-flow.test.ts +0 -159
- package/src/tests/runtime/dom-properties.test.ts +0 -138
- package/src/tests/runtime/events.test.ts +0 -301
- package/src/tests/runtime/harness.ts +0 -94
- package/src/tests/runtime/pr-352-shapes.test.ts +0 -121
- package/src/tests/runtime/reactive-props.test.ts +0 -81
- package/src/tests/runtime/signals.test.ts +0 -129
- package/src/tests/runtime/whitespace.test.ts +0 -106
- package/src/tests/signal-autocall-shadow.test.ts +0 -86
- package/src/tests/sourcemap-fidelity.test.ts +0 -77
- package/src/tests/ssg-audit.test.ts +0 -402
- package/src/tests/static-text-baking.test.ts +0 -64
- package/src/tests/test-audit.test.ts +0 -549
- package/src/tests/transform-state-isolation.test.ts +0 -49
package/src/tests/jsx.test.ts
DELETED
|
@@ -1,2908 +0,0 @@
|
|
|
1
|
-
import { transformJSX } from '../jsx'
|
|
2
|
-
|
|
3
|
-
// Helper: transform and return the code string
|
|
4
|
-
const t = (code: string) => transformJSX(code, 'input.tsx').code
|
|
5
|
-
|
|
6
|
-
// ─── Children ────────────────────────────────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
describe('JSX transform — children', () => {
|
|
9
|
-
test('wraps dynamic child expression', () => {
|
|
10
|
-
const result = t('<div>{count()}</div>')
|
|
11
|
-
expect(result).toContain('_tpl(')
|
|
12
|
-
// Single-signal text binding uses _bindText for direct subscription
|
|
13
|
-
expect(result).toContain('_bindText(count,')
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
test('does NOT wrap string literal child', () => {
|
|
17
|
-
expect(t(`<div>{"static"}</div>`)).not.toContain('() =>')
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
test('does NOT wrap numeric literal child', () => {
|
|
21
|
-
expect(t('<div>{42}</div>')).not.toContain('() =>')
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
test('does NOT wrap null child', () => {
|
|
25
|
-
expect(t('<div>{null}</div>')).not.toContain('() =>')
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
test('does NOT double-wrap existing arrow function', () => {
|
|
29
|
-
const result = t('<div>{() => count()}</div>')
|
|
30
|
-
// Arrow should be unwrapped by template emission into _bindText(count, __t)
|
|
31
|
-
// The original () => count() should NOT appear in the output
|
|
32
|
-
expect(result).toContain('_bindText(count,')
|
|
33
|
-
expect(result).not.toContain('() => count()')
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test('does NOT wrap a function expression child', () => {
|
|
37
|
-
const result = t('<div>{function() { return x }}</div>')
|
|
38
|
-
// Function expression body should be unwrapped by template emission
|
|
39
|
-
expect(result).toContain('_bind')
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('does NOT wrap plain identifier (no call = not reactive)', () => {
|
|
43
|
-
expect(t('<div>{title}</div>')).not.toContain('() =>')
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test('does NOT wrap ternary without calls', () => {
|
|
47
|
-
expect(t('<div>{a ? b : c}</div>')).not.toContain('() =>')
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
test('wraps ternary that contains a call', () => {
|
|
51
|
-
const result = t('<div>{a() ? b : c}</div>')
|
|
52
|
-
expect(result).toContain('_tpl(')
|
|
53
|
-
expect(result).toContain('.data = a() ? b : c')
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
test('does NOT wrap logical expression without calls', () => {
|
|
57
|
-
expect(t('<div>{show && <span />}</div>')).not.toContain('() =>')
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
test('wraps logical expression containing a call', () => {
|
|
61
|
-
expect(t('<div>{show() && <span />}</div>')).toContain('() => show() && <span />')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
test('does NOT wrap object literal child', () => {
|
|
65
|
-
expect(t("<div>{{ color: 'red' }}</div>")).not.toContain('() =>')
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
test('does NOT wrap array literal child', () => {
|
|
69
|
-
expect(t('<div>{[1, 2, 3]}</div>')).not.toContain('() =>')
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
test('does NOT wrap boolean true literal', () => {
|
|
73
|
-
expect(t('<div>{true}</div>')).not.toContain('() =>')
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
test('does NOT wrap boolean false literal', () => {
|
|
77
|
-
expect(t('<div>{false}</div>')).not.toContain('() =>')
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
test('does NOT wrap undefined literal', () => {
|
|
81
|
-
expect(t('<div>{undefined}</div>')).not.toContain('() =>')
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
test('does NOT wrap template literal without calls (no substitution)', () => {
|
|
85
|
-
expect(t('<div>{`hello`}</div>')).not.toContain('() =>')
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
test('wraps template literal containing a call', () => {
|
|
89
|
-
expect(t('<div>{`hello ${name()}`}</div>')).toContain('() =>')
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
test('wraps member access with call', () => {
|
|
93
|
-
const result = t('<div>{obj.getValue()}</div>')
|
|
94
|
-
expect(result).toContain('_tpl(')
|
|
95
|
-
// Property access calls use _bind (not _bindText) to preserve this context
|
|
96
|
-
expect(result).toContain('_bind')
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
test('does NOT wrap member access without call', () => {
|
|
100
|
-
expect(t('<div>{obj.value}</div>')).not.toContain('() =>')
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
test('wraps binary expression containing a call', () => {
|
|
104
|
-
const result = t('<div>{count() + 1}</div>')
|
|
105
|
-
expect(result).toContain('_tpl(')
|
|
106
|
-
expect(result).toContain('.data = count() + 1')
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
test('does NOT wrap binary expression without calls', () => {
|
|
110
|
-
expect(t('<div>{a + b}</div>')).not.toContain('() =>')
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
test('wraps tagged template expression', () => {
|
|
114
|
-
expect(t('<div>{css`color: red`}</div>')).toContain('() =>')
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
test('empty JSX expression {} gets _tpl optimization', () => {
|
|
118
|
-
const result = t('<div>{/* comment */}</div>')
|
|
119
|
-
expect(result).toContain('_tpl(')
|
|
120
|
-
expect(result).toContain('() => null')
|
|
121
|
-
})
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
// ─── Props ────────────────────────────────────────────────────────────────────
|
|
125
|
-
|
|
126
|
-
describe('JSX transform — props', () => {
|
|
127
|
-
test('wraps dynamic class prop', () => {
|
|
128
|
-
expect(t('<div class={activeClass()} />')).toContain('() => activeClass()')
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
test('wraps dynamic style prop', () => {
|
|
132
|
-
expect(t('<div style={styles()} />')).toContain('() => styles()')
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
test('does NOT wrap string literal prop', () => {
|
|
136
|
-
expect(t(`<div class="foo" />`)).not.toContain('() =>')
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
test('does NOT wrap JSX string attribute', () => {
|
|
140
|
-
expect(t(`<div class={"foo"} />`)).not.toContain('() =>')
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
test('does NOT wrap onClick (event handler)', () => {
|
|
144
|
-
const result = t('<button onClick={handleClick} />')
|
|
145
|
-
expect(result).not.toContain('() => handleClick')
|
|
146
|
-
expect(result).toContain('handleClick') // still present
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
test('does NOT wrap onInput (event handler)', () => {
|
|
150
|
-
expect(t('<input onInput={handler} />')).not.toContain('() => handler')
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
test('does NOT wrap onMouseEnter (event handler)', () => {
|
|
154
|
-
expect(t('<div onMouseEnter={fn} />')).not.toContain('() => fn')
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
test('does NOT wrap key prop', () => {
|
|
158
|
-
expect(t('<div key={id} />')).not.toContain('() => id')
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
test('does NOT wrap ref prop', () => {
|
|
162
|
-
expect(t('<div ref={myRef} />')).not.toContain('() => myRef')
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
test('does NOT wrap already-wrapped prop', () => {
|
|
166
|
-
const result = t('<div class={() => cls()} />')
|
|
167
|
-
expect(result.match(/\(\) =>/g)?.length).toBe(1)
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
test('does NOT wrap object literal prop (style)', () => {
|
|
171
|
-
expect(t('<div style={{ color: "red" }} />')).not.toContain('() =>')
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
test('wraps object literal prop when it contains a call', () => {
|
|
175
|
-
expect(t('<div style={{ color: theme() }} />')).toContain('() =>')
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
test('does NOT wrap boolean shorthand attribute', () => {
|
|
179
|
-
// <input disabled /> — no initializer at all
|
|
180
|
-
expect(t('<input disabled />')).not.toContain('() =>')
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
test('wraps dynamic data-* attribute', () => {
|
|
184
|
-
expect(t('<div data-id={getId()} />')).toContain('() => getId()')
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
test('wraps dynamic aria-* attribute', () => {
|
|
188
|
-
expect(t('<div aria-label={getLabel()} />')).toContain('() => getLabel()')
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
test('does NOT wrap onFocus (event handler)', () => {
|
|
192
|
-
expect(t('<input onFocus={handler} />')).not.toContain('() => handler')
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
test('does NOT wrap onChange (event handler)', () => {
|
|
196
|
-
expect(t('<input onChange={handler} />')).not.toContain('() => handler')
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
test('wraps conditional prop expression with call', () => {
|
|
200
|
-
expect(t("<div title={isActive() ? 'yes' : 'no'} />")).toContain('() =>')
|
|
201
|
-
})
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
// ─── Component elements ──────────────────────────────────────────────────────
|
|
205
|
-
|
|
206
|
-
describe('JSX transform — component elements', () => {
|
|
207
|
-
test('wraps reactive props on component elements with _rp brand', () => {
|
|
208
|
-
const result = t('<MyComponent value={count()} />')
|
|
209
|
-
expect(result).toContain('_rp(() => count())')
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
test('wraps reactive props on any uppercase component with _rp brand', () => {
|
|
213
|
-
const result = t('<Button label={getText()} />')
|
|
214
|
-
expect(result).toContain('_rp(() => getText())')
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
test('emits _rp import when component has reactive props', () => {
|
|
218
|
-
const result = t('<Button label={getText()} />')
|
|
219
|
-
expect(result).toContain('import { _rp } from "@pyreon/core"')
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
test('does NOT wrap static props on component elements', () => {
|
|
223
|
-
const result = t('<Button size={12} />')
|
|
224
|
-
expect(result).not.toContain('() =>')
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
test('does NOT wrap event handlers on component elements', () => {
|
|
228
|
-
const result = t('<Button onClick={handleClick} />')
|
|
229
|
-
expect(result).not.toContain('() => handleClick')
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
test('does NOT wrap arrow function props on component elements', () => {
|
|
233
|
-
const result = t('<Button render={() => "hello"} />')
|
|
234
|
-
expect(result).not.toContain('() => () =>')
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
test('does NOT wrap single JSX element prop — recurses into inner props', () => {
|
|
238
|
-
const result = t('<Wrapper icon={<Icon name={getName()} />} />')
|
|
239
|
-
// The outer <Icon> should NOT be wrapped in _rp
|
|
240
|
-
expect(result).not.toContain('_rp(() => <Icon')
|
|
241
|
-
// But Icon's name prop should be wrapped (it contains a call)
|
|
242
|
-
expect(result).toContain('() => getName()')
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
test('DOES wrap conditional JSX element prop', () => {
|
|
246
|
-
const result = t('<Wrapper icon={show() ? <Icon /> : null} />')
|
|
247
|
-
// Conditional contains a call — wraps the whole expression
|
|
248
|
-
expect(result).toContain('_rp(')
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
test('wraps children of component elements (via JSX expression)', () => {
|
|
252
|
-
// Children in expression containers are still wrapped
|
|
253
|
-
const result = t('<MyComponent>{count()}</MyComponent>')
|
|
254
|
-
expect(result).toContain('() => count()')
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
test('wraps props on lowercase DOM elements', () => {
|
|
258
|
-
expect(t('<div title={getTitle()} />')).toContain('() => getTitle()')
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
test('wraps ternary with call in component prop', () => {
|
|
262
|
-
const result = t(`<Comp x={a() ? 'yes' : 'no'} />`)
|
|
263
|
-
expect(result).toContain('_rp(')
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
test('wraps template literal with call in component prop', () => {
|
|
267
|
-
const result = t('<Comp label={`${count()} items`} />')
|
|
268
|
-
expect(result).toContain('_rp(')
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
test('wraps multiple reactive props independently', () => {
|
|
272
|
-
const result = t('<Comp a={x()} b={y()} c={12} />')
|
|
273
|
-
// Two reactive props should produce two _rp wrappers
|
|
274
|
-
const rpCount = (result.match(/_rp\(/g) || []).length
|
|
275
|
-
expect(rpCount).toBe(2)
|
|
276
|
-
// Static prop should remain plain (JSX attribute syntax)
|
|
277
|
-
expect(result).toContain('c={12}')
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
test('wraps children prop with call (children not in SKIP_PROPS)', () => {
|
|
281
|
-
const result = t('<Comp children={items()} />')
|
|
282
|
-
// children is NOT in SKIP_PROPS, so it gets _rp wrapping
|
|
283
|
-
expect(result).toContain('_rp(')
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
test('spread props on component are wrapped with _wrapSpread to preserve reactivity', () => {
|
|
287
|
-
const result = t('<Comp {...getProps()} label="hi" />')
|
|
288
|
-
// Spread argument is wrapped so getter-shaped reactive props survive
|
|
289
|
-
// esbuild's JS-level object spread in the automatic JSX runtime.
|
|
290
|
-
expect(result).toContain('{..._wrapSpread(getProps())}')
|
|
291
|
-
// Static label should not be wrapped
|
|
292
|
-
expect(result).not.toContain('_rp(() => "hi")')
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
test('spread props on DOM elements are NOT wrapped (handled by template path)', () => {
|
|
296
|
-
const result = t('<div {...rest} class="x" />')
|
|
297
|
-
// DOM-element spreads go through the template path's _applyProps.
|
|
298
|
-
expect(result).toContain('{...rest}')
|
|
299
|
-
expect(result).not.toContain('_wrapSpread')
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
test('multiple spread sources on a component each get wrapped independently', () => {
|
|
303
|
-
const result = t('<Comp {...a} {...b} foo="x" />')
|
|
304
|
-
expect(result).toContain('{..._wrapSpread(a)}')
|
|
305
|
-
expect(result).toContain('{..._wrapSpread(b)}')
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
test('_wrapSpread emission is idempotent on re-compilation', () => {
|
|
309
|
-
const result = t('<Comp {..._wrapSpread(rest)} />')
|
|
310
|
-
// Should not double-wrap.
|
|
311
|
-
expect(result).not.toContain('_wrapSpread(_wrapSpread(')
|
|
312
|
-
})
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
// ─── Spread attributes ──────────────────────────────────────────────────────
|
|
316
|
-
|
|
317
|
-
describe('JSX transform — spread attributes', () => {
|
|
318
|
-
test('spread props are left unchanged (not wrapped)', () => {
|
|
319
|
-
const result = t('<div {...props} />')
|
|
320
|
-
// Spread should remain as-is, no reactive wrapping
|
|
321
|
-
expect(result).toContain('{...props}')
|
|
322
|
-
expect(result).not.toContain('() => ...props')
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
test('spread with other props — only non-spread dynamic props get wrapped', () => {
|
|
326
|
-
const result = t('<div {...props} class={cls()} />')
|
|
327
|
-
expect(result).toContain('{...props}')
|
|
328
|
-
expect(result).toContain('() => cls()')
|
|
329
|
-
})
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
// ─── Static hoisting ─────────────────────────────────────────────────────────
|
|
333
|
-
|
|
334
|
-
describe('JSX transform — static hoisting', () => {
|
|
335
|
-
test('hoists static JSX child to module scope', () => {
|
|
336
|
-
const result = t('<div>{<span>Hello</span>}</div>')
|
|
337
|
-
expect(result).toContain('const _$h0')
|
|
338
|
-
expect(result).toContain('<span>Hello</span>')
|
|
339
|
-
expect(result).toContain('{_$h0}')
|
|
340
|
-
})
|
|
341
|
-
|
|
342
|
-
test('hoists static self-closing JSX', () => {
|
|
343
|
-
const result = t('<div>{<br />}</div>')
|
|
344
|
-
expect(result).toContain('const _$h0')
|
|
345
|
-
expect(result).toContain('{_$h0}')
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
test('does NOT hoist JSX with dynamic props', () => {
|
|
349
|
-
const result = t('<div>{<span class={cls()}>text</span>}</div>')
|
|
350
|
-
expect(result).not.toContain('const _$h0')
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
test('hoists JSX with static string prop', () => {
|
|
354
|
-
const result = t(`<div>{<span class="foo">text</span>}</div>`)
|
|
355
|
-
expect(result).toContain('const _$h0')
|
|
356
|
-
})
|
|
357
|
-
|
|
358
|
-
test('hoists multiple static JSX children independently', () => {
|
|
359
|
-
const result = t('<div>{<span>A</span>}{<span>B</span>}</div>')
|
|
360
|
-
expect(result).toContain('const _$h0')
|
|
361
|
-
expect(result).toContain('const _$h1')
|
|
362
|
-
})
|
|
363
|
-
|
|
364
|
-
test('hoists static fragment', () => {
|
|
365
|
-
const result = t('<div>{<>text</>}</div>')
|
|
366
|
-
expect(result).toContain('const _$h0')
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
test('does NOT hoist fragment with dynamic child', () => {
|
|
370
|
-
const result = t('<div>{<>{count()}</>}</div>')
|
|
371
|
-
expect(result).not.toContain('const _$h0')
|
|
372
|
-
})
|
|
373
|
-
|
|
374
|
-
test('hoisted declarations include @__PURE__ annotation', () => {
|
|
375
|
-
const result = t('<div>{<span>Hello</span>}</div>')
|
|
376
|
-
expect(result).toContain('/*@__PURE__*/')
|
|
377
|
-
})
|
|
378
|
-
|
|
379
|
-
test('does NOT hoist JSX with spread attributes (always dynamic)', () => {
|
|
380
|
-
const result = t('<div>{<span {...props}>text</span>}</div>')
|
|
381
|
-
expect(result).not.toContain('const _$h0')
|
|
382
|
-
})
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
// ─── Mixed ────────────────────────────────────────────────────────────────────
|
|
386
|
-
|
|
387
|
-
describe('JSX transform — mixed', () => {
|
|
388
|
-
test('wraps props and children independently', () => {
|
|
389
|
-
const result = t('<div class={cls()}>{text()}</div>')
|
|
390
|
-
expect(result).toContain('_tpl(')
|
|
391
|
-
// className uses _bindDirect (single-signal), text uses _bindText
|
|
392
|
-
expect(result).toContain('_bindDirect(cls,')
|
|
393
|
-
expect(result).toContain('_bindText(text,')
|
|
394
|
-
})
|
|
395
|
-
|
|
396
|
-
test('preserves static siblings of dynamic children', () => {
|
|
397
|
-
const result = t('<div>static{count()}</div>')
|
|
398
|
-
expect(result).toContain('_tpl(')
|
|
399
|
-
expect(result).toContain('static')
|
|
400
|
-
expect(result).toContain('_bindText(count,')
|
|
401
|
-
})
|
|
402
|
-
|
|
403
|
-
test('leaves code outside JSX completely unchanged', () => {
|
|
404
|
-
const input = 'const x = count() + 1'
|
|
405
|
-
expect(t(input)).toBe(input)
|
|
406
|
-
})
|
|
407
|
-
|
|
408
|
-
test('handles multiple JSX elements in one file', () => {
|
|
409
|
-
const input = `
|
|
410
|
-
const A = <div>{a()}</div>
|
|
411
|
-
const B = <span>{b()}</span>
|
|
412
|
-
`
|
|
413
|
-
const result = t(input)
|
|
414
|
-
expect(result).toContain('_tpl(')
|
|
415
|
-
expect(result).toContain('_bindText(a,')
|
|
416
|
-
expect(result).toContain('_bindText(b,')
|
|
417
|
-
})
|
|
418
|
-
|
|
419
|
-
test('handles deeply nested JSX', () => {
|
|
420
|
-
const result = t('<div><span><em>{count()}</em></span></div>')
|
|
421
|
-
// Template emission: 3 DOM elements → _tpl() call with _bindText binding
|
|
422
|
-
expect(result).toContain('_tpl(')
|
|
423
|
-
expect(result).toContain('_bindText(count,')
|
|
424
|
-
})
|
|
425
|
-
|
|
426
|
-
test('returns unchanged code when no JSX present', () => {
|
|
427
|
-
const input = 'const x = 1 + 2'
|
|
428
|
-
expect(t(input)).toBe(input)
|
|
429
|
-
})
|
|
430
|
-
|
|
431
|
-
test('handles empty JSX element', () => {
|
|
432
|
-
const result = t('<div></div>')
|
|
433
|
-
expect(result).toContain('_tpl(')
|
|
434
|
-
expect(result).toContain('<div></div>')
|
|
435
|
-
expect(result).toContain('() => null')
|
|
436
|
-
})
|
|
437
|
-
|
|
438
|
-
test('handles self-closing element with no props', () => {
|
|
439
|
-
const result = t('<br />')
|
|
440
|
-
expect(result).toBe('<br />')
|
|
441
|
-
})
|
|
442
|
-
})
|
|
443
|
-
|
|
444
|
-
// ─── Edge cases ──────────────────────────────────────────────────────────────
|
|
445
|
-
|
|
446
|
-
describe('JSX transform — edge cases', () => {
|
|
447
|
-
test('wraps chained method call', () => {
|
|
448
|
-
expect(t('<div>{items().map(x => x)}</div>')).toContain('() =>')
|
|
449
|
-
})
|
|
450
|
-
|
|
451
|
-
test('does not emit _bindText for method calls (preserves this context)', () => {
|
|
452
|
-
// value.toLocaleString() — property access must NOT use _bindText
|
|
453
|
-
// because detaching the method loses `this` context
|
|
454
|
-
const result = t('<div><p>{value.toLocaleString()}</p></div>')
|
|
455
|
-
expect(result).not.toContain('_bindText(value.toLocaleString,')
|
|
456
|
-
expect(result).toContain('_bind')
|
|
457
|
-
})
|
|
458
|
-
|
|
459
|
-
test('toLocaleString on signal read preserves this context', () => {
|
|
460
|
-
// {() => count().toLocaleString()} should NOT detach .toLocaleString
|
|
461
|
-
const result = t('<div>{() => count().toLocaleString()}</div>')
|
|
462
|
-
expect(result).not.toContain('_bindText(count,')
|
|
463
|
-
// The arrow wraps a chained call — it should use _bind, not _bindText
|
|
464
|
-
expect(result).toContain('_bind')
|
|
465
|
-
})
|
|
466
|
-
|
|
467
|
-
test('wraps nested call in array expression', () => {
|
|
468
|
-
expect(t('<div>{[getItem()]}</div>')).toContain('() =>')
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
test('handles JSX with only text children (no expression)', () => {
|
|
472
|
-
const result = t('<div>hello world</div>')
|
|
473
|
-
expect(result).toContain('_tpl(')
|
|
474
|
-
expect(result).toContain('hello world')
|
|
475
|
-
expect(result).toContain('() => null')
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
test('does NOT wrap arrow function with params', () => {
|
|
479
|
-
const result = t('<div>{(x: number) => x + 1}</div>')
|
|
480
|
-
expect(result).not.toContain('() => (x')
|
|
481
|
-
})
|
|
482
|
-
|
|
483
|
-
test('handles .jsx file extension', () => {
|
|
484
|
-
const result = transformJSX('<div>{count()}</div>', 'file.jsx').code
|
|
485
|
-
expect(result).toContain('_tpl(')
|
|
486
|
-
expect(result).toContain('_bindText(count,')
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
test('handles .ts file extension (treated as TSX)', () => {
|
|
490
|
-
const result = transformJSX('<div>{count()}</div>', 'file.ts').code
|
|
491
|
-
expect(result).toContain('_tpl(')
|
|
492
|
-
expect(result).toContain('_bindText(count,')
|
|
493
|
-
})
|
|
494
|
-
|
|
495
|
-
test('wraps call inside array map', () => {
|
|
496
|
-
expect(t('<ul>{items().map(i => <li>{i}</li>)}</ul>')).toContain('() =>')
|
|
497
|
-
})
|
|
498
|
-
|
|
499
|
-
test('does NOT wrap callback function expression inside event prop', () => {
|
|
500
|
-
const result = t('<button onClick={() => doSomething()} />')
|
|
501
|
-
// onClick is an event handler, should not be wrapped at all
|
|
502
|
-
expect(result).not.toContain('() => () =>')
|
|
503
|
-
})
|
|
504
|
-
|
|
505
|
-
test('wraps call deep in property access chain', () => {
|
|
506
|
-
expect(t('<div>{store.getState().count}</div>')).toContain('() =>')
|
|
507
|
-
})
|
|
508
|
-
|
|
509
|
-
test('does NOT wrap function expression child (named)', () => {
|
|
510
|
-
const result = t('<div>{function foo() { return 1 }}</div>')
|
|
511
|
-
expect(result).not.toContain('() => function')
|
|
512
|
-
})
|
|
513
|
-
})
|
|
514
|
-
|
|
515
|
-
// ─── TransformResult type ────────────────────────────────────────────────────
|
|
516
|
-
|
|
517
|
-
describe('transformJSX return value', () => {
|
|
518
|
-
test('returns object with code property', () => {
|
|
519
|
-
const result = transformJSX('<div>{count()}</div>')
|
|
520
|
-
expect(typeof result.code).toBe('string')
|
|
521
|
-
})
|
|
522
|
-
|
|
523
|
-
test('default filename is input.tsx', () => {
|
|
524
|
-
// Should not throw with default filename
|
|
525
|
-
const result = transformJSX('<div>{count()}</div>')
|
|
526
|
-
expect(result.code).toContain('_tpl(')
|
|
527
|
-
expect(result.code).toContain('_bindText(count,')
|
|
528
|
-
})
|
|
529
|
-
})
|
|
530
|
-
|
|
531
|
-
// ─── Template emission ──────────────────────────────────────────────────────
|
|
532
|
-
|
|
533
|
-
describe('JSX transform — template emission', () => {
|
|
534
|
-
test('emits _tpl for 2+ element tree', () => {
|
|
535
|
-
const result = t('<div><span>hello</span></div>')
|
|
536
|
-
expect(result).toContain('_tpl(')
|
|
537
|
-
expect(result).toContain('<div><span>hello</span></div>')
|
|
538
|
-
})
|
|
539
|
-
|
|
540
|
-
test('emits _tpl for single element', () => {
|
|
541
|
-
const result = t('<div>hello</div>')
|
|
542
|
-
expect(result).toContain('_tpl(')
|
|
543
|
-
expect(result).toContain('hello')
|
|
544
|
-
})
|
|
545
|
-
|
|
546
|
-
test('does NOT emit _tpl for component elements', () => {
|
|
547
|
-
const result = t('<div><MyComponent /></div>')
|
|
548
|
-
expect(result).not.toContain('_tpl(')
|
|
549
|
-
})
|
|
550
|
-
|
|
551
|
-
test('emits _tpl for root spread with _applyProps in bind', () => {
|
|
552
|
-
const result = t('<div {...props}><span /></div>')
|
|
553
|
-
expect(result).toContain('_tpl(')
|
|
554
|
-
expect(result).toContain('_applyProps(__root, props)')
|
|
555
|
-
})
|
|
556
|
-
|
|
557
|
-
test('does NOT emit _tpl for spread on inner elements', () => {
|
|
558
|
-
const result = t('<div><span {...innerProps} /></div>')
|
|
559
|
-
expect(result).not.toContain('_tpl(')
|
|
560
|
-
})
|
|
561
|
-
|
|
562
|
-
test('does NOT emit _tpl for keyed elements', () => {
|
|
563
|
-
const result = t('<div key={id}><span /></div>')
|
|
564
|
-
expect(result).not.toContain('_tpl(')
|
|
565
|
-
})
|
|
566
|
-
|
|
567
|
-
test('bakes static string attributes into HTML', () => {
|
|
568
|
-
const result = t('<div class="box"><span /></div>')
|
|
569
|
-
// Quotes are escaped inside the _tpl("...") string literal
|
|
570
|
-
expect(result).toContain('class=\\"box\\"')
|
|
571
|
-
expect(result).toContain('_tpl(')
|
|
572
|
-
})
|
|
573
|
-
|
|
574
|
-
test('bakes boolean shorthand attributes into HTML', () => {
|
|
575
|
-
const result = t('<div><input disabled /></div>')
|
|
576
|
-
expect(result).toContain(' disabled')
|
|
577
|
-
expect(result).toContain('_tpl(')
|
|
578
|
-
})
|
|
579
|
-
|
|
580
|
-
test('generates _bindDirect for reactive class with single signal', () => {
|
|
581
|
-
const result = t('<div class={cls()}><span /></div>')
|
|
582
|
-
expect(result).toContain('_bindDirect(cls,')
|
|
583
|
-
expect(result).toContain('className')
|
|
584
|
-
})
|
|
585
|
-
|
|
586
|
-
test('generates _bindText for reactive text child with single signal', () => {
|
|
587
|
-
const result = t('<div><span>{name()}</span></div>')
|
|
588
|
-
expect(result).toContain('_bindText(name,')
|
|
589
|
-
})
|
|
590
|
-
|
|
591
|
-
test('generates one-time set for static expression text', () => {
|
|
592
|
-
const result = t('<div><span>{label}</span></div>')
|
|
593
|
-
expect(result).toContain('textContent = label')
|
|
594
|
-
expect(result).not.toContain('_bind(')
|
|
595
|
-
})
|
|
596
|
-
|
|
597
|
-
test('generates delegated event for common events', () => {
|
|
598
|
-
const result = t('<div><button onClick={handler}>click</button></div>')
|
|
599
|
-
// click is delegated — uses expando property instead of addEventListener
|
|
600
|
-
expect(result).toContain('__ev_click = handler')
|
|
601
|
-
})
|
|
602
|
-
|
|
603
|
-
// Regression: multi-word event-name casing was broken — `onKeyDown`
|
|
604
|
-
// produced `addEventListener("keyDown", ...)` (camelCase) instead of
|
|
605
|
-
// `addEventListener("keydown", ...)` (DOM convention). The handler
|
|
606
|
-
// never fired because `keyDown` is not a real DOM event name.
|
|
607
|
-
// Same bug class affected `onMouseEnter`, `onMouseLeave`, etc.
|
|
608
|
-
test('lowercases multi-word event names (onKeyDown → keydown — delegated)', () => {
|
|
609
|
-
const result = t('<div><input onKeyDown={handler} /></div>')
|
|
610
|
-
// keydown IS in DELEGATED_EVENTS — must use the expando, not addEventListener.
|
|
611
|
-
// Prior behavior: addEventListener("keyDown", ...) — wrong casing AND
|
|
612
|
-
// wrong path (delegated check missed because case mismatched).
|
|
613
|
-
expect(result).toContain('__ev_keydown = handler')
|
|
614
|
-
expect(result).not.toContain('__ev_keyDown')
|
|
615
|
-
expect(result).not.toContain('"keyDown"')
|
|
616
|
-
})
|
|
617
|
-
|
|
618
|
-
test('lowercases multi-word event names (onMouseEnter → mouseenter — non-delegated)', () => {
|
|
619
|
-
const result = t('<div><span onMouseEnter={handler}>hi</span></div>')
|
|
620
|
-
// mouseenter is NOT delegated — must reach addEventListener with lowercase name
|
|
621
|
-
expect(result).toContain('addEventListener("mouseenter", handler)')
|
|
622
|
-
expect(result).not.toContain('"mouseEnter"')
|
|
623
|
-
})
|
|
624
|
-
|
|
625
|
-
test('lowercases multi-word event names for input change (onChange → change — delegated)', () => {
|
|
626
|
-
const result = t('<div><input onChange={handler} /></div>')
|
|
627
|
-
expect(result).toContain('__ev_change = handler')
|
|
628
|
-
})
|
|
629
|
-
|
|
630
|
-
test('lowercases multi-word event names with multiple capitals (onPointerLeave → pointerleave)', () => {
|
|
631
|
-
const result = t('<div><span onPointerLeave={handler}>hi</span></div>')
|
|
632
|
-
expect(result).toContain('addEventListener("pointerleave", handler)')
|
|
633
|
-
expect(result).not.toContain('"pointerLeave"')
|
|
634
|
-
})
|
|
635
|
-
|
|
636
|
-
test('uses element children indexing for nested access', () => {
|
|
637
|
-
const result = t('<div><span>{a()}</span><em>{b()}</em></div>')
|
|
638
|
-
// Can't have two expression children in same parent, but each is in its own element
|
|
639
|
-
expect(result).toContain('__root.children[0]')
|
|
640
|
-
expect(result).toContain('__root.children[1]')
|
|
641
|
-
})
|
|
642
|
-
|
|
643
|
-
test('handles deeply nested element paths', () => {
|
|
644
|
-
const result = t('<table><tbody><tr><td>{text()}</td></tr></tbody></table>')
|
|
645
|
-
expect(result).toContain('_tpl(')
|
|
646
|
-
expect(result).toContain('_bindText(text,')
|
|
647
|
-
})
|
|
648
|
-
|
|
649
|
-
test('adds template imports when _tpl is emitted', () => {
|
|
650
|
-
const result = transformJSX('<div><span>text</span></div>')
|
|
651
|
-
expect(result.code).toContain('import { _tpl } from "@pyreon/runtime-dom"')
|
|
652
|
-
expect(result.usesTemplates).toBe(true)
|
|
653
|
-
})
|
|
654
|
-
|
|
655
|
-
test('adds template imports for single element', () => {
|
|
656
|
-
const result = transformJSX('<div>text</div>')
|
|
657
|
-
expect(result.code).toContain('import { _tpl } from "@pyreon/runtime-dom"')
|
|
658
|
-
expect(result.usesTemplates).toBe(true)
|
|
659
|
-
})
|
|
660
|
-
|
|
661
|
-
test('wraps _tpl call in braces when child of JSX element', () => {
|
|
662
|
-
// <Comp> is a component, so outer element is not templateized
|
|
663
|
-
// but <span><em> inside it has 2 elements
|
|
664
|
-
const result = t('<Comp><span><em>text</em></span></Comp>')
|
|
665
|
-
// The inner span+em gets templateized inside the component children
|
|
666
|
-
expect(result).toContain('{_tpl(')
|
|
667
|
-
})
|
|
668
|
-
|
|
669
|
-
test('handles self-closing void elements in template', () => {
|
|
670
|
-
const result = t('<div><br /><span>text</span></div>')
|
|
671
|
-
expect(result).toContain('_tpl(')
|
|
672
|
-
expect(result).toContain('<br>')
|
|
673
|
-
expect(result).not.toContain('</br>')
|
|
674
|
-
})
|
|
675
|
-
|
|
676
|
-
test('handles mixed static text and element children', () => {
|
|
677
|
-
const result = t('<div class="c"><span>inner</span></div>')
|
|
678
|
-
expect(result).toContain('_tpl(')
|
|
679
|
-
expect(result).toContain('<span>inner</span>')
|
|
680
|
-
})
|
|
681
|
-
|
|
682
|
-
test('escapes quotes in HTML attribute values', () => {
|
|
683
|
-
const result = t('<div title="say "hi""><span /></div>')
|
|
684
|
-
expect(result).toContain('_tpl(')
|
|
685
|
-
})
|
|
686
|
-
|
|
687
|
-
test('returns null cleanup when no dynamic bindings', () => {
|
|
688
|
-
const result = t('<div><span>static</span></div>')
|
|
689
|
-
expect(result).toContain('() => null')
|
|
690
|
-
})
|
|
691
|
-
|
|
692
|
-
test('composes multiple disposers in cleanup', () => {
|
|
693
|
-
const result = t('<div class={a()}><span>{b()}</span></div>')
|
|
694
|
-
expect(result).toContain('__d0()')
|
|
695
|
-
expect(result).toContain('__d1()')
|
|
696
|
-
})
|
|
697
|
-
|
|
698
|
-
test('maps className to class in HTML', () => {
|
|
699
|
-
const result = t('<div className="box"><span /></div>')
|
|
700
|
-
// Quotes escaped in _tpl string literal
|
|
701
|
-
expect(result).toContain('class=\\"box\\"')
|
|
702
|
-
expect(result).not.toContain('className')
|
|
703
|
-
})
|
|
704
|
-
|
|
705
|
-
test('maps htmlFor to for in HTML', () => {
|
|
706
|
-
const result = t('<div><label htmlFor="name">Name</label></div>')
|
|
707
|
-
expect(result).toContain('for=\\"name\\"')
|
|
708
|
-
})
|
|
709
|
-
|
|
710
|
-
test('inlines fragments inside template', () => {
|
|
711
|
-
const result = t('<div><><span>text</span></></div>')
|
|
712
|
-
// Fragment children are inlined as direct children
|
|
713
|
-
expect(result).toContain('_tpl(')
|
|
714
|
-
expect(result).toContain('<span>text</span>')
|
|
715
|
-
})
|
|
716
|
-
|
|
717
|
-
test('bails on expression children containing JSX', () => {
|
|
718
|
-
const result = t('<div><span />{show() && <em />}</div>')
|
|
719
|
-
expect(result).not.toContain('_tpl(')
|
|
720
|
-
})
|
|
721
|
-
|
|
722
|
-
test('handles mixed element + expression children', () => {
|
|
723
|
-
const result = t('<div><span />{text()}</div>')
|
|
724
|
-
// Mixed element + expression children use childNodes indexing
|
|
725
|
-
expect(result).toContain('_tpl(')
|
|
726
|
-
expect(result).toContain('childNodes[')
|
|
727
|
-
expect(result).toContain('_bindText(text,')
|
|
728
|
-
})
|
|
729
|
-
|
|
730
|
-
test('benchmark-like row structure', () => {
|
|
731
|
-
const result = t(
|
|
732
|
-
'<tr class={cls()}><td class="id">{String(row.id)}</td><td>{row.label()}</td></tr>',
|
|
733
|
-
)
|
|
734
|
-
expect(result).toContain('_tpl(')
|
|
735
|
-
expect(result).toContain('<td class=\\"id\\"></td><td></td>')
|
|
736
|
-
// className uses _bindDirect (single-signal cls())
|
|
737
|
-
expect(result).toContain('_bindDirect(cls,')
|
|
738
|
-
// String(row.id) has args → combined _bind; row.label() is property access → _bind
|
|
739
|
-
expect(result).toContain('.data = String(row.id)')
|
|
740
|
-
expect(result).toContain('row.label()')
|
|
741
|
-
})
|
|
742
|
-
|
|
743
|
-
test('handles multiple expression children', () => {
|
|
744
|
-
const result = t('<div><span>{a()}{b()}</span></div>')
|
|
745
|
-
expect(result).toContain('_tpl(')
|
|
746
|
-
// Both are single-signal → _bindText
|
|
747
|
-
expect(result).toContain('_bindText(a,')
|
|
748
|
-
expect(result).toContain('_bindText(b,')
|
|
749
|
-
// Each expression gets its own placeholder and childNodes access
|
|
750
|
-
expect(result).toContain('childNodes[0]')
|
|
751
|
-
expect(result).toContain('childNodes[1]')
|
|
752
|
-
})
|
|
753
|
-
|
|
754
|
-
test('handles mixed text + element + expression children', () => {
|
|
755
|
-
const result = t('<div>hello<span />{name()}</div>')
|
|
756
|
-
expect(result).toContain('_tpl(')
|
|
757
|
-
expect(result).toContain('childNodes[')
|
|
758
|
-
expect(result).toContain('_bindText(name,')
|
|
759
|
-
})
|
|
760
|
-
|
|
761
|
-
test('handles fragment with element children inside template', () => {
|
|
762
|
-
const result = t('<div><><span>a</span><em>b</em></></div>')
|
|
763
|
-
expect(result).toContain('_tpl(')
|
|
764
|
-
expect(result).toContain('<span>a</span>')
|
|
765
|
-
expect(result).toContain('<em>b</em>')
|
|
766
|
-
})
|
|
767
|
-
|
|
768
|
-
test('bails on fragment with non-eligible children', () => {
|
|
769
|
-
const result = t('<div><><Component /></></div>')
|
|
770
|
-
expect(result).not.toContain('_tpl(')
|
|
771
|
-
})
|
|
772
|
-
|
|
773
|
-
test('handles static expression in mixed children', () => {
|
|
774
|
-
const result = t('<div><span />{label}</div>')
|
|
775
|
-
expect(result).toContain('_tpl(')
|
|
776
|
-
expect(result).toContain('childNodes[')
|
|
777
|
-
expect(result).toContain('createTextNode(label)')
|
|
778
|
-
})
|
|
779
|
-
|
|
780
|
-
test('bakes static numeric literal attr into HTML', () => {
|
|
781
|
-
const result = t('<div tabindex={0}><span /></div>')
|
|
782
|
-
expect(result).toContain('_tpl(')
|
|
783
|
-
expect(result).toContain('tabindex=\\"0\\"')
|
|
784
|
-
})
|
|
785
|
-
|
|
786
|
-
test('bakes static true keyword attr into HTML', () => {
|
|
787
|
-
const result = t('<div hidden={true}><span /></div>')
|
|
788
|
-
expect(result).toContain('_tpl(')
|
|
789
|
-
expect(result).toContain(' hidden')
|
|
790
|
-
})
|
|
791
|
-
|
|
792
|
-
test('omits false keyword attr from HTML', () => {
|
|
793
|
-
const result = t('<div hidden={false}><span /></div>')
|
|
794
|
-
expect(result).toContain('_tpl(')
|
|
795
|
-
expect(result).not.toContain('hidden')
|
|
796
|
-
})
|
|
797
|
-
|
|
798
|
-
test('omits null keyword attr from HTML', () => {
|
|
799
|
-
const result = t('<div hidden={null}><span /></div>')
|
|
800
|
-
expect(result).toContain('_tpl(')
|
|
801
|
-
expect(result).not.toContain('hidden')
|
|
802
|
-
})
|
|
803
|
-
|
|
804
|
-
test('emits setAttribute for undefined keyword attr', () => {
|
|
805
|
-
const result = t('<div hidden={undefined}><span /></div>')
|
|
806
|
-
expect(result).toContain('_tpl(')
|
|
807
|
-
expect(result).toContain('setAttribute("hidden", undefined)')
|
|
808
|
-
})
|
|
809
|
-
|
|
810
|
-
test('one-time set for non-class static expression attribute', () => {
|
|
811
|
-
const result = t('<div title={someVar}><span /></div>')
|
|
812
|
-
expect(result).toContain('setAttribute("title", someVar)')
|
|
813
|
-
expect(result).not.toContain('_bind(')
|
|
814
|
-
})
|
|
815
|
-
|
|
816
|
-
test('_bindDirect for non-class single-signal dynamic attribute', () => {
|
|
817
|
-
const result = t('<div title={getTitle()}><span /></div>')
|
|
818
|
-
expect(result).toContain('_bindDirect(getTitle,')
|
|
819
|
-
expect(result).toContain('setAttribute("title"')
|
|
820
|
-
})
|
|
821
|
-
|
|
822
|
-
test('ref attribute in template binds .current for object refs', () => {
|
|
823
|
-
const result = t('<div ref={myRef}><span /></div>')
|
|
824
|
-
// Object refs go through a runtime check (could also be a function)
|
|
825
|
-
expect(result).toContain('myRef')
|
|
826
|
-
expect(result).toContain('.current = __root')
|
|
827
|
-
})
|
|
828
|
-
|
|
829
|
-
test('ref attribute in template calls function refs', () => {
|
|
830
|
-
const result = t('<div ref={(el) => { myEl = el }}><span /></div>')
|
|
831
|
-
// Arrow function refs are called with the element
|
|
832
|
-
expect(result).toContain('((el) => { myEl = el })(__root)')
|
|
833
|
-
})
|
|
834
|
-
|
|
835
|
-
test('block-arrow ref on a child element with adjacent reactive props compiles cleanly', () => {
|
|
836
|
-
// Regression: a child element (NOT __root) with `hasDynamic=true`
|
|
837
|
-
// used to emit `const __e0 = __root.children[N]` followed by an
|
|
838
|
-
// unterminated ref-call `((el) => { x = el })(__e0)`. Without a
|
|
839
|
-
// trailing `;` on the const line, ASI did NOT insert one (because
|
|
840
|
-
// `__root.children[N]((el) => ...)` is a valid function call), and
|
|
841
|
-
// the two lines parsed as ONE expression:
|
|
842
|
-
// `const __e0 = __root.children[N]((el) => ...)(__e0)`
|
|
843
|
-
// — calling `children[N]` as a function with the arrow as arg, and
|
|
844
|
-
// self-referencing `__e0` before assignment. Surfaced when the
|
|
845
|
-
// app-showcase /dnd demo used `ref={(el) => { letVar = el }}` next
|
|
846
|
-
// to `data-X={signal()}` reactive props on the same element. Fix:
|
|
847
|
-
// append `;` to every bind line (`bindLines.map(l => ` ${l};`)`).
|
|
848
|
-
//
|
|
849
|
-
// This test asserts the OUTPUT is well-formed: the const line ends
|
|
850
|
-
// in `;` and the ref call IIFE follows on its own line.
|
|
851
|
-
const result = t(
|
|
852
|
-
'<div><span ref={(el) => { x = el }} data-state={cls()} /></div>',
|
|
853
|
-
)
|
|
854
|
-
// Const declaration must terminate before the ref IIFE.
|
|
855
|
-
expect(result).toMatch(/const __e0 = __root\.children\[0\];\s*\n/)
|
|
856
|
-
// The ref IIFE is its own statement, calling __e0 (not chained).
|
|
857
|
-
expect(result).toContain('((el) => { x = el })(__e0)')
|
|
858
|
-
// And the chained-call shape MUST NOT appear (the bug pattern).
|
|
859
|
-
expect(result).not.toMatch(/__root\.children\[0\]\(\(/)
|
|
860
|
-
})
|
|
861
|
-
|
|
862
|
-
test('compiled output parses cleanly for block-arrow ref + reactive prop', () => {
|
|
863
|
-
// Functional regression: prove the compiled module is well-formed JS.
|
|
864
|
-
// Pre-fix the AST-level shape was malformed and execution threw
|
|
865
|
-
// "TypeError: __root.children[N] is not a function" at mount time
|
|
866
|
-
// because the const declaration chained into the ref IIFE.
|
|
867
|
-
//
|
|
868
|
-
// Strip imports, stub framework calls, and parse the body via the
|
|
869
|
-
// Function constructor. If the const line lacks its terminator, the
|
|
870
|
-
// ref-call IIFE would silently turn the RHS into a function call —
|
|
871
|
-
// valid JS, wrong runtime behavior. So we BOTH parse-check AND
|
|
872
|
-
// string-shape-check: parse to catch syntax errors, regex to catch
|
|
873
|
-
// the silent-merge case (which parses fine but means the wrong thing).
|
|
874
|
-
const result = t(
|
|
875
|
-
'<div><span ref={(el) => { x = el }} data-state={cls()} /></div>',
|
|
876
|
-
)
|
|
877
|
-
const codeOnly = result.replace(/^\s*import\b[^;]+;?\s*/gm, '')
|
|
878
|
-
const wrapped = `let x;\nconst _tpl = () => {}; const _bind = () => {}; const _bindDirect = () => {}; const cls = () => "v";\nreturn ${codeOnly};`
|
|
879
|
-
expect(() => new Function(wrapped)).not.toThrow()
|
|
880
|
-
// And the buggy chained-call shape MUST NOT appear.
|
|
881
|
-
expect(result).not.toMatch(/children\[0\]\(\(/)
|
|
882
|
-
})
|
|
883
|
-
|
|
884
|
-
test('handles non-void self-closing element as closing tag', () => {
|
|
885
|
-
const result = t('<div><span></span></div>')
|
|
886
|
-
expect(result).toContain('_tpl(')
|
|
887
|
-
expect(result).toContain('<span></span>')
|
|
888
|
-
})
|
|
889
|
-
|
|
890
|
-
test('handles nested fragment with expression child', () => {
|
|
891
|
-
const result = t('<div><><span />{name()}</></div>')
|
|
892
|
-
// Fragment with expression is inlined, expression with JSX is not present
|
|
893
|
-
expect(result).toContain('_tpl(')
|
|
894
|
-
})
|
|
895
|
-
|
|
896
|
-
test('handles fragment with expression containing no JSX', () => {
|
|
897
|
-
const result = t('<div><><span />{count()}</></div>')
|
|
898
|
-
expect(result).toContain('_tpl(')
|
|
899
|
-
expect(result).toContain('_bindText(count,')
|
|
900
|
-
})
|
|
901
|
-
|
|
902
|
-
test('handles nested fragment with text children', () => {
|
|
903
|
-
const result = t('<div><>hello</></div>')
|
|
904
|
-
expect(result).toContain('_tpl(')
|
|
905
|
-
expect(result).toContain('hello')
|
|
906
|
-
})
|
|
907
|
-
|
|
908
|
-
test('bails on fragment with non-element non-expression child', () => {
|
|
909
|
-
// Fragment containing a component should bail
|
|
910
|
-
const result = t('<div><><MyComp /></></div>')
|
|
911
|
-
expect(result).not.toContain('_tpl(')
|
|
912
|
-
})
|
|
913
|
-
|
|
914
|
-
test('empty expression inside template is handled', () => {
|
|
915
|
-
const result = t('<div><span />{/* comment */}</div>')
|
|
916
|
-
expect(result).toContain('_tpl(')
|
|
917
|
-
})
|
|
918
|
-
|
|
919
|
-
test('static expression with multi-expression context uses placeholder', () => {
|
|
920
|
-
const result = t('<div><span>{label}{other}</span></div>')
|
|
921
|
-
expect(result).toContain('_tpl(')
|
|
922
|
-
expect(result).toContain('childNodes[0]')
|
|
923
|
-
expect(result).toContain('childNodes[1]')
|
|
924
|
-
})
|
|
925
|
-
})
|
|
926
|
-
|
|
927
|
-
// ─── Compiler warnings ─────────────────────────────────────────────────────
|
|
928
|
-
|
|
929
|
-
describe('JSX transform — warnings', () => {
|
|
930
|
-
test('warns on <For> without by prop', () => {
|
|
931
|
-
const result = transformJSX('<For each={items}>{(item) => <li>{item}</li>}</For>')
|
|
932
|
-
expect(result.warnings).toHaveLength(1)
|
|
933
|
-
expect(result.warnings[0]?.code).toBe('missing-key-on-for')
|
|
934
|
-
expect(result.warnings[0]?.line).toBeGreaterThan(0)
|
|
935
|
-
expect(result.warnings[0]?.column).toBeGreaterThanOrEqual(0)
|
|
936
|
-
})
|
|
937
|
-
|
|
938
|
-
test('no warning on <For> with by prop', () => {
|
|
939
|
-
const result = transformJSX(
|
|
940
|
-
'<For each={items} by={(item) => item.id}>{(item) => <li>{item}</li>}</For>',
|
|
941
|
-
)
|
|
942
|
-
const forWarnings = result.warnings.filter((w) => w.code === 'missing-key-on-for')
|
|
943
|
-
expect(forWarnings).toHaveLength(0)
|
|
944
|
-
})
|
|
945
|
-
|
|
946
|
-
test('no warning on non-For elements without by', () => {
|
|
947
|
-
const result = transformJSX('<div each={items}>{text()}</div>')
|
|
948
|
-
const forWarnings = result.warnings.filter((w) => w.code === 'missing-key-on-for')
|
|
949
|
-
expect(forWarnings).toHaveLength(0)
|
|
950
|
-
})
|
|
951
|
-
})
|
|
952
|
-
|
|
953
|
-
// ─── Hoisting in prop position ──────────────────────────────────────────────
|
|
954
|
-
|
|
955
|
-
describe('JSX transform — static JSX attribute hoisting', () => {
|
|
956
|
-
test('hoists static JSX in a DOM element prop', () => {
|
|
957
|
-
const result = t('<div icon={<span>icon</span>} />')
|
|
958
|
-
expect(result).toContain('const _$h0')
|
|
959
|
-
expect(result).toContain('<span>icon</span>')
|
|
960
|
-
})
|
|
961
|
-
})
|
|
962
|
-
|
|
963
|
-
// ─── Additional branch coverage tests ────────────────────────────────────────
|
|
964
|
-
|
|
965
|
-
describe('JSX transform — child expression branches (non-template context)', () => {
|
|
966
|
-
test('wraps dynamic child expression inside a component (non-template path)', () => {
|
|
967
|
-
// Component elements skip template emission, so the child expression
|
|
968
|
-
// goes through the walk() JSX expression handler (lines 195-209)
|
|
969
|
-
const result = t('<MyComponent>{count()}</MyComponent>')
|
|
970
|
-
expect(result).toContain('() => count()')
|
|
971
|
-
})
|
|
972
|
-
|
|
973
|
-
test('does NOT wrap non-dynamic child expression inside a component', () => {
|
|
974
|
-
// Component context: child expression with no calls — shouldWrap returns false
|
|
975
|
-
// This hits the else branch where neither hoist nor wrap applies (lines 202-204)
|
|
976
|
-
const result = t('<MyComponent>{someVar}</MyComponent>')
|
|
977
|
-
expect(result).not.toContain('() =>')
|
|
978
|
-
expect(result).toContain('someVar')
|
|
979
|
-
})
|
|
980
|
-
|
|
981
|
-
test('empty expression in component child is left unchanged', () => {
|
|
982
|
-
// Empty expression (comment) inside component — expr is undefined, line 205-208
|
|
983
|
-
const result = t('<MyComponent>{/* comment */}</MyComponent>')
|
|
984
|
-
expect(result).not.toContain('() =>')
|
|
985
|
-
})
|
|
986
|
-
})
|
|
987
|
-
|
|
988
|
-
describe('JSX transform — nested fragment in templateFragmentCount', () => {
|
|
989
|
-
test('handles nested fragment inside fragment in template', () => {
|
|
990
|
-
// This triggers templateFragmentCount being called recursively for nested fragments
|
|
991
|
-
// (lines 318-323)
|
|
992
|
-
const result = t('<div><><><span>text</span></></></div>')
|
|
993
|
-
expect(result).toContain('_tpl(')
|
|
994
|
-
expect(result).toContain('<span>text</span>')
|
|
995
|
-
})
|
|
996
|
-
|
|
997
|
-
test('bails on nested fragment with non-eligible child', () => {
|
|
998
|
-
// Nested fragment containing a component — hits line 325 (return -1)
|
|
999
|
-
const result = t('<div><><><MyComp /></></></div>')
|
|
1000
|
-
expect(result).not.toContain('_tpl(')
|
|
1001
|
-
})
|
|
1002
|
-
|
|
1003
|
-
test('nested fragment with expression child in templateFragmentCount', () => {
|
|
1004
|
-
// Fragment in fragment with expression — templateFragmentCount handles expression
|
|
1005
|
-
const result = t('<div><><>{count()}</></></div>')
|
|
1006
|
-
expect(result).toContain('_tpl(')
|
|
1007
|
-
expect(result).toContain('_bindText(count,')
|
|
1008
|
-
})
|
|
1009
|
-
|
|
1010
|
-
test('nested fragment with expression containing JSX bails', () => {
|
|
1011
|
-
// Fragment in fragment with JSX-containing expression — bails
|
|
1012
|
-
const result = t('<div><><>{show() && <em />}</></></div>')
|
|
1013
|
-
expect(result).not.toContain('_tpl(')
|
|
1014
|
-
})
|
|
1015
|
-
|
|
1016
|
-
test('nested fragment with empty expression in templateFragmentCount', () => {
|
|
1017
|
-
// Fragment in fragment with empty expression (comment)
|
|
1018
|
-
const result = t('<div><><>{/* comment */}</></></div>')
|
|
1019
|
-
expect(result).toContain('_tpl(')
|
|
1020
|
-
})
|
|
1021
|
-
})
|
|
1022
|
-
|
|
1023
|
-
describe('JSX transform — template attribute string expression', () => {
|
|
1024
|
-
test('bakes string expression attribute into HTML in template', () => {
|
|
1025
|
-
// class={"static"} — string literal in JSX expression → baked into HTML (line 427)
|
|
1026
|
-
const result = t('<div class={"static-value"}><span /></div>')
|
|
1027
|
-
expect(result).toContain('_tpl(')
|
|
1028
|
-
expect(result).toContain('class=\\"static-value\\"')
|
|
1029
|
-
expect(result).not.toContain('className')
|
|
1030
|
-
})
|
|
1031
|
-
|
|
1032
|
-
test('bakes non-class string expression attribute into HTML', () => {
|
|
1033
|
-
// title={"hello"} as expression — different attr name (line 427)
|
|
1034
|
-
const result = t('<div title={"hello"}><span /></div>')
|
|
1035
|
-
expect(result).toContain('_tpl(')
|
|
1036
|
-
expect(result).toContain('title=\\"hello\\"')
|
|
1037
|
-
})
|
|
1038
|
-
})
|
|
1039
|
-
|
|
1040
|
-
describe('JSX transform — one-time className set in template', () => {
|
|
1041
|
-
test('one-time className assignment for non-reactive class expression', () => {
|
|
1042
|
-
// class={someVar} where someVar has no calls — one-time set (line 450)
|
|
1043
|
-
const result = t('<div class={someVar}><span /></div>')
|
|
1044
|
-
expect(result).toContain('_tpl(')
|
|
1045
|
-
expect(result).toContain('className = someVar')
|
|
1046
|
-
expect(result).not.toContain('_bind(')
|
|
1047
|
-
})
|
|
1048
|
-
})
|
|
1049
|
-
|
|
1050
|
-
describe('JSX transform — isStaticAttrs edge cases', () => {
|
|
1051
|
-
test('static JSX with boolean expression prop is static', () => {
|
|
1052
|
-
// Boolean literal in expression: disabled={true} — isStatic returns true
|
|
1053
|
-
const result = t('<div>{<input disabled={true} />}</div>')
|
|
1054
|
-
expect(result).toContain('const _$h0')
|
|
1055
|
-
})
|
|
1056
|
-
|
|
1057
|
-
test('static JSX with false expression prop is static', () => {
|
|
1058
|
-
const result = t('<div>{<input disabled={false} />}</div>')
|
|
1059
|
-
expect(result).toContain('const _$h0')
|
|
1060
|
-
})
|
|
1061
|
-
|
|
1062
|
-
test('static JSX with null expression prop is static', () => {
|
|
1063
|
-
const result = t('<div>{<input disabled={null} />}</div>')
|
|
1064
|
-
expect(result).toContain('const _$h0')
|
|
1065
|
-
})
|
|
1066
|
-
|
|
1067
|
-
test('static JSX with numeric expression prop is static', () => {
|
|
1068
|
-
const result = t('<div>{<input tabindex={0} />}</div>')
|
|
1069
|
-
expect(result).toContain('const _$h0')
|
|
1070
|
-
})
|
|
1071
|
-
|
|
1072
|
-
test('static JSX with true expression prop is static', () => {
|
|
1073
|
-
const result = t('<div>{<input disabled={true} />}</div>')
|
|
1074
|
-
expect(result).toContain('const _$h0')
|
|
1075
|
-
})
|
|
1076
|
-
|
|
1077
|
-
test('static JSX with empty expression prop is static', () => {
|
|
1078
|
-
// Empty expression in attribute: disabled={/* comment */} — expr is undefined
|
|
1079
|
-
const result = t('<div>{<input disabled={/* comment */} />}</div>')
|
|
1080
|
-
expect(result).toContain('const _$h0')
|
|
1081
|
-
})
|
|
1082
|
-
})
|
|
1083
|
-
|
|
1084
|
-
describe('JSX transform — isStaticChild edge cases', () => {
|
|
1085
|
-
test('nested static fragment child is recognized as static', () => {
|
|
1086
|
-
// Fragment as child of a JSX element being checked for staticness
|
|
1087
|
-
const result = t('<div>{<div><>text</></div>}</div>')
|
|
1088
|
-
expect(result).toContain('const _$h0')
|
|
1089
|
-
})
|
|
1090
|
-
|
|
1091
|
-
test('nested fragment with dynamic child prevents hoisting', () => {
|
|
1092
|
-
const result = t('<div>{<div><>{count()}</></div>}</div>')
|
|
1093
|
-
expect(result).not.toContain('const _$h0')
|
|
1094
|
-
})
|
|
1095
|
-
|
|
1096
|
-
test('expression child in static check — static literal', () => {
|
|
1097
|
-
// Expression container with static value inside a JSX node being checked for staticness
|
|
1098
|
-
const result = t('<div>{<div>{"hello"}</div>}</div>')
|
|
1099
|
-
expect(result).toContain('const _$h0')
|
|
1100
|
-
})
|
|
1101
|
-
|
|
1102
|
-
test('expression child in static check — dynamic call prevents hoisting', () => {
|
|
1103
|
-
const result = t('<div>{<div>{count()}</div>}</div>')
|
|
1104
|
-
expect(result).not.toContain('const _$h0')
|
|
1105
|
-
})
|
|
1106
|
-
|
|
1107
|
-
test('expression child with empty expression is static', () => {
|
|
1108
|
-
const result = t('<div>{<div>{/* comment */}</div>}</div>')
|
|
1109
|
-
expect(result).toContain('const _$h0')
|
|
1110
|
-
})
|
|
1111
|
-
})
|
|
1112
|
-
|
|
1113
|
-
// ─── Additional branch coverage for 95%+ ──────────────────────────────────────
|
|
1114
|
-
|
|
1115
|
-
describe('JSX transform — isStaticAttrs boolean shorthand (hoisting path)', () => {
|
|
1116
|
-
test('hoists static JSX with boolean shorthand attribute (no initializer)', () => {
|
|
1117
|
-
// This triggers isStaticAttrs → !prop.initializer → return true (line 719/2340)
|
|
1118
|
-
const result = t('<div>{<input disabled />}</div>')
|
|
1119
|
-
expect(result).toContain('const _$h0')
|
|
1120
|
-
})
|
|
1121
|
-
})
|
|
1122
|
-
|
|
1123
|
-
describe('JSX transform — isStaticChild with element/self-closing children', () => {
|
|
1124
|
-
test('hoists static JSX with self-closing element child', () => {
|
|
1125
|
-
// Triggers isStaticChild → isJsxSelfClosingElement path (line 735/2356)
|
|
1126
|
-
const result = t('<div>{<div><br /></div>}</div>')
|
|
1127
|
-
expect(result).toContain('const _$h0')
|
|
1128
|
-
})
|
|
1129
|
-
|
|
1130
|
-
test('hoists static JSX with nested element child', () => {
|
|
1131
|
-
// Triggers isStaticChild → isJsxElement path (line 736/2357)
|
|
1132
|
-
const result = t('<div>{<div><span>text</span></div>}</div>')
|
|
1133
|
-
expect(result).toContain('const _$h0')
|
|
1134
|
-
})
|
|
1135
|
-
|
|
1136
|
-
test('does NOT hoist when nested element child has dynamic props', () => {
|
|
1137
|
-
// isStaticChild → isJsxElement → isStaticJSXNode returns false
|
|
1138
|
-
const result = t('<div>{<div><span class={cls()}>text</span></div>}</div>')
|
|
1139
|
-
expect(result).not.toContain('const _$h0')
|
|
1140
|
-
})
|
|
1141
|
-
|
|
1142
|
-
test('does NOT hoist when self-closing child has dynamic props', () => {
|
|
1143
|
-
// isStaticChild → isJsxSelfClosingElement → isStaticJSXNode returns false
|
|
1144
|
-
const result = t('<div>{<div><input value={val()} /></div>}</div>')
|
|
1145
|
-
expect(result).not.toContain('const _$h0')
|
|
1146
|
-
})
|
|
1147
|
-
})
|
|
1148
|
-
|
|
1149
|
-
describe('JSX transform — template ref/event without expression', () => {
|
|
1150
|
-
test('ref shorthand (no expression) in template is handled', () => {
|
|
1151
|
-
// Triggers the else branch of ref initializer check (line 2003)
|
|
1152
|
-
const result = t('<div ref><span /></div>')
|
|
1153
|
-
expect(result).toContain('_tpl(')
|
|
1154
|
-
expect(result).not.toContain('.current')
|
|
1155
|
-
})
|
|
1156
|
-
|
|
1157
|
-
test('onClick shorthand (no expression) in template is handled', () => {
|
|
1158
|
-
// Triggers the else branch of event initializer check (line 2017)
|
|
1159
|
-
const result = t('<div onClick><span /></div>')
|
|
1160
|
-
expect(result).toContain('_tpl(')
|
|
1161
|
-
expect(result).not.toContain('addEventListener')
|
|
1162
|
-
})
|
|
1163
|
-
})
|
|
1164
|
-
|
|
1165
|
-
describe('JSX transform — empty expression in DOM prop (non-template path)', () => {
|
|
1166
|
-
test('empty expression in DOM prop with spread (non-template) is handled', () => {
|
|
1167
|
-
// Spread prevents template emission → walk handles attrs
|
|
1168
|
-
// class={/* comment */} has no expression → else branch at line 1799
|
|
1169
|
-
const result = t('<div {...props} class={/* comment */} />')
|
|
1170
|
-
expect(result).not.toContain('() =>')
|
|
1171
|
-
expect(result).toContain('{...props}')
|
|
1172
|
-
})
|
|
1173
|
-
})
|
|
1174
|
-
|
|
1175
|
-
describe('JSX transform — whitespace-only text stripped in flattenChildren', () => {
|
|
1176
|
-
test('whitespace-only text between elements is stripped in template', () => {
|
|
1177
|
-
// Triggers the else branch of `if (trimmed)` in flattenChildren (line 2207)
|
|
1178
|
-
const result = t(`<div>
|
|
1179
|
-
<span>a</span>
|
|
1180
|
-
<em>b</em>
|
|
1181
|
-
</div>`)
|
|
1182
|
-
expect(result).toContain('_tpl(')
|
|
1183
|
-
expect(result).toContain('<span>a</span>')
|
|
1184
|
-
expect(result).toContain('<em>b</em>')
|
|
1185
|
-
})
|
|
1186
|
-
})
|
|
1187
|
-
|
|
1188
|
-
describe('JSX transform — fragment inside flattenChildren', () => {
|
|
1189
|
-
test('fragment children are flattened during template child processing', () => {
|
|
1190
|
-
// This specifically exercises the isJsxFragment branch in flattenChildren (line 2223)
|
|
1191
|
-
// The key is that this fragment is processed via flattenChildren (not templateFragmentCount)
|
|
1192
|
-
// because the outer element is a template-eligible JsxElement
|
|
1193
|
-
const result = t('<div><><span>one</span><em>two</em></></div>')
|
|
1194
|
-
expect(result).toContain('_tpl(')
|
|
1195
|
-
expect(result).toContain('<span>one</span>')
|
|
1196
|
-
expect(result).toContain('<em>two</em>')
|
|
1197
|
-
})
|
|
1198
|
-
})
|
|
1199
|
-
|
|
1200
|
-
describe('JSX transform — member expression tag names', () => {
|
|
1201
|
-
test('member expression tag name treated as empty in warnings', () => {
|
|
1202
|
-
// <ns.Component> has non-identifier tagName → tagName is "" (line 1762)
|
|
1203
|
-
const result = transformJSX('<ns.Comp value={x} />')
|
|
1204
|
-
expect(result.warnings).toHaveLength(0)
|
|
1205
|
-
})
|
|
1206
|
-
|
|
1207
|
-
test('member expression tag in element position triggers non-identifier path', () => {
|
|
1208
|
-
// <ns.div> has a member expression tag → jsxTagName returns "" → templateElementCount returns -1
|
|
1209
|
-
const result = t('<ns.div><span /></ns.div>')
|
|
1210
|
-
// Should not produce template since tagName is not an identifier
|
|
1211
|
-
expect(result).not.toContain('_tpl(')
|
|
1212
|
-
})
|
|
1213
|
-
})
|
|
1214
|
-
|
|
1215
|
-
// ─── Template emission edge cases ─────────────────────────────────────────────
|
|
1216
|
-
|
|
1217
|
-
describe('JSX transform — template emission edge cases', () => {
|
|
1218
|
-
test('non-delegated event (onMouseEnter) uses addEventListener not delegation', () => {
|
|
1219
|
-
const result = t('<div onMouseEnter={handler}><span /></div>')
|
|
1220
|
-
expect(result).toContain('_tpl(')
|
|
1221
|
-
// mouseenter is NOT in DELEGATED_EVENTS → must use addEventListener.
|
|
1222
|
-
// The event name is the JSX attribute with the "on" prefix dropped
|
|
1223
|
-
// and the rest fully lowercased (`onMouseEnter` → `mouseenter`)
|
|
1224
|
-
// — DOM events are all-lowercase. Prior to the fix this emitted
|
|
1225
|
-
// `mouseEnter` (camelCase) which the browser never dispatches.
|
|
1226
|
-
expect(result).toContain('addEventListener("mouseenter"')
|
|
1227
|
-
expect(result).not.toContain('mouseEnter')
|
|
1228
|
-
expect(result).not.toContain('__ev_')
|
|
1229
|
-
})
|
|
1230
|
-
|
|
1231
|
-
// Regression: `onDoubleClick` is the one React→DOM event-name where
|
|
1232
|
-
// the simple-lowercase rule is wrong. The DOM event is `dblclick`,
|
|
1233
|
-
// NOT `doubleclick`. Pre-fix the compiler emitted `doubleclick`,
|
|
1234
|
-
// attaching a listener the browser never fires. Proven broken in
|
|
1235
|
-
// real Chromium via `e2e/app.spec.ts:dbl-click button increments by 10`.
|
|
1236
|
-
test('onDoubleClick maps to dblclick (not doubleclick)', () => {
|
|
1237
|
-
const result = t('<div onDoubleClick={handler}><span /></div>')
|
|
1238
|
-
expect(result).toContain('_tpl(')
|
|
1239
|
-
expect(result).toContain('__ev_dblclick = handler')
|
|
1240
|
-
expect(result).not.toContain('doubleclick')
|
|
1241
|
-
})
|
|
1242
|
-
|
|
1243
|
-
test('template with both dynamic attribute AND dynamic child text', () => {
|
|
1244
|
-
const result = t('<div title={getTitle()}>{count()}</div>')
|
|
1245
|
-
expect(result).toContain('_tpl(')
|
|
1246
|
-
// Dynamic attribute binding
|
|
1247
|
-
expect(result).toContain('_bindDirect(getTitle,')
|
|
1248
|
-
// Dynamic child text binding
|
|
1249
|
-
expect(result).toContain('_bindText(count,')
|
|
1250
|
-
})
|
|
1251
|
-
|
|
1252
|
-
test('template with multiple dynamic attributes on same element', () => {
|
|
1253
|
-
const result = t('<div class={cls()} title={getTitle()}><span /></div>')
|
|
1254
|
-
expect(result).toContain('_tpl(')
|
|
1255
|
-
// Both attributes should get _bindDirect bindings
|
|
1256
|
-
expect(result).toContain('_bindDirect(cls,')
|
|
1257
|
-
expect(result).toContain('_bindDirect(getTitle,')
|
|
1258
|
-
})
|
|
1259
|
-
|
|
1260
|
-
test('template with static + dynamic children mixed', () => {
|
|
1261
|
-
const result = t('<div><span>static text</span>{count()}</div>')
|
|
1262
|
-
expect(result).toContain('_tpl(')
|
|
1263
|
-
expect(result).toContain('static text')
|
|
1264
|
-
expect(result).toContain('_bindText(count,')
|
|
1265
|
-
// Mixed children use childNodes indexing
|
|
1266
|
-
expect(result).toContain('childNodes[')
|
|
1267
|
-
})
|
|
1268
|
-
|
|
1269
|
-
test('template with nested component inside DOM elements bails', () => {
|
|
1270
|
-
// Component child inside a DOM element prevents template emission
|
|
1271
|
-
const result = t('<div><span><MyComponent /></span></div>')
|
|
1272
|
-
expect(result).not.toContain('_tpl(')
|
|
1273
|
-
})
|
|
1274
|
-
|
|
1275
|
-
test('fragment with template-eligible children inside template', () => {
|
|
1276
|
-
const result = t('<div><><span>a</span>{name()}</></div>')
|
|
1277
|
-
expect(result).toContain('_tpl(')
|
|
1278
|
-
expect(result).toContain('<span>a</span>')
|
|
1279
|
-
expect(result).toContain('_bindText(name,')
|
|
1280
|
-
})
|
|
1281
|
-
})
|
|
1282
|
-
|
|
1283
|
-
// ─── Style attribute handling in templates ───────────────────────────────────
|
|
1284
|
-
|
|
1285
|
-
describe('JSX transform — style attribute in templates', () => {
|
|
1286
|
-
test('style object literal uses Object.assign in _bind', () => {
|
|
1287
|
-
const result = t('<div style={{ overflow: "hidden" }}>text</div>')
|
|
1288
|
-
expect(result).toContain('_tpl(')
|
|
1289
|
-
expect(result).toContain('Object.assign(__root.style,')
|
|
1290
|
-
expect(result).toContain('overflow: "hidden"')
|
|
1291
|
-
})
|
|
1292
|
-
|
|
1293
|
-
test('style string literal inlines as HTML attribute', () => {
|
|
1294
|
-
const result = t('<div style="color: red">text</div>')
|
|
1295
|
-
expect(result).toContain('_tpl(')
|
|
1296
|
-
expect(result).toContain('style=\\"color: red\\"')
|
|
1297
|
-
// Static string should NOT go through _bind
|
|
1298
|
-
expect(result).not.toContain('Object.assign')
|
|
1299
|
-
expect(result).not.toContain('cssText')
|
|
1300
|
-
})
|
|
1301
|
-
|
|
1302
|
-
test('reactive style uses cssText in _bind', () => {
|
|
1303
|
-
const result = t('<div style={() => getStyle()}>text</div>')
|
|
1304
|
-
expect(result).toContain('_tpl(')
|
|
1305
|
-
expect(result).toContain('style.cssText')
|
|
1306
|
-
})
|
|
1307
|
-
})
|
|
1308
|
-
|
|
1309
|
-
// ─── Pure call detection ────────────────────────────────────────────────────
|
|
1310
|
-
|
|
1311
|
-
describe('JSX transform — pure call detection', () => {
|
|
1312
|
-
test('Math.max with static args is not wrapped', () => {
|
|
1313
|
-
const result = t('<div>{Math.max(5, 10)}</div>')
|
|
1314
|
-
expect(result).not.toContain('() =>')
|
|
1315
|
-
})
|
|
1316
|
-
|
|
1317
|
-
test('JSON.stringify with string arg is not wrapped', () => {
|
|
1318
|
-
const result = t('<div>{JSON.stringify("hello")}</div>')
|
|
1319
|
-
expect(result).not.toContain('() =>')
|
|
1320
|
-
})
|
|
1321
|
-
|
|
1322
|
-
test('JSON.stringify with object arg IS wrapped (object not static)', () => {
|
|
1323
|
-
const result = t('<div>{JSON.stringify({a: 1})}</div>')
|
|
1324
|
-
// Object literals are not considered static by the compiler
|
|
1325
|
-
expect(result).toContain('.data =')
|
|
1326
|
-
})
|
|
1327
|
-
|
|
1328
|
-
test('Math.max with dynamic arg (signal call) IS wrapped', () => {
|
|
1329
|
-
const result = t('<div>{Math.max(count(), 10)}</div>')
|
|
1330
|
-
// Dynamic argument means the result depends on a signal
|
|
1331
|
-
expect(result).toContain('Math.max(count(), 10)')
|
|
1332
|
-
expect(result).toContain('.data =')
|
|
1333
|
-
})
|
|
1334
|
-
|
|
1335
|
-
test('unknown function call IS wrapped', () => {
|
|
1336
|
-
const result = t('<div>{unknownFn(5)}</div>')
|
|
1337
|
-
// Unknown function is not in PURE_CALLS, so it gets wrapped
|
|
1338
|
-
expect(result).toContain('.data =')
|
|
1339
|
-
})
|
|
1340
|
-
|
|
1341
|
-
test('Math.floor with static arg is not wrapped', () => {
|
|
1342
|
-
const result = t('<div>{Math.floor(3.14)}</div>')
|
|
1343
|
-
expect(result).not.toContain('() =>')
|
|
1344
|
-
})
|
|
1345
|
-
|
|
1346
|
-
test('Number.parseInt with static arg is not wrapped', () => {
|
|
1347
|
-
const result = t('<div>{Number.parseInt("42", 10)}</div>')
|
|
1348
|
-
expect(result).not.toContain('() =>')
|
|
1349
|
-
})
|
|
1350
|
-
})
|
|
1351
|
-
|
|
1352
|
-
// ─── Per-text-node bind (separate bindings) ─────────────────────────────────
|
|
1353
|
-
|
|
1354
|
-
describe('JSX transform — per-text-node bind', () => {
|
|
1355
|
-
test('two adjacent signal calls produce two separate _bindText calls', () => {
|
|
1356
|
-
const result = t('<div>{a()}{b()}</div>')
|
|
1357
|
-
expect(result).toContain('_bindText(a,')
|
|
1358
|
-
expect(result).toContain('_bindText(b,')
|
|
1359
|
-
})
|
|
1360
|
-
|
|
1361
|
-
test('two signal expressions with text between produce separate bindings', () => {
|
|
1362
|
-
const result = t('<div>{a()} and {b()}</div>')
|
|
1363
|
-
expect(result).toContain('_bindText(a,')
|
|
1364
|
-
expect(result).toContain('_bindText(b,')
|
|
1365
|
-
})
|
|
1366
|
-
|
|
1367
|
-
test('three signal calls produce three separate _bindText calls', () => {
|
|
1368
|
-
const result = t('<div>{a()}{b()}{c()}</div>')
|
|
1369
|
-
expect(result).toContain('_bindText(a,')
|
|
1370
|
-
expect(result).toContain('_bindText(b,')
|
|
1371
|
-
expect(result).toContain('_bindText(c,')
|
|
1372
|
-
})
|
|
1373
|
-
})
|
|
1374
|
-
|
|
1375
|
-
// ─── Reactive props auto-detection ──────────────────────────────────────────
|
|
1376
|
-
|
|
1377
|
-
describe('JSX transform — reactive props detection', () => {
|
|
1378
|
-
test('props.x in text child is reactive (wrapped in _bind)', () => {
|
|
1379
|
-
const result = t('function Comp(props) { return <div>{props.name}</div> }')
|
|
1380
|
-
expect(result).toContain('_bind(() => {')
|
|
1381
|
-
expect(result).toContain('props.name')
|
|
1382
|
-
})
|
|
1383
|
-
|
|
1384
|
-
test('props.x in attribute is reactive (wrapped in _bind)', () => {
|
|
1385
|
-
const result = t('function Comp(props) { return <div class={props.cls}></div> }')
|
|
1386
|
-
expect(result).toContain('_bind(() => {')
|
|
1387
|
-
expect(result).toContain('props.cls')
|
|
1388
|
-
})
|
|
1389
|
-
|
|
1390
|
-
test('prop-derived variable inlined in text child', () => {
|
|
1391
|
-
const result = t('function Comp(props) { const x = props.name ?? "anon"; return <div>{x}</div> }')
|
|
1392
|
-
expect(result).toContain('_bind(() => {')
|
|
1393
|
-
expect(result).toContain('props.name ?? "anon"')
|
|
1394
|
-
// x should be inlined, not used directly
|
|
1395
|
-
expect(result).not.toMatch(/__t\d+\.data = x\b/)
|
|
1396
|
-
})
|
|
1397
|
-
|
|
1398
|
-
test('prop-derived variable inlined in attribute', () => {
|
|
1399
|
-
const result = t('function Comp(props) { const align = props.alignX ?? "left"; return <div class={align}></div> }')
|
|
1400
|
-
expect(result).toContain('_bind(() => {')
|
|
1401
|
-
expect(result).toContain('props.alignX ?? "left"')
|
|
1402
|
-
})
|
|
1403
|
-
|
|
1404
|
-
test('splitProps results tracked as props-like', () => {
|
|
1405
|
-
const result = t('function Comp(props) { const [own, rest] = splitProps(props, ["x"]); const v = own.x ?? 5; return <div>{v}</div> }')
|
|
1406
|
-
expect(result).toContain('_bind(() => {')
|
|
1407
|
-
expect(result).toContain('own.x ?? 5')
|
|
1408
|
-
})
|
|
1409
|
-
|
|
1410
|
-
test('non-component function NOT tracked (no JSX)', () => {
|
|
1411
|
-
const result = t('function helper(props) { const x = props.y; return x }')
|
|
1412
|
-
expect(result).not.toContain('_bind')
|
|
1413
|
-
expect(result).not.toContain('_tpl')
|
|
1414
|
-
})
|
|
1415
|
-
|
|
1416
|
-
test('static values unchanged by props tracking', () => {
|
|
1417
|
-
const result = t('function Comp(props) { return <div class="static">text</div> }')
|
|
1418
|
-
expect(result).toContain('_tpl("<div class=\\"static\\">text</div>"')
|
|
1419
|
-
expect(result).not.toContain('_bind')
|
|
1420
|
-
})
|
|
1421
|
-
|
|
1422
|
-
test('signal calls still work alongside props detection', () => {
|
|
1423
|
-
const result = t('function Comp(props) { return <div>{count()}</div> }')
|
|
1424
|
-
expect(result).toContain('_bindText(count,')
|
|
1425
|
-
})
|
|
1426
|
-
|
|
1427
|
-
test('arrow function component detected', () => {
|
|
1428
|
-
const result = t('const Comp = (props) => <div>{props.x}</div>')
|
|
1429
|
-
expect(result).toContain('_bind(() => {')
|
|
1430
|
-
expect(result).toContain('props.x')
|
|
1431
|
-
})
|
|
1432
|
-
})
|
|
1433
|
-
|
|
1434
|
-
// ─── Transitive prop derivation ─────────────────────────────────────────────
|
|
1435
|
-
|
|
1436
|
-
describe('JSX transform — transitive prop derivation', () => {
|
|
1437
|
-
test('const b = a + 1 where a is prop-derived', () => {
|
|
1438
|
-
const result = t('function Comp(props) { const a = props.x; const b = a + 1; return <div>{b}</div> }')
|
|
1439
|
-
expect(result).toContain('_bind(() => {')
|
|
1440
|
-
expect(result).toContain('props.x')
|
|
1441
|
-
// b should be inlined transitively
|
|
1442
|
-
expect(result).not.toMatch(/__t\d+\.data = b\b/)
|
|
1443
|
-
})
|
|
1444
|
-
|
|
1445
|
-
test('deep chain: c = b * 2, b = a + 1, a = props.x', () => {
|
|
1446
|
-
const result = t('function Comp(props) { const a = props.x; const b = a + 1; const c = b * 2; return <div>{c}</div> }')
|
|
1447
|
-
expect(result).toContain('props.x')
|
|
1448
|
-
// Full chain inlined
|
|
1449
|
-
expect(result).toContain('_bind')
|
|
1450
|
-
})
|
|
1451
|
-
|
|
1452
|
-
test('non-prop-derived variable NOT inlined', () => {
|
|
1453
|
-
const result = t('function Comp(props) { const x = 42; return <div>{x}</div> }')
|
|
1454
|
-
// x = 42 is static, not prop-derived — should NOT be wrapped
|
|
1455
|
-
expect(result).not.toContain('_bind')
|
|
1456
|
-
})
|
|
1457
|
-
|
|
1458
|
-
test('let variables NOT tracked (mutable — can be reassigned)', () => {
|
|
1459
|
-
const result = t('function Comp(props) { let x = props.y; x = "override"; return <div>{x}</div> }')
|
|
1460
|
-
// let is excluded — x is NOT inlined, set statically
|
|
1461
|
-
expect(result).toContain('textContent = x')
|
|
1462
|
-
expect(result).not.toContain('_bind')
|
|
1463
|
-
})
|
|
1464
|
-
|
|
1465
|
-
test('mixed props and signals in same expression', () => {
|
|
1466
|
-
const result = t('function Comp(props) { return <div class={`${props.base} ${count()}`}></div> }')
|
|
1467
|
-
expect(result).toContain('_bind(() => {')
|
|
1468
|
-
})
|
|
1469
|
-
|
|
1470
|
-
test('prop-derived used in non-JSX stays static', () => {
|
|
1471
|
-
// The variable is still captured — only JSX usage is inlined
|
|
1472
|
-
const result = t('function Comp(props) { const x = props.y; console.log(x); return <div>{x}</div> }')
|
|
1473
|
-
// console.log(x) uses the captured value — compiler doesn't touch it
|
|
1474
|
-
expect(result).toContain('console.log(x)')
|
|
1475
|
-
// JSX usage is inlined
|
|
1476
|
-
expect(result).toContain('props.y')
|
|
1477
|
-
expect(result).toContain('_bind')
|
|
1478
|
-
})
|
|
1479
|
-
|
|
1480
|
-
test('.map() callback params NOT treated as props', () => {
|
|
1481
|
-
const result = t('function App(props) { return <div>{tabs.map((tab) => { const C = tab.component; return <div><C /></div> })}</div> }')
|
|
1482
|
-
// tab is a callback param, not a component's props — should NOT be tracked
|
|
1483
|
-
expect(result).not.toContain('(tab.component)')
|
|
1484
|
-
})
|
|
1485
|
-
|
|
1486
|
-
test('prop read with ?? default used multiple times', () => {
|
|
1487
|
-
const result = t('function Comp(props) { const x = props.a ?? "def"; return <div class={x}>{x}</div> }')
|
|
1488
|
-
// Both uses should be inlined
|
|
1489
|
-
const matches = result.match(/props\.a \?\? "def"/g)
|
|
1490
|
-
expect(matches?.length).toBeGreaterThanOrEqual(2)
|
|
1491
|
-
})
|
|
1492
|
-
})
|
|
1493
|
-
|
|
1494
|
-
// ─── AST-based inlining edge cases ──────────────────────────────────────────
|
|
1495
|
-
|
|
1496
|
-
describe('JSX transform — AST inlining (template literals, ternaries)', () => {
|
|
1497
|
-
test('template literal with prop-derived var is inlined', () => {
|
|
1498
|
-
const result = t('function C(props) { const x = props.name; return <div>{`hello ${x}`}</div> }')
|
|
1499
|
-
expect(result).toContain('props.name')
|
|
1500
|
-
expect(result).toContain('_bind')
|
|
1501
|
-
})
|
|
1502
|
-
|
|
1503
|
-
test('ternary with prop-derived var is inlined', () => {
|
|
1504
|
-
const result = t('function C(props) { const v = props.x; return <div>{v ? "yes" : "no"}</div> }')
|
|
1505
|
-
expect(result).toContain('props.x')
|
|
1506
|
-
expect(result).toContain('? "yes" : "no"')
|
|
1507
|
-
})
|
|
1508
|
-
|
|
1509
|
-
test('both branches of ternary inlined when both are prop-derived', () => {
|
|
1510
|
-
const result = t('function C(props) { const a = props.x; const b = props.y; return <div>{a ? b : "none"}</div> }')
|
|
1511
|
-
expect(result).toContain('props.x')
|
|
1512
|
-
expect(result).toContain('props.y')
|
|
1513
|
-
})
|
|
1514
|
-
|
|
1515
|
-
test('concatenation with prop-derived inlined', () => {
|
|
1516
|
-
const result = t('function C(props) { const x = props.cls; return <div class={x + " extra"}></div> }')
|
|
1517
|
-
expect(result).toContain('props.cls')
|
|
1518
|
-
expect(result).toContain('" extra"')
|
|
1519
|
-
})
|
|
1520
|
-
|
|
1521
|
-
test('object property access on prop-derived NOT confused', () => {
|
|
1522
|
-
const result = t('function C(props) { const data = props.data; return <div>{data.name}</div> }')
|
|
1523
|
-
// data.name → (props.data).name — the .name is preserved, not replaced
|
|
1524
|
-
expect(result).toContain('props.data')
|
|
1525
|
-
expect(result).toContain('.name')
|
|
1526
|
-
})
|
|
1527
|
-
|
|
1528
|
-
test('deep transitive: c = b * 2, b = a + 1, a = props.x via AST', () => {
|
|
1529
|
-
const result = t('function C(props) { const a = props.x; const b = a + 1; const c = b * 2; return <div>{c}</div> }')
|
|
1530
|
-
expect(result).toContain('props.x')
|
|
1531
|
-
// Full chain resolved via AST visitor
|
|
1532
|
-
expect(result).toContain('_bind')
|
|
1533
|
-
})
|
|
1534
|
-
|
|
1535
|
-
test('array destructuring NOT tracked (only simple identifier)', () => {
|
|
1536
|
-
const result = t('function C(props) { const [a, b] = props.items; return <div>{a}</div> }')
|
|
1537
|
-
// Array destructuring is not a simple identifier — not tracked
|
|
1538
|
-
expect(result).not.toContain('(props.items)')
|
|
1539
|
-
})
|
|
1540
|
-
|
|
1541
|
-
test('computed property not confused with prop-derived', () => {
|
|
1542
|
-
const result = t('function C(props) { const key = props.key; return <div>{obj[key]}</div> }')
|
|
1543
|
-
expect(result).toContain('props.key')
|
|
1544
|
-
})
|
|
1545
|
-
|
|
1546
|
-
test('type cast "as" in prop-derived var does not crash compiler', () => {
|
|
1547
|
-
// Regression: const sel = state.selected() as string[] inside JSX arrow
|
|
1548
|
-
// caused ParenthesizedExpression to replace a BindingName, crashing ts.visitEachChild
|
|
1549
|
-
const result = t(`
|
|
1550
|
-
function C(props) {
|
|
1551
|
-
const items = props.data
|
|
1552
|
-
return <div>{() => {
|
|
1553
|
-
const sel = items as any
|
|
1554
|
-
return sel
|
|
1555
|
-
}}</div>
|
|
1556
|
-
}
|
|
1557
|
-
`)
|
|
1558
|
-
expect(result).toBeDefined()
|
|
1559
|
-
})
|
|
1560
|
-
|
|
1561
|
-
test('variable declaration name is not inlined by resolveExprTransitive', () => {
|
|
1562
|
-
const result = t(`
|
|
1563
|
-
function C(props) {
|
|
1564
|
-
const x = props.val
|
|
1565
|
-
const y = x
|
|
1566
|
-
return <div>{y}</div>
|
|
1567
|
-
}
|
|
1568
|
-
`)
|
|
1569
|
-
expect(result).toContain('props.val')
|
|
1570
|
-
})
|
|
1571
|
-
|
|
1572
|
-
test('parameter name matching prop-derived var does not crash', () => {
|
|
1573
|
-
// (val: string) => ... where "val" might match a prop-derived var
|
|
1574
|
-
const result = t(`
|
|
1575
|
-
function C(props) {
|
|
1576
|
-
const val = props.value
|
|
1577
|
-
return <div>{() => [1,2].map((val) => <span>{val}</span>)}</div>
|
|
1578
|
-
}
|
|
1579
|
-
`)
|
|
1580
|
-
expect(result).toBeDefined()
|
|
1581
|
-
})
|
|
1582
|
-
|
|
1583
|
-
test('catch clause variable matching prop-derived var does not crash', () => {
|
|
1584
|
-
const result = t(`
|
|
1585
|
-
function C(props) {
|
|
1586
|
-
const err = props.error
|
|
1587
|
-
return <div>{() => { try {} catch(err) { return err } }}</div>
|
|
1588
|
-
}
|
|
1589
|
-
`)
|
|
1590
|
-
expect(result).toBeDefined()
|
|
1591
|
-
})
|
|
1592
|
-
|
|
1593
|
-
test('binding element matching prop-derived var does not crash', () => {
|
|
1594
|
-
const result = t(`
|
|
1595
|
-
function C(props) {
|
|
1596
|
-
const x = props.x
|
|
1597
|
-
return <div>{() => { const { x } = obj; return x }}</div>
|
|
1598
|
-
}
|
|
1599
|
-
`)
|
|
1600
|
-
expect(result).toBeDefined()
|
|
1601
|
-
})
|
|
1602
|
-
|
|
1603
|
-
test('HTML entities in JSX text are not double-escaped', () => {
|
|
1604
|
-
const result = t('function C() { return <button>< prev</button> }')
|
|
1605
|
-
expect(result).toContain('<')
|
|
1606
|
-
expect(result).not.toContain('&lt;')
|
|
1607
|
-
})
|
|
1608
|
-
|
|
1609
|
-
test('mixed HTML entities and raw ampersands', () => {
|
|
1610
|
-
const result = t('function C() { return <span>A & B < C</span> }')
|
|
1611
|
-
expect(result).toContain('&')
|
|
1612
|
-
expect(result).toContain('<')
|
|
1613
|
-
expect(result).not.toContain('&amp;')
|
|
1614
|
-
expect(result).not.toContain('&lt;')
|
|
1615
|
-
})
|
|
1616
|
-
|
|
1617
|
-
test('props.children in template uses _mountSlot instead of createTextNode', () => {
|
|
1618
|
-
const result = t('function C(props) { return <div>{props.children}</div> }')
|
|
1619
|
-
expect(result).toContain('_mountSlot')
|
|
1620
|
-
expect(result).not.toContain('createTextNode')
|
|
1621
|
-
expect(result).not.toContain('.data')
|
|
1622
|
-
})
|
|
1623
|
-
|
|
1624
|
-
test('own.children in template uses _mountSlot', () => {
|
|
1625
|
-
const result = t('function C(props) { const own = props; return <label><input/>{own.children}</label> }')
|
|
1626
|
-
expect(result).toContain('_mountSlot')
|
|
1627
|
-
expect(result).toContain('own.children')
|
|
1628
|
-
})
|
|
1629
|
-
|
|
1630
|
-
test('non-children prop access still uses text node binding', () => {
|
|
1631
|
-
const result = t('function C(props) { return <div>{props.name}</div> }')
|
|
1632
|
-
expect(result).not.toContain('_mountSlot')
|
|
1633
|
-
expect(result).toContain('.data')
|
|
1634
|
-
})
|
|
1635
|
-
|
|
1636
|
-
test('signal() calls are NOT inlined as prop-derived vars', () => {
|
|
1637
|
-
const result = t(`
|
|
1638
|
-
function C(props) {
|
|
1639
|
-
const open = signal(props.defaultOpen ?? false)
|
|
1640
|
-
return <div>{() => open() ? 'yes' : 'no'}</div>
|
|
1641
|
-
}
|
|
1642
|
-
`)
|
|
1643
|
-
// open should be referenced as-is, NOT replaced with signal(props.defaultOpen ?? false)
|
|
1644
|
-
expect(result).toContain('open()')
|
|
1645
|
-
// signal() should appear only once — in the original declaration
|
|
1646
|
-
const signalMatches = result.match(/signal\(props\.defaultOpen/g)
|
|
1647
|
-
expect(signalMatches?.length ?? 0).toBe(1)
|
|
1648
|
-
})
|
|
1649
|
-
})
|
|
1650
|
-
|
|
1651
|
-
// ─── Circular reference safety ─────────────────────────────────────────────
|
|
1652
|
-
|
|
1653
|
-
describe('JSX transform — circular prop-derived var cycles do not crash', () => {
|
|
1654
|
-
// Before the fix (PR #204), resolveExprTransitive used a single
|
|
1655
|
-
// `excludeVar` parameter that only prevented immediate re-entry on the
|
|
1656
|
-
// same variable. Multi-step cycles (a → b → a) alternated between
|
|
1657
|
-
// the two identifiers and recursed infinitely, crashing the compiler
|
|
1658
|
-
// with "Maximum call stack size exceeded."
|
|
1659
|
-
//
|
|
1660
|
-
// The fix replaces `excludeVar` with a `visited: Set<string>` that
|
|
1661
|
-
// tracks the entire call stack. When a variable is already visited,
|
|
1662
|
-
// the identifier is left as-is (falls back to the captured const
|
|
1663
|
-
// value at runtime) and a compiler warning is emitted.
|
|
1664
|
-
|
|
1665
|
-
// Use transformJSX directly (not the `t` helper) so we can assert
|
|
1666
|
-
// on both the code AND the warnings.
|
|
1667
|
-
const full = (code: string) => transformJSX(code, 'input.tsx')
|
|
1668
|
-
|
|
1669
|
-
test('two-variable cycle: a ↔ b does not stack-overflow and emits warning', () => {
|
|
1670
|
-
const result = full(`
|
|
1671
|
-
function Comp(props) {
|
|
1672
|
-
const a = b + props.x;
|
|
1673
|
-
const b = a + 1;
|
|
1674
|
-
return <div>{a}</div>
|
|
1675
|
-
}
|
|
1676
|
-
`)
|
|
1677
|
-
// Should compile (not crash) and produce valid output
|
|
1678
|
-
expect(result.code).toBeDefined()
|
|
1679
|
-
expect(result.code).toContain('_tpl')
|
|
1680
|
-
|
|
1681
|
-
// Should emit a circular-prop-derived warning
|
|
1682
|
-
const cycleWarnings = result.warnings.filter((w) => w.code === 'circular-prop-derived')
|
|
1683
|
-
expect(cycleWarnings.length).toBeGreaterThanOrEqual(1)
|
|
1684
|
-
expect(cycleWarnings[0]?.message).toMatch(/Circular prop-derived/)
|
|
1685
|
-
})
|
|
1686
|
-
|
|
1687
|
-
test('three-variable cycle: a → b → c → a does not stack-overflow', () => {
|
|
1688
|
-
const result = full(`
|
|
1689
|
-
function Comp(props) {
|
|
1690
|
-
const a = c + props.x;
|
|
1691
|
-
const b = a + 1;
|
|
1692
|
-
const c = b + 2;
|
|
1693
|
-
return <div>{a}</div>
|
|
1694
|
-
}
|
|
1695
|
-
`)
|
|
1696
|
-
expect(result.code).toBeDefined()
|
|
1697
|
-
expect(result.code).toContain('_tpl')
|
|
1698
|
-
|
|
1699
|
-
const cycleWarnings = result.warnings.filter((w) => w.code === 'circular-prop-derived')
|
|
1700
|
-
expect(cycleWarnings.length).toBeGreaterThanOrEqual(1)
|
|
1701
|
-
})
|
|
1702
|
-
|
|
1703
|
-
test('self-referencing variable does not stack-overflow', () => {
|
|
1704
|
-
const result = full(`
|
|
1705
|
-
function Comp(props) {
|
|
1706
|
-
const a = a + props.x;
|
|
1707
|
-
return <div>{a}</div>
|
|
1708
|
-
}
|
|
1709
|
-
`)
|
|
1710
|
-
expect(result.code).toBeDefined()
|
|
1711
|
-
|
|
1712
|
-
const cycleWarnings = result.warnings.filter((w) => w.code === 'circular-prop-derived')
|
|
1713
|
-
expect(cycleWarnings.length).toBeGreaterThanOrEqual(1)
|
|
1714
|
-
expect(cycleWarnings[0]?.message).toContain('a')
|
|
1715
|
-
})
|
|
1716
|
-
|
|
1717
|
-
test('cycle still inlines the non-cyclic parts correctly', () => {
|
|
1718
|
-
// const a = b + props.x; const b = a + 1;
|
|
1719
|
-
// When resolving `a` for JSX: `b + props.x` → `b` is cycle-broken
|
|
1720
|
-
// (left as identifier `b`), `props.x` is inlined as `props.x`.
|
|
1721
|
-
const result = full(`
|
|
1722
|
-
function Comp(props) {
|
|
1723
|
-
const a = b + props.x;
|
|
1724
|
-
const b = a + 1;
|
|
1725
|
-
return <div>{a}</div>
|
|
1726
|
-
}
|
|
1727
|
-
`)
|
|
1728
|
-
// Non-cyclic part (props.x) is still inlined reactively
|
|
1729
|
-
expect(result.code).toContain('props.x')
|
|
1730
|
-
expect(result.code).toContain('_bind')
|
|
1731
|
-
// Cyclic identifier `b` is left as-is (not further resolved)
|
|
1732
|
-
// The _bind should reference `b` directly since it's the cycle-break point
|
|
1733
|
-
expect(result.code).toMatch(/\bb\b/)
|
|
1734
|
-
})
|
|
1735
|
-
|
|
1736
|
-
test('non-cyclic deep chain still works after the cycle fix', () => {
|
|
1737
|
-
// Regression guard: the visited-set fix must NOT break the existing
|
|
1738
|
-
// non-cyclic transitive resolution (a → b → c, no cycle).
|
|
1739
|
-
const result = full(`
|
|
1740
|
-
function Comp(props) {
|
|
1741
|
-
const a = props.x;
|
|
1742
|
-
const b = a + 1;
|
|
1743
|
-
const c = b * 2;
|
|
1744
|
-
return <div>{c}</div>
|
|
1745
|
-
}
|
|
1746
|
-
`)
|
|
1747
|
-
expect(result.code).toContain('props.x')
|
|
1748
|
-
expect(result.code).toContain('_bind')
|
|
1749
|
-
// c should be fully inlined — not left as a static reference
|
|
1750
|
-
expect(result.code).not.toMatch(/__t\d+\.data = c\b/)
|
|
1751
|
-
// No cycle warnings on a non-cyclic chain
|
|
1752
|
-
const cycleWarnings = result.warnings.filter((w) => w.code === 'circular-prop-derived')
|
|
1753
|
-
expect(cycleWarnings.length).toBe(0)
|
|
1754
|
-
})
|
|
1755
|
-
|
|
1756
|
-
test('warning message includes the cycle chain for debugging', () => {
|
|
1757
|
-
const result = full(`
|
|
1758
|
-
function Comp(props) {
|
|
1759
|
-
const a = b + props.x;
|
|
1760
|
-
const b = a + 1;
|
|
1761
|
-
return <div>{a}</div>
|
|
1762
|
-
}
|
|
1763
|
-
`)
|
|
1764
|
-
const cycleWarnings = result.warnings.filter((w) => w.code === 'circular-prop-derived')
|
|
1765
|
-
expect(cycleWarnings.length).toBeGreaterThanOrEqual(1)
|
|
1766
|
-
// The warning should mention both variables in the cycle chain
|
|
1767
|
-
const msg = cycleWarnings[0]!.message
|
|
1768
|
-
expect(msg).toContain('a')
|
|
1769
|
-
expect(msg).toContain('b')
|
|
1770
|
-
// And suggest how to fix it
|
|
1771
|
-
expect(msg).toContain('props.*')
|
|
1772
|
-
})
|
|
1773
|
-
|
|
1774
|
-
test('property access with name matching tracked var is NOT flagged as cycle', () => {
|
|
1775
|
-
// Regression: `own.beforeContentDirection` where `beforeContentDirection`
|
|
1776
|
-
// is ALSO a tracked const used to trigger a false-positive self-cycle
|
|
1777
|
-
// warning. The property name identifier happens to match a prop-derived
|
|
1778
|
-
// var name, but it's not a reference — it's a property name. The
|
|
1779
|
-
// declaration-position check MUST run before the cycle check to
|
|
1780
|
-
// skip property names, binding names, and shorthand keys.
|
|
1781
|
-
//
|
|
1782
|
-
// This pattern is extremely common in Elements component.tsx and
|
|
1783
|
-
// similar splitProps-heavy destructuring codebases.
|
|
1784
|
-
const result = full(`
|
|
1785
|
-
function Comp(props) {
|
|
1786
|
-
const [own, rest] = splitProps(props, ['beforeContentDirection'])
|
|
1787
|
-
const defaultDirection = 'inline'
|
|
1788
|
-
const beforeContentDirection = own.beforeContentDirection ?? defaultDirection
|
|
1789
|
-
return <div data-dir={beforeContentDirection}>hello</div>
|
|
1790
|
-
}
|
|
1791
|
-
`)
|
|
1792
|
-
const cycleWarnings = result.warnings.filter((w) => w.code === 'circular-prop-derived')
|
|
1793
|
-
// No cycle — the property name `beforeContentDirection` in
|
|
1794
|
-
// `own.beforeContentDirection` is a property access, not a reference.
|
|
1795
|
-
expect(cycleWarnings.length).toBe(0)
|
|
1796
|
-
// And the inlining should still work — props.beforeContentDirection
|
|
1797
|
-
// appears in the bind (via own destructure from splitProps).
|
|
1798
|
-
expect(result.code).toContain('beforeContentDirection')
|
|
1799
|
-
})
|
|
1800
|
-
|
|
1801
|
-
test('shorthand property key matching tracked var is NOT flagged as cycle', () => {
|
|
1802
|
-
// Another false-positive shape: `{ foo }` shorthand in an object
|
|
1803
|
-
// literal where `foo` is a tracked var. The shorthand key would
|
|
1804
|
-
// match propDerivedVars but its parent is ShorthandPropertyAssignment.
|
|
1805
|
-
const result = full(`
|
|
1806
|
-
function Comp(props) {
|
|
1807
|
-
const foo = props.x
|
|
1808
|
-
const config = { foo }
|
|
1809
|
-
return <div data-config={JSON.stringify(config)}>{foo}</div>
|
|
1810
|
-
}
|
|
1811
|
-
`)
|
|
1812
|
-
const cycleWarnings = result.warnings.filter((w) => w.code === 'circular-prop-derived')
|
|
1813
|
-
expect(cycleWarnings.length).toBe(0)
|
|
1814
|
-
})
|
|
1815
|
-
})
|
|
1816
|
-
|
|
1817
|
-
describe('JSX transform — SSR mode', () => {
|
|
1818
|
-
test('skips _tpl emission for SSR builds — falls back to plain JSX (h() via runtime)', () => {
|
|
1819
|
-
const code = `function Btn() { return <button onClick={() => null}>Click {() => x()}</button> }`
|
|
1820
|
-
const ssr = transformJSX(code, 'btn.tsx', { ssr: true }).code
|
|
1821
|
-
const dom = transformJSX(code, 'btn.tsx').code
|
|
1822
|
-
|
|
1823
|
-
// Client (default) build uses the template fast path
|
|
1824
|
-
expect(dom).toContain('_tpl(')
|
|
1825
|
-
expect(dom).toContain('@pyreon/runtime-dom')
|
|
1826
|
-
|
|
1827
|
-
// SSR build emits plain JSX (the runtime's JSX automatic transform turns
|
|
1828
|
-
// it into jsx() / h() calls that runtime-server can walk to a string)
|
|
1829
|
-
expect(ssr).not.toContain('_tpl(')
|
|
1830
|
-
expect(ssr).not.toContain('@pyreon/runtime-dom')
|
|
1831
|
-
expect(ssr).toContain('<button')
|
|
1832
|
-
})
|
|
1833
|
-
|
|
1834
|
-
test('default (no options) still emits _tpl — backwards compatible', () => {
|
|
1835
|
-
const code = `function X() { return <div>hi</div> }`
|
|
1836
|
-
const out = transformJSX(code, 'x.tsx').code
|
|
1837
|
-
expect(out).toContain('_tpl(')
|
|
1838
|
-
})
|
|
1839
|
-
})
|
|
1840
|
-
|
|
1841
|
-
// ─── Signal auto-call in JSX ────────────────────────────────────────────────
|
|
1842
|
-
|
|
1843
|
-
describe('JSX transform — signal auto-call', () => {
|
|
1844
|
-
test('bare signal in text child is auto-called', () => {
|
|
1845
|
-
const result = t('function C() { const name = signal("Vít"); return <div>{name}</div> }')
|
|
1846
|
-
expect(result).toContain('name()')
|
|
1847
|
-
expect(result).toContain('_bind')
|
|
1848
|
-
})
|
|
1849
|
-
|
|
1850
|
-
test('signal in attribute expression is auto-called', () => {
|
|
1851
|
-
const result = t('function C() { const show = signal(false); return <div class={show ? "active" : ""}></div> }')
|
|
1852
|
-
expect(result).toContain('show()')
|
|
1853
|
-
expect(result).toContain('_bind')
|
|
1854
|
-
})
|
|
1855
|
-
|
|
1856
|
-
test('signal already called is NOT double-called', () => {
|
|
1857
|
-
const result = t('function C() { const count = signal(0); return <div>{count()}</div> }')
|
|
1858
|
-
expect(result).not.toContain('count()()')
|
|
1859
|
-
expect(result).toContain('count')
|
|
1860
|
-
})
|
|
1861
|
-
|
|
1862
|
-
test('signal in ternary is auto-called', () => {
|
|
1863
|
-
const result = t('function C() { const show = signal(false); return <div>{show ? "yes" : "no"}</div> }')
|
|
1864
|
-
expect(result).toContain('show()')
|
|
1865
|
-
expect(result).toContain('? "yes" : "no"')
|
|
1866
|
-
})
|
|
1867
|
-
|
|
1868
|
-
test('signal in template literal is auto-called', () => {
|
|
1869
|
-
const result = t('function C() { const name = signal("world"); return <div>{`hello ${name}`}</div> }')
|
|
1870
|
-
expect(result).toContain('name()')
|
|
1871
|
-
})
|
|
1872
|
-
|
|
1873
|
-
test('signal in component prop is auto-called with _rp', () => {
|
|
1874
|
-
const result = t('function C() { const val = signal(42); return <MyComp value={val} /> }')
|
|
1875
|
-
expect(result).toContain('_rp(() => val())')
|
|
1876
|
-
})
|
|
1877
|
-
|
|
1878
|
-
test('multiple signals in one expression are all auto-called', () => {
|
|
1879
|
-
const result = t('function C() { const a = signal(1); const b = signal(2); return <div>{a + b}</div> }')
|
|
1880
|
-
expect(result).toContain('a()')
|
|
1881
|
-
expect(result).toContain('b()')
|
|
1882
|
-
})
|
|
1883
|
-
|
|
1884
|
-
test('signal in conditional attribute is auto-called', () => {
|
|
1885
|
-
const result = t('function C() { const active = signal(false); return <div title={active ? "on" : "off"}></div> }')
|
|
1886
|
-
expect(result).toContain('active()')
|
|
1887
|
-
})
|
|
1888
|
-
|
|
1889
|
-
test('non-signal const is NOT auto-called', () => {
|
|
1890
|
-
const result = t('function C() { const x = 42; return <div>{x}</div> }')
|
|
1891
|
-
expect(result).not.toContain('x()')
|
|
1892
|
-
})
|
|
1893
|
-
|
|
1894
|
-
test('computed() IS auto-called (same callable pattern as signal)', () => {
|
|
1895
|
-
const result = t('function C() { const doubled = computed(() => 2); return <div>{doubled}</div> }')
|
|
1896
|
-
expect(result).toContain('doubled()')
|
|
1897
|
-
expect(result).toContain('_bind')
|
|
1898
|
-
})
|
|
1899
|
-
|
|
1900
|
-
test('computed already called is NOT double-called', () => {
|
|
1901
|
-
const result = t('function C() { const doubled = computed(() => 2); return <div>{doubled()}</div> }')
|
|
1902
|
-
expect(result).not.toContain('doubled()()')
|
|
1903
|
-
})
|
|
1904
|
-
|
|
1905
|
-
test('signal + computed in same expression both auto-called', () => {
|
|
1906
|
-
const result = t('function C() { const count = signal(0); const doubled = computed(() => count() * 2); return <div>{count} + {doubled}</div> }')
|
|
1907
|
-
expect(result).toContain('.data = count()')
|
|
1908
|
-
expect(result).toContain('.data = doubled()')
|
|
1909
|
-
})
|
|
1910
|
-
|
|
1911
|
-
test('signal in arrow function child is NOT auto-called (already reactive)', () => {
|
|
1912
|
-
const result = t('function C() { const count = signal(0); return <div>{() => count()}</div> }')
|
|
1913
|
-
// The arrow function is already reactive — no auto-call on the inner count
|
|
1914
|
-
expect(result).not.toContain('count()()')
|
|
1915
|
-
})
|
|
1916
|
-
|
|
1917
|
-
test('signal used in non-JSX context is NOT modified', () => {
|
|
1918
|
-
const result = t('function C() { const x = signal(0); console.log(x); return <div>{x}</div> }')
|
|
1919
|
-
// console.log(x) should keep bare x, only JSX usage gets auto-called
|
|
1920
|
-
expect(result).toContain('console.log(x)')
|
|
1921
|
-
// But JSX usage gets auto-called
|
|
1922
|
-
expect(result).toContain('.data = x()')
|
|
1923
|
-
})
|
|
1924
|
-
|
|
1925
|
-
test('signal as event handler value IS auto-called (unwraps to the handler fn)', () => {
|
|
1926
|
-
const result = t('function C() { const handler = signal(() => {}); return <div onClick={handler}></div> }')
|
|
1927
|
-
// onClick={handler} where handler is a signal → handler() unwraps to the function
|
|
1928
|
-
// This is correct — the event listener gets the unwrapped function value
|
|
1929
|
-
expect(result).toContain('handler()')
|
|
1930
|
-
})
|
|
1931
|
-
|
|
1932
|
-
test('module-scope signal IS tracked and auto-called', () => {
|
|
1933
|
-
const result = t('const globalSig = signal(0); function C() { return <div>{globalSig}</div> }')
|
|
1934
|
-
// Module-scope signal declarations are tracked by the single-pass walk
|
|
1935
|
-
expect(result).toContain('globalSig()')
|
|
1936
|
-
})
|
|
1937
|
-
|
|
1938
|
-
test('knownSignals option enables cross-module auto-call', () => {
|
|
1939
|
-
const code = 'import { count } from "./store"; function App() { return <div>{count}</div> }'
|
|
1940
|
-
const result = transformJSX(code, 'test.tsx', { knownSignals: ['count'] }).code
|
|
1941
|
-
expect(result).toContain('count()')
|
|
1942
|
-
expect(result).toContain('_bind')
|
|
1943
|
-
})
|
|
1944
|
-
|
|
1945
|
-
test('knownSignals with alias — local name is used', () => {
|
|
1946
|
-
const code = 'import { count as c } from "./store"; function App() { return <div>{c}</div> }'
|
|
1947
|
-
const result = transformJSX(code, 'test.tsx', { knownSignals: ['c'] }).code
|
|
1948
|
-
expect(result).toContain('c()')
|
|
1949
|
-
})
|
|
1950
|
-
|
|
1951
|
-
test('knownSignals does not double-call already-called signals', () => {
|
|
1952
|
-
const code = 'import { count } from "./store"; function App() { return <div>{count()}</div> }'
|
|
1953
|
-
const result = transformJSX(code, 'test.tsx', { knownSignals: ['count'] }).code
|
|
1954
|
-
expect(result).not.toContain('count()()')
|
|
1955
|
-
})
|
|
1956
|
-
|
|
1957
|
-
test('knownSignals respects scope shadowing', () => {
|
|
1958
|
-
const code = 'import { count } from "./store"; function App() { const count = "shadow"; return <div>{count}</div> }'
|
|
1959
|
-
const result = transformJSX(code, 'test.tsx', { knownSignals: ['count'] }).code
|
|
1960
|
-
expect(result).not.toContain('.data = count()')
|
|
1961
|
-
})
|
|
1962
|
-
|
|
1963
|
-
test('props.x is still inlined alongside signal auto-call', () => {
|
|
1964
|
-
const result = t('function C(props) { const show = signal(false); const label = props.label; return <div class={show ? label : "default"}></div> }')
|
|
1965
|
-
expect(result).toContain('show()')
|
|
1966
|
-
expect(result).toContain('props.label')
|
|
1967
|
-
})
|
|
1968
|
-
|
|
1969
|
-
// Regression: signal-method calls were getting double-wrapped — the
|
|
1970
|
-
// auto-call inserted `()` after the bare signal reference inside
|
|
1971
|
-
// `signal.set(value)`, producing `signal().set(value)`. That calls
|
|
1972
|
-
// the signal (returns its current value, e.g. a string) then tries
|
|
1973
|
-
// `.set` on the string (undefined → TypeError). Every `signal.set`,
|
|
1974
|
-
// `signal.peek`, `signal.update` call inside event handlers / hot
|
|
1975
|
-
// paths was silently broken.
|
|
1976
|
-
test('signal.set() in event handler does NOT auto-call the bare signal reference', () => {
|
|
1977
|
-
const result = t(
|
|
1978
|
-
'function C() { const value = signal(""); return <input onInput={(e) => value.set(e.target.value)} /> }',
|
|
1979
|
-
)
|
|
1980
|
-
// Must keep `value.set(...)` — NOT `value().set(...)`.
|
|
1981
|
-
expect(result).toContain('value.set(e.target.value)')
|
|
1982
|
-
expect(result).not.toContain('value().set')
|
|
1983
|
-
})
|
|
1984
|
-
|
|
1985
|
-
test('signal.peek() does NOT auto-call', () => {
|
|
1986
|
-
const result = t(
|
|
1987
|
-
'function C() { const count = signal(0); return <button onClick={() => console.log(count.peek())}>x</button> }',
|
|
1988
|
-
)
|
|
1989
|
-
expect(result).toContain('count.peek()')
|
|
1990
|
-
expect(result).not.toContain('count().peek')
|
|
1991
|
-
})
|
|
1992
|
-
|
|
1993
|
-
test('signal.update() does NOT auto-call', () => {
|
|
1994
|
-
const result = t(
|
|
1995
|
-
'function C() { const count = signal(0); return <button onClick={() => count.update(n => n + 1)}>+</button> }',
|
|
1996
|
-
)
|
|
1997
|
-
expect(result).toContain('count.update(')
|
|
1998
|
-
expect(result).not.toContain('count().update')
|
|
1999
|
-
})
|
|
2000
|
-
|
|
2001
|
-
// Bare member-access on a signal (no call) STILL auto-calls — preserves
|
|
2002
|
-
// the existing convention where a signal containing an object can be
|
|
2003
|
-
// dereferenced via `signalContainingObj.someProp` (compiles to
|
|
2004
|
-
// `signalContainingObj().someProp`). Only the CALLED form
|
|
2005
|
-
// (`signal.method(...)`) skips the auto-call. See findSignalIdents
|
|
2006
|
-
// in jsx.ts for the rationale.
|
|
2007
|
-
test('signal.someProp (bare member access) DOES auto-call', () => {
|
|
2008
|
-
const result = t(
|
|
2009
|
-
'function C() { const data = signal({ count: 0 }); return <div>{data.count}</div> }',
|
|
2010
|
-
)
|
|
2011
|
-
expect(result).toContain('data().count')
|
|
2012
|
-
})
|
|
2013
|
-
|
|
2014
|
-
// The bare signal reference STILL auto-calls in JSX text — make sure
|
|
2015
|
-
// the fix doesn't over-correct.
|
|
2016
|
-
test('bare signal in JSX text still auto-calls (fix does not over-correct)', () => {
|
|
2017
|
-
const result = t('function C() { const count = signal(0); return <div>{count}</div> }')
|
|
2018
|
-
expect(result).toContain('count()')
|
|
2019
|
-
})
|
|
2020
|
-
|
|
2021
|
-
// ── JSX text/expression whitespace (regression) ─────────────────────
|
|
2022
|
-
// The compiler used `.replace(/\n\s*/g, '').trim()` on JSX text which
|
|
2023
|
-
// stripped ALL leading/trailing whitespace — even spaces adjacent to
|
|
2024
|
-
// expressions on the same line. So `<p>doubled: {x}</p>` produced
|
|
2025
|
-
// `<p>doubled:</p>` + appended text node, rendering "doubled:0"
|
|
2026
|
-
// instead of "doubled: 0". Same class for `<p>{x} remaining</p>` →
|
|
2027
|
-
// text "remaining" loses its leading space, rendering as "Xremaining".
|
|
2028
|
-
// Fix: only strip whitespace adjacent to newlines (multi-line JSX
|
|
2029
|
-
// formatting), preserve same-line whitespace adjacent to expressions.
|
|
2030
|
-
test('preserves trailing space in JSX text before expression on same line', () => {
|
|
2031
|
-
const result = t('<p>doubled: {x()}</p>')
|
|
2032
|
-
// The static text portion of the template must keep "doubled: "
|
|
2033
|
-
// (with trailing space) so the appended expression value renders
|
|
2034
|
-
// as "doubled: 0", not "doubled:0".
|
|
2035
|
-
expect(result).toContain('doubled: ')
|
|
2036
|
-
})
|
|
2037
|
-
|
|
2038
|
-
test('preserves leading space in JSX text after expression on same line', () => {
|
|
2039
|
-
const result = t('<p>{x()} remaining</p>')
|
|
2040
|
-
// Static portion must include " remaining" (with leading space).
|
|
2041
|
-
expect(result).toContain(' remaining')
|
|
2042
|
-
})
|
|
2043
|
-
|
|
2044
|
-
test('strips multi-line JSX text whitespace adjacent to newlines', () => {
|
|
2045
|
-
// Multi-line JSX with indentation should still collapse — only
|
|
2046
|
-
// SAME-LINE whitespace adjacent to expressions is preserved.
|
|
2047
|
-
const result = t(`<div>
|
|
2048
|
-
<span>hello</span>
|
|
2049
|
-
</div>`)
|
|
2050
|
-
// The newlines + indentation should not produce stray text nodes.
|
|
2051
|
-
expect(result).toContain('hello')
|
|
2052
|
-
expect(result).not.toContain('"\\n "')
|
|
2053
|
-
})
|
|
2054
|
-
|
|
2055
|
-
test('shadowed signal variable by const is NOT auto-called', () => {
|
|
2056
|
-
const result = t(`
|
|
2057
|
-
function App() {
|
|
2058
|
-
const show = signal(false)
|
|
2059
|
-
function Inner() {
|
|
2060
|
-
const show = 'not a signal'
|
|
2061
|
-
return <div>{show}</div>
|
|
2062
|
-
}
|
|
2063
|
-
return <div>{show}</div>
|
|
2064
|
-
}
|
|
2065
|
-
`)
|
|
2066
|
-
// Inner's show is a plain string, NOT a signal — should NOT be auto-called
|
|
2067
|
-
// But App's show IS a signal — should be auto-called
|
|
2068
|
-
expect(result).toContain('show()') // App's usage
|
|
2069
|
-
expect(result).toContain('textContent = show') // Inner's usage (static)
|
|
2070
|
-
})
|
|
2071
|
-
|
|
2072
|
-
test('function parameter shadowing signal is NOT auto-called', () => {
|
|
2073
|
-
const result = t(`
|
|
2074
|
-
function App() {
|
|
2075
|
-
const count = signal(0)
|
|
2076
|
-
function Display(count) {
|
|
2077
|
-
return <div>{count}</div>
|
|
2078
|
-
}
|
|
2079
|
-
return <div>{count}</div>
|
|
2080
|
-
}
|
|
2081
|
-
`)
|
|
2082
|
-
// Display's count is a parameter, not the signal
|
|
2083
|
-
expect(result).toContain('textContent = count') // Display: static
|
|
2084
|
-
expect(result).toContain('.data = count()') // App: auto-called
|
|
2085
|
-
})
|
|
2086
|
-
|
|
2087
|
-
test('destructured parameter shadowing signal is NOT auto-called', () => {
|
|
2088
|
-
const result = t(`
|
|
2089
|
-
function App() {
|
|
2090
|
-
const name = signal('Vít')
|
|
2091
|
-
function Greet({ name }) {
|
|
2092
|
-
return <div>{name}</div>
|
|
2093
|
-
}
|
|
2094
|
-
return <div>{name}</div>
|
|
2095
|
-
}
|
|
2096
|
-
`)
|
|
2097
|
-
// Greet's name is destructured from props — shadows the signal
|
|
2098
|
-
expect(result).toContain('textContent = name') // Greet: static
|
|
2099
|
-
expect(result).toContain('.data = name()') // App: auto-called
|
|
2100
|
-
})
|
|
2101
|
-
|
|
2102
|
-
test('signal in outer scope is auto-called when NOT shadowed', () => {
|
|
2103
|
-
const result = t(`
|
|
2104
|
-
function App() {
|
|
2105
|
-
const name = signal('Vít')
|
|
2106
|
-
function Inner() {
|
|
2107
|
-
return <div>{name}</div>
|
|
2108
|
-
}
|
|
2109
|
-
return <div>{name}</div>
|
|
2110
|
-
}
|
|
2111
|
-
`)
|
|
2112
|
-
// name is NOT shadowed in Inner — auto-called in both
|
|
2113
|
-
const autoCallCount = (result.match(/name\(\)/g) || []).length
|
|
2114
|
-
expect(autoCallCount).toBeGreaterThanOrEqual(2)
|
|
2115
|
-
})
|
|
2116
|
-
|
|
2117
|
-
test('array destructured parameter shadowing signal is NOT auto-called', () => {
|
|
2118
|
-
const result = t(`
|
|
2119
|
-
function App() {
|
|
2120
|
-
const item = signal('x')
|
|
2121
|
-
function Inner([item]) {
|
|
2122
|
-
return <div>{item}</div>
|
|
2123
|
-
}
|
|
2124
|
-
return <div>{item}</div>
|
|
2125
|
-
}
|
|
2126
|
-
`)
|
|
2127
|
-
// Inner's item is array-destructured — shadows the signal
|
|
2128
|
-
expect(result).toContain('textContent = item') // Inner: static
|
|
2129
|
-
expect(result).toContain('.data = item()') // App: auto-called
|
|
2130
|
-
})
|
|
2131
|
-
|
|
2132
|
-
test('signal re-declared as signal in inner scope is still auto-called', () => {
|
|
2133
|
-
const result = t(`
|
|
2134
|
-
function App() {
|
|
2135
|
-
const count = signal(0)
|
|
2136
|
-
function Inner() {
|
|
2137
|
-
const count = signal(10)
|
|
2138
|
-
return <div>{count}</div>
|
|
2139
|
-
}
|
|
2140
|
-
return <div>{count}</div>
|
|
2141
|
-
}
|
|
2142
|
-
`)
|
|
2143
|
-
// Both are signal() calls — both should be auto-called
|
|
2144
|
-
const autoCallCount = (result.match(/count\(\)/g) || []).length
|
|
2145
|
-
expect(autoCallCount).toBeGreaterThanOrEqual(2)
|
|
2146
|
-
})
|
|
2147
|
-
|
|
2148
|
-
test('signal shadowing does not leak across sibling functions', () => {
|
|
2149
|
-
const result = t(`
|
|
2150
|
-
function App() {
|
|
2151
|
-
const show = signal(false)
|
|
2152
|
-
function A() {
|
|
2153
|
-
const show = 'text'
|
|
2154
|
-
return <div>{show}</div>
|
|
2155
|
-
}
|
|
2156
|
-
function B() {
|
|
2157
|
-
return <div>{show}</div>
|
|
2158
|
-
}
|
|
2159
|
-
return <div>{show}</div>
|
|
2160
|
-
}
|
|
2161
|
-
`)
|
|
2162
|
-
// A shadows show — static
|
|
2163
|
-
expect(result).toContain('textContent = show')
|
|
2164
|
-
// B does NOT shadow — auto-called
|
|
2165
|
-
// App does NOT shadow — auto-called
|
|
2166
|
-
const autoCallCount = (result.match(/show\(\)/g) || []).length
|
|
2167
|
-
expect(autoCallCount).toBeGreaterThanOrEqual(2)
|
|
2168
|
-
})
|
|
2169
|
-
|
|
2170
|
-
test('signal in deeply nested expression is auto-called', () => {
|
|
2171
|
-
const result = t('function C() { const x = signal(1); return <div>{x + x + x}</div> }')
|
|
2172
|
-
// All three references should be auto-called
|
|
2173
|
-
const autoCallCount = (result.match(/x\(\)/g) || []).length
|
|
2174
|
-
expect(autoCallCount).toBe(3)
|
|
2175
|
-
})
|
|
2176
|
-
|
|
2177
|
-
test('signal in object property value (not key) is auto-called', () => {
|
|
2178
|
-
const result = t('function C() { const x = signal(1); return <MyComp data={{ value: x }} /> }')
|
|
2179
|
-
expect(result).toContain('x()')
|
|
2180
|
-
expect(result).toContain('_rp(')
|
|
2181
|
-
})
|
|
2182
|
-
|
|
2183
|
-
test('signal + prop-derived in same expression both resolved', () => {
|
|
2184
|
-
const result = t('function C(props) { const x = signal(0); const label = props.label; return <div>{x ? label : "none"}</div> }')
|
|
2185
|
-
expect(result).toContain('x()')
|
|
2186
|
-
expect(result).toContain('props.label')
|
|
2187
|
-
expect(result).toContain('_bind')
|
|
2188
|
-
})
|
|
2189
|
-
|
|
2190
|
-
test('signal with no init (const x = signal()) tracked', () => {
|
|
2191
|
-
const result = t('function C() { const x = signal(); return <div>{x}</div> }')
|
|
2192
|
-
expect(result).toContain('x()')
|
|
2193
|
-
})
|
|
2194
|
-
|
|
2195
|
-
test('signal in member expression property position is NOT auto-called', () => {
|
|
2196
|
-
const result = t('function C() { const x = signal(0); return <div>{obj.x}</div> }')
|
|
2197
|
-
// x as a property name is not a signal reference
|
|
2198
|
-
expect(result).not.toContain('obj.x()')
|
|
2199
|
-
})
|
|
2200
|
-
|
|
2201
|
-
test('signal as member expression object IS auto-called', () => {
|
|
2202
|
-
const result = t('function C() { const x = signal({ a: 1 }); return <div>{x.a}</div> }')
|
|
2203
|
-
// x is the object, should be auto-called
|
|
2204
|
-
expect(result).toContain('x().a')
|
|
2205
|
-
})
|
|
2206
|
-
|
|
2207
|
-
test('const declared without init is not tracked as signal', () => {
|
|
2208
|
-
const result = t('function C() { const x = signal(0); function Inner() { let x; return <div>{x}</div> } return <div>{x}</div> }')
|
|
2209
|
-
// Inner's x is let, not tracked. App's x is signal
|
|
2210
|
-
expect(result).toContain('.data = x()')
|
|
2211
|
-
})
|
|
2212
|
-
|
|
2213
|
-
test('signal shadowed by let declaration in inner scope', () => {
|
|
2214
|
-
const result = t(`
|
|
2215
|
-
function C() {
|
|
2216
|
-
const show = signal(false)
|
|
2217
|
-
function Inner() {
|
|
2218
|
-
let show = true
|
|
2219
|
-
return <div>{show}</div>
|
|
2220
|
-
}
|
|
2221
|
-
return <div>{show}</div>
|
|
2222
|
-
}
|
|
2223
|
-
`)
|
|
2224
|
-
// Inner's let show shadows the signal — but let is not tracked by the declarator check
|
|
2225
|
-
// (let is not const so it's not in signalVars, but it's also not tracked as shadow)
|
|
2226
|
-
// Actually findShadowingNames only checks top-level VariableDeclaration declarations
|
|
2227
|
-
// let is VariableDeclaration kind=let — it should shadow
|
|
2228
|
-
expect(result).toContain('.data = show()') // outer: auto-called
|
|
2229
|
-
})
|
|
2230
|
-
|
|
2231
|
-
test('knownSignals with empty array does not crash', () => {
|
|
2232
|
-
const code = 'function App() { return <div>hello</div> }'
|
|
2233
|
-
const result = transformJSX(code, 'test.tsx', { knownSignals: [] }).code
|
|
2234
|
-
expect(result).toContain('hello')
|
|
2235
|
-
})
|
|
2236
|
-
|
|
2237
|
-
test('knownSignals combined with local signal declarations', () => {
|
|
2238
|
-
const code = 'import { count } from "./store"; function App() { const local = signal(0); return <div>{count}{local}</div> }'
|
|
2239
|
-
const result = transformJSX(code, 'test.tsx', { knownSignals: ['count'] }).code
|
|
2240
|
-
// Both the imported signal (via knownSignals) and the local signal should be auto-called
|
|
2241
|
-
expect(result).toContain('count()')
|
|
2242
|
-
expect(result).toContain('local()')
|
|
2243
|
-
})
|
|
2244
|
-
|
|
2245
|
-
test('knownSignals with default import name', () => {
|
|
2246
|
-
// When a default import resolves to a signal, the local name should be auto-called
|
|
2247
|
-
const code = 'import count from "./store"; function App() { return <div>{count}</div> }'
|
|
2248
|
-
const result = transformJSX(code, 'test.tsx', { knownSignals: ['count'] }).code
|
|
2249
|
-
expect(result).toContain('count()')
|
|
2250
|
-
expect(result).toContain('_bind')
|
|
2251
|
-
})
|
|
2252
|
-
})
|
|
2253
|
-
|
|
2254
|
-
// ─── Additional branch coverage for >= 90% ─────────────────────────────────
|
|
2255
|
-
|
|
2256
|
-
describe('JSX transform — template reactive style _bindDirect path', () => {
|
|
2257
|
-
test('reactive style accessor uses _bindDirect with cssText updater', () => {
|
|
2258
|
-
const result = t('<div style={getStyle()}><span /></div>')
|
|
2259
|
-
expect(result).toContain('_bindDirect(getStyle,')
|
|
2260
|
-
expect(result).toContain('style.cssText')
|
|
2261
|
-
})
|
|
2262
|
-
|
|
2263
|
-
test('reactive style accessor with object check in updater', () => {
|
|
2264
|
-
const result = t('<div style={styleSignal()}><span /></div>')
|
|
2265
|
-
expect(result).toContain('_bindDirect(styleSignal,')
|
|
2266
|
-
// The updater should handle both string and object
|
|
2267
|
-
expect(result).toContain('typeof v === "string"')
|
|
2268
|
-
expect(result).toContain('Object.assign')
|
|
2269
|
-
})
|
|
2270
|
-
})
|
|
2271
|
-
|
|
2272
|
-
// ── DOM-property assignment for value/checked/etc. (regression) ─────────
|
|
2273
|
-
// The compiler used `setAttribute("value", v)` for ALL non-class/style
|
|
2274
|
-
// attributes. For inputs that's wrong: `value` is a live DOM property,
|
|
2275
|
-
// `setAttribute` only sets the initial attribute. After the user types,
|
|
2276
|
-
// the property and attribute drift. Then `input.set('')` runs the
|
|
2277
|
-
// _bindDirect updater — which only resets the attribute, leaving the
|
|
2278
|
-
// stale typed text in the visible field. Same for `checked` on
|
|
2279
|
-
// checkboxes (presence of the attribute means checked, regardless of
|
|
2280
|
-
// value). Fix: emit property assignment for known DOM properties.
|
|
2281
|
-
describe('JSX transform — DOM properties use property assignment', () => {
|
|
2282
|
-
test('reactive value on input emits property assignment, not setAttribute', () => {
|
|
2283
|
-
const result = t('<div><input value={() => input()} /></div>')
|
|
2284
|
-
// Should be `el.value = v`, not `setAttribute("value", ...)`
|
|
2285
|
-
expect(result).toContain('.value = v')
|
|
2286
|
-
expect(result).not.toContain('setAttribute("value"')
|
|
2287
|
-
})
|
|
2288
|
-
|
|
2289
|
-
test('reactive checked on input emits property assignment', () => {
|
|
2290
|
-
const result = t('<div><input checked={done()} /></div>')
|
|
2291
|
-
expect(result).toContain('.checked = v')
|
|
2292
|
-
expect(result).not.toContain('setAttribute("checked"')
|
|
2293
|
-
})
|
|
2294
|
-
|
|
2295
|
-
test('static-call value on input emits property assignment', () => {
|
|
2296
|
-
// Non-signal-direct dynamic expression goes through reactiveBindExprs
|
|
2297
|
-
const result = t('<div><input value={x.y} /></div>')
|
|
2298
|
-
expect(result).toContain('.value = x.y')
|
|
2299
|
-
expect(result).not.toContain('setAttribute("value"')
|
|
2300
|
-
})
|
|
2301
|
-
|
|
2302
|
-
test('selected on option emits property assignment', () => {
|
|
2303
|
-
const result = t('<div><option selected={isSelected()}>x</option></div>')
|
|
2304
|
-
expect(result).toContain('.selected = v')
|
|
2305
|
-
expect(result).not.toContain('setAttribute("selected"')
|
|
2306
|
-
})
|
|
2307
|
-
|
|
2308
|
-
test('disabled on button emits property assignment', () => {
|
|
2309
|
-
const result = t('<div><button disabled={isDisabled()}>x</button></div>')
|
|
2310
|
-
expect(result).toContain('.disabled = v')
|
|
2311
|
-
expect(result).not.toContain('setAttribute("disabled"')
|
|
2312
|
-
})
|
|
2313
|
-
|
|
2314
|
-
test('non-DOM-prop attribute still uses setAttribute', () => {
|
|
2315
|
-
// placeholder is a real attribute, not a property-divergent IDL prop
|
|
2316
|
-
const result = t('<div><input placeholder={msg()} /></div>')
|
|
2317
|
-
expect(result).toContain('setAttribute("placeholder"')
|
|
2318
|
-
})
|
|
2319
|
-
})
|
|
2320
|
-
|
|
2321
|
-
describe('JSX transform — template combined _bind for complex expressions', () => {
|
|
2322
|
-
test('complex attribute expression uses combined _bind', () => {
|
|
2323
|
-
const result = t('<div class={`${a()} ${b()}`}><span /></div>')
|
|
2324
|
-
expect(result).toContain('_bind(() => {')
|
|
2325
|
-
expect(result).toContain('className')
|
|
2326
|
-
})
|
|
2327
|
-
|
|
2328
|
-
test('dynamic spread in template uses _applyProps in reactive _bind', () => {
|
|
2329
|
-
const result = t('<div {...getProps()}><span /></div>')
|
|
2330
|
-
expect(result).toContain('_tpl(')
|
|
2331
|
-
expect(result).toContain('_applyProps')
|
|
2332
|
-
expect(result).toContain('_bind')
|
|
2333
|
-
})
|
|
2334
|
-
})
|
|
2335
|
-
|
|
2336
|
-
describe('JSX transform — children expression as bareIdentifier "children"', () => {
|
|
2337
|
-
test('bare children identifier uses _mountSlot', () => {
|
|
2338
|
-
const result = t('function C(props) { const children = props.children; return <div>{children}</div> }')
|
|
2339
|
-
expect(result).toContain('_mountSlot')
|
|
2340
|
-
})
|
|
2341
|
-
})
|
|
2342
|
-
|
|
2343
|
-
describe('JSX transform — template static expression string attr via JSX expression', () => {
|
|
2344
|
-
test('numeric expression attribute baked into HTML', () => {
|
|
2345
|
-
const result = t('<div tabindex={3}><span /></div>')
|
|
2346
|
-
expect(result).toContain('tabindex=\\"3\\"')
|
|
2347
|
-
})
|
|
2348
|
-
|
|
2349
|
-
test('boolean false expression attribute omitted from HTML', () => {
|
|
2350
|
-
const result = t('<div hidden={false}><span /></div>')
|
|
2351
|
-
expect(result).not.toContain('hidden')
|
|
2352
|
-
})
|
|
2353
|
-
|
|
2354
|
-
test('null expression attribute omitted from HTML', () => {
|
|
2355
|
-
const result = t('<div hidden={null}><span /></div>')
|
|
2356
|
-
expect(result).not.toContain('hidden')
|
|
2357
|
-
})
|
|
2358
|
-
})
|
|
2359
|
-
|
|
2360
|
-
describe('JSX transform — isPureStaticCall edge cases', () => {
|
|
2361
|
-
test('pure call with spread argument IS wrapped (not pure)', () => {
|
|
2362
|
-
const result = t('<div>{Math.max(...nums)}</div>')
|
|
2363
|
-
expect(result).toContain('.data =')
|
|
2364
|
-
})
|
|
2365
|
-
|
|
2366
|
-
test('Array.isArray with static arg is not wrapped', () => {
|
|
2367
|
-
const result = t('<div>{Array.isArray(null)}</div>')
|
|
2368
|
-
expect(result).not.toContain('() =>')
|
|
2369
|
-
})
|
|
2370
|
-
|
|
2371
|
-
test('encodeURIComponent with static string is not wrapped', () => {
|
|
2372
|
-
const result = t('<div>{encodeURIComponent("hello world")}</div>')
|
|
2373
|
-
expect(result).not.toContain('() =>')
|
|
2374
|
-
})
|
|
2375
|
-
|
|
2376
|
-
test('Date.now is not wrapped (no args)', () => {
|
|
2377
|
-
const result = t('<div>{Date.now()}</div>')
|
|
2378
|
-
expect(result).not.toContain('() =>')
|
|
2379
|
-
})
|
|
2380
|
-
|
|
2381
|
-
test('standalone parseInt with static arg is not wrapped', () => {
|
|
2382
|
-
const result = t('<div>{parseInt("42")}</div>')
|
|
2383
|
-
expect(result).not.toContain('() =>')
|
|
2384
|
-
})
|
|
2385
|
-
})
|
|
2386
|
-
|
|
2387
|
-
describe('JSX transform — isStatic edge cases', () => {
|
|
2388
|
-
test('template literal with no substitutions is static', () => {
|
|
2389
|
-
const result = t('<div>{`plain text`}</div>')
|
|
2390
|
-
expect(result).not.toContain('_bind')
|
|
2391
|
-
})
|
|
2392
|
-
|
|
2393
|
-
test('template literal with substitution is dynamic', () => {
|
|
2394
|
-
const result = t('<div>{`${x()}`}</div>')
|
|
2395
|
-
expect(result).toContain('_bind')
|
|
2396
|
-
})
|
|
2397
|
-
})
|
|
2398
|
-
|
|
2399
|
-
describe('JSX transform — signal auto-call in template _bind expressions', () => {
|
|
2400
|
-
test('signal in _bind reactive attribute expression', () => {
|
|
2401
|
-
const result = t('function C() { const cls = signal("a"); return <div class={`${cls} extra`}><span /></div> }')
|
|
2402
|
-
expect(result).toContain('cls()')
|
|
2403
|
-
expect(result).toContain('_bind')
|
|
2404
|
-
})
|
|
2405
|
-
|
|
2406
|
-
test('signal in template text child expression', () => {
|
|
2407
|
-
const result = t('function C() { const name = signal("X"); return <div>{`Hello ${name}`}</div> }')
|
|
2408
|
-
expect(result).toContain('name()')
|
|
2409
|
-
})
|
|
2410
|
-
|
|
2411
|
-
test('signal auto-call with addition', () => {
|
|
2412
|
-
const result = t('function C() { const a = signal(1); const b = signal(2); return <div>{a + b}</div> }')
|
|
2413
|
-
expect(result).toContain('a()')
|
|
2414
|
-
expect(result).toContain('b()')
|
|
2415
|
-
})
|
|
2416
|
-
})
|
|
2417
|
-
|
|
2418
|
-
describe('JSX transform — walkNode edge cases for scope cleanup', () => {
|
|
2419
|
-
test('JSXExpressionContainer at top level within function', () => {
|
|
2420
|
-
// This exercises the walkNode JSXExpressionContainer path with scope shadows
|
|
2421
|
-
const result = t(`
|
|
2422
|
-
function App() {
|
|
2423
|
-
const x = signal(0)
|
|
2424
|
-
function Inner() {
|
|
2425
|
-
const x = 'plain'
|
|
2426
|
-
return <MyComp>{x}</MyComp>
|
|
2427
|
-
}
|
|
2428
|
-
return <div>{x}</div>
|
|
2429
|
-
}
|
|
2430
|
-
`)
|
|
2431
|
-
expect(result).toContain('.data = x()')
|
|
2432
|
-
})
|
|
2433
|
-
|
|
2434
|
-
test('template emit within scoped function with signal shadowing', () => {
|
|
2435
|
-
const result = t(`
|
|
2436
|
-
function App() {
|
|
2437
|
-
const count = signal(0)
|
|
2438
|
-
function Nested() {
|
|
2439
|
-
const count = 'static'
|
|
2440
|
-
return <div><span>{count}</span></div>
|
|
2441
|
-
}
|
|
2442
|
-
return <div><span>{count}</span></div>
|
|
2443
|
-
}
|
|
2444
|
-
`)
|
|
2445
|
-
// Nested: count is shadowed, static
|
|
2446
|
-
expect(result).toContain('textContent = count')
|
|
2447
|
-
// App: count is signal, auto-called
|
|
2448
|
-
expect(result).toContain('.data = count()')
|
|
2449
|
-
})
|
|
2450
|
-
})
|
|
2451
|
-
|
|
2452
|
-
describe('JSX transform — parse error handling', () => {
|
|
2453
|
-
test('returns original code on parse error', () => {
|
|
2454
|
-
const result = transformJSX('this is not {valid js <>', 'bad.tsx')
|
|
2455
|
-
expect(result.code).toBe('this is not {valid js <>')
|
|
2456
|
-
expect(result.warnings).toHaveLength(0)
|
|
2457
|
-
})
|
|
2458
|
-
})
|
|
2459
|
-
|
|
2460
|
-
describe('JSX transform — reactive combined _bind for multiple reactive attrs', () => {
|
|
2461
|
-
test('multiple reactive attributes on same element with complex expressions', () => {
|
|
2462
|
-
const result = t('<div class={`${a()} b`} title={`${c()} d`}><span /></div>')
|
|
2463
|
-
expect(result).toContain('_bind(() => {')
|
|
2464
|
-
expect(result).toContain('className')
|
|
2465
|
-
expect(result).toContain('setAttribute("title"')
|
|
2466
|
-
})
|
|
2467
|
-
})
|
|
2468
|
-
|
|
2469
|
-
describe('JSX transform — signalVars.size > shadowedSignals.size check', () => {
|
|
2470
|
-
test('when all signals are shadowed, no auto-call happens', () => {
|
|
2471
|
-
const result = t(`
|
|
2472
|
-
function App() {
|
|
2473
|
-
const x = signal(0)
|
|
2474
|
-
function Inner() {
|
|
2475
|
-
const x = 'plain'
|
|
2476
|
-
return <div class={x + " extra"}></div>
|
|
2477
|
-
}
|
|
2478
|
-
return <div>{x}</div>
|
|
2479
|
-
}
|
|
2480
|
-
`)
|
|
2481
|
-
// Inner's x is NOT auto-called
|
|
2482
|
-
expect(result).toContain('className = x + " extra"')
|
|
2483
|
-
// App's x IS auto-called
|
|
2484
|
-
expect(result).toContain('.data = x()')
|
|
2485
|
-
})
|
|
2486
|
-
})
|
|
2487
|
-
|
|
2488
|
-
describe('JSX transform — _isDynamic with signal member expression and call position', () => {
|
|
2489
|
-
test('signal.set() is NOT flagged as dynamic (signal in callee position)', () => {
|
|
2490
|
-
// When signal is the callee of a call expression, it's already being called
|
|
2491
|
-
const result = t('function C() { const x = signal(0); return <button onClick={() => x.set(1)}>click</button> }')
|
|
2492
|
-
// onClick is an event handler — not wrapped regardless
|
|
2493
|
-
expect(result).not.toContain('_rp')
|
|
2494
|
-
})
|
|
2495
|
-
|
|
2496
|
-
test('signal in property name position of member expression is NOT dynamic', () => {
|
|
2497
|
-
const result = t('function C() { const x = signal(0); return <div title={obj.x}></div> }')
|
|
2498
|
-
// obj.x — x is property name, not signal reference
|
|
2499
|
-
expect(result).not.toContain('_bind')
|
|
2500
|
-
})
|
|
2501
|
-
})
|
|
2502
|
-
|
|
2503
|
-
// ─── Branch coverage: referencesPropDerived with computed MemberExpression ──
|
|
2504
|
-
|
|
2505
|
-
describe('JSX transform — referencesPropDerived computed access', () => {
|
|
2506
|
-
test('prop-derived var used as computed property key is treated as reference', () => {
|
|
2507
|
-
const result = t('function C(props) { const key = props.key; return <div title={obj[key]}></div> }')
|
|
2508
|
-
// key is used as computed property — it IS a reference (p.computed === true)
|
|
2509
|
-
expect(result).toContain('props.key')
|
|
2510
|
-
expect(result).toContain('_bind')
|
|
2511
|
-
})
|
|
2512
|
-
|
|
2513
|
-
test('prop-derived var in non-computed property position is NOT a reference', () => {
|
|
2514
|
-
const result = t('function C(props) { const data = props.data; return <div title={result.data}></div> }')
|
|
2515
|
-
// result.data — 'data' is a non-computed property name, NOT a prop-derived reference
|
|
2516
|
-
expect(result).not.toContain('_bind')
|
|
2517
|
-
})
|
|
2518
|
-
})
|
|
2519
|
-
|
|
2520
|
-
// ─── Branch coverage: template attrSetter for style (line 940) ──────────────
|
|
2521
|
-
|
|
2522
|
-
describe('JSX transform — template style attribute combined _bind', () => {
|
|
2523
|
-
test('complex reactive style uses cssText in combined _bind', () => {
|
|
2524
|
-
const result = t('<div style={getStyle() + "extra"}>text</div>')
|
|
2525
|
-
expect(result).toContain('style.cssText')
|
|
2526
|
-
expect(result).toContain('_bind(() => {')
|
|
2527
|
-
})
|
|
2528
|
-
})
|
|
2529
|
-
|
|
2530
|
-
// ─── Branch coverage: processOneAttr key attr (line 1008) ───────────────────
|
|
2531
|
-
|
|
2532
|
-
describe('JSX transform — template with key attribute on child element', () => {
|
|
2533
|
-
test('key attribute on child element is stripped in template', () => {
|
|
2534
|
-
// key on inner child doesn't bail template (only root key bails)
|
|
2535
|
-
// But templateElementCount bails on key attr on any element
|
|
2536
|
-
const result = t('<div><span key="a">text</span></div>')
|
|
2537
|
-
expect(result).not.toContain('_tpl(')
|
|
2538
|
-
})
|
|
2539
|
-
})
|
|
2540
|
-
|
|
2541
|
-
// ─── Branch coverage: selfClosing template bail (line 313) ──────────────────
|
|
2542
|
-
|
|
2543
|
-
describe('JSX transform — self-closing element template bail', () => {
|
|
2544
|
-
test('self-closing elements skip template emission', () => {
|
|
2545
|
-
const result = t('<div class={cls()} />')
|
|
2546
|
-
// Self-closing root element — tryTemplateEmit returns false
|
|
2547
|
-
expect(result).toContain('() => cls()')
|
|
2548
|
-
expect(result).not.toContain('_tpl(')
|
|
2549
|
-
})
|
|
2550
|
-
})
|
|
2551
|
-
|
|
2552
|
-
// ─── Branch coverage: isStatic types (line 1388-1389) ───────────────────────
|
|
2553
|
-
|
|
2554
|
-
describe('JSX transform — isStatic for various literal types', () => {
|
|
2555
|
-
test('NullLiteral is static', () => {
|
|
2556
|
-
const result = t('<div>{<span data-x={null} />}</div>')
|
|
2557
|
-
expect(result).toContain('const _$h0')
|
|
2558
|
-
})
|
|
2559
|
-
|
|
2560
|
-
test('template literal with expressions is not static', () => {
|
|
2561
|
-
const result = t('<div>{<span data-x={`${x}`} />}</div>')
|
|
2562
|
-
expect(result).not.toContain('const _$h0')
|
|
2563
|
-
})
|
|
2564
|
-
})
|
|
2565
|
-
|
|
2566
|
-
// ─── Branch coverage: accessesProps with arrow function inside (line 679) ────
|
|
2567
|
-
|
|
2568
|
-
describe('JSX transform — accessesProps stops at nested functions', () => {
|
|
2569
|
-
test('props read inside arrow function does not make outer expression reactive', () => {
|
|
2570
|
-
const result = t('function C(props) { return <div title={items.map(x => props.fmt(x))}></div> }')
|
|
2571
|
-
// The arrow function contains a props read, but accessesProps stops at arrow boundaries
|
|
2572
|
-
expect(result).toContain('.map')
|
|
2573
|
-
})
|
|
2574
|
-
})
|
|
2575
|
-
|
|
2576
|
-
// ─── Branch coverage: shouldWrap pure static call (line 688) ────────────────
|
|
2577
|
-
|
|
2578
|
-
describe('JSX transform — shouldWrap skips pure static calls', () => {
|
|
2579
|
-
test('Array.from with static arg in attribute position', () => {
|
|
2580
|
-
const result = t('<div data-arr={Array.from("abc")}></div>')
|
|
2581
|
-
expect(result).not.toContain('_bind')
|
|
2582
|
-
})
|
|
2583
|
-
})
|
|
2584
|
-
|
|
2585
|
-
// ─── Branch coverage: isChildrenExpression fallthrough (line 1321) ──────────
|
|
2586
|
-
|
|
2587
|
-
describe('JSX transform — isChildrenExpression edge cases', () => {
|
|
2588
|
-
test('expression ending with .children uses _mountSlot', () => {
|
|
2589
|
-
const result = t('function C(props) { return <div>{config.children}</div> }')
|
|
2590
|
-
expect(result).toContain('_mountSlot')
|
|
2591
|
-
})
|
|
2592
|
-
|
|
2593
|
-
test('identifier named exactly children uses _mountSlot', () => {
|
|
2594
|
-
const result = t('function C() { return <div>{children}</div> }')
|
|
2595
|
-
expect(result).toContain('_mountSlot')
|
|
2596
|
-
})
|
|
2597
|
-
|
|
2598
|
-
test('expression NOT ending with .children does NOT use _mountSlot', () => {
|
|
2599
|
-
const result = t('function C(props) { return <div>{config.items}</div> }')
|
|
2600
|
-
expect(result).not.toContain('_mountSlot')
|
|
2601
|
-
})
|
|
2602
|
-
})
|
|
2603
|
-
|
|
2604
|
-
// ─── Branch coverage: _isDynamic ArrowFunctionExpression stop (line 656) ────
|
|
2605
|
-
|
|
2606
|
-
describe('JSX transform — _isDynamic stops at nested arrow functions', () => {
|
|
2607
|
-
test('call inside arrow function does not make outer expression dynamic', () => {
|
|
2608
|
-
const result = t('<MyComp render={() => fn()} />')
|
|
2609
|
-
// Arrow function prevents _isDynamic from recursing into fn()
|
|
2610
|
-
expect(result).not.toContain('_rp(')
|
|
2611
|
-
})
|
|
2612
|
-
})
|
|
2613
|
-
|
|
2614
|
-
// ─── Branch coverage: tryDirectSignalRef edge cases (line 922) ──────────────
|
|
2615
|
-
|
|
2616
|
-
describe('JSX transform — tryDirectSignalRef with arguments', () => {
|
|
2617
|
-
test('call with arguments does NOT use _bindDirect', () => {
|
|
2618
|
-
const result = t('<div class={getClass("primary")}><span /></div>')
|
|
2619
|
-
// Has arguments — not a direct signal ref
|
|
2620
|
-
expect(result).not.toContain('_bindDirect')
|
|
2621
|
-
expect(result).toContain('_bind(() => {')
|
|
2622
|
-
})
|
|
2623
|
-
})
|
|
2624
|
-
|
|
2625
|
-
// ─── Branch coverage: unwrapAccessor for function expression (line 928) ─────
|
|
2626
|
-
|
|
2627
|
-
describe('JSX transform — unwrapAccessor with function expression', () => {
|
|
2628
|
-
test('function expression in attribute is called in bind', () => {
|
|
2629
|
-
const result = t('<div class={function() { return "cls" }}><span /></div>')
|
|
2630
|
-
expect(result).toContain('_bind')
|
|
2631
|
-
})
|
|
2632
|
-
})
|
|
2633
|
-
|
|
2634
|
-
// ─── Branch coverage: collectPropDerivedFromDecl callbackDepth (line 498) ───
|
|
2635
|
-
|
|
2636
|
-
describe('JSX transform — prop-derived vars inside callbacks excluded', () => {
|
|
2637
|
-
test('const inside .map callback is NOT tracked as prop-derived', () => {
|
|
2638
|
-
const result = t('function C(props) { return <div>{items.map(item => { const x = props.y; return <span>{x}</span> })}</div> }')
|
|
2639
|
-
// x is declared inside a callback (callbackDepth > 0) — not tracked
|
|
2640
|
-
expect(result).toContain('() =>')
|
|
2641
|
-
})
|
|
2642
|
-
})
|
|
2643
|
-
|
|
2644
|
-
// ─── Branch coverage: static JSX in component prop hoisting (line 359) ──────
|
|
2645
|
-
|
|
2646
|
-
describe('JSX transform — component prop static JSX hoisting', () => {
|
|
2647
|
-
test('single JSX element in component prop is NOT hoisted but walked', () => {
|
|
2648
|
-
const result = t('<MyComp icon={<span>icon</span>} />')
|
|
2649
|
-
// Single JSX element prop → walked (line 354-356), not hoisted
|
|
2650
|
-
expect(result).not.toContain('const _$h0')
|
|
2651
|
-
expect(result).toContain('<span>icon</span>')
|
|
2652
|
-
})
|
|
2653
|
-
|
|
2654
|
-
test('non-JSX static expression in component prop gets hoisted', () => {
|
|
2655
|
-
// This exercises the maybeHoist path (line 358-360)
|
|
2656
|
-
const result = t('<MyComp render={12} />')
|
|
2657
|
-
// Static numeric — no wrapping needed
|
|
2658
|
-
expect(result).not.toContain('_rp(')
|
|
2659
|
-
})
|
|
2660
|
-
})
|
|
2661
|
-
|
|
2662
|
-
// ─── Branch coverage: templateElementCount bail at non-lowercase (line 825) ──
|
|
2663
|
-
|
|
2664
|
-
describe('JSX transform — template element count bail on uppercase', () => {
|
|
2665
|
-
test('component element inside template bails', () => {
|
|
2666
|
-
const result = t('<div><Component /><span /></div>')
|
|
2667
|
-
expect(result).not.toContain('_tpl(')
|
|
2668
|
-
})
|
|
2669
|
-
})
|
|
2670
|
-
|
|
2671
|
-
// ─── Branch coverage: maybeRegisterComponentProps with no params (line 518) ──
|
|
2672
|
-
|
|
2673
|
-
describe('JSX transform — component with no params not tracked', () => {
|
|
2674
|
-
test('parameterless function not tracked as component props', () => {
|
|
2675
|
-
const result = t('function C() { return <div>hello</div> }')
|
|
2676
|
-
expect(result).toContain('_tpl(')
|
|
2677
|
-
expect(result).not.toContain('_bind')
|
|
2678
|
-
})
|
|
2679
|
-
})
|
|
2680
|
-
|
|
2681
|
-
// ─── Branch coverage: tpl null cleanup return (line 1156/1171/1179) ────────
|
|
2682
|
-
|
|
2683
|
-
describe('JSX transform — template processChildren null bail', () => {
|
|
2684
|
-
test('template bails when child element has no tag name', () => {
|
|
2685
|
-
// Member expression tag → empty tag name → processElement returns null
|
|
2686
|
-
const result = t('<div><ns.Comp><span /></ns.Comp></div>')
|
|
2687
|
-
expect(result).not.toContain('_tpl(')
|
|
2688
|
-
})
|
|
2689
|
-
})
|
|
2690
|
-
|
|
2691
|
-
// ─── Branch coverage: more edge cases for various ?? and ?. operators ───────
|
|
2692
|
-
|
|
2693
|
-
describe('JSX transform — additional branch coverage paths', () => {
|
|
2694
|
-
test('arrow function with expression body (no block statement)', () => {
|
|
2695
|
-
const result = t('<div class={() => cls()}><span /></div>')
|
|
2696
|
-
expect(result).toContain('_bindDirect(cls,')
|
|
2697
|
-
})
|
|
2698
|
-
|
|
2699
|
-
test('function expression with block body in attribute', () => {
|
|
2700
|
-
const result = t('<div class={function() { return cls() }}><span /></div>')
|
|
2701
|
-
expect(result).toContain('_bind')
|
|
2702
|
-
})
|
|
2703
|
-
|
|
2704
|
-
test('prop-derived var used inside a nested function arg but NOT as callback', () => {
|
|
2705
|
-
const result = t('function C(props) { const x = props.y; return <div>{x + other(x)}</div> }')
|
|
2706
|
-
expect(result).toContain('props.y')
|
|
2707
|
-
expect(result).toContain('_bind')
|
|
2708
|
-
})
|
|
2709
|
-
|
|
2710
|
-
test('mixed static and dynamic props on template element', () => {
|
|
2711
|
-
const result = t('<div class="static" title={x()} data-id={42}><span /></div>')
|
|
2712
|
-
expect(result).toContain('class=\\"static\\"')
|
|
2713
|
-
expect(result).toContain('_bindDirect(x,')
|
|
2714
|
-
expect(result).toContain('data-id=\\"42\\"')
|
|
2715
|
-
})
|
|
2716
|
-
|
|
2717
|
-
test('template with nested elements each having dynamic attributes', () => {
|
|
2718
|
-
const result = t('<div><span class={a()}><em title={b()}>text</em></span></div>')
|
|
2719
|
-
expect(result).toContain('_tpl(')
|
|
2720
|
-
expect(result).toContain('_bindDirect(a,')
|
|
2721
|
-
expect(result).toContain('_bindDirect(b,')
|
|
2722
|
-
})
|
|
2723
|
-
|
|
2724
|
-
test('signal auto-call works inside template _bind for text', () => {
|
|
2725
|
-
const result = t('function C() { const x = signal(1); return <div>{x + 1}</div> }')
|
|
2726
|
-
expect(result).toContain('x() + 1')
|
|
2727
|
-
expect(result).toContain('_bind')
|
|
2728
|
-
})
|
|
2729
|
-
|
|
2730
|
-
test('signal auto-call inside template attribute _bind', () => {
|
|
2731
|
-
const result = t('function C() { const cls = signal("a"); return <div class={cls + " b"}><span /></div> }')
|
|
2732
|
-
expect(result).toContain('cls() + " b"')
|
|
2733
|
-
expect(result).toContain('_bind')
|
|
2734
|
-
})
|
|
2735
|
-
|
|
2736
|
-
test('template with event + ref + dynamic attr + text child', () => {
|
|
2737
|
-
const result = t('<div ref={myRef} onClick={handler} class={cls()} title="static">{text()}</div>')
|
|
2738
|
-
expect(result).toContain('_tpl(')
|
|
2739
|
-
expect(result).toContain('myRef')
|
|
2740
|
-
expect(result).toContain('__ev_click = handler')
|
|
2741
|
-
expect(result).toContain('_bindDirect(cls,')
|
|
2742
|
-
expect(result).toContain('_bindText(text,')
|
|
2743
|
-
})
|
|
2744
|
-
|
|
2745
|
-
test('template with non-delegated event using addEventListener', () => {
|
|
2746
|
-
const result = t('<div onScroll={handler}><span /></div>')
|
|
2747
|
-
expect(result).toContain('addEventListener("scroll", handler)')
|
|
2748
|
-
expect(result).not.toContain('__ev_')
|
|
2749
|
-
})
|
|
2750
|
-
|
|
2751
|
-
test('forEachChild with non-array non-object values', () => {
|
|
2752
|
-
// Edge case: JSX text node has primitive value property
|
|
2753
|
-
const result = t('<div>plain text between elements<span /></div>')
|
|
2754
|
-
expect(result).toContain('_tpl(')
|
|
2755
|
-
})
|
|
2756
|
-
|
|
2757
|
-
test('self-closing void element in mixed children template', () => {
|
|
2758
|
-
const result = t('<div><input />{value()}</div>')
|
|
2759
|
-
expect(result).toContain('_tpl(')
|
|
2760
|
-
expect(result).toContain('childNodes[')
|
|
2761
|
-
expect(result).toContain('_bindText(value,')
|
|
2762
|
-
})
|
|
2763
|
-
|
|
2764
|
-
test('multiple signals from same component all tracked', () => {
|
|
2765
|
-
const result = t(`
|
|
2766
|
-
function C() {
|
|
2767
|
-
const a = signal(1)
|
|
2768
|
-
const b = signal(2)
|
|
2769
|
-
const c = signal(3)
|
|
2770
|
-
return <div>
|
|
2771
|
-
<span>{a}</span>
|
|
2772
|
-
<em>{b}</em>
|
|
2773
|
-
<strong>{c}</strong>
|
|
2774
|
-
</div>
|
|
2775
|
-
}
|
|
2776
|
-
`)
|
|
2777
|
-
expect(result).toContain('a()')
|
|
2778
|
-
expect(result).toContain('b()')
|
|
2779
|
-
expect(result).toContain('c()')
|
|
2780
|
-
})
|
|
2781
|
-
|
|
2782
|
-
test('signal auto-call with binary and unary expressions', () => {
|
|
2783
|
-
const result = t('function C() { const x = signal(5); return <div>{-x}</div> }')
|
|
2784
|
-
expect(result).toContain('x()')
|
|
2785
|
-
expect(result).toContain('_bind')
|
|
2786
|
-
})
|
|
2787
|
-
|
|
2788
|
-
test('signal in computed property access is auto-called', () => {
|
|
2789
|
-
const result = t('function C() { const idx = signal(0); return <div>{arr[idx]}</div> }')
|
|
2790
|
-
expect(result).toContain('idx()')
|
|
2791
|
-
})
|
|
2792
|
-
|
|
2793
|
-
test('signal variable reference not confused with same-name property', () => {
|
|
2794
|
-
const result = t('function C() { const x = signal(0); return <div data-val={obj.method(x)}></div> }')
|
|
2795
|
-
expect(result).toContain('x()')
|
|
2796
|
-
expect(result).toContain('_bind')
|
|
2797
|
-
})
|
|
2798
|
-
|
|
2799
|
-
test('template with static spread on root and dynamic inner attr', () => {
|
|
2800
|
-
const result = t('<div {...staticProps}><span class={cls()}>text</span></div>')
|
|
2801
|
-
expect(result).toContain('_tpl(')
|
|
2802
|
-
expect(result).toContain('_applyProps')
|
|
2803
|
-
expect(result).toContain('_bindDirect(cls,')
|
|
2804
|
-
})
|
|
2805
|
-
|
|
2806
|
-
test('empty JSX expression in template attribute position', () => {
|
|
2807
|
-
const result = t('<div class={/* comment */}><span /></div>')
|
|
2808
|
-
expect(result).toContain('_tpl(')
|
|
2809
|
-
})
|
|
2810
|
-
|
|
2811
|
-
test('ternary in template attribute without signal', () => {
|
|
2812
|
-
const result = t('<div class={x ? "a" : "b"}><span /></div>')
|
|
2813
|
-
// No calls — not dynamic
|
|
2814
|
-
expect(result).toContain('className = x ? "a" : "b"')
|
|
2815
|
-
})
|
|
2816
|
-
|
|
2817
|
-
test('variable declaration kind is let — not tracked for prop-derived', () => {
|
|
2818
|
-
const result = t('function C(props) { let x = props.y; return <div>{x}</div> }')
|
|
2819
|
-
// let is not tracked — x is static
|
|
2820
|
-
expect(result).toContain('textContent = x')
|
|
2821
|
-
})
|
|
2822
|
-
|
|
2823
|
-
test('FunctionDeclaration with JSX detected as component', () => {
|
|
2824
|
-
const result = t('function MyComp(props) { return <div class={props.cls}></div> }')
|
|
2825
|
-
expect(result).toContain('_bind')
|
|
2826
|
-
expect(result).toContain('props.cls')
|
|
2827
|
-
})
|
|
2828
|
-
|
|
2829
|
-
test('ArrowFunctionExpression with JSX and single param detected as component', () => {
|
|
2830
|
-
const result = t('const MyComp = (props) => <div class={props.cls}></div>')
|
|
2831
|
-
expect(result).toContain('_bind')
|
|
2832
|
-
expect(result).toContain('props.cls')
|
|
2833
|
-
})
|
|
2834
|
-
|
|
2835
|
-
test('signal NOT tracked inside callback arg (callbackDepth > 0)', () => {
|
|
2836
|
-
// collectPropDerivedFromDecl skips when callbackDepth > 0
|
|
2837
|
-
const result = t('function C(props) { return <div>{items.map(item => { const x = signal(0); return <span>{x}</span> })}</div> }')
|
|
2838
|
-
// x is inside a callback — signal tracking doesn't apply at callback depth
|
|
2839
|
-
expect(result).toContain('() =>')
|
|
2840
|
-
})
|
|
2841
|
-
|
|
2842
|
-
test('template with empty expression in attribute (attrIsDynamic false branch)', () => {
|
|
2843
|
-
// Empty expression in attribute: data-x={/* */} — attrIsDynamic returns false
|
|
2844
|
-
const result = t('<div data-x={/* comment */}><span /></div>')
|
|
2845
|
-
expect(result).toContain('_tpl(')
|
|
2846
|
-
})
|
|
2847
|
-
|
|
2848
|
-
test('template with only static attributes — elementHasDynamic false', () => {
|
|
2849
|
-
const result = t('<div class="a" title="b"><span class="c">text</span></div>')
|
|
2850
|
-
expect(result).toContain('_tpl(')
|
|
2851
|
-
// No _bind needed for fully static tree
|
|
2852
|
-
expect(result).toContain('() => null')
|
|
2853
|
-
})
|
|
2854
|
-
|
|
2855
|
-
test('signal auto-call with signal as callee of call expression', () => {
|
|
2856
|
-
// signal()(args) — signal IS the callee of a call, already being called
|
|
2857
|
-
const result = t('function C() { const fn = signal(() => 1); return <div>{fn()}</div> }')
|
|
2858
|
-
// fn() is already a call — no double call
|
|
2859
|
-
expect(result).not.toContain('fn()()')
|
|
2860
|
-
})
|
|
2861
|
-
|
|
2862
|
-
test('signal auto-call not triggered on arrow function children', () => {
|
|
2863
|
-
// Arrow functions in JSX are not recursed into by referencesSignalVar
|
|
2864
|
-
const result = t('function C() { const x = signal(0); return <div>{() => { const x = "shadow"; return x }}</div> }')
|
|
2865
|
-
// The arrow function is not touched
|
|
2866
|
-
expect(result).toBeDefined()
|
|
2867
|
-
})
|
|
2868
|
-
|
|
2869
|
-
test('template with deeply nested mixed expressions', () => {
|
|
2870
|
-
const result = t('<div><span><em>{a()}</em></span><strong>{b()}</strong></div>')
|
|
2871
|
-
expect(result).toContain('_tpl(')
|
|
2872
|
-
expect(result).toContain('_bindText(a,')
|
|
2873
|
-
expect(result).toContain('_bindText(b,')
|
|
2874
|
-
})
|
|
2875
|
-
|
|
2876
|
-
test('signal in JSX attribute expression container — auto-called in bind', () => {
|
|
2877
|
-
const result = t('function C() { const x = signal(0); return <div data-val={x}><span /></div> }')
|
|
2878
|
-
// x is a signal identifier in an attribute — should be auto-called
|
|
2879
|
-
expect(result).toContain('x()')
|
|
2880
|
-
expect(result).toContain('_bind')
|
|
2881
|
-
})
|
|
2882
|
-
|
|
2883
|
-
test('namespace attribute in template element', () => {
|
|
2884
|
-
// xml:lang or xlink:href — JSXNamespacedName, not JSXIdentifier
|
|
2885
|
-
const result = t('<svg><use xlink:href="#icon"><rect /></use></svg>')
|
|
2886
|
-
expect(result).toBeDefined()
|
|
2887
|
-
})
|
|
2888
|
-
|
|
2889
|
-
test('signal as only child of component uses auto-call', () => {
|
|
2890
|
-
const result = t('function C() { const x = signal(0); return <MyComp>{x}</MyComp> }')
|
|
2891
|
-
expect(result).toContain('() => x()')
|
|
2892
|
-
})
|
|
2893
|
-
|
|
2894
|
-
test('multiple signals with complex nesting', () => {
|
|
2895
|
-
const result = t(`
|
|
2896
|
-
function C() {
|
|
2897
|
-
const a = signal(1)
|
|
2898
|
-
const b = signal('text')
|
|
2899
|
-
return <div class={a ? 'active' : 'inactive'}>
|
|
2900
|
-
<span>{b}</span>
|
|
2901
|
-
<em>{a > 0 ? b : 'none'}</em>
|
|
2902
|
-
</div>
|
|
2903
|
-
}
|
|
2904
|
-
`)
|
|
2905
|
-
expect(result).toContain('a()')
|
|
2906
|
-
expect(result).toContain('b()')
|
|
2907
|
-
})
|
|
2908
|
-
})
|