@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.
@@ -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
- // onMouseEnter eventName = "m" + "ouseEnter" = "mouseEnter"
1121
- expect(result).toContain('addEventListener(')
1122
- expect(result).toContain('mouseEnter')
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
+ })