@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
|
@@ -1,924 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cross-backend equivalence tests.
|
|
3
|
-
* Runs every test input through BOTH the JS and Rust implementations
|
|
4
|
-
* and asserts identical output. This catches any behavioral divergence
|
|
5
|
-
* between the two backends.
|
|
6
|
-
*/
|
|
7
|
-
import { transformJSX_JS } from '../jsx'
|
|
8
|
-
import type { ReactivitySpan } from '../jsx'
|
|
9
|
-
|
|
10
|
-
// Load native if available
|
|
11
|
-
let nativeTransform:
|
|
12
|
-
| ((
|
|
13
|
-
code: string,
|
|
14
|
-
filename: string,
|
|
15
|
-
ssr: boolean,
|
|
16
|
-
knownSignals: string[] | null,
|
|
17
|
-
reactivityLens?: boolean,
|
|
18
|
-
) => {
|
|
19
|
-
code: string
|
|
20
|
-
usesTemplates?: boolean | null
|
|
21
|
-
warnings: Array<{ message: string; line: number; column: number; code: string }>
|
|
22
|
-
reactivityLens?: ReactivitySpan[] | null
|
|
23
|
-
})
|
|
24
|
-
| null = null
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
const path = require('node:path')
|
|
28
|
-
const native = require(path.join(__dirname, '..', '..', 'native', 'pyreon-compiler.node'))
|
|
29
|
-
nativeTransform = native.transformJsx
|
|
30
|
-
} catch {
|
|
31
|
-
// Native not available — skip tests
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const describeNative = nativeTransform ? describe : describe.skip
|
|
35
|
-
|
|
36
|
-
function compare(input: string, filename = 'test.tsx') {
|
|
37
|
-
const js = transformJSX_JS(input, filename)
|
|
38
|
-
const rs = nativeTransform!(input, filename, false, null)
|
|
39
|
-
expect(rs.code).toBe(js.code)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function compareWithSignals(input: string, knownSignals: string[]) {
|
|
43
|
-
const js = transformJSX_JS(input, 'test.tsx', { knownSignals })
|
|
44
|
-
const rs = nativeTransform!(input, 'test.tsx', false, knownSignals)
|
|
45
|
-
expect(rs.code).toBe(js.code)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function compareSsr(input: string) {
|
|
49
|
-
const js = transformJSX_JS(input, 'test.tsx', { ssr: true })
|
|
50
|
-
const rs = nativeTransform!(input, 'test.tsx', true, null)
|
|
51
|
-
expect(rs.code).toBe(js.code)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Reactivity-lens cross-backend gate (Phase 3). Asserts the Rust binary
|
|
55
|
-
// emits the SAME sidecar the JS oracle does. Two contracts:
|
|
56
|
-
// 1. ADDITIVE — `code` is byte-identical with the lens collected vs
|
|
57
|
-
// not, on BOTH backends (the option never affects codegen).
|
|
58
|
-
// 2. PARITY — the SET of recorded spans is identical JS↔Rust.
|
|
59
|
-
// Spans are compared order-independently: the LSP consumer sorts before
|
|
60
|
-
// rendering, so traversal order is NOT part of the contract — the SET of
|
|
61
|
-
// codegen decisions is. Sorting by (start,end,kind,detail) makes a
|
|
62
|
-
// missing/extra/wrong span fail loudly while ignoring walk order.
|
|
63
|
-
function canon(spans: ReactivitySpan[] | null | undefined): string[] {
|
|
64
|
-
return (spans ?? [])
|
|
65
|
-
.map(
|
|
66
|
-
(s) =>
|
|
67
|
-
`${s.start}|${s.end}|${s.line}|${s.column}|${s.endLine}|${s.endColumn}|${s.kind}|${s.detail}`,
|
|
68
|
-
)
|
|
69
|
-
.sort()
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function compareLens(input: string, filename = 'test.tsx') {
|
|
73
|
-
const jsOff = transformJSX_JS(input, filename)
|
|
74
|
-
const jsOn = transformJSX_JS(input, filename, { reactivityLens: true })
|
|
75
|
-
const rsOff = nativeTransform!(input, filename, false, null, false)
|
|
76
|
-
const rsOn = nativeTransform!(input, filename, false, null, true)
|
|
77
|
-
|
|
78
|
-
// (1) additive — collecting the lens never changes emitted code, on
|
|
79
|
-
// either backend, and both backends still agree on that code.
|
|
80
|
-
expect(jsOn.code).toBe(jsOff.code)
|
|
81
|
-
expect(rsOn.code).toBe(rsOff.code)
|
|
82
|
-
expect(rsOn.code).toBe(jsOn.code)
|
|
83
|
-
|
|
84
|
-
// The opt-out path must NOT carry the sidecar (parity with JS, which
|
|
85
|
-
// omits the field entirely when not collecting).
|
|
86
|
-
expect(rsOff.reactivityLens == null).toBe(true)
|
|
87
|
-
expect(jsOff.reactivityLens == null).toBe(true)
|
|
88
|
-
|
|
89
|
-
// (2) parity — identical SET of spans.
|
|
90
|
-
const j = canon(jsOn.reactivityLens)
|
|
91
|
-
const r = canon(rsOn.reactivityLens)
|
|
92
|
-
expect(r).toEqual(j)
|
|
93
|
-
// Guard against the degenerate "both empty → trivially equal" pass:
|
|
94
|
-
// every fixture below is chosen to produce ≥1 span.
|
|
95
|
-
expect(j.length).toBeGreaterThan(0)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ─── Cross-backend equivalence ──────────────────────────────────────────────
|
|
99
|
-
|
|
100
|
-
describeNative('Native vs JS equivalence — basic', () => {
|
|
101
|
-
test('simple signal child', () => compare('<div>{count()}</div>'))
|
|
102
|
-
test('static string child', () => compare('<div>{"static"}</div>'))
|
|
103
|
-
test('numeric child', () => compare('<div>{42}</div>'))
|
|
104
|
-
test('null child', () => compare('<div>{null}</div>'))
|
|
105
|
-
test('boolean true child', () => compare('<div>{true}</div>'))
|
|
106
|
-
test('boolean false child', () => compare('<div>{false}</div>'))
|
|
107
|
-
test('undefined child', () => compare('<div>{undefined}</div>'))
|
|
108
|
-
test('arrow function child', () => compare('<div>{() => count()}</div>'))
|
|
109
|
-
test('ternary with call', () => compare('<div>{a() ? b : c}</div>'))
|
|
110
|
-
test('ternary without call', () => compare('<div>{a ? b : c}</div>'))
|
|
111
|
-
test('logical AND with JSX', () => compare('<div>{show() && <span />}</div>'))
|
|
112
|
-
test('template literal with call', () => compare('<div>{`hello ${name()}`}</div>'))
|
|
113
|
-
test('template literal static', () => compare('<div>{`hello`}</div>'))
|
|
114
|
-
test('tagged template', () => compare('<div>{css`color: red`}</div>'))
|
|
115
|
-
test('binary with call', () => compare('<div>{count() + 1}</div>'))
|
|
116
|
-
test('binary without call', () => compare('<div>{a + b}</div>'))
|
|
117
|
-
test('member call', () => compare('<div>{obj.getValue()}</div>'))
|
|
118
|
-
test('chained method', () => compare('<div>{items().map(x => x)}</div>'))
|
|
119
|
-
test('empty expression', () => compare('<div>{/* comment */}</div>'))
|
|
120
|
-
test('plain text', () => compare('<div>hello world</div>'))
|
|
121
|
-
test('no JSX', () => compare('const x = 1 + 2'))
|
|
122
|
-
test('empty element', () => compare('<div></div>'))
|
|
123
|
-
test('self-closing', () => compare('<br />'))
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
describeNative('Native vs JS equivalence — props', () => {
|
|
127
|
-
test('dynamic class', () => compare('<div class={activeClass()} />'))
|
|
128
|
-
test('dynamic style', () => compare('<div style={styles()} />'))
|
|
129
|
-
test('string class', () => compare('<div class="foo" />'))
|
|
130
|
-
test('expression string class', () => compare('<div class={"foo"} />'))
|
|
131
|
-
test('onClick handler', () => compare('<button onClick={handleClick} />'))
|
|
132
|
-
test('onInput handler', () => compare('<input onInput={handler} />'))
|
|
133
|
-
test('key prop', () => compare('<div key={id} />'))
|
|
134
|
-
test('ref prop', () => compare('<div ref={myRef} />'))
|
|
135
|
-
test('already wrapped', () => compare('<div class={() => cls()} />'))
|
|
136
|
-
test('object style', () => compare('<div style={{ color: "red" }} />'))
|
|
137
|
-
test('object style with call', () => compare('<div style={{ color: theme() }} />'))
|
|
138
|
-
test('boolean shorthand', () => compare('<input disabled />'))
|
|
139
|
-
test('data attribute', () => compare('<div data-id={getId()} />'))
|
|
140
|
-
test('conditional prop', () => compare("<div title={isActive() ? 'yes' : 'no'} />"))
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
describeNative('Native vs JS equivalence — components', () => {
|
|
144
|
-
test('reactive prop with _rp', () => compare('<MyComponent value={count()} />'))
|
|
145
|
-
test('_rp import', () => compare('<Button label={getText()} />'))
|
|
146
|
-
test('static prop', () => compare('<Button size={12} />'))
|
|
147
|
-
test('event handler on component', () => compare('<Button onClick={handleClick} />'))
|
|
148
|
-
test('arrow prop', () => compare('<Button render={() => "hello"} />'))
|
|
149
|
-
test('children expression', () => compare('<MyComponent>{count()}</MyComponent>'))
|
|
150
|
-
test('multiple reactive props', () => compare('<Comp a={x()} b={y()} c={12} />'))
|
|
151
|
-
test('children prop', () => compare('<Comp children={items()} />'))
|
|
152
|
-
test('spread on component', () => compare('<Comp {...getProps()} label="hi" />'))
|
|
153
|
-
test('ternary in component prop', () => compare("<Comp x={a() ? 'yes' : 'no'} />"))
|
|
154
|
-
test('template literal in component prop', () => compare('<Comp label={`${count()} items`} />'))
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
describeNative('Native vs JS equivalence — template emission', () => {
|
|
158
|
-
test('nested elements', () => compare('<div><span>hello</span></div>'))
|
|
159
|
-
test('single element with text', () => compare('<div>hello</div>'))
|
|
160
|
-
test('component child bails', () => compare('<div><MyComponent /></div>'))
|
|
161
|
-
test('root spread with _applyProps', () => compare('<div {...props}><span /></div>'))
|
|
162
|
-
test('inner spread bails', () => compare('<div><span {...innerProps} /></div>'))
|
|
163
|
-
test('keyed element bails', () => compare('<div key={id}><span /></div>'))
|
|
164
|
-
test('static class in HTML', () => compare('<div class="box"><span /></div>'))
|
|
165
|
-
test('boolean attr in HTML', () => compare('<div><input disabled /></div>'))
|
|
166
|
-
test('_bindDirect for signal class', () => compare('<div class={cls()}><span /></div>'))
|
|
167
|
-
test('_bindText for signal child', () => compare('<div><span>{name()}</span></div>'))
|
|
168
|
-
test('static expression text', () => compare('<div><span>{label}</span></div>'))
|
|
169
|
-
test('delegated event', () => compare('<div><button onClick={handler}>click</button></div>'))
|
|
170
|
-
test('element children indexing', () => compare('<div><span>{a()}</span><em>{b()}</em></div>'))
|
|
171
|
-
test('deep nesting', () => compare('<table><tbody><tr><td>{text()}</td></tr></tbody></table>'))
|
|
172
|
-
test('className to class mapping', () => compare('<div className="box"><span /></div>'))
|
|
173
|
-
test('htmlFor mapping', () => compare('<div><label htmlFor="name">Name</label></div>'))
|
|
174
|
-
test('fragment inlining', () => compare('<div><><span>text</span></></div>'))
|
|
175
|
-
test('expression with JSX bails', () => compare('<div><span />{show() && <em />}</div>'))
|
|
176
|
-
test('mixed text + element + expr', () => compare('<div>hello<span />{name()}</div>'))
|
|
177
|
-
test('multiple expressions', () => compare('<div><span>{a()}{b()}</span></div>'))
|
|
178
|
-
test('void element', () => compare('<div><br /><span>text</span></div>'))
|
|
179
|
-
test('ref in template (object)', () => compare('<div ref={myRef}><span /></div>'))
|
|
180
|
-
test('ref in template (arrow)', () => compare('<div ref={(el) => { myEl = el }}><span /></div>'))
|
|
181
|
-
|
|
182
|
-
// Regression: a child element with a block-arrow ref AND adjacent
|
|
183
|
-
// reactive props used to emit `const __e0 = __root.children[N]`
|
|
184
|
-
// followed by `((el) => { ... })(__e0)` with NO `;` between, so JS's
|
|
185
|
-
// ASI merged them into one expression `const __e0 = X((el) => ...)(__e0)`
|
|
186
|
-
// (calling X as fn, self-referencing __e0). Both backends now append
|
|
187
|
-
// `;` to every bind line. This test asserts both emit the SAME `;`-
|
|
188
|
-
// terminated output and the chained-call shape never appears.
|
|
189
|
-
test('block-arrow ref on child element with adjacent reactive prop', () => {
|
|
190
|
-
const input = '<div><span ref={(el) => { x = el }} data-state={cls()} /></div>'
|
|
191
|
-
compare(input)
|
|
192
|
-
// Tighter assertion: neither backend may emit the silent-merge shape.
|
|
193
|
-
const js = transformJSX_JS(input, 'test.tsx')
|
|
194
|
-
expect(js.code).not.toMatch(/children\[0\]\(\(/)
|
|
195
|
-
expect(js.code).toMatch(/const __e0 = __root\.children\[0\];/)
|
|
196
|
-
const rs = nativeTransform!(input, 'test.tsx', false, null)
|
|
197
|
-
expect(rs.code).not.toMatch(/children\[0\]\(\(/)
|
|
198
|
-
expect(rs.code).toMatch(/const __e0 = __root\.children\[0\];/)
|
|
199
|
-
})
|
|
200
|
-
test('non-delegated event', () => compare('<div onMouseEnter={handler}><span /></div>'))
|
|
201
|
-
test('style object in template', () => compare('<div style={{ overflow: "hidden" }}>text</div>'))
|
|
202
|
-
test('style string in template', () => compare('<div style="color: red">text</div>'))
|
|
203
|
-
test('reactive style in template', () => compare('<div style={() => getStyle()}>text</div>'))
|
|
204
|
-
test('tabindex numeric attr', () => compare('<div tabindex={0}><span /></div>'))
|
|
205
|
-
test('hidden=true', () => compare('<div hidden={true}><span /></div>'))
|
|
206
|
-
test('hidden=false', () => compare('<div hidden={false}><span /></div>'))
|
|
207
|
-
test('hidden=null', () => compare('<div hidden={null}><span /></div>'))
|
|
208
|
-
test('hidden=undefined', () => compare('<div hidden={undefined}><span /></div>'))
|
|
209
|
-
test('one-time set for variable', () => compare('<div title={someVar}><span /></div>'))
|
|
210
|
-
test('benchmark-like row', () => compare('<tr class={cls()}><td class="id">{String(row.id)}</td><td>{row.label()}</td></tr>'))
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
describeNative('Native vs JS equivalence — hoisting', () => {
|
|
214
|
-
test('static JSX child', () => compare('<div>{<span>Hello</span>}</div>'))
|
|
215
|
-
test('static self-closing', () => compare('<div>{<br />}</div>'))
|
|
216
|
-
test('dynamic JSX not hoisted', () => compare('<div>{<span class={cls()}>text</span>}</div>'))
|
|
217
|
-
test('static with string prop', () => compare('<div>{<span class="foo">text</span>}</div>'))
|
|
218
|
-
test('multiple hoists', () => compare('<div>{<span>A</span>}{<span>B</span>}</div>'))
|
|
219
|
-
test('static fragment', () => compare('<div>{<>text</>}</div>'))
|
|
220
|
-
test('dynamic fragment not hoisted', () => compare('<div>{<>{count()}</>}</div>'))
|
|
221
|
-
test('spread prevents hoisting', () => compare('<div>{<span {...props}>text</span>}</div>'))
|
|
222
|
-
test('static boolean attr', () => compare('<div>{<input disabled />}</div>'))
|
|
223
|
-
test('static true expression', () => compare('<div>{<input disabled={true} />}</div>'))
|
|
224
|
-
test('static false expression', () => compare('<div>{<input disabled={false} />}</div>'))
|
|
225
|
-
test('static null expression', () => compare('<div>{<input disabled={null} />}</div>'))
|
|
226
|
-
test('static numeric expression', () => compare('<div>{<input tabindex={0} />}</div>'))
|
|
227
|
-
test('empty expression attr', () => compare('<div>{<input disabled={/* comment */} />}</div>'))
|
|
228
|
-
test('nested static element', () => compare('<div>{<div><span>text</span></div>}</div>'))
|
|
229
|
-
test('nested static self-closing', () => compare('<div>{<div><br /></div>}</div>'))
|
|
230
|
-
test('nested static fragment', () => compare('<div>{<div><>text</></div>}</div>'))
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
describeNative('Native vs JS equivalence — pure calls', () => {
|
|
234
|
-
test('Math.max static', () => compare('<div>{Math.max(5, 10)}</div>'))
|
|
235
|
-
test('JSON.stringify static', () => compare('<div>{JSON.stringify("hello")}</div>'))
|
|
236
|
-
test('JSON.stringify non-static', () => compare('<div>{JSON.stringify({a: 1})}</div>'))
|
|
237
|
-
test('Math.max with signal', () => compare('<div>{Math.max(count(), 10)}</div>'))
|
|
238
|
-
test('unknown function', () => compare('<div>{unknownFn(5)}</div>'))
|
|
239
|
-
test('Math.floor static', () => compare('<div>{Math.floor(3.14)}</div>'))
|
|
240
|
-
test('Number.parseInt', () => compare('<div>{Number.parseInt("42", 10)}</div>'))
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
describeNative('Native vs JS equivalence — props detection', () => {
|
|
244
|
-
test('props.x in child', () => compare('function Comp(props) { return <div>{props.name}</div> }'))
|
|
245
|
-
test('props.x in attr', () => compare('function Comp(props) { return <div class={props.cls}></div> }'))
|
|
246
|
-
test('prop-derived inline', () => compare('function Comp(props) { const x = props.name ?? "anon"; return <div>{x}</div> }'))
|
|
247
|
-
test('prop-derived in attr', () => compare('function Comp(props) { const align = props.alignX ?? "left"; return <div class={align}></div> }'))
|
|
248
|
-
test('splitProps tracking', () => compare('function Comp(props) { const [own, rest] = splitProps(props, ["x"]); const v = own.x ?? 5; return <div>{v}</div> }'))
|
|
249
|
-
test('non-component not tracked', () => compare('function helper(props) { const x = props.y; return x }'))
|
|
250
|
-
test('signal alongside props', () => compare('function Comp(props) { return <div>{count()}</div> }'))
|
|
251
|
-
test('arrow component', () => compare('const Comp = (props) => <div>{props.x}</div>'))
|
|
252
|
-
test('prop used multiple times', () => compare('function Comp(props) { const x = props.a ?? "def"; return <div class={x}>{x}</div> }'))
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
describeNative('Native vs JS equivalence — transitive derivation', () => {
|
|
256
|
-
test('simple chain', () => compare('function Comp(props) { const a = props.x; const b = a + 1; return <div>{b}</div> }'))
|
|
257
|
-
test('non-prop const', () => compare('function Comp(props) { const x = 42; return <div>{x}</div> }'))
|
|
258
|
-
test('let not tracked', () => compare('function Comp(props) { let x = props.y; x = "override"; return <div>{x}</div> }'))
|
|
259
|
-
test('deep chain', () => compare('function Comp(props) { const a = props.x; const b = a + 1; const c = b * 2; return <div>{c}</div> }'))
|
|
260
|
-
test('mixed props and signals', () => compare('function Comp(props) { return <div class={`${props.base} ${count()}`}></div> }'))
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
// ─── Edge cases that previously broke ───────────────────────────────────────
|
|
264
|
-
|
|
265
|
-
describeNative('Native vs JS equivalence — TypeScript syntax', () => {
|
|
266
|
-
test('as expression in prop', () => compare('function C(props) { return <div>{(props.x as string)}</div> }'))
|
|
267
|
-
test('as in variable init', () => compare(`
|
|
268
|
-
function C(props) {
|
|
269
|
-
const items = props.data as any[]
|
|
270
|
-
return <div>{items}</div>
|
|
271
|
-
}
|
|
272
|
-
`))
|
|
273
|
-
test('non-null assertion', () => compare('function C(props) { return <div>{props.name!}</div> }'))
|
|
274
|
-
test('satisfies', () => compare('function C(props) { return <div>{(props.x satisfies string)}</div> }'))
|
|
275
|
-
test('type annotation on arrow', () => compare('const C = (props: { x: string }) => <div>{props.x}</div>'))
|
|
276
|
-
test('generic component', () => compare('function List<T>(props: { items: T[] }) { return <div>{props.items}</div> }'))
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
describeNative('Native vs JS equivalence — export forms', () => {
|
|
280
|
-
test('export default function', () => compare('export default function App(props) { return <div>{props.name}</div> }'))
|
|
281
|
-
test('export const arrow', () => compare('export const App = (props) => <div>{props.name}</div>'))
|
|
282
|
-
test('export named function', () => compare('export function App(props) { return <div>{props.name}</div> }'))
|
|
283
|
-
test('export const with signal', () => compare('export const view = <div>{count()}</div>'))
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
describeNative('Native vs JS equivalence — control flow', () => {
|
|
287
|
-
test('if statement before JSX', () => compare(`
|
|
288
|
-
function C(props) {
|
|
289
|
-
if (!props.show) return null
|
|
290
|
-
return <div>{props.name}</div>
|
|
291
|
-
}
|
|
292
|
-
`))
|
|
293
|
-
test('for loop before JSX', () => compare(`
|
|
294
|
-
function C(props) {
|
|
295
|
-
for (let i = 0; i < 10; i++) {}
|
|
296
|
-
return <div>{props.name}</div>
|
|
297
|
-
}
|
|
298
|
-
`))
|
|
299
|
-
test('try/catch wrapping JSX', () => compare(`
|
|
300
|
-
function C(props) {
|
|
301
|
-
try { return <div>{props.name}</div> }
|
|
302
|
-
catch(e) { return <div>error</div> }
|
|
303
|
-
}
|
|
304
|
-
`))
|
|
305
|
-
test('switch statement', () => compare(`
|
|
306
|
-
function C(props) {
|
|
307
|
-
switch (props.mode) {
|
|
308
|
-
case 'a': return <div>A</div>
|
|
309
|
-
default: return <div>B</div>
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
`))
|
|
313
|
-
test('while loop', () => compare(`
|
|
314
|
-
function C() {
|
|
315
|
-
let items = []
|
|
316
|
-
while (items.length < 10) { items.push(1) }
|
|
317
|
-
return <div>{items.length}</div>
|
|
318
|
-
}
|
|
319
|
-
`))
|
|
320
|
-
test('ternary return', () => compare(`
|
|
321
|
-
function C(props) {
|
|
322
|
-
return props.show ? <div>yes</div> : <div>no</div>
|
|
323
|
-
}
|
|
324
|
-
`))
|
|
325
|
-
test('logical AND return', () => compare(`
|
|
326
|
-
function C(props) {
|
|
327
|
-
return props.show && <div>yes</div>
|
|
328
|
-
}
|
|
329
|
-
`))
|
|
330
|
-
test('arrow with block body', () => compare(`
|
|
331
|
-
const C = (props) => {
|
|
332
|
-
const x = props.name
|
|
333
|
-
return <div>{x}</div>
|
|
334
|
-
}
|
|
335
|
-
`))
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
describeNative('Native vs JS equivalence — callback depth', () => {
|
|
339
|
-
test('.map callback not tracked', () => compare(`
|
|
340
|
-
function App(props) {
|
|
341
|
-
return <div>{tabs.map((tab) => {
|
|
342
|
-
const C = tab.component
|
|
343
|
-
return <div><C /></div>
|
|
344
|
-
})}</div>
|
|
345
|
-
}
|
|
346
|
-
`))
|
|
347
|
-
test('.filter callback not tracked', () => compare(`
|
|
348
|
-
function App(props) {
|
|
349
|
-
return <div>{items.filter(i => i.visible).map(i => <span>{i.name()}</span>)}</div>
|
|
350
|
-
}
|
|
351
|
-
`))
|
|
352
|
-
test('nested callback', () => compare(`
|
|
353
|
-
function App(props) {
|
|
354
|
-
return <ul>{items.map(item => (
|
|
355
|
-
<li class={item.done() ? 'done' : ''}>
|
|
356
|
-
<span>{item.text()}</span>
|
|
357
|
-
</li>
|
|
358
|
-
))}</ul>
|
|
359
|
-
}
|
|
360
|
-
`))
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
describeNative('Native vs JS equivalence — children slot', () => {
|
|
364
|
-
test('props.children uses _mountSlot', () => compare('function C(props) { return <div>{props.children}</div> }'))
|
|
365
|
-
test('own.children uses _mountSlot', () => compare('function C(props) { const own = props; return <label><input/>{own.children}</label> }'))
|
|
366
|
-
test('non-children prop uses text bind', () => compare('function C(props) { return <div>{props.name}</div> }'))
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
describeNative('Native vs JS equivalence — SSR mode', () => {
|
|
370
|
-
test('SSR skips _tpl', () => {
|
|
371
|
-
const code = 'function Btn() { return <button onClick={() => null}>Click {() => x()}</button> }'
|
|
372
|
-
compareSsr(code)
|
|
373
|
-
})
|
|
374
|
-
test('SSR simple element', () => compareSsr('<div>hello</div>'))
|
|
375
|
-
})
|
|
376
|
-
|
|
377
|
-
describeNative('Native vs JS equivalence — warnings', () => {
|
|
378
|
-
test('<For> without by produces warning', () => {
|
|
379
|
-
const js = transformJSX_JS('<For each={items}>{(item) => <li>{item}</li>}</For>')
|
|
380
|
-
const rs = nativeTransform!('<For each={items}>{(item) => <li>{item}</li>}</For>', 'test.tsx', false, null)
|
|
381
|
-
expect(rs.warnings.length).toBe(js.warnings.length)
|
|
382
|
-
if (js.warnings.length > 0) {
|
|
383
|
-
expect(rs.warnings[0]!.code).toBe(js.warnings[0]!.code)
|
|
384
|
-
}
|
|
385
|
-
})
|
|
386
|
-
test('<For> with by has no warning', () => {
|
|
387
|
-
const js = transformJSX_JS('<For each={items} by={(i) => i.id}>{(item) => <li>{item}</li>}</For>')
|
|
388
|
-
const rs = nativeTransform!('<For each={items} by={(i) => i.id}>{(item) => <li>{item}</li>}</For>', 'test.tsx', false, null)
|
|
389
|
-
expect(rs.warnings.length).toBe(js.warnings.length)
|
|
390
|
-
})
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
describeNative('Native vs JS equivalence — HTML escaping', () => {
|
|
394
|
-
test('HTML entities preserved', () => compare('function C() { return <button>< prev</button> }'))
|
|
395
|
-
test('mixed entities and ampersands', () => compare('function C() { return <span>A & B < C</span> }'))
|
|
396
|
-
test('quotes in attributes', () => compare('<div title="say "hi""><span /></div>'))
|
|
397
|
-
})
|
|
398
|
-
|
|
399
|
-
describeNative('Native vs JS equivalence — signal() not inlined', () => {
|
|
400
|
-
test('signal() call not tracked as prop-derived', () => compare(`
|
|
401
|
-
function C(props) {
|
|
402
|
-
const open = signal(props.defaultOpen ?? false)
|
|
403
|
-
return <div>{() => open() ? 'yes' : 'no'}</div>
|
|
404
|
-
}
|
|
405
|
-
`))
|
|
406
|
-
})
|
|
407
|
-
|
|
408
|
-
describeNative('Native vs JS equivalence — circular references', () => {
|
|
409
|
-
test('two-variable cycle does not crash', () => compare(`
|
|
410
|
-
function Comp(props) {
|
|
411
|
-
const a = b + props.x;
|
|
412
|
-
const b = a + 1;
|
|
413
|
-
return <div>{a}</div>
|
|
414
|
-
}
|
|
415
|
-
`))
|
|
416
|
-
test('self-referencing variable', () => compare(`
|
|
417
|
-
function Comp(props) {
|
|
418
|
-
const a = a + props.x;
|
|
419
|
-
return <div>{a}</div>
|
|
420
|
-
}
|
|
421
|
-
`))
|
|
422
|
-
test('non-cyclic deep chain', () => compare(`
|
|
423
|
-
function Comp(props) {
|
|
424
|
-
const a = props.x;
|
|
425
|
-
const b = a + 1;
|
|
426
|
-
const c = b * 2;
|
|
427
|
-
return <div>{c}</div>
|
|
428
|
-
}
|
|
429
|
-
`))
|
|
430
|
-
})
|
|
431
|
-
|
|
432
|
-
describeNative('Native vs JS equivalence — complex real-world patterns', () => {
|
|
433
|
-
test('todo app component', () => compare(`
|
|
434
|
-
const TodoApp = (props) => {
|
|
435
|
-
const [items, rest] = splitProps(props, ['items'])
|
|
436
|
-
const count = items.items?.length ?? 0
|
|
437
|
-
return (
|
|
438
|
-
<div class="app">
|
|
439
|
-
<h1>Todos</h1>
|
|
440
|
-
<footer>{count} items left</footer>
|
|
441
|
-
</div>
|
|
442
|
-
)
|
|
443
|
-
}
|
|
444
|
-
`))
|
|
445
|
-
|
|
446
|
-
test('form with multiple props', () => compare(`
|
|
447
|
-
function FormField(props) {
|
|
448
|
-
const label = props.label ?? 'Field'
|
|
449
|
-
const required = props.required
|
|
450
|
-
return (
|
|
451
|
-
<div class={required ? 'required' : ''}>
|
|
452
|
-
<label>{label}</label>
|
|
453
|
-
<input value={props.value} onInput={props.onInput} />
|
|
454
|
-
</div>
|
|
455
|
-
)
|
|
456
|
-
}
|
|
457
|
-
`))
|
|
458
|
-
|
|
459
|
-
test('list with For and template rows', () => compare(`
|
|
460
|
-
function UserList(props) {
|
|
461
|
-
return (
|
|
462
|
-
<table>
|
|
463
|
-
<thead><tr><th>Name</th><th>Email</th></tr></thead>
|
|
464
|
-
<tbody>
|
|
465
|
-
<For each={props.users} by={(u) => u.id}>
|
|
466
|
-
{(user) => (
|
|
467
|
-
<tr class={user.active() ? 'active' : ''}>
|
|
468
|
-
<td>{user.name()}</td>
|
|
469
|
-
<td>{user.email()}</td>
|
|
470
|
-
</tr>
|
|
471
|
-
)}
|
|
472
|
-
</For>
|
|
473
|
-
</tbody>
|
|
474
|
-
</table>
|
|
475
|
-
)
|
|
476
|
-
}
|
|
477
|
-
`))
|
|
478
|
-
|
|
479
|
-
test('conditional rendering with Show', () => compare(`
|
|
480
|
-
function Modal(props) {
|
|
481
|
-
return (
|
|
482
|
-
<Show when={props.open}>
|
|
483
|
-
<div class="overlay" onClick={props.onClose}>
|
|
484
|
-
<div class="modal">
|
|
485
|
-
<h2>{props.title}</h2>
|
|
486
|
-
{props.children}
|
|
487
|
-
</div>
|
|
488
|
-
</div>
|
|
489
|
-
</Show>
|
|
490
|
-
)
|
|
491
|
-
}
|
|
492
|
-
`))
|
|
493
|
-
})
|
|
494
|
-
|
|
495
|
-
// ─── Unicode and multi-byte character safety ────────────────────────────────
|
|
496
|
-
|
|
497
|
-
describeNative('Native vs JS equivalence — Unicode', () => {
|
|
498
|
-
test('emoji before JSX expression', () => compare(`
|
|
499
|
-
function C() { return <div>🔥{count()}</div> }
|
|
500
|
-
`))
|
|
501
|
-
test('CJK characters before JSX', () => compare(`
|
|
502
|
-
function C() { return <div>日本語{name()}</div> }
|
|
503
|
-
`))
|
|
504
|
-
test('accented chars in identifier', () => compare(`
|
|
505
|
-
function C() { const café = "test"; return <div>{café}</div> }
|
|
506
|
-
`))
|
|
507
|
-
test('emoji in prop value', () => compare(`
|
|
508
|
-
<div title="🎉 Party">text</div>
|
|
509
|
-
`))
|
|
510
|
-
test('multi-byte chars in template literal', () => compare(`
|
|
511
|
-
<div>{\`Hello 世界 \${name()}\`}</div>
|
|
512
|
-
`))
|
|
513
|
-
test('unicode in component name', () => compare(`
|
|
514
|
-
<Ñoño value={x()} />
|
|
515
|
-
`))
|
|
516
|
-
test('mixed unicode and ASCII', () => compare(`
|
|
517
|
-
function Comp(props) {
|
|
518
|
-
const naïve = props.naïve ?? "default"
|
|
519
|
-
return <div class={naïve}>{props.résumé}</div>
|
|
520
|
-
}
|
|
521
|
-
`))
|
|
522
|
-
})
|
|
523
|
-
|
|
524
|
-
// ─── String literal collision resistance ────────────────────────────────────
|
|
525
|
-
|
|
526
|
-
describeNative('Native vs JS equivalence — string literal collision', () => {
|
|
527
|
-
test('prop name matches string in ternary', () => compare(`
|
|
528
|
-
function C(props) {
|
|
529
|
-
const required = props.required
|
|
530
|
-
return <div class={required ? 'required' : ''}>{required}</div>
|
|
531
|
-
}
|
|
532
|
-
`))
|
|
533
|
-
test('prop name in template literal string part', () => compare(`
|
|
534
|
-
function C(props) {
|
|
535
|
-
const mode = props.mode
|
|
536
|
-
return <div>{\`mode is \${mode}\`}</div>
|
|
537
|
-
}
|
|
538
|
-
`))
|
|
539
|
-
test('prop name in object key position', () => compare(`
|
|
540
|
-
function C(props) {
|
|
541
|
-
const x = props.x
|
|
542
|
-
return <div style={{ x: x }}></div>
|
|
543
|
-
}
|
|
544
|
-
`))
|
|
545
|
-
test('prop name in nested string', () => compare(`
|
|
546
|
-
function C(props) {
|
|
547
|
-
const label = props.label
|
|
548
|
-
return <div title={label || "label"}>text</div>
|
|
549
|
-
}
|
|
550
|
-
`))
|
|
551
|
-
})
|
|
552
|
-
|
|
553
|
-
// ─── Additional robustness tests ────────────────────────────────────────────
|
|
554
|
-
|
|
555
|
-
describeNative('Native vs JS equivalence — additional edge cases', () => {
|
|
556
|
-
test('deeply nested template', () => compare(`
|
|
557
|
-
<div><section><article><header><h1>{title()}</h1></header><p>{body()}</p></article></section></div>
|
|
558
|
-
`))
|
|
559
|
-
test('multiple components in one file', () => compare(`
|
|
560
|
-
function A(props) { return <div>{props.a}</div> }
|
|
561
|
-
function B(props) { return <span>{props.b}</span> }
|
|
562
|
-
`))
|
|
563
|
-
test('component returning fragment', () => compare(`
|
|
564
|
-
function C(props) { return <>{props.children}</> }
|
|
565
|
-
`))
|
|
566
|
-
test('empty component', () => compare(`
|
|
567
|
-
function C() { return <div></div> }
|
|
568
|
-
`))
|
|
569
|
-
test('template inside fragment wraps in braces', () => compare(`
|
|
570
|
-
function C() { return <><button type="button">text</button><span /></> }
|
|
571
|
-
`))
|
|
572
|
-
test('template inside nested fragment', () => compare(`
|
|
573
|
-
function C() { return <><><div>inner</div></></> }
|
|
574
|
-
`))
|
|
575
|
-
test('template in JSX attribute value (not brace-wrapped)', () => compare(`
|
|
576
|
-
<Show fallback={<div><p>Not logged in</p></div>}><span /></Show>
|
|
577
|
-
`))
|
|
578
|
-
test('full Showcase pattern with Show + fallback', () => compare(`
|
|
579
|
-
function Demo(props) {
|
|
580
|
-
const open = signal(false)
|
|
581
|
-
return (
|
|
582
|
-
<Show
|
|
583
|
-
when={() => open()}
|
|
584
|
-
fallback={<div class="demo"><p>Fallback</p><button type="button">Action</button></div>}
|
|
585
|
-
>
|
|
586
|
-
<div><p>Content</p></div>
|
|
587
|
-
</Show>
|
|
588
|
-
)
|
|
589
|
-
}
|
|
590
|
-
`))
|
|
591
|
-
test('array destructuring from signal', () => compare(`
|
|
592
|
-
function C(props) {
|
|
593
|
-
const [a, b] = props.items
|
|
594
|
-
return <div>{a}</div>
|
|
595
|
-
}
|
|
596
|
-
`))
|
|
597
|
-
test('nested function not confused with component', () => compare(`
|
|
598
|
-
function App(props) {
|
|
599
|
-
function helper() { return props.x + 1 }
|
|
600
|
-
return <div>{helper()}</div>
|
|
601
|
-
}
|
|
602
|
-
`))
|
|
603
|
-
test('class with JSX method', () => compare(`
|
|
604
|
-
class C { render(props) { return <div>{props.name}</div> } }
|
|
605
|
-
`))
|
|
606
|
-
test('immediately invoked arrow', () => compare(`
|
|
607
|
-
const el = (() => <div>{count()}</div>)()
|
|
608
|
-
`))
|
|
609
|
-
test('JSX in variable init', () => compare(`
|
|
610
|
-
const header = <header><h1>Title</h1></header>
|
|
611
|
-
`))
|
|
612
|
-
test('multiple JSX returns', () => compare(`
|
|
613
|
-
function C(props) {
|
|
614
|
-
if (props.loading) return <div>Loading...</div>
|
|
615
|
-
if (props.error) return <div>Error</div>
|
|
616
|
-
return <div>{props.data}</div>
|
|
617
|
-
}
|
|
618
|
-
`))
|
|
619
|
-
})
|
|
620
|
-
|
|
621
|
-
// ─── Signal auto-call cross-backend equivalence ─────────────────────────────
|
|
622
|
-
|
|
623
|
-
describeNative('Native vs JS equivalence — signal auto-call', () => {
|
|
624
|
-
test('bare signal in text child', () => compare(
|
|
625
|
-
'function C() { const name = signal("Vít"); return <div>{name}</div> }',
|
|
626
|
-
))
|
|
627
|
-
test('signal in attribute', () => compare(
|
|
628
|
-
'function C() { const show = signal(false); return <div class={show ? "active" : ""}></div> }',
|
|
629
|
-
))
|
|
630
|
-
test('already called NOT double-called', () => compare(
|
|
631
|
-
'function C() { const count = signal(0); return <div>{count()}</div> }',
|
|
632
|
-
))
|
|
633
|
-
test('computed auto-called', () => compare(
|
|
634
|
-
'function C() { const d = computed(() => 2); return <div>{d}</div> }',
|
|
635
|
-
))
|
|
636
|
-
test('signal in ternary', () => compare(
|
|
637
|
-
'function C() { const show = signal(false); return <div>{show ? "yes" : "no"}</div> }',
|
|
638
|
-
))
|
|
639
|
-
test('signal in template literal', () => compare(
|
|
640
|
-
'function C() { const name = signal("world"); return <div>{`hello ${name}`}</div> }',
|
|
641
|
-
))
|
|
642
|
-
test('signal in component prop with _rp', () => compare(
|
|
643
|
-
'function C() { const val = signal(42); return <MyComp value={val} /> }',
|
|
644
|
-
))
|
|
645
|
-
test('multiple signals', () => compare(
|
|
646
|
-
'function C() { const a = signal(1); const b = signal(2); return <div>{a + b}</div> }',
|
|
647
|
-
))
|
|
648
|
-
test('signal + computed together', () => compare(
|
|
649
|
-
'function C() { const count = signal(0); const doubled = computed(() => count() * 2); return <div>{count} + {doubled}</div> }',
|
|
650
|
-
))
|
|
651
|
-
test('non-signal const NOT auto-called', () => compare(
|
|
652
|
-
'function C() { const x = 42; return <div>{x}</div> }',
|
|
653
|
-
))
|
|
654
|
-
test('shorthand property NOT auto-called', () => compare(
|
|
655
|
-
'function C() { const name = signal("x"); return <div>{t("hi", { name })}</div> }',
|
|
656
|
-
))
|
|
657
|
-
test('non-shorthand property value auto-called', () => compare(
|
|
658
|
-
'function C() { const name = signal("x"); return <div>{t("hi", { label: name })}</div> }',
|
|
659
|
-
))
|
|
660
|
-
test('signal in object property value', () => compare(
|
|
661
|
-
'function C() { const x = signal(0); return <div>{({val: x})}</div> }',
|
|
662
|
-
))
|
|
663
|
-
test('signal as member expression object', () => compare(
|
|
664
|
-
'function C() { const x = signal(0); return <div>{x.toString()}</div> }',
|
|
665
|
-
))
|
|
666
|
-
test('signal in computed property access', () => compare(
|
|
667
|
-
'function C() { const idx = signal(0); return <div>{arr[idx]}</div> }',
|
|
668
|
-
))
|
|
669
|
-
test('shadowed by inner const', () => compare(`
|
|
670
|
-
const show = signal(false)
|
|
671
|
-
function Inner() {
|
|
672
|
-
const show = 'not a signal'
|
|
673
|
-
return <div>{show}</div>
|
|
674
|
-
}
|
|
675
|
-
`))
|
|
676
|
-
test('shadowed by function parameter', () => compare(`
|
|
677
|
-
const count = signal(0)
|
|
678
|
-
function Display(count) {
|
|
679
|
-
return <div>{count}</div>
|
|
680
|
-
}
|
|
681
|
-
`))
|
|
682
|
-
test('shadowed by destructured parameter', () => compare(`
|
|
683
|
-
const name = signal('Vít')
|
|
684
|
-
function Greet({ name }) {
|
|
685
|
-
return <div>{name}</div>
|
|
686
|
-
}
|
|
687
|
-
`))
|
|
688
|
-
test('export default function with shadow', () => compare(`
|
|
689
|
-
const show = signal(false)
|
|
690
|
-
export default function App(show) {
|
|
691
|
-
return <div>{show}</div>
|
|
692
|
-
}
|
|
693
|
-
`))
|
|
694
|
-
test('export named function with shadow', () => compare(`
|
|
695
|
-
const show = signal(false)
|
|
696
|
-
export function App(show) {
|
|
697
|
-
return <div>{show}</div>
|
|
698
|
-
}
|
|
699
|
-
`))
|
|
700
|
-
test('module-scope signal auto-called', () => compare(
|
|
701
|
-
'const globalSig = signal(0); function C() { return <div>{globalSig}</div> }',
|
|
702
|
-
))
|
|
703
|
-
test('props + signal in same expression', () => compare(
|
|
704
|
-
'function C(props) { const show = signal(false); const label = props.label; return <div class={show ? label : "default"}></div> }',
|
|
705
|
-
))
|
|
706
|
-
})
|
|
707
|
-
|
|
708
|
-
describeNative('Native vs JS equivalence — knownSignals cross-module', () => {
|
|
709
|
-
test('imported signal auto-called', () => compareWithSignals(
|
|
710
|
-
'import { count } from "./store"; function App() { return <div>{count}</div> }',
|
|
711
|
-
['count'],
|
|
712
|
-
))
|
|
713
|
-
test('imported signal with alias', () => compareWithSignals(
|
|
714
|
-
'import { count as c } from "./store"; function App() { return <div>{c}</div> }',
|
|
715
|
-
['c'],
|
|
716
|
-
))
|
|
717
|
-
test('imported signal not double-called', () => compareWithSignals(
|
|
718
|
-
'import { count } from "./store"; function App() { return <div>{count()}</div> }',
|
|
719
|
-
['count'],
|
|
720
|
-
))
|
|
721
|
-
test('imported signal respects shadow', () => compareWithSignals(
|
|
722
|
-
'import { count } from "./store"; function App() { const count = "shadow"; return <div>{count}</div> }',
|
|
723
|
-
['count'],
|
|
724
|
-
))
|
|
725
|
-
test('knownSignals combined with local signals', () => compareWithSignals(
|
|
726
|
-
'import { theme } from "./store"; function App() { const count = signal(0); return <div class={theme}>{count}</div> }',
|
|
727
|
-
['theme'],
|
|
728
|
-
))
|
|
729
|
-
})
|
|
730
|
-
|
|
731
|
-
// PR #352 added a `DOM_PROPS` set so `<input value={x()} />` inside a
|
|
732
|
-
// template-emitting context compiles to `el.value = x()` (property
|
|
733
|
-
// assignment) instead of `el.setAttribute("value", x())` (content
|
|
734
|
-
// attribute). The two diverge for IDL properties whose live state
|
|
735
|
-
// differs from the content attribute (`value`, `checked`, etc.). The
|
|
736
|
-
// Rust native backend reimplements this list separately. A typo in
|
|
737
|
-
// either side's list would silently produce wrong output for one
|
|
738
|
-
// DOM_PROP without breaking any other test. This block enumerates
|
|
739
|
-
// every DOM_PROP under template context (the only context where
|
|
740
|
-
// DOM_PROPS actually fires — root-level standalone JSX uses the
|
|
741
|
-
// `h()` path, not `_tpl() + _bind()`) and asserts JS↔Rust agreement,
|
|
742
|
-
// so a drift between the two lists fails one specific test.
|
|
743
|
-
//
|
|
744
|
-
// Reference: packages/core/compiler/src/jsx.ts:1389 — DOM_PROPS Set.
|
|
745
|
-
describeNative('Native vs JS equivalence — DOM properties', () => {
|
|
746
|
-
const DOM_PROPS = [
|
|
747
|
-
'value',
|
|
748
|
-
'checked',
|
|
749
|
-
'selected',
|
|
750
|
-
'disabled',
|
|
751
|
-
'multiple',
|
|
752
|
-
'readOnly',
|
|
753
|
-
'indeterminate',
|
|
754
|
-
] as const
|
|
755
|
-
|
|
756
|
-
for (const prop of DOM_PROPS) {
|
|
757
|
-
test(`DOM_PROP in template: <div><input ${prop}={x()} /></div> (reactive)`, () => {
|
|
758
|
-
compare(`<div><input ${prop}={x()} /></div>`)
|
|
759
|
-
})
|
|
760
|
-
|
|
761
|
-
test(`DOM_PROP in template: <div><input ${prop}={() => x()} /></div> (accessor)`, () => {
|
|
762
|
-
compare(`<div><input ${prop}={() => x()} /></div>`)
|
|
763
|
-
})
|
|
764
|
-
|
|
765
|
-
test(`DOM_PROP in template: <div><input ${prop}={true} /></div> (literal)`, () => {
|
|
766
|
-
compare(`<div><input ${prop}={true} /></div>`)
|
|
767
|
-
})
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
test('regression: all DOM_PROPS together in one template', () => {
|
|
771
|
-
// Sentinel — if a future PR adds a new DOM property to either
|
|
772
|
-
// backend without adding it to the other, the loop above won't
|
|
773
|
-
// notice unless that prop is in the test list. This single test
|
|
774
|
-
// compiles JSX with ALL known DOM_PROPS together and verifies
|
|
775
|
-
// both backends agree on the combined output.
|
|
776
|
-
const allProps = DOM_PROPS.map((p) => `${p}={x()}`).join(' ')
|
|
777
|
-
compare(`<div><input ${allProps} /></div>`)
|
|
778
|
-
})
|
|
779
|
-
|
|
780
|
-
test('non-DOM-prop control: title in template uses setAttribute, not assignment', () => {
|
|
781
|
-
// Negative control — `title` is NOT a DOM_PROP, so it should
|
|
782
|
-
// compile through setAttribute. If this test starts failing,
|
|
783
|
-
// someone added `title` to DOM_PROPS — verify intent before
|
|
784
|
-
// updating.
|
|
785
|
-
compare('<div><input title={x()} /></div>')
|
|
786
|
-
})
|
|
787
|
-
})
|
|
788
|
-
|
|
789
|
-
// ─── Reactivity-lens parity (Phase 3) ───────────────────────────────────────
|
|
790
|
-
// The Rust binary must emit the SAME sidecar as the JS oracle so the
|
|
791
|
-
// ~80% of users on the native path get the Lens too. Each fixture is
|
|
792
|
-
// chosen to exercise one of the five structural kinds; `compareLens`
|
|
793
|
-
// also asserts the additive guarantee (codegen byte-identical with the
|
|
794
|
-
// option on vs off, both backends) so this block doubles as the native
|
|
795
|
-
// regression guard for "the lens option must never affect output".
|
|
796
|
-
//
|
|
797
|
-
// Bisect-verified: removing ANY of the 6 `ctx.lens(...)` calls in
|
|
798
|
-
// native/src/lib.rs fails the matching fixture below with an
|
|
799
|
-
// array-length / element mismatch in `compareLens`'s parity assertion
|
|
800
|
-
// (e.g. dropping the `reactive-prop` call → `<Comp value={x()} />` fails
|
|
801
|
-
// `expect(r).toEqual(j)` because the Rust set is missing that span);
|
|
802
|
-
// restored → 9/9 pass.
|
|
803
|
-
describeNative('Reactivity-lens — JS↔Rust span parity', () => {
|
|
804
|
-
test('reactive text child (_bindText)', () =>
|
|
805
|
-
compareLens('<div>{count()}</div>'))
|
|
806
|
-
|
|
807
|
-
test('reactive accessor text child (() => …)', () =>
|
|
808
|
-
compareLens('<div>{() => count()}</div>'))
|
|
809
|
-
|
|
810
|
-
test('static-text child (baked once — the high-precision negative)', () =>
|
|
811
|
-
compareLens('<div>{someConst}</div>'))
|
|
812
|
-
|
|
813
|
-
test('reactive-prop on a component (_rp(() => …))', () =>
|
|
814
|
-
compareLens('<Comp value={count()} />'))
|
|
815
|
-
|
|
816
|
-
test('reactive-attr on a DOM element (live binding)', () =>
|
|
817
|
-
compareLens('<div><span title={count()}>hi</span></div>'))
|
|
818
|
-
|
|
819
|
-
test('hoisted-static (module-scope hoist)', () =>
|
|
820
|
-
compareLens('<Comp>{<b class="x">hi</b>}</Comp>'))
|
|
821
|
-
|
|
822
|
-
test('mixed: reactive + static + prop in one tree', () =>
|
|
823
|
-
compareLens(
|
|
824
|
-
'<section><Comp value={count()} /><p>{count()}</p><p>{label}</p></section>',
|
|
825
|
-
))
|
|
826
|
-
|
|
827
|
-
test('multi-line source — line/column parity across newlines', () =>
|
|
828
|
-
compareLens('<div>\n {count()}\n <span title={other()}>\n z</span>\n</div>'))
|
|
829
|
-
|
|
830
|
-
test('signal auto-call shape — declared signal, bare {count} → reactive', () =>
|
|
831
|
-
compareLens('const count = signal(0); const App = () => <div>{count}</div>', 'auto.tsx'))
|
|
832
|
-
})
|
|
833
|
-
|
|
834
|
-
describeNative('cross-backend: component-child stable-reference carve-out', () => {
|
|
835
|
-
test('bare Identifier (splitProps-derived const) emitted bare in component child', () =>
|
|
836
|
-
compare(`
|
|
837
|
-
const Kinetic = (props) => {
|
|
838
|
-
const [childHolder, restHtml] = splitProps(props, ['children'])
|
|
839
|
-
const children = childHolder.children
|
|
840
|
-
return <StaggerRenderer htmlProps={restHtml}>{children}</StaggerRenderer>
|
|
841
|
-
}
|
|
842
|
-
`))
|
|
843
|
-
|
|
844
|
-
test('simple MemberExpression chain emitted bare in component child', () =>
|
|
845
|
-
compare(`
|
|
846
|
-
const Comp = (props) => {
|
|
847
|
-
const [obj] = splitProps(props, ['deep'])
|
|
848
|
-
return <Inner>{obj.deep.x}</Inner>
|
|
849
|
-
}
|
|
850
|
-
`))
|
|
851
|
-
|
|
852
|
-
test('CallExpression keeps the wrap in component child', () =>
|
|
853
|
-
compare(`
|
|
854
|
-
const count = signal(0)
|
|
855
|
-
const Comp = () => <Inner>{count()}</Inner>
|
|
856
|
-
`))
|
|
857
|
-
|
|
858
|
-
test('BinaryExpression keeps the wrap in component child', () =>
|
|
859
|
-
compare(`
|
|
860
|
-
const Comp = (props) => {
|
|
861
|
-
const [own] = splitProps(props, ['a', 'b'])
|
|
862
|
-
return <Inner>{own.a + own.b}</Inner>
|
|
863
|
-
}
|
|
864
|
-
`))
|
|
865
|
-
|
|
866
|
-
test('DOM-element parent keeps reactive binding (no carve-out)', () =>
|
|
867
|
-
compare(`
|
|
868
|
-
const Comp = (props) => {
|
|
869
|
-
const [own] = splitProps(props, ['children'])
|
|
870
|
-
const children = own.children
|
|
871
|
-
return <div>{children}</div>
|
|
872
|
-
}
|
|
873
|
-
`))
|
|
874
|
-
|
|
875
|
-
test('user-written accessor child passes through unchanged', () =>
|
|
876
|
-
compare(`
|
|
877
|
-
const x = signal('a')
|
|
878
|
-
const Comp = () => <Inner>{() => x()}</Inner>
|
|
879
|
-
`))
|
|
880
|
-
|
|
881
|
-
test('bare signal identifier in component child — KEEPS wrap (auto-call + reactivity)', () =>
|
|
882
|
-
compare(`
|
|
883
|
-
function C() {
|
|
884
|
-
const count = signal(0)
|
|
885
|
-
return <MyComp>{count}</MyComp>
|
|
886
|
-
}
|
|
887
|
-
`))
|
|
888
|
-
|
|
889
|
-
test('TS-cast wrapper (`children as VNode[]`) is transparent — both backends', () =>
|
|
890
|
-
compare(`
|
|
891
|
-
const Kinetic = (props) => {
|
|
892
|
-
const [childHolder] = splitProps(props, ['children'])
|
|
893
|
-
const children = childHolder.children
|
|
894
|
-
return <Inner>{children as VNode[]}</Inner>
|
|
895
|
-
}
|
|
896
|
-
`))
|
|
897
|
-
|
|
898
|
-
test('non-null `!` postfix is transparent — both backends', () =>
|
|
899
|
-
compare(`
|
|
900
|
-
const Comp = (props) => {
|
|
901
|
-
const [own] = splitProps(props, ['children'])
|
|
902
|
-
return <Inner>{own.children!}</Inner>
|
|
903
|
-
}
|
|
904
|
-
`))
|
|
905
|
-
|
|
906
|
-
test('fragment child of component does NOT propagate component context', () =>
|
|
907
|
-
compare(`
|
|
908
|
-
const Comp = (props) => {
|
|
909
|
-
const [own] = splitProps(props, ['children'])
|
|
910
|
-
const children = own.children
|
|
911
|
-
return <Inner><>{children}</></Inner>
|
|
912
|
-
}
|
|
913
|
-
`))
|
|
914
|
-
|
|
915
|
-
test('static-array children (rest-args form, no expression container) — unchanged', () =>
|
|
916
|
-
compare(`
|
|
917
|
-
const Comp = (props) => (
|
|
918
|
-
<Inner>
|
|
919
|
-
<A />
|
|
920
|
-
<B />
|
|
921
|
-
</Inner>
|
|
922
|
-
)
|
|
923
|
-
`))
|
|
924
|
-
})
|