@pyreon/compiler 0.13.1 → 0.15.0
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/README.md +14 -4
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +1330 -409
- package/lib/types/index.d.ts +152 -14
- package/package.json +12 -1
- package/src/event-names.ts +65 -0
- package/src/index.ts +10 -1
- package/src/jsx.ts +974 -784
- package/src/pyreon-intercept.ts +728 -0
- package/src/test-audit.ts +435 -0
- package/src/tests/depth-stress.test.ts +16 -0
- package/src/tests/detector-tag-consistency.test.ts +86 -0
- package/src/tests/jsx.test.ts +1170 -4
- package/src/tests/native-equivalence.test.ts +731 -0
- package/src/tests/project-scanner.test.ts +30 -0
- package/src/tests/pyreon-intercept.test.ts +486 -0
- package/src/tests/react-intercept.test.ts +354 -0
- package/src/tests/runtime/control-flow.test.ts +159 -0
- package/src/tests/runtime/dom-properties.test.ts +138 -0
- package/src/tests/runtime/events.test.ts +301 -0
- package/src/tests/runtime/harness.ts +94 -0
- package/src/tests/runtime/pr-352-shapes.test.ts +121 -0
- package/src/tests/runtime/reactive-props.test.ts +81 -0
- package/src/tests/runtime/signals.test.ts +129 -0
- package/src/tests/runtime/whitespace.test.ts +106 -0
- package/src/tests/test-audit.test.ts +549 -0
- package/lib/index.js.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
package/src/tests/jsx.test.ts
CHANGED
|
@@ -580,6 +580,39 @@ describe('JSX transform — template emission', () => {
|
|
|
580
580
|
expect(result).toContain('__ev_click = handler')
|
|
581
581
|
})
|
|
582
582
|
|
|
583
|
+
// Regression: multi-word event-name casing was broken — `onKeyDown`
|
|
584
|
+
// produced `addEventListener("keyDown", ...)` (camelCase) instead of
|
|
585
|
+
// `addEventListener("keydown", ...)` (DOM convention). The handler
|
|
586
|
+
// never fired because `keyDown` is not a real DOM event name.
|
|
587
|
+
// Same bug class affected `onMouseEnter`, `onMouseLeave`, etc.
|
|
588
|
+
test('lowercases multi-word event names (onKeyDown → keydown — delegated)', () => {
|
|
589
|
+
const result = t('<div><input onKeyDown={handler} /></div>')
|
|
590
|
+
// keydown IS in DELEGATED_EVENTS — must use the expando, not addEventListener.
|
|
591
|
+
// Prior behavior: addEventListener("keyDown", ...) — wrong casing AND
|
|
592
|
+
// wrong path (delegated check missed because case mismatched).
|
|
593
|
+
expect(result).toContain('__ev_keydown = handler')
|
|
594
|
+
expect(result).not.toContain('__ev_keyDown')
|
|
595
|
+
expect(result).not.toContain('"keyDown"')
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
test('lowercases multi-word event names (onMouseEnter → mouseenter — non-delegated)', () => {
|
|
599
|
+
const result = t('<div><span onMouseEnter={handler}>hi</span></div>')
|
|
600
|
+
// mouseenter is NOT delegated — must reach addEventListener with lowercase name
|
|
601
|
+
expect(result).toContain('addEventListener("mouseenter", handler)')
|
|
602
|
+
expect(result).not.toContain('"mouseEnter"')
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
test('lowercases multi-word event names for input change (onChange → change — delegated)', () => {
|
|
606
|
+
const result = t('<div><input onChange={handler} /></div>')
|
|
607
|
+
expect(result).toContain('__ev_change = handler')
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
test('lowercases multi-word event names with multiple capitals (onPointerLeave → pointerleave)', () => {
|
|
611
|
+
const result = t('<div><span onPointerLeave={handler}>hi</span></div>')
|
|
612
|
+
expect(result).toContain('addEventListener("pointerleave", handler)')
|
|
613
|
+
expect(result).not.toContain('"pointerLeave"')
|
|
614
|
+
})
|
|
615
|
+
|
|
583
616
|
test('uses element children indexing for nested access', () => {
|
|
584
617
|
const result = t('<div><span>{a()}</span><em>{b()}</em></div>')
|
|
585
618
|
// Can't have two expression children in same parent, but each is in its own element
|
|
@@ -779,6 +812,55 @@ describe('JSX transform — template emission', () => {
|
|
|
779
812
|
expect(result).toContain('((el) => { myEl = el })(__root)')
|
|
780
813
|
})
|
|
781
814
|
|
|
815
|
+
test('block-arrow ref on a child element with adjacent reactive props compiles cleanly', () => {
|
|
816
|
+
// Regression: a child element (NOT __root) with `hasDynamic=true`
|
|
817
|
+
// used to emit `const __e0 = __root.children[N]` followed by an
|
|
818
|
+
// unterminated ref-call `((el) => { x = el })(__e0)`. Without a
|
|
819
|
+
// trailing `;` on the const line, ASI did NOT insert one (because
|
|
820
|
+
// `__root.children[N]((el) => ...)` is a valid function call), and
|
|
821
|
+
// the two lines parsed as ONE expression:
|
|
822
|
+
// `const __e0 = __root.children[N]((el) => ...)(__e0)`
|
|
823
|
+
// — calling `children[N]` as a function with the arrow as arg, and
|
|
824
|
+
// self-referencing `__e0` before assignment. Surfaced when the
|
|
825
|
+
// app-showcase /dnd demo used `ref={(el) => { letVar = el }}` next
|
|
826
|
+
// to `data-X={signal()}` reactive props on the same element. Fix:
|
|
827
|
+
// append `;` to every bind line (`bindLines.map(l => ` ${l};`)`).
|
|
828
|
+
//
|
|
829
|
+
// This test asserts the OUTPUT is well-formed: the const line ends
|
|
830
|
+
// in `;` and the ref call IIFE follows on its own line.
|
|
831
|
+
const result = t(
|
|
832
|
+
'<div><span ref={(el) => { x = el }} data-state={cls()} /></div>',
|
|
833
|
+
)
|
|
834
|
+
// Const declaration must terminate before the ref IIFE.
|
|
835
|
+
expect(result).toMatch(/const __e0 = __root\.children\[0\];\s*\n/)
|
|
836
|
+
// The ref IIFE is its own statement, calling __e0 (not chained).
|
|
837
|
+
expect(result).toContain('((el) => { x = el })(__e0)')
|
|
838
|
+
// And the chained-call shape MUST NOT appear (the bug pattern).
|
|
839
|
+
expect(result).not.toMatch(/__root\.children\[0\]\(\(/)
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
test('compiled output parses cleanly for block-arrow ref + reactive prop', () => {
|
|
843
|
+
// Functional regression: prove the compiled module is well-formed JS.
|
|
844
|
+
// Pre-fix the AST-level shape was malformed and execution threw
|
|
845
|
+
// "TypeError: __root.children[N] is not a function" at mount time
|
|
846
|
+
// because the const declaration chained into the ref IIFE.
|
|
847
|
+
//
|
|
848
|
+
// Strip imports, stub framework calls, and parse the body via the
|
|
849
|
+
// Function constructor. If the const line lacks its terminator, the
|
|
850
|
+
// ref-call IIFE would silently turn the RHS into a function call —
|
|
851
|
+
// valid JS, wrong runtime behavior. So we BOTH parse-check AND
|
|
852
|
+
// string-shape-check: parse to catch syntax errors, regex to catch
|
|
853
|
+
// the silent-merge case (which parses fine but means the wrong thing).
|
|
854
|
+
const result = t(
|
|
855
|
+
'<div><span ref={(el) => { x = el }} data-state={cls()} /></div>',
|
|
856
|
+
)
|
|
857
|
+
const codeOnly = result.replace(/^\s*import\b[^;]+;?\s*/gm, '')
|
|
858
|
+
const wrapped = `let x;\nconst _tpl = () => {}; const _bind = () => {}; const _bindDirect = () => {}; const cls = () => "v";\nreturn ${codeOnly};`
|
|
859
|
+
expect(() => new Function(wrapped)).not.toThrow()
|
|
860
|
+
// And the buggy chained-call shape MUST NOT appear.
|
|
861
|
+
expect(result).not.toMatch(/children\[0\]\(\(/)
|
|
862
|
+
})
|
|
863
|
+
|
|
782
864
|
test('handles non-void self-closing element as closing tag', () => {
|
|
783
865
|
const result = t('<div><span></span></div>')
|
|
784
866
|
expect(result).toContain('_tpl(')
|
|
@@ -1116,13 +1198,28 @@ describe('JSX transform — template emission edge cases', () => {
|
|
|
1116
1198
|
test('non-delegated event (onMouseEnter) uses addEventListener not delegation', () => {
|
|
1117
1199
|
const result = t('<div onMouseEnter={handler}><span /></div>')
|
|
1118
1200
|
expect(result).toContain('_tpl(')
|
|
1119
|
-
// mouseenter is NOT in DELEGATED_EVENTS → must use addEventListener
|
|
1120
|
-
//
|
|
1121
|
-
|
|
1122
|
-
|
|
1201
|
+
// mouseenter is NOT in DELEGATED_EVENTS → must use addEventListener.
|
|
1202
|
+
// The event name is the JSX attribute with the "on" prefix dropped
|
|
1203
|
+
// and the rest fully lowercased (`onMouseEnter` → `mouseenter`)
|
|
1204
|
+
// — DOM events are all-lowercase. Prior to the fix this emitted
|
|
1205
|
+
// `mouseEnter` (camelCase) which the browser never dispatches.
|
|
1206
|
+
expect(result).toContain('addEventListener("mouseenter"')
|
|
1207
|
+
expect(result).not.toContain('mouseEnter')
|
|
1123
1208
|
expect(result).not.toContain('__ev_')
|
|
1124
1209
|
})
|
|
1125
1210
|
|
|
1211
|
+
// Regression: `onDoubleClick` is the one React→DOM event-name where
|
|
1212
|
+
// the simple-lowercase rule is wrong. The DOM event is `dblclick`,
|
|
1213
|
+
// NOT `doubleclick`. Pre-fix the compiler emitted `doubleclick`,
|
|
1214
|
+
// attaching a listener the browser never fires. Proven broken in
|
|
1215
|
+
// real Chromium via `e2e/app.spec.ts:dbl-click button increments by 10`.
|
|
1216
|
+
test('onDoubleClick maps to dblclick (not doubleclick)', () => {
|
|
1217
|
+
const result = t('<div onDoubleClick={handler}><span /></div>')
|
|
1218
|
+
expect(result).toContain('_tpl(')
|
|
1219
|
+
expect(result).toContain('__ev_dblclick = handler')
|
|
1220
|
+
expect(result).not.toContain('doubleclick')
|
|
1221
|
+
})
|
|
1222
|
+
|
|
1126
1223
|
test('template with both dynamic attribute AND dynamic child text', () => {
|
|
1127
1224
|
const result = t('<div title={getTitle()}>{count()}</div>')
|
|
1128
1225
|
expect(result).toContain('_tpl(')
|
|
@@ -1720,3 +1817,1072 @@ describe('JSX transform — SSR mode', () => {
|
|
|
1720
1817
|
expect(out).toContain('_tpl(')
|
|
1721
1818
|
})
|
|
1722
1819
|
})
|
|
1820
|
+
|
|
1821
|
+
// ─── Signal auto-call in JSX ────────────────────────────────────────────────
|
|
1822
|
+
|
|
1823
|
+
describe('JSX transform — signal auto-call', () => {
|
|
1824
|
+
test('bare signal in text child is auto-called', () => {
|
|
1825
|
+
const result = t('function C() { const name = signal("Vít"); return <div>{name}</div> }')
|
|
1826
|
+
expect(result).toContain('name()')
|
|
1827
|
+
expect(result).toContain('_bind')
|
|
1828
|
+
})
|
|
1829
|
+
|
|
1830
|
+
test('signal in attribute expression is auto-called', () => {
|
|
1831
|
+
const result = t('function C() { const show = signal(false); return <div class={show ? "active" : ""}></div> }')
|
|
1832
|
+
expect(result).toContain('show()')
|
|
1833
|
+
expect(result).toContain('_bind')
|
|
1834
|
+
})
|
|
1835
|
+
|
|
1836
|
+
test('signal already called is NOT double-called', () => {
|
|
1837
|
+
const result = t('function C() { const count = signal(0); return <div>{count()}</div> }')
|
|
1838
|
+
expect(result).not.toContain('count()()')
|
|
1839
|
+
expect(result).toContain('count')
|
|
1840
|
+
})
|
|
1841
|
+
|
|
1842
|
+
test('signal in ternary is auto-called', () => {
|
|
1843
|
+
const result = t('function C() { const show = signal(false); return <div>{show ? "yes" : "no"}</div> }')
|
|
1844
|
+
expect(result).toContain('show()')
|
|
1845
|
+
expect(result).toContain('? "yes" : "no"')
|
|
1846
|
+
})
|
|
1847
|
+
|
|
1848
|
+
test('signal in template literal is auto-called', () => {
|
|
1849
|
+
const result = t('function C() { const name = signal("world"); return <div>{`hello ${name}`}</div> }')
|
|
1850
|
+
expect(result).toContain('name()')
|
|
1851
|
+
})
|
|
1852
|
+
|
|
1853
|
+
test('signal in component prop is auto-called with _rp', () => {
|
|
1854
|
+
const result = t('function C() { const val = signal(42); return <MyComp value={val} /> }')
|
|
1855
|
+
expect(result).toContain('_rp(() => val())')
|
|
1856
|
+
})
|
|
1857
|
+
|
|
1858
|
+
test('multiple signals in one expression are all auto-called', () => {
|
|
1859
|
+
const result = t('function C() { const a = signal(1); const b = signal(2); return <div>{a + b}</div> }')
|
|
1860
|
+
expect(result).toContain('a()')
|
|
1861
|
+
expect(result).toContain('b()')
|
|
1862
|
+
})
|
|
1863
|
+
|
|
1864
|
+
test('signal in conditional attribute is auto-called', () => {
|
|
1865
|
+
const result = t('function C() { const active = signal(false); return <div title={active ? "on" : "off"}></div> }')
|
|
1866
|
+
expect(result).toContain('active()')
|
|
1867
|
+
})
|
|
1868
|
+
|
|
1869
|
+
test('non-signal const is NOT auto-called', () => {
|
|
1870
|
+
const result = t('function C() { const x = 42; return <div>{x}</div> }')
|
|
1871
|
+
expect(result).not.toContain('x()')
|
|
1872
|
+
})
|
|
1873
|
+
|
|
1874
|
+
test('computed() IS auto-called (same callable pattern as signal)', () => {
|
|
1875
|
+
const result = t('function C() { const doubled = computed(() => 2); return <div>{doubled}</div> }')
|
|
1876
|
+
expect(result).toContain('doubled()')
|
|
1877
|
+
expect(result).toContain('_bind')
|
|
1878
|
+
})
|
|
1879
|
+
|
|
1880
|
+
test('computed already called is NOT double-called', () => {
|
|
1881
|
+
const result = t('function C() { const doubled = computed(() => 2); return <div>{doubled()}</div> }')
|
|
1882
|
+
expect(result).not.toContain('doubled()()')
|
|
1883
|
+
})
|
|
1884
|
+
|
|
1885
|
+
test('signal + computed in same expression both auto-called', () => {
|
|
1886
|
+
const result = t('function C() { const count = signal(0); const doubled = computed(() => count() * 2); return <div>{count} + {doubled}</div> }')
|
|
1887
|
+
expect(result).toContain('.data = count()')
|
|
1888
|
+
expect(result).toContain('.data = doubled()')
|
|
1889
|
+
})
|
|
1890
|
+
|
|
1891
|
+
test('signal in arrow function child is NOT auto-called (already reactive)', () => {
|
|
1892
|
+
const result = t('function C() { const count = signal(0); return <div>{() => count()}</div> }')
|
|
1893
|
+
// The arrow function is already reactive — no auto-call on the inner count
|
|
1894
|
+
expect(result).not.toContain('count()()')
|
|
1895
|
+
})
|
|
1896
|
+
|
|
1897
|
+
test('signal used in non-JSX context is NOT modified', () => {
|
|
1898
|
+
const result = t('function C() { const x = signal(0); console.log(x); return <div>{x}</div> }')
|
|
1899
|
+
// console.log(x) should keep bare x, only JSX usage gets auto-called
|
|
1900
|
+
expect(result).toContain('console.log(x)')
|
|
1901
|
+
// But JSX usage gets auto-called
|
|
1902
|
+
expect(result).toContain('.data = x()')
|
|
1903
|
+
})
|
|
1904
|
+
|
|
1905
|
+
test('signal as event handler value IS auto-called (unwraps to the handler fn)', () => {
|
|
1906
|
+
const result = t('function C() { const handler = signal(() => {}); return <div onClick={handler}></div> }')
|
|
1907
|
+
// onClick={handler} where handler is a signal → handler() unwraps to the function
|
|
1908
|
+
// This is correct — the event listener gets the unwrapped function value
|
|
1909
|
+
expect(result).toContain('handler()')
|
|
1910
|
+
})
|
|
1911
|
+
|
|
1912
|
+
test('module-scope signal IS tracked and auto-called', () => {
|
|
1913
|
+
const result = t('const globalSig = signal(0); function C() { return <div>{globalSig}</div> }')
|
|
1914
|
+
// Module-scope signal declarations are tracked by the single-pass walk
|
|
1915
|
+
expect(result).toContain('globalSig()')
|
|
1916
|
+
})
|
|
1917
|
+
|
|
1918
|
+
test('knownSignals option enables cross-module auto-call', () => {
|
|
1919
|
+
const code = 'import { count } from "./store"; function App() { return <div>{count}</div> }'
|
|
1920
|
+
const result = transformJSX(code, 'test.tsx', { knownSignals: ['count'] }).code
|
|
1921
|
+
expect(result).toContain('count()')
|
|
1922
|
+
expect(result).toContain('_bind')
|
|
1923
|
+
})
|
|
1924
|
+
|
|
1925
|
+
test('knownSignals with alias — local name is used', () => {
|
|
1926
|
+
const code = 'import { count as c } from "./store"; function App() { return <div>{c}</div> }'
|
|
1927
|
+
const result = transformJSX(code, 'test.tsx', { knownSignals: ['c'] }).code
|
|
1928
|
+
expect(result).toContain('c()')
|
|
1929
|
+
})
|
|
1930
|
+
|
|
1931
|
+
test('knownSignals does not double-call already-called signals', () => {
|
|
1932
|
+
const code = 'import { count } from "./store"; function App() { return <div>{count()}</div> }'
|
|
1933
|
+
const result = transformJSX(code, 'test.tsx', { knownSignals: ['count'] }).code
|
|
1934
|
+
expect(result).not.toContain('count()()')
|
|
1935
|
+
})
|
|
1936
|
+
|
|
1937
|
+
test('knownSignals respects scope shadowing', () => {
|
|
1938
|
+
const code = 'import { count } from "./store"; function App() { const count = "shadow"; return <div>{count}</div> }'
|
|
1939
|
+
const result = transformJSX(code, 'test.tsx', { knownSignals: ['count'] }).code
|
|
1940
|
+
expect(result).not.toContain('.data = count()')
|
|
1941
|
+
})
|
|
1942
|
+
|
|
1943
|
+
test('props.x is still inlined alongside signal auto-call', () => {
|
|
1944
|
+
const result = t('function C(props) { const show = signal(false); const label = props.label; return <div class={show ? label : "default"}></div> }')
|
|
1945
|
+
expect(result).toContain('show()')
|
|
1946
|
+
expect(result).toContain('props.label')
|
|
1947
|
+
})
|
|
1948
|
+
|
|
1949
|
+
// Regression: signal-method calls were getting double-wrapped — the
|
|
1950
|
+
// auto-call inserted `()` after the bare signal reference inside
|
|
1951
|
+
// `signal.set(value)`, producing `signal().set(value)`. That calls
|
|
1952
|
+
// the signal (returns its current value, e.g. a string) then tries
|
|
1953
|
+
// `.set` on the string (undefined → TypeError). Every `signal.set`,
|
|
1954
|
+
// `signal.peek`, `signal.update` call inside event handlers / hot
|
|
1955
|
+
// paths was silently broken.
|
|
1956
|
+
test('signal.set() in event handler does NOT auto-call the bare signal reference', () => {
|
|
1957
|
+
const result = t(
|
|
1958
|
+
'function C() { const value = signal(""); return <input onInput={(e) => value.set(e.target.value)} /> }',
|
|
1959
|
+
)
|
|
1960
|
+
// Must keep `value.set(...)` — NOT `value().set(...)`.
|
|
1961
|
+
expect(result).toContain('value.set(e.target.value)')
|
|
1962
|
+
expect(result).not.toContain('value().set')
|
|
1963
|
+
})
|
|
1964
|
+
|
|
1965
|
+
test('signal.peek() does NOT auto-call', () => {
|
|
1966
|
+
const result = t(
|
|
1967
|
+
'function C() { const count = signal(0); return <button onClick={() => console.log(count.peek())}>x</button> }',
|
|
1968
|
+
)
|
|
1969
|
+
expect(result).toContain('count.peek()')
|
|
1970
|
+
expect(result).not.toContain('count().peek')
|
|
1971
|
+
})
|
|
1972
|
+
|
|
1973
|
+
test('signal.update() does NOT auto-call', () => {
|
|
1974
|
+
const result = t(
|
|
1975
|
+
'function C() { const count = signal(0); return <button onClick={() => count.update(n => n + 1)}>+</button> }',
|
|
1976
|
+
)
|
|
1977
|
+
expect(result).toContain('count.update(')
|
|
1978
|
+
expect(result).not.toContain('count().update')
|
|
1979
|
+
})
|
|
1980
|
+
|
|
1981
|
+
// Bare member-access on a signal (no call) STILL auto-calls — preserves
|
|
1982
|
+
// the existing convention where a signal containing an object can be
|
|
1983
|
+
// dereferenced via `signalContainingObj.someProp` (compiles to
|
|
1984
|
+
// `signalContainingObj().someProp`). Only the CALLED form
|
|
1985
|
+
// (`signal.method(...)`) skips the auto-call. See findSignalIdents
|
|
1986
|
+
// in jsx.ts for the rationale.
|
|
1987
|
+
test('signal.someProp (bare member access) DOES auto-call', () => {
|
|
1988
|
+
const result = t(
|
|
1989
|
+
'function C() { const data = signal({ count: 0 }); return <div>{data.count}</div> }',
|
|
1990
|
+
)
|
|
1991
|
+
expect(result).toContain('data().count')
|
|
1992
|
+
})
|
|
1993
|
+
|
|
1994
|
+
// The bare signal reference STILL auto-calls in JSX text — make sure
|
|
1995
|
+
// the fix doesn't over-correct.
|
|
1996
|
+
test('bare signal in JSX text still auto-calls (fix does not over-correct)', () => {
|
|
1997
|
+
const result = t('function C() { const count = signal(0); return <div>{count}</div> }')
|
|
1998
|
+
expect(result).toContain('count()')
|
|
1999
|
+
})
|
|
2000
|
+
|
|
2001
|
+
// ── JSX text/expression whitespace (regression) ─────────────────────
|
|
2002
|
+
// The compiler used `.replace(/\n\s*/g, '').trim()` on JSX text which
|
|
2003
|
+
// stripped ALL leading/trailing whitespace — even spaces adjacent to
|
|
2004
|
+
// expressions on the same line. So `<p>doubled: {x}</p>` produced
|
|
2005
|
+
// `<p>doubled:</p>` + appended text node, rendering "doubled:0"
|
|
2006
|
+
// instead of "doubled: 0". Same class for `<p>{x} remaining</p>` →
|
|
2007
|
+
// text "remaining" loses its leading space, rendering as "Xremaining".
|
|
2008
|
+
// Fix: only strip whitespace adjacent to newlines (multi-line JSX
|
|
2009
|
+
// formatting), preserve same-line whitespace adjacent to expressions.
|
|
2010
|
+
test('preserves trailing space in JSX text before expression on same line', () => {
|
|
2011
|
+
const result = t('<p>doubled: {x()}</p>')
|
|
2012
|
+
// The static text portion of the template must keep "doubled: "
|
|
2013
|
+
// (with trailing space) so the appended expression value renders
|
|
2014
|
+
// as "doubled: 0", not "doubled:0".
|
|
2015
|
+
expect(result).toContain('doubled: ')
|
|
2016
|
+
})
|
|
2017
|
+
|
|
2018
|
+
test('preserves leading space in JSX text after expression on same line', () => {
|
|
2019
|
+
const result = t('<p>{x()} remaining</p>')
|
|
2020
|
+
// Static portion must include " remaining" (with leading space).
|
|
2021
|
+
expect(result).toContain(' remaining')
|
|
2022
|
+
})
|
|
2023
|
+
|
|
2024
|
+
test('strips multi-line JSX text whitespace adjacent to newlines', () => {
|
|
2025
|
+
// Multi-line JSX with indentation should still collapse — only
|
|
2026
|
+
// SAME-LINE whitespace adjacent to expressions is preserved.
|
|
2027
|
+
const result = t(`<div>
|
|
2028
|
+
<span>hello</span>
|
|
2029
|
+
</div>`)
|
|
2030
|
+
// The newlines + indentation should not produce stray text nodes.
|
|
2031
|
+
expect(result).toContain('hello')
|
|
2032
|
+
expect(result).not.toContain('"\\n "')
|
|
2033
|
+
})
|
|
2034
|
+
|
|
2035
|
+
test('shadowed signal variable by const is NOT auto-called', () => {
|
|
2036
|
+
const result = t(`
|
|
2037
|
+
function App() {
|
|
2038
|
+
const show = signal(false)
|
|
2039
|
+
function Inner() {
|
|
2040
|
+
const show = 'not a signal'
|
|
2041
|
+
return <div>{show}</div>
|
|
2042
|
+
}
|
|
2043
|
+
return <div>{show}</div>
|
|
2044
|
+
}
|
|
2045
|
+
`)
|
|
2046
|
+
// Inner's show is a plain string, NOT a signal — should NOT be auto-called
|
|
2047
|
+
// But App's show IS a signal — should be auto-called
|
|
2048
|
+
expect(result).toContain('show()') // App's usage
|
|
2049
|
+
expect(result).toContain('textContent = show') // Inner's usage (static)
|
|
2050
|
+
})
|
|
2051
|
+
|
|
2052
|
+
test('function parameter shadowing signal is NOT auto-called', () => {
|
|
2053
|
+
const result = t(`
|
|
2054
|
+
function App() {
|
|
2055
|
+
const count = signal(0)
|
|
2056
|
+
function Display(count) {
|
|
2057
|
+
return <div>{count}</div>
|
|
2058
|
+
}
|
|
2059
|
+
return <div>{count}</div>
|
|
2060
|
+
}
|
|
2061
|
+
`)
|
|
2062
|
+
// Display's count is a parameter, not the signal
|
|
2063
|
+
expect(result).toContain('textContent = count') // Display: static
|
|
2064
|
+
expect(result).toContain('.data = count()') // App: auto-called
|
|
2065
|
+
})
|
|
2066
|
+
|
|
2067
|
+
test('destructured parameter shadowing signal is NOT auto-called', () => {
|
|
2068
|
+
const result = t(`
|
|
2069
|
+
function App() {
|
|
2070
|
+
const name = signal('Vít')
|
|
2071
|
+
function Greet({ name }) {
|
|
2072
|
+
return <div>{name}</div>
|
|
2073
|
+
}
|
|
2074
|
+
return <div>{name}</div>
|
|
2075
|
+
}
|
|
2076
|
+
`)
|
|
2077
|
+
// Greet's name is destructured from props — shadows the signal
|
|
2078
|
+
expect(result).toContain('textContent = name') // Greet: static
|
|
2079
|
+
expect(result).toContain('.data = name()') // App: auto-called
|
|
2080
|
+
})
|
|
2081
|
+
|
|
2082
|
+
test('signal in outer scope is auto-called when NOT shadowed', () => {
|
|
2083
|
+
const result = t(`
|
|
2084
|
+
function App() {
|
|
2085
|
+
const name = signal('Vít')
|
|
2086
|
+
function Inner() {
|
|
2087
|
+
return <div>{name}</div>
|
|
2088
|
+
}
|
|
2089
|
+
return <div>{name}</div>
|
|
2090
|
+
}
|
|
2091
|
+
`)
|
|
2092
|
+
// name is NOT shadowed in Inner — auto-called in both
|
|
2093
|
+
const autoCallCount = (result.match(/name\(\)/g) || []).length
|
|
2094
|
+
expect(autoCallCount).toBeGreaterThanOrEqual(2)
|
|
2095
|
+
})
|
|
2096
|
+
|
|
2097
|
+
test('array destructured parameter shadowing signal is NOT auto-called', () => {
|
|
2098
|
+
const result = t(`
|
|
2099
|
+
function App() {
|
|
2100
|
+
const item = signal('x')
|
|
2101
|
+
function Inner([item]) {
|
|
2102
|
+
return <div>{item}</div>
|
|
2103
|
+
}
|
|
2104
|
+
return <div>{item}</div>
|
|
2105
|
+
}
|
|
2106
|
+
`)
|
|
2107
|
+
// Inner's item is array-destructured — shadows the signal
|
|
2108
|
+
expect(result).toContain('textContent = item') // Inner: static
|
|
2109
|
+
expect(result).toContain('.data = item()') // App: auto-called
|
|
2110
|
+
})
|
|
2111
|
+
|
|
2112
|
+
test('signal re-declared as signal in inner scope is still auto-called', () => {
|
|
2113
|
+
const result = t(`
|
|
2114
|
+
function App() {
|
|
2115
|
+
const count = signal(0)
|
|
2116
|
+
function Inner() {
|
|
2117
|
+
const count = signal(10)
|
|
2118
|
+
return <div>{count}</div>
|
|
2119
|
+
}
|
|
2120
|
+
return <div>{count}</div>
|
|
2121
|
+
}
|
|
2122
|
+
`)
|
|
2123
|
+
// Both are signal() calls — both should be auto-called
|
|
2124
|
+
const autoCallCount = (result.match(/count\(\)/g) || []).length
|
|
2125
|
+
expect(autoCallCount).toBeGreaterThanOrEqual(2)
|
|
2126
|
+
})
|
|
2127
|
+
|
|
2128
|
+
test('signal shadowing does not leak across sibling functions', () => {
|
|
2129
|
+
const result = t(`
|
|
2130
|
+
function App() {
|
|
2131
|
+
const show = signal(false)
|
|
2132
|
+
function A() {
|
|
2133
|
+
const show = 'text'
|
|
2134
|
+
return <div>{show}</div>
|
|
2135
|
+
}
|
|
2136
|
+
function B() {
|
|
2137
|
+
return <div>{show}</div>
|
|
2138
|
+
}
|
|
2139
|
+
return <div>{show}</div>
|
|
2140
|
+
}
|
|
2141
|
+
`)
|
|
2142
|
+
// A shadows show — static
|
|
2143
|
+
expect(result).toContain('textContent = show')
|
|
2144
|
+
// B does NOT shadow — auto-called
|
|
2145
|
+
// App does NOT shadow — auto-called
|
|
2146
|
+
const autoCallCount = (result.match(/show\(\)/g) || []).length
|
|
2147
|
+
expect(autoCallCount).toBeGreaterThanOrEqual(2)
|
|
2148
|
+
})
|
|
2149
|
+
|
|
2150
|
+
test('signal in deeply nested expression is auto-called', () => {
|
|
2151
|
+
const result = t('function C() { const x = signal(1); return <div>{x + x + x}</div> }')
|
|
2152
|
+
// All three references should be auto-called
|
|
2153
|
+
const autoCallCount = (result.match(/x\(\)/g) || []).length
|
|
2154
|
+
expect(autoCallCount).toBe(3)
|
|
2155
|
+
})
|
|
2156
|
+
|
|
2157
|
+
test('signal in object property value (not key) is auto-called', () => {
|
|
2158
|
+
const result = t('function C() { const x = signal(1); return <MyComp data={{ value: x }} /> }')
|
|
2159
|
+
expect(result).toContain('x()')
|
|
2160
|
+
expect(result).toContain('_rp(')
|
|
2161
|
+
})
|
|
2162
|
+
|
|
2163
|
+
test('signal + prop-derived in same expression both resolved', () => {
|
|
2164
|
+
const result = t('function C(props) { const x = signal(0); const label = props.label; return <div>{x ? label : "none"}</div> }')
|
|
2165
|
+
expect(result).toContain('x()')
|
|
2166
|
+
expect(result).toContain('props.label')
|
|
2167
|
+
expect(result).toContain('_bind')
|
|
2168
|
+
})
|
|
2169
|
+
|
|
2170
|
+
test('signal with no init (const x = signal()) tracked', () => {
|
|
2171
|
+
const result = t('function C() { const x = signal(); return <div>{x}</div> }')
|
|
2172
|
+
expect(result).toContain('x()')
|
|
2173
|
+
})
|
|
2174
|
+
|
|
2175
|
+
test('signal in member expression property position is NOT auto-called', () => {
|
|
2176
|
+
const result = t('function C() { const x = signal(0); return <div>{obj.x}</div> }')
|
|
2177
|
+
// x as a property name is not a signal reference
|
|
2178
|
+
expect(result).not.toContain('obj.x()')
|
|
2179
|
+
})
|
|
2180
|
+
|
|
2181
|
+
test('signal as member expression object IS auto-called', () => {
|
|
2182
|
+
const result = t('function C() { const x = signal({ a: 1 }); return <div>{x.a}</div> }')
|
|
2183
|
+
// x is the object, should be auto-called
|
|
2184
|
+
expect(result).toContain('x().a')
|
|
2185
|
+
})
|
|
2186
|
+
|
|
2187
|
+
test('const declared without init is not tracked as signal', () => {
|
|
2188
|
+
const result = t('function C() { const x = signal(0); function Inner() { let x; return <div>{x}</div> } return <div>{x}</div> }')
|
|
2189
|
+
// Inner's x is let, not tracked. App's x is signal
|
|
2190
|
+
expect(result).toContain('.data = x()')
|
|
2191
|
+
})
|
|
2192
|
+
|
|
2193
|
+
test('signal shadowed by let declaration in inner scope', () => {
|
|
2194
|
+
const result = t(`
|
|
2195
|
+
function C() {
|
|
2196
|
+
const show = signal(false)
|
|
2197
|
+
function Inner() {
|
|
2198
|
+
let show = true
|
|
2199
|
+
return <div>{show}</div>
|
|
2200
|
+
}
|
|
2201
|
+
return <div>{show}</div>
|
|
2202
|
+
}
|
|
2203
|
+
`)
|
|
2204
|
+
// Inner's let show shadows the signal — but let is not tracked by the declarator check
|
|
2205
|
+
// (let is not const so it's not in signalVars, but it's also not tracked as shadow)
|
|
2206
|
+
// Actually findShadowingNames only checks top-level VariableDeclaration declarations
|
|
2207
|
+
// let is VariableDeclaration kind=let — it should shadow
|
|
2208
|
+
expect(result).toContain('.data = show()') // outer: auto-called
|
|
2209
|
+
})
|
|
2210
|
+
|
|
2211
|
+
test('knownSignals with empty array does not crash', () => {
|
|
2212
|
+
const code = 'function App() { return <div>hello</div> }'
|
|
2213
|
+
const result = transformJSX(code, 'test.tsx', { knownSignals: [] }).code
|
|
2214
|
+
expect(result).toContain('hello')
|
|
2215
|
+
})
|
|
2216
|
+
|
|
2217
|
+
test('knownSignals combined with local signal declarations', () => {
|
|
2218
|
+
const code = 'import { count } from "./store"; function App() { const local = signal(0); return <div>{count}{local}</div> }'
|
|
2219
|
+
const result = transformJSX(code, 'test.tsx', { knownSignals: ['count'] }).code
|
|
2220
|
+
// Both the imported signal (via knownSignals) and the local signal should be auto-called
|
|
2221
|
+
expect(result).toContain('count()')
|
|
2222
|
+
expect(result).toContain('local()')
|
|
2223
|
+
})
|
|
2224
|
+
|
|
2225
|
+
test('knownSignals with default import name', () => {
|
|
2226
|
+
// When a default import resolves to a signal, the local name should be auto-called
|
|
2227
|
+
const code = 'import count from "./store"; function App() { return <div>{count}</div> }'
|
|
2228
|
+
const result = transformJSX(code, 'test.tsx', { knownSignals: ['count'] }).code
|
|
2229
|
+
expect(result).toContain('count()')
|
|
2230
|
+
expect(result).toContain('_bind')
|
|
2231
|
+
})
|
|
2232
|
+
})
|
|
2233
|
+
|
|
2234
|
+
// ─── Additional branch coverage for >= 90% ─────────────────────────────────
|
|
2235
|
+
|
|
2236
|
+
describe('JSX transform — template reactive style _bindDirect path', () => {
|
|
2237
|
+
test('reactive style accessor uses _bindDirect with cssText updater', () => {
|
|
2238
|
+
const result = t('<div style={getStyle()}><span /></div>')
|
|
2239
|
+
expect(result).toContain('_bindDirect(getStyle,')
|
|
2240
|
+
expect(result).toContain('style.cssText')
|
|
2241
|
+
})
|
|
2242
|
+
|
|
2243
|
+
test('reactive style accessor with object check in updater', () => {
|
|
2244
|
+
const result = t('<div style={styleSignal()}><span /></div>')
|
|
2245
|
+
expect(result).toContain('_bindDirect(styleSignal,')
|
|
2246
|
+
// The updater should handle both string and object
|
|
2247
|
+
expect(result).toContain('typeof v === "string"')
|
|
2248
|
+
expect(result).toContain('Object.assign')
|
|
2249
|
+
})
|
|
2250
|
+
})
|
|
2251
|
+
|
|
2252
|
+
// ── DOM-property assignment for value/checked/etc. (regression) ─────────
|
|
2253
|
+
// The compiler used `setAttribute("value", v)` for ALL non-class/style
|
|
2254
|
+
// attributes. For inputs that's wrong: `value` is a live DOM property,
|
|
2255
|
+
// `setAttribute` only sets the initial attribute. After the user types,
|
|
2256
|
+
// the property and attribute drift. Then `input.set('')` runs the
|
|
2257
|
+
// _bindDirect updater — which only resets the attribute, leaving the
|
|
2258
|
+
// stale typed text in the visible field. Same for `checked` on
|
|
2259
|
+
// checkboxes (presence of the attribute means checked, regardless of
|
|
2260
|
+
// value). Fix: emit property assignment for known DOM properties.
|
|
2261
|
+
describe('JSX transform — DOM properties use property assignment', () => {
|
|
2262
|
+
test('reactive value on input emits property assignment, not setAttribute', () => {
|
|
2263
|
+
const result = t('<div><input value={() => input()} /></div>')
|
|
2264
|
+
// Should be `el.value = v`, not `setAttribute("value", ...)`
|
|
2265
|
+
expect(result).toContain('.value = v')
|
|
2266
|
+
expect(result).not.toContain('setAttribute("value"')
|
|
2267
|
+
})
|
|
2268
|
+
|
|
2269
|
+
test('reactive checked on input emits property assignment', () => {
|
|
2270
|
+
const result = t('<div><input checked={done()} /></div>')
|
|
2271
|
+
expect(result).toContain('.checked = v')
|
|
2272
|
+
expect(result).not.toContain('setAttribute("checked"')
|
|
2273
|
+
})
|
|
2274
|
+
|
|
2275
|
+
test('static-call value on input emits property assignment', () => {
|
|
2276
|
+
// Non-signal-direct dynamic expression goes through reactiveBindExprs
|
|
2277
|
+
const result = t('<div><input value={x.y} /></div>')
|
|
2278
|
+
expect(result).toContain('.value = x.y')
|
|
2279
|
+
expect(result).not.toContain('setAttribute("value"')
|
|
2280
|
+
})
|
|
2281
|
+
|
|
2282
|
+
test('selected on option emits property assignment', () => {
|
|
2283
|
+
const result = t('<div><option selected={isSelected()}>x</option></div>')
|
|
2284
|
+
expect(result).toContain('.selected = v')
|
|
2285
|
+
expect(result).not.toContain('setAttribute("selected"')
|
|
2286
|
+
})
|
|
2287
|
+
|
|
2288
|
+
test('disabled on button emits property assignment', () => {
|
|
2289
|
+
const result = t('<div><button disabled={isDisabled()}>x</button></div>')
|
|
2290
|
+
expect(result).toContain('.disabled = v')
|
|
2291
|
+
expect(result).not.toContain('setAttribute("disabled"')
|
|
2292
|
+
})
|
|
2293
|
+
|
|
2294
|
+
test('non-DOM-prop attribute still uses setAttribute', () => {
|
|
2295
|
+
// placeholder is a real attribute, not a property-divergent IDL prop
|
|
2296
|
+
const result = t('<div><input placeholder={msg()} /></div>')
|
|
2297
|
+
expect(result).toContain('setAttribute("placeholder"')
|
|
2298
|
+
})
|
|
2299
|
+
})
|
|
2300
|
+
|
|
2301
|
+
describe('JSX transform — template combined _bind for complex expressions', () => {
|
|
2302
|
+
test('complex attribute expression uses combined _bind', () => {
|
|
2303
|
+
const result = t('<div class={`${a()} ${b()}`}><span /></div>')
|
|
2304
|
+
expect(result).toContain('_bind(() => {')
|
|
2305
|
+
expect(result).toContain('className')
|
|
2306
|
+
})
|
|
2307
|
+
|
|
2308
|
+
test('dynamic spread in template uses _applyProps in reactive _bind', () => {
|
|
2309
|
+
const result = t('<div {...getProps()}><span /></div>')
|
|
2310
|
+
expect(result).toContain('_tpl(')
|
|
2311
|
+
expect(result).toContain('_applyProps')
|
|
2312
|
+
expect(result).toContain('_bind')
|
|
2313
|
+
})
|
|
2314
|
+
})
|
|
2315
|
+
|
|
2316
|
+
describe('JSX transform — children expression as bareIdentifier "children"', () => {
|
|
2317
|
+
test('bare children identifier uses _mountSlot', () => {
|
|
2318
|
+
const result = t('function C(props) { const children = props.children; return <div>{children}</div> }')
|
|
2319
|
+
expect(result).toContain('_mountSlot')
|
|
2320
|
+
})
|
|
2321
|
+
})
|
|
2322
|
+
|
|
2323
|
+
describe('JSX transform — template static expression string attr via JSX expression', () => {
|
|
2324
|
+
test('numeric expression attribute baked into HTML', () => {
|
|
2325
|
+
const result = t('<div tabindex={3}><span /></div>')
|
|
2326
|
+
expect(result).toContain('tabindex=\\"3\\"')
|
|
2327
|
+
})
|
|
2328
|
+
|
|
2329
|
+
test('boolean false expression attribute omitted from HTML', () => {
|
|
2330
|
+
const result = t('<div hidden={false}><span /></div>')
|
|
2331
|
+
expect(result).not.toContain('hidden')
|
|
2332
|
+
})
|
|
2333
|
+
|
|
2334
|
+
test('null expression attribute omitted from HTML', () => {
|
|
2335
|
+
const result = t('<div hidden={null}><span /></div>')
|
|
2336
|
+
expect(result).not.toContain('hidden')
|
|
2337
|
+
})
|
|
2338
|
+
})
|
|
2339
|
+
|
|
2340
|
+
describe('JSX transform — isPureStaticCall edge cases', () => {
|
|
2341
|
+
test('pure call with spread argument IS wrapped (not pure)', () => {
|
|
2342
|
+
const result = t('<div>{Math.max(...nums)}</div>')
|
|
2343
|
+
expect(result).toContain('.data =')
|
|
2344
|
+
})
|
|
2345
|
+
|
|
2346
|
+
test('Array.isArray with static arg is not wrapped', () => {
|
|
2347
|
+
const result = t('<div>{Array.isArray(null)}</div>')
|
|
2348
|
+
expect(result).not.toContain('() =>')
|
|
2349
|
+
})
|
|
2350
|
+
|
|
2351
|
+
test('encodeURIComponent with static string is not wrapped', () => {
|
|
2352
|
+
const result = t('<div>{encodeURIComponent("hello world")}</div>')
|
|
2353
|
+
expect(result).not.toContain('() =>')
|
|
2354
|
+
})
|
|
2355
|
+
|
|
2356
|
+
test('Date.now is not wrapped (no args)', () => {
|
|
2357
|
+
const result = t('<div>{Date.now()}</div>')
|
|
2358
|
+
expect(result).not.toContain('() =>')
|
|
2359
|
+
})
|
|
2360
|
+
|
|
2361
|
+
test('standalone parseInt with static arg is not wrapped', () => {
|
|
2362
|
+
const result = t('<div>{parseInt("42")}</div>')
|
|
2363
|
+
expect(result).not.toContain('() =>')
|
|
2364
|
+
})
|
|
2365
|
+
})
|
|
2366
|
+
|
|
2367
|
+
describe('JSX transform — isStatic edge cases', () => {
|
|
2368
|
+
test('template literal with no substitutions is static', () => {
|
|
2369
|
+
const result = t('<div>{`plain text`}</div>')
|
|
2370
|
+
expect(result).not.toContain('_bind')
|
|
2371
|
+
})
|
|
2372
|
+
|
|
2373
|
+
test('template literal with substitution is dynamic', () => {
|
|
2374
|
+
const result = t('<div>{`${x()}`}</div>')
|
|
2375
|
+
expect(result).toContain('_bind')
|
|
2376
|
+
})
|
|
2377
|
+
})
|
|
2378
|
+
|
|
2379
|
+
describe('JSX transform — signal auto-call in template _bind expressions', () => {
|
|
2380
|
+
test('signal in _bind reactive attribute expression', () => {
|
|
2381
|
+
const result = t('function C() { const cls = signal("a"); return <div class={`${cls} extra`}><span /></div> }')
|
|
2382
|
+
expect(result).toContain('cls()')
|
|
2383
|
+
expect(result).toContain('_bind')
|
|
2384
|
+
})
|
|
2385
|
+
|
|
2386
|
+
test('signal in template text child expression', () => {
|
|
2387
|
+
const result = t('function C() { const name = signal("X"); return <div>{`Hello ${name}`}</div> }')
|
|
2388
|
+
expect(result).toContain('name()')
|
|
2389
|
+
})
|
|
2390
|
+
|
|
2391
|
+
test('signal auto-call with addition', () => {
|
|
2392
|
+
const result = t('function C() { const a = signal(1); const b = signal(2); return <div>{a + b}</div> }')
|
|
2393
|
+
expect(result).toContain('a()')
|
|
2394
|
+
expect(result).toContain('b()')
|
|
2395
|
+
})
|
|
2396
|
+
})
|
|
2397
|
+
|
|
2398
|
+
describe('JSX transform — walkNode edge cases for scope cleanup', () => {
|
|
2399
|
+
test('JSXExpressionContainer at top level within function', () => {
|
|
2400
|
+
// This exercises the walkNode JSXExpressionContainer path with scope shadows
|
|
2401
|
+
const result = t(`
|
|
2402
|
+
function App() {
|
|
2403
|
+
const x = signal(0)
|
|
2404
|
+
function Inner() {
|
|
2405
|
+
const x = 'plain'
|
|
2406
|
+
return <MyComp>{x}</MyComp>
|
|
2407
|
+
}
|
|
2408
|
+
return <div>{x}</div>
|
|
2409
|
+
}
|
|
2410
|
+
`)
|
|
2411
|
+
expect(result).toContain('.data = x()')
|
|
2412
|
+
})
|
|
2413
|
+
|
|
2414
|
+
test('template emit within scoped function with signal shadowing', () => {
|
|
2415
|
+
const result = t(`
|
|
2416
|
+
function App() {
|
|
2417
|
+
const count = signal(0)
|
|
2418
|
+
function Nested() {
|
|
2419
|
+
const count = 'static'
|
|
2420
|
+
return <div><span>{count}</span></div>
|
|
2421
|
+
}
|
|
2422
|
+
return <div><span>{count}</span></div>
|
|
2423
|
+
}
|
|
2424
|
+
`)
|
|
2425
|
+
// Nested: count is shadowed, static
|
|
2426
|
+
expect(result).toContain('textContent = count')
|
|
2427
|
+
// App: count is signal, auto-called
|
|
2428
|
+
expect(result).toContain('.data = count()')
|
|
2429
|
+
})
|
|
2430
|
+
})
|
|
2431
|
+
|
|
2432
|
+
describe('JSX transform — parse error handling', () => {
|
|
2433
|
+
test('returns original code on parse error', () => {
|
|
2434
|
+
const result = transformJSX('this is not {valid js <>', 'bad.tsx')
|
|
2435
|
+
expect(result.code).toBe('this is not {valid js <>')
|
|
2436
|
+
expect(result.warnings).toHaveLength(0)
|
|
2437
|
+
})
|
|
2438
|
+
})
|
|
2439
|
+
|
|
2440
|
+
describe('JSX transform — reactive combined _bind for multiple reactive attrs', () => {
|
|
2441
|
+
test('multiple reactive attributes on same element with complex expressions', () => {
|
|
2442
|
+
const result = t('<div class={`${a()} b`} title={`${c()} d`}><span /></div>')
|
|
2443
|
+
expect(result).toContain('_bind(() => {')
|
|
2444
|
+
expect(result).toContain('className')
|
|
2445
|
+
expect(result).toContain('setAttribute("title"')
|
|
2446
|
+
})
|
|
2447
|
+
})
|
|
2448
|
+
|
|
2449
|
+
describe('JSX transform — signalVars.size > shadowedSignals.size check', () => {
|
|
2450
|
+
test('when all signals are shadowed, no auto-call happens', () => {
|
|
2451
|
+
const result = t(`
|
|
2452
|
+
function App() {
|
|
2453
|
+
const x = signal(0)
|
|
2454
|
+
function Inner() {
|
|
2455
|
+
const x = 'plain'
|
|
2456
|
+
return <div class={x + " extra"}></div>
|
|
2457
|
+
}
|
|
2458
|
+
return <div>{x}</div>
|
|
2459
|
+
}
|
|
2460
|
+
`)
|
|
2461
|
+
// Inner's x is NOT auto-called
|
|
2462
|
+
expect(result).toContain('className = x + " extra"')
|
|
2463
|
+
// App's x IS auto-called
|
|
2464
|
+
expect(result).toContain('.data = x()')
|
|
2465
|
+
})
|
|
2466
|
+
})
|
|
2467
|
+
|
|
2468
|
+
describe('JSX transform — _isDynamic with signal member expression and call position', () => {
|
|
2469
|
+
test('signal.set() is NOT flagged as dynamic (signal in callee position)', () => {
|
|
2470
|
+
// When signal is the callee of a call expression, it's already being called
|
|
2471
|
+
const result = t('function C() { const x = signal(0); return <button onClick={() => x.set(1)}>click</button> }')
|
|
2472
|
+
// onClick is an event handler — not wrapped regardless
|
|
2473
|
+
expect(result).not.toContain('_rp')
|
|
2474
|
+
})
|
|
2475
|
+
|
|
2476
|
+
test('signal in property name position of member expression is NOT dynamic', () => {
|
|
2477
|
+
const result = t('function C() { const x = signal(0); return <div title={obj.x}></div> }')
|
|
2478
|
+
// obj.x — x is property name, not signal reference
|
|
2479
|
+
expect(result).not.toContain('_bind')
|
|
2480
|
+
})
|
|
2481
|
+
})
|
|
2482
|
+
|
|
2483
|
+
// ─── Branch coverage: referencesPropDerived with computed MemberExpression ──
|
|
2484
|
+
|
|
2485
|
+
describe('JSX transform — referencesPropDerived computed access', () => {
|
|
2486
|
+
test('prop-derived var used as computed property key is treated as reference', () => {
|
|
2487
|
+
const result = t('function C(props) { const key = props.key; return <div title={obj[key]}></div> }')
|
|
2488
|
+
// key is used as computed property — it IS a reference (p.computed === true)
|
|
2489
|
+
expect(result).toContain('props.key')
|
|
2490
|
+
expect(result).toContain('_bind')
|
|
2491
|
+
})
|
|
2492
|
+
|
|
2493
|
+
test('prop-derived var in non-computed property position is NOT a reference', () => {
|
|
2494
|
+
const result = t('function C(props) { const data = props.data; return <div title={result.data}></div> }')
|
|
2495
|
+
// result.data — 'data' is a non-computed property name, NOT a prop-derived reference
|
|
2496
|
+
expect(result).not.toContain('_bind')
|
|
2497
|
+
})
|
|
2498
|
+
})
|
|
2499
|
+
|
|
2500
|
+
// ─── Branch coverage: template attrSetter for style (line 940) ──────────────
|
|
2501
|
+
|
|
2502
|
+
describe('JSX transform — template style attribute combined _bind', () => {
|
|
2503
|
+
test('complex reactive style uses cssText in combined _bind', () => {
|
|
2504
|
+
const result = t('<div style={getStyle() + "extra"}>text</div>')
|
|
2505
|
+
expect(result).toContain('style.cssText')
|
|
2506
|
+
expect(result).toContain('_bind(() => {')
|
|
2507
|
+
})
|
|
2508
|
+
})
|
|
2509
|
+
|
|
2510
|
+
// ─── Branch coverage: processOneAttr key attr (line 1008) ───────────────────
|
|
2511
|
+
|
|
2512
|
+
describe('JSX transform — template with key attribute on child element', () => {
|
|
2513
|
+
test('key attribute on child element is stripped in template', () => {
|
|
2514
|
+
// key on inner child doesn't bail template (only root key bails)
|
|
2515
|
+
// But templateElementCount bails on key attr on any element
|
|
2516
|
+
const result = t('<div><span key="a">text</span></div>')
|
|
2517
|
+
expect(result).not.toContain('_tpl(')
|
|
2518
|
+
})
|
|
2519
|
+
})
|
|
2520
|
+
|
|
2521
|
+
// ─── Branch coverage: selfClosing template bail (line 313) ──────────────────
|
|
2522
|
+
|
|
2523
|
+
describe('JSX transform — self-closing element template bail', () => {
|
|
2524
|
+
test('self-closing elements skip template emission', () => {
|
|
2525
|
+
const result = t('<div class={cls()} />')
|
|
2526
|
+
// Self-closing root element — tryTemplateEmit returns false
|
|
2527
|
+
expect(result).toContain('() => cls()')
|
|
2528
|
+
expect(result).not.toContain('_tpl(')
|
|
2529
|
+
})
|
|
2530
|
+
})
|
|
2531
|
+
|
|
2532
|
+
// ─── Branch coverage: isStatic types (line 1388-1389) ───────────────────────
|
|
2533
|
+
|
|
2534
|
+
describe('JSX transform — isStatic for various literal types', () => {
|
|
2535
|
+
test('NullLiteral is static', () => {
|
|
2536
|
+
const result = t('<div>{<span data-x={null} />}</div>')
|
|
2537
|
+
expect(result).toContain('const _$h0')
|
|
2538
|
+
})
|
|
2539
|
+
|
|
2540
|
+
test('template literal with expressions is not static', () => {
|
|
2541
|
+
const result = t('<div>{<span data-x={`${x}`} />}</div>')
|
|
2542
|
+
expect(result).not.toContain('const _$h0')
|
|
2543
|
+
})
|
|
2544
|
+
})
|
|
2545
|
+
|
|
2546
|
+
// ─── Branch coverage: accessesProps with arrow function inside (line 679) ────
|
|
2547
|
+
|
|
2548
|
+
describe('JSX transform — accessesProps stops at nested functions', () => {
|
|
2549
|
+
test('props read inside arrow function does not make outer expression reactive', () => {
|
|
2550
|
+
const result = t('function C(props) { return <div title={items.map(x => props.fmt(x))}></div> }')
|
|
2551
|
+
// The arrow function contains a props read, but accessesProps stops at arrow boundaries
|
|
2552
|
+
expect(result).toContain('.map')
|
|
2553
|
+
})
|
|
2554
|
+
})
|
|
2555
|
+
|
|
2556
|
+
// ─── Branch coverage: shouldWrap pure static call (line 688) ────────────────
|
|
2557
|
+
|
|
2558
|
+
describe('JSX transform — shouldWrap skips pure static calls', () => {
|
|
2559
|
+
test('Array.from with static arg in attribute position', () => {
|
|
2560
|
+
const result = t('<div data-arr={Array.from("abc")}></div>')
|
|
2561
|
+
expect(result).not.toContain('_bind')
|
|
2562
|
+
})
|
|
2563
|
+
})
|
|
2564
|
+
|
|
2565
|
+
// ─── Branch coverage: isChildrenExpression fallthrough (line 1321) ──────────
|
|
2566
|
+
|
|
2567
|
+
describe('JSX transform — isChildrenExpression edge cases', () => {
|
|
2568
|
+
test('expression ending with .children uses _mountSlot', () => {
|
|
2569
|
+
const result = t('function C(props) { return <div>{config.children}</div> }')
|
|
2570
|
+
expect(result).toContain('_mountSlot')
|
|
2571
|
+
})
|
|
2572
|
+
|
|
2573
|
+
test('identifier named exactly children uses _mountSlot', () => {
|
|
2574
|
+
const result = t('function C() { return <div>{children}</div> }')
|
|
2575
|
+
expect(result).toContain('_mountSlot')
|
|
2576
|
+
})
|
|
2577
|
+
|
|
2578
|
+
test('expression NOT ending with .children does NOT use _mountSlot', () => {
|
|
2579
|
+
const result = t('function C(props) { return <div>{config.items}</div> }')
|
|
2580
|
+
expect(result).not.toContain('_mountSlot')
|
|
2581
|
+
})
|
|
2582
|
+
})
|
|
2583
|
+
|
|
2584
|
+
// ─── Branch coverage: _isDynamic ArrowFunctionExpression stop (line 656) ────
|
|
2585
|
+
|
|
2586
|
+
describe('JSX transform — _isDynamic stops at nested arrow functions', () => {
|
|
2587
|
+
test('call inside arrow function does not make outer expression dynamic', () => {
|
|
2588
|
+
const result = t('<MyComp render={() => fn()} />')
|
|
2589
|
+
// Arrow function prevents _isDynamic from recursing into fn()
|
|
2590
|
+
expect(result).not.toContain('_rp(')
|
|
2591
|
+
})
|
|
2592
|
+
})
|
|
2593
|
+
|
|
2594
|
+
// ─── Branch coverage: tryDirectSignalRef edge cases (line 922) ──────────────
|
|
2595
|
+
|
|
2596
|
+
describe('JSX transform — tryDirectSignalRef with arguments', () => {
|
|
2597
|
+
test('call with arguments does NOT use _bindDirect', () => {
|
|
2598
|
+
const result = t('<div class={getClass("primary")}><span /></div>')
|
|
2599
|
+
// Has arguments — not a direct signal ref
|
|
2600
|
+
expect(result).not.toContain('_bindDirect')
|
|
2601
|
+
expect(result).toContain('_bind(() => {')
|
|
2602
|
+
})
|
|
2603
|
+
})
|
|
2604
|
+
|
|
2605
|
+
// ─── Branch coverage: unwrapAccessor for function expression (line 928) ─────
|
|
2606
|
+
|
|
2607
|
+
describe('JSX transform — unwrapAccessor with function expression', () => {
|
|
2608
|
+
test('function expression in attribute is called in bind', () => {
|
|
2609
|
+
const result = t('<div class={function() { return "cls" }}><span /></div>')
|
|
2610
|
+
expect(result).toContain('_bind')
|
|
2611
|
+
})
|
|
2612
|
+
})
|
|
2613
|
+
|
|
2614
|
+
// ─── Branch coverage: collectPropDerivedFromDecl callbackDepth (line 498) ───
|
|
2615
|
+
|
|
2616
|
+
describe('JSX transform — prop-derived vars inside callbacks excluded', () => {
|
|
2617
|
+
test('const inside .map callback is NOT tracked as prop-derived', () => {
|
|
2618
|
+
const result = t('function C(props) { return <div>{items.map(item => { const x = props.y; return <span>{x}</span> })}</div> }')
|
|
2619
|
+
// x is declared inside a callback (callbackDepth > 0) — not tracked
|
|
2620
|
+
expect(result).toContain('() =>')
|
|
2621
|
+
})
|
|
2622
|
+
})
|
|
2623
|
+
|
|
2624
|
+
// ─── Branch coverage: static JSX in component prop hoisting (line 359) ──────
|
|
2625
|
+
|
|
2626
|
+
describe('JSX transform — component prop static JSX hoisting', () => {
|
|
2627
|
+
test('single JSX element in component prop is NOT hoisted but walked', () => {
|
|
2628
|
+
const result = t('<MyComp icon={<span>icon</span>} />')
|
|
2629
|
+
// Single JSX element prop → walked (line 354-356), not hoisted
|
|
2630
|
+
expect(result).not.toContain('const _$h0')
|
|
2631
|
+
expect(result).toContain('<span>icon</span>')
|
|
2632
|
+
})
|
|
2633
|
+
|
|
2634
|
+
test('non-JSX static expression in component prop gets hoisted', () => {
|
|
2635
|
+
// This exercises the maybeHoist path (line 358-360)
|
|
2636
|
+
const result = t('<MyComp render={12} />')
|
|
2637
|
+
// Static numeric — no wrapping needed
|
|
2638
|
+
expect(result).not.toContain('_rp(')
|
|
2639
|
+
})
|
|
2640
|
+
})
|
|
2641
|
+
|
|
2642
|
+
// ─── Branch coverage: templateElementCount bail at non-lowercase (line 825) ──
|
|
2643
|
+
|
|
2644
|
+
describe('JSX transform — template element count bail on uppercase', () => {
|
|
2645
|
+
test('component element inside template bails', () => {
|
|
2646
|
+
const result = t('<div><Component /><span /></div>')
|
|
2647
|
+
expect(result).not.toContain('_tpl(')
|
|
2648
|
+
})
|
|
2649
|
+
})
|
|
2650
|
+
|
|
2651
|
+
// ─── Branch coverage: maybeRegisterComponentProps with no params (line 518) ──
|
|
2652
|
+
|
|
2653
|
+
describe('JSX transform — component with no params not tracked', () => {
|
|
2654
|
+
test('parameterless function not tracked as component props', () => {
|
|
2655
|
+
const result = t('function C() { return <div>hello</div> }')
|
|
2656
|
+
expect(result).toContain('_tpl(')
|
|
2657
|
+
expect(result).not.toContain('_bind')
|
|
2658
|
+
})
|
|
2659
|
+
})
|
|
2660
|
+
|
|
2661
|
+
// ─── Branch coverage: tpl null cleanup return (line 1156/1171/1179) ────────
|
|
2662
|
+
|
|
2663
|
+
describe('JSX transform — template processChildren null bail', () => {
|
|
2664
|
+
test('template bails when child element has no tag name', () => {
|
|
2665
|
+
// Member expression tag → empty tag name → processElement returns null
|
|
2666
|
+
const result = t('<div><ns.Comp><span /></ns.Comp></div>')
|
|
2667
|
+
expect(result).not.toContain('_tpl(')
|
|
2668
|
+
})
|
|
2669
|
+
})
|
|
2670
|
+
|
|
2671
|
+
// ─── Branch coverage: more edge cases for various ?? and ?. operators ───────
|
|
2672
|
+
|
|
2673
|
+
describe('JSX transform — additional branch coverage paths', () => {
|
|
2674
|
+
test('arrow function with expression body (no block statement)', () => {
|
|
2675
|
+
const result = t('<div class={() => cls()}><span /></div>')
|
|
2676
|
+
expect(result).toContain('_bindDirect(cls,')
|
|
2677
|
+
})
|
|
2678
|
+
|
|
2679
|
+
test('function expression with block body in attribute', () => {
|
|
2680
|
+
const result = t('<div class={function() { return cls() }}><span /></div>')
|
|
2681
|
+
expect(result).toContain('_bind')
|
|
2682
|
+
})
|
|
2683
|
+
|
|
2684
|
+
test('prop-derived var used inside a nested function arg but NOT as callback', () => {
|
|
2685
|
+
const result = t('function C(props) { const x = props.y; return <div>{x + other(x)}</div> }')
|
|
2686
|
+
expect(result).toContain('props.y')
|
|
2687
|
+
expect(result).toContain('_bind')
|
|
2688
|
+
})
|
|
2689
|
+
|
|
2690
|
+
test('mixed static and dynamic props on template element', () => {
|
|
2691
|
+
const result = t('<div class="static" title={x()} data-id={42}><span /></div>')
|
|
2692
|
+
expect(result).toContain('class=\\"static\\"')
|
|
2693
|
+
expect(result).toContain('_bindDirect(x,')
|
|
2694
|
+
expect(result).toContain('data-id=\\"42\\"')
|
|
2695
|
+
})
|
|
2696
|
+
|
|
2697
|
+
test('template with nested elements each having dynamic attributes', () => {
|
|
2698
|
+
const result = t('<div><span class={a()}><em title={b()}>text</em></span></div>')
|
|
2699
|
+
expect(result).toContain('_tpl(')
|
|
2700
|
+
expect(result).toContain('_bindDirect(a,')
|
|
2701
|
+
expect(result).toContain('_bindDirect(b,')
|
|
2702
|
+
})
|
|
2703
|
+
|
|
2704
|
+
test('signal auto-call works inside template _bind for text', () => {
|
|
2705
|
+
const result = t('function C() { const x = signal(1); return <div>{x + 1}</div> }')
|
|
2706
|
+
expect(result).toContain('x() + 1')
|
|
2707
|
+
expect(result).toContain('_bind')
|
|
2708
|
+
})
|
|
2709
|
+
|
|
2710
|
+
test('signal auto-call inside template attribute _bind', () => {
|
|
2711
|
+
const result = t('function C() { const cls = signal("a"); return <div class={cls + " b"}><span /></div> }')
|
|
2712
|
+
expect(result).toContain('cls() + " b"')
|
|
2713
|
+
expect(result).toContain('_bind')
|
|
2714
|
+
})
|
|
2715
|
+
|
|
2716
|
+
test('template with event + ref + dynamic attr + text child', () => {
|
|
2717
|
+
const result = t('<div ref={myRef} onClick={handler} class={cls()} title="static">{text()}</div>')
|
|
2718
|
+
expect(result).toContain('_tpl(')
|
|
2719
|
+
expect(result).toContain('myRef')
|
|
2720
|
+
expect(result).toContain('__ev_click = handler')
|
|
2721
|
+
expect(result).toContain('_bindDirect(cls,')
|
|
2722
|
+
expect(result).toContain('_bindText(text,')
|
|
2723
|
+
})
|
|
2724
|
+
|
|
2725
|
+
test('template with non-delegated event using addEventListener', () => {
|
|
2726
|
+
const result = t('<div onScroll={handler}><span /></div>')
|
|
2727
|
+
expect(result).toContain('addEventListener("scroll", handler)')
|
|
2728
|
+
expect(result).not.toContain('__ev_')
|
|
2729
|
+
})
|
|
2730
|
+
|
|
2731
|
+
test('forEachChild with non-array non-object values', () => {
|
|
2732
|
+
// Edge case: JSX text node has primitive value property
|
|
2733
|
+
const result = t('<div>plain text between elements<span /></div>')
|
|
2734
|
+
expect(result).toContain('_tpl(')
|
|
2735
|
+
})
|
|
2736
|
+
|
|
2737
|
+
test('self-closing void element in mixed children template', () => {
|
|
2738
|
+
const result = t('<div><input />{value()}</div>')
|
|
2739
|
+
expect(result).toContain('_tpl(')
|
|
2740
|
+
expect(result).toContain('childNodes[')
|
|
2741
|
+
expect(result).toContain('_bindText(value,')
|
|
2742
|
+
})
|
|
2743
|
+
|
|
2744
|
+
test('multiple signals from same component all tracked', () => {
|
|
2745
|
+
const result = t(`
|
|
2746
|
+
function C() {
|
|
2747
|
+
const a = signal(1)
|
|
2748
|
+
const b = signal(2)
|
|
2749
|
+
const c = signal(3)
|
|
2750
|
+
return <div>
|
|
2751
|
+
<span>{a}</span>
|
|
2752
|
+
<em>{b}</em>
|
|
2753
|
+
<strong>{c}</strong>
|
|
2754
|
+
</div>
|
|
2755
|
+
}
|
|
2756
|
+
`)
|
|
2757
|
+
expect(result).toContain('a()')
|
|
2758
|
+
expect(result).toContain('b()')
|
|
2759
|
+
expect(result).toContain('c()')
|
|
2760
|
+
})
|
|
2761
|
+
|
|
2762
|
+
test('signal auto-call with binary and unary expressions', () => {
|
|
2763
|
+
const result = t('function C() { const x = signal(5); return <div>{-x}</div> }')
|
|
2764
|
+
expect(result).toContain('x()')
|
|
2765
|
+
expect(result).toContain('_bind')
|
|
2766
|
+
})
|
|
2767
|
+
|
|
2768
|
+
test('signal in computed property access is auto-called', () => {
|
|
2769
|
+
const result = t('function C() { const idx = signal(0); return <div>{arr[idx]}</div> }')
|
|
2770
|
+
expect(result).toContain('idx()')
|
|
2771
|
+
})
|
|
2772
|
+
|
|
2773
|
+
test('signal variable reference not confused with same-name property', () => {
|
|
2774
|
+
const result = t('function C() { const x = signal(0); return <div data-val={obj.method(x)}></div> }')
|
|
2775
|
+
expect(result).toContain('x()')
|
|
2776
|
+
expect(result).toContain('_bind')
|
|
2777
|
+
})
|
|
2778
|
+
|
|
2779
|
+
test('template with static spread on root and dynamic inner attr', () => {
|
|
2780
|
+
const result = t('<div {...staticProps}><span class={cls()}>text</span></div>')
|
|
2781
|
+
expect(result).toContain('_tpl(')
|
|
2782
|
+
expect(result).toContain('_applyProps')
|
|
2783
|
+
expect(result).toContain('_bindDirect(cls,')
|
|
2784
|
+
})
|
|
2785
|
+
|
|
2786
|
+
test('empty JSX expression in template attribute position', () => {
|
|
2787
|
+
const result = t('<div class={/* comment */}><span /></div>')
|
|
2788
|
+
expect(result).toContain('_tpl(')
|
|
2789
|
+
})
|
|
2790
|
+
|
|
2791
|
+
test('ternary in template attribute without signal', () => {
|
|
2792
|
+
const result = t('<div class={x ? "a" : "b"}><span /></div>')
|
|
2793
|
+
// No calls — not dynamic
|
|
2794
|
+
expect(result).toContain('className = x ? "a" : "b"')
|
|
2795
|
+
})
|
|
2796
|
+
|
|
2797
|
+
test('variable declaration kind is let — not tracked for prop-derived', () => {
|
|
2798
|
+
const result = t('function C(props) { let x = props.y; return <div>{x}</div> }')
|
|
2799
|
+
// let is not tracked — x is static
|
|
2800
|
+
expect(result).toContain('textContent = x')
|
|
2801
|
+
})
|
|
2802
|
+
|
|
2803
|
+
test('FunctionDeclaration with JSX detected as component', () => {
|
|
2804
|
+
const result = t('function MyComp(props) { return <div class={props.cls}></div> }')
|
|
2805
|
+
expect(result).toContain('_bind')
|
|
2806
|
+
expect(result).toContain('props.cls')
|
|
2807
|
+
})
|
|
2808
|
+
|
|
2809
|
+
test('ArrowFunctionExpression with JSX and single param detected as component', () => {
|
|
2810
|
+
const result = t('const MyComp = (props) => <div class={props.cls}></div>')
|
|
2811
|
+
expect(result).toContain('_bind')
|
|
2812
|
+
expect(result).toContain('props.cls')
|
|
2813
|
+
})
|
|
2814
|
+
|
|
2815
|
+
test('signal NOT tracked inside callback arg (callbackDepth > 0)', () => {
|
|
2816
|
+
// collectPropDerivedFromDecl skips when callbackDepth > 0
|
|
2817
|
+
const result = t('function C(props) { return <div>{items.map(item => { const x = signal(0); return <span>{x}</span> })}</div> }')
|
|
2818
|
+
// x is inside a callback — signal tracking doesn't apply at callback depth
|
|
2819
|
+
expect(result).toContain('() =>')
|
|
2820
|
+
})
|
|
2821
|
+
|
|
2822
|
+
test('template with empty expression in attribute (attrIsDynamic false branch)', () => {
|
|
2823
|
+
// Empty expression in attribute: data-x={/* */} — attrIsDynamic returns false
|
|
2824
|
+
const result = t('<div data-x={/* comment */}><span /></div>')
|
|
2825
|
+
expect(result).toContain('_tpl(')
|
|
2826
|
+
})
|
|
2827
|
+
|
|
2828
|
+
test('template with only static attributes — elementHasDynamic false', () => {
|
|
2829
|
+
const result = t('<div class="a" title="b"><span class="c">text</span></div>')
|
|
2830
|
+
expect(result).toContain('_tpl(')
|
|
2831
|
+
// No _bind needed for fully static tree
|
|
2832
|
+
expect(result).toContain('() => null')
|
|
2833
|
+
})
|
|
2834
|
+
|
|
2835
|
+
test('signal auto-call with signal as callee of call expression', () => {
|
|
2836
|
+
// signal()(args) — signal IS the callee of a call, already being called
|
|
2837
|
+
const result = t('function C() { const fn = signal(() => 1); return <div>{fn()}</div> }')
|
|
2838
|
+
// fn() is already a call — no double call
|
|
2839
|
+
expect(result).not.toContain('fn()()')
|
|
2840
|
+
})
|
|
2841
|
+
|
|
2842
|
+
test('signal auto-call not triggered on arrow function children', () => {
|
|
2843
|
+
// Arrow functions in JSX are not recursed into by referencesSignalVar
|
|
2844
|
+
const result = t('function C() { const x = signal(0); return <div>{() => { const x = "shadow"; return x }}</div> }')
|
|
2845
|
+
// The arrow function is not touched
|
|
2846
|
+
expect(result).toBeDefined()
|
|
2847
|
+
})
|
|
2848
|
+
|
|
2849
|
+
test('template with deeply nested mixed expressions', () => {
|
|
2850
|
+
const result = t('<div><span><em>{a()}</em></span><strong>{b()}</strong></div>')
|
|
2851
|
+
expect(result).toContain('_tpl(')
|
|
2852
|
+
expect(result).toContain('_bindText(a,')
|
|
2853
|
+
expect(result).toContain('_bindText(b,')
|
|
2854
|
+
})
|
|
2855
|
+
|
|
2856
|
+
test('signal in JSX attribute expression container — auto-called in bind', () => {
|
|
2857
|
+
const result = t('function C() { const x = signal(0); return <div data-val={x}><span /></div> }')
|
|
2858
|
+
// x is a signal identifier in an attribute — should be auto-called
|
|
2859
|
+
expect(result).toContain('x()')
|
|
2860
|
+
expect(result).toContain('_bind')
|
|
2861
|
+
})
|
|
2862
|
+
|
|
2863
|
+
test('namespace attribute in template element', () => {
|
|
2864
|
+
// xml:lang or xlink:href — JSXNamespacedName, not JSXIdentifier
|
|
2865
|
+
const result = t('<svg><use xlink:href="#icon"><rect /></use></svg>')
|
|
2866
|
+
expect(result).toBeDefined()
|
|
2867
|
+
})
|
|
2868
|
+
|
|
2869
|
+
test('signal as only child of component uses auto-call', () => {
|
|
2870
|
+
const result = t('function C() { const x = signal(0); return <MyComp>{x}</MyComp> }')
|
|
2871
|
+
expect(result).toContain('() => x()')
|
|
2872
|
+
})
|
|
2873
|
+
|
|
2874
|
+
test('multiple signals with complex nesting', () => {
|
|
2875
|
+
const result = t(`
|
|
2876
|
+
function C() {
|
|
2877
|
+
const a = signal(1)
|
|
2878
|
+
const b = signal('text')
|
|
2879
|
+
return <div class={a ? 'active' : 'inactive'}>
|
|
2880
|
+
<span>{b}</span>
|
|
2881
|
+
<em>{a > 0 ? b : 'none'}</em>
|
|
2882
|
+
</div>
|
|
2883
|
+
}
|
|
2884
|
+
`)
|
|
2885
|
+
expect(result).toContain('a()')
|
|
2886
|
+
expect(result).toContain('b()')
|
|
2887
|
+
})
|
|
2888
|
+
})
|