@pyreon/compiler 0.24.5 → 0.24.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/package.json +11 -13
  2. package/src/defer-inline.ts +0 -686
  3. package/src/event-names.ts +0 -65
  4. package/src/index.ts +0 -61
  5. package/src/island-audit.ts +0 -675
  6. package/src/jsx.ts +0 -2792
  7. package/src/load-native.ts +0 -156
  8. package/src/lpih.ts +0 -270
  9. package/src/manifest.ts +0 -280
  10. package/src/project-scanner.ts +0 -214
  11. package/src/pyreon-intercept.ts +0 -1029
  12. package/src/react-intercept.ts +0 -1217
  13. package/src/reactivity-lens.ts +0 -190
  14. package/src/ssg-audit.ts +0 -513
  15. package/src/test-audit.ts +0 -435
  16. package/src/tests/backend-parity-r7-r9.test.ts +0 -91
  17. package/src/tests/backend-prop-derived-callback-divergence.test.ts +0 -74
  18. package/src/tests/collapse-bail-census.test.ts +0 -330
  19. package/src/tests/collapse-key-source-hygiene.test.ts +0 -88
  20. package/src/tests/component-child-no-wrap.test.ts +0 -204
  21. package/src/tests/defer-inline.test.ts +0 -387
  22. package/src/tests/depth-stress.test.ts +0 -16
  23. package/src/tests/detector-tag-consistency.test.ts +0 -101
  24. package/src/tests/dynamic-collapse-detector.test.ts +0 -164
  25. package/src/tests/dynamic-collapse-emit.test.ts +0 -192
  26. package/src/tests/dynamic-collapse-scan.test.ts +0 -111
  27. package/src/tests/element-valued-const-child.test.ts +0 -61
  28. package/src/tests/falsy-child-characterization.test.ts +0 -48
  29. package/src/tests/island-audit.test.ts +0 -524
  30. package/src/tests/jsx.test.ts +0 -2908
  31. package/src/tests/load-native.test.ts +0 -53
  32. package/src/tests/lpih.test.ts +0 -404
  33. package/src/tests/malformed-input-resilience.test.ts +0 -50
  34. package/src/tests/manifest-snapshot.test.ts +0 -55
  35. package/src/tests/native-equivalence.test.ts +0 -924
  36. package/src/tests/partial-collapse-detector.test.ts +0 -121
  37. package/src/tests/partial-collapse-emit.test.ts +0 -104
  38. package/src/tests/partial-collapse-robustness.test.ts +0 -53
  39. package/src/tests/project-scanner.test.ts +0 -269
  40. package/src/tests/prop-derived-shadow.test.ts +0 -96
  41. package/src/tests/pure-call-reactive-args.test.ts +0 -50
  42. package/src/tests/pyreon-intercept.test.ts +0 -816
  43. package/src/tests/r13-callback-stmt-equivalence.test.ts +0 -58
  44. package/src/tests/r14-ssr-mode-parity.test.ts +0 -51
  45. package/src/tests/r15-elemconst-propderived.test.ts +0 -47
  46. package/src/tests/r19-defer-inline-robust.test.ts +0 -54
  47. package/src/tests/r20-backend-equivalence-sweep.test.ts +0 -50
  48. package/src/tests/react-intercept.test.ts +0 -1104
  49. package/src/tests/reactivity-lens.test.ts +0 -170
  50. package/src/tests/rocketstyle-collapse.test.ts +0 -208
  51. package/src/tests/runtime/control-flow.test.ts +0 -159
  52. package/src/tests/runtime/dom-properties.test.ts +0 -138
  53. package/src/tests/runtime/events.test.ts +0 -301
  54. package/src/tests/runtime/harness.ts +0 -94
  55. package/src/tests/runtime/pr-352-shapes.test.ts +0 -121
  56. package/src/tests/runtime/reactive-props.test.ts +0 -81
  57. package/src/tests/runtime/signals.test.ts +0 -129
  58. package/src/tests/runtime/whitespace.test.ts +0 -106
  59. package/src/tests/signal-autocall-shadow.test.ts +0 -86
  60. package/src/tests/sourcemap-fidelity.test.ts +0 -77
  61. package/src/tests/ssg-audit.test.ts +0 -402
  62. package/src/tests/static-text-baking.test.ts +0 -64
  63. package/src/tests/test-audit.test.ts +0 -549
  64. package/src/tests/transform-state-isolation.test.ts +0 -49
@@ -1,301 +0,0 @@
1
- // @vitest-environment happy-dom
2
- /// <reference lib="dom" />
3
- import { signal } from '@pyreon/reactivity'
4
- import { describe, expect, it } from 'vitest'
5
- import { compileAndMount } from './harness'
6
-
7
- /**
8
- * Compiler-runtime tests — event handler emission.
9
- *
10
- * Coverage matrix: delegated (single-word common events) vs non-delegated
11
- * (multi-word + uncommon) × static handler ref vs inline arrow × event
12
- * dispatch hits the handler. The #352 event-name casing bug surfaced
13
- * because non-delegated events used the wrong casing; this file locks in
14
- * each shape independently.
15
- */
16
-
17
- describe('compiler-runtime — events', () => {
18
- // ── Delegated events (single-word, common) ──────────────────────────
19
- it('onClick fires on bubbled click', () => {
20
- let fired = 0
21
- const handler = () => {
22
- fired++
23
- }
24
- const { container, unmount } = compileAndMount(
25
- `<div><button id="b" onClick={handler}>x</button></div>`,
26
- { handler },
27
- )
28
- container.querySelector<HTMLButtonElement>('#b')!.click()
29
- expect(fired).toBe(1)
30
- unmount()
31
- })
32
-
33
- it('onClick with inline arrow handler fires', () => {
34
- const count = signal(0)
35
- const { container, unmount } = compileAndMount(
36
- `<div><button id="b" onClick={() => count.set(count() + 1)}>x</button></div>`,
37
- { count },
38
- )
39
- const btn = container.querySelector<HTMLButtonElement>('#b')!
40
- btn.click()
41
- btn.click()
42
- expect(count()).toBe(2)
43
- unmount()
44
- })
45
-
46
- it('onInput fires on real input event', () => {
47
- let value = ''
48
- const handler = (e: Event) => {
49
- value = (e.target as HTMLInputElement).value
50
- }
51
- const { container, unmount } = compileAndMount(
52
- `<div><input id="i" onInput={handler} /></div>`,
53
- { handler },
54
- )
55
- const input = container.querySelector<HTMLInputElement>('#i')!
56
- input.value = 'hello'
57
- input.dispatchEvent(new Event('input', { bubbles: true }))
58
- expect(value).toBe('hello')
59
- unmount()
60
- })
61
-
62
- it('onChange fires on real change event', () => {
63
- let changed = 0
64
- const handler = () => {
65
- changed++
66
- }
67
- const { container, unmount } = compileAndMount(
68
- `<div><input id="i" onChange={handler} /></div>`,
69
- { handler },
70
- )
71
- container
72
- .querySelector<HTMLInputElement>('#i')!
73
- .dispatchEvent(new Event('change', { bubbles: true }))
74
- expect(changed).toBe(1)
75
- unmount()
76
- })
77
-
78
- it('onSubmit fires on form submit', () => {
79
- let submitted = 0
80
- const handler = (e: Event) => {
81
- e.preventDefault()
82
- submitted++
83
- }
84
- const { container, unmount } = compileAndMount(
85
- `<div><form id="f" onSubmit={handler}><button type="submit">go</button></form></div>`,
86
- { handler },
87
- )
88
- const form = container.querySelector<HTMLFormElement>('#f')!
89
- form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
90
- expect(submitted).toBe(1)
91
- unmount()
92
- })
93
-
94
- it('onFocus fires on focus event', () => {
95
- let focused = 0
96
- const handler = () => {
97
- focused++
98
- }
99
- const { container, unmount } = compileAndMount(
100
- `<div><input id="i" onFocus={handler} /></div>`,
101
- { handler },
102
- )
103
- container
104
- .querySelector<HTMLInputElement>('#i')!
105
- .dispatchEvent(new Event('focus', { bubbles: true }))
106
- expect(focused).toBe(1)
107
- unmount()
108
- })
109
-
110
- it('onBlur fires on blur event', () => {
111
- let blurred = 0
112
- const handler = () => {
113
- blurred++
114
- }
115
- const { container, unmount } = compileAndMount(
116
- `<div><input id="i" onBlur={handler} /></div>`,
117
- { handler },
118
- )
119
- container
120
- .querySelector<HTMLInputElement>('#i')!
121
- .dispatchEvent(new Event('blur', { bubbles: true }))
122
- expect(blurred).toBe(1)
123
- unmount()
124
- })
125
-
126
- // ── Non-delegated events (multi-word — were broken pre-#352) ────────
127
- it('onKeyDown fires (multi-word event-name lowercase regression)', () => {
128
- let key = ''
129
- const handler = (e: KeyboardEvent) => {
130
- key = e.key
131
- }
132
- const { container, unmount } = compileAndMount(
133
- `<div><input id="i" onKeyDown={handler} /></div>`,
134
- { handler },
135
- )
136
- container
137
- .querySelector<HTMLInputElement>('#i')!
138
- .dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }))
139
- expect(key).toBe('Tab')
140
- unmount()
141
- })
142
-
143
- it('onKeyUp fires (multi-word event-name lowercase regression)', () => {
144
- let key = ''
145
- const handler = (e: KeyboardEvent) => {
146
- key = e.key
147
- }
148
- const { container, unmount } = compileAndMount(
149
- `<div><input id="i" onKeyUp={handler} /></div>`,
150
- { handler },
151
- )
152
- container
153
- .querySelector<HTMLInputElement>('#i')!
154
- .dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true }))
155
- expect(key).toBe('Enter')
156
- unmount()
157
- })
158
-
159
- it('onMouseEnter fires (non-bubbling event)', () => {
160
- let entered = 0
161
- const handler = () => {
162
- entered++
163
- }
164
- const { container, unmount } = compileAndMount(
165
- `<div><span id="s" onMouseEnter={handler}>hover</span></div>`,
166
- { handler },
167
- )
168
- container
169
- .querySelector<HTMLSpanElement>('#s')!
170
- .dispatchEvent(new MouseEvent('mouseenter'))
171
- expect(entered).toBe(1)
172
- unmount()
173
- })
174
-
175
- it('onPointerLeave fires (non-bubbling event)', () => {
176
- let left = 0
177
- const handler = () => {
178
- left++
179
- }
180
- const { container, unmount } = compileAndMount(
181
- `<div><span id="s" onPointerLeave={handler}>hover</span></div>`,
182
- { handler },
183
- )
184
- container
185
- .querySelector<HTMLSpanElement>('#s')!
186
- .dispatchEvent(new PointerEvent('pointerleave'))
187
- expect(left).toBe(1)
188
- unmount()
189
- })
190
-
191
- // Locks in the React→DOM event-name mapping for `onDoubleClick` →
192
- // `dblclick`. The mapping lives in BOTH compiler backends:
193
- // - JS fallback: packages/core/compiler/src/jsx.ts (doubleclick → dblclick)
194
- // - Rust native: packages/core/compiler/native/src/lib.rs (same shape)
195
- // `onContextMenu` lowercases correctly (contextmenu) — no remap needed.
196
- it('onDoubleClick fires (multi-word + delegated)', () => {
197
- let dbl = 0
198
- const handler = () => {
199
- dbl++
200
- }
201
- const { container, unmount } = compileAndMount(
202
- `<div><button id="b" onDoubleClick={handler}>x</button></div>`,
203
- { handler },
204
- )
205
- container
206
- .querySelector<HTMLButtonElement>('#b')!
207
- .dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
208
- expect(dbl).toBe(1)
209
- unmount()
210
- })
211
-
212
- it('onContextMenu fires (multi-word, lowercases to contextmenu)', () => {
213
- let menu = 0
214
- const handler = () => {
215
- menu++
216
- }
217
- const { container, unmount } = compileAndMount(
218
- `<div><button id="b" onContextMenu={handler}>x</button></div>`,
219
- { handler },
220
- )
221
- container
222
- .querySelector<HTMLButtonElement>('#b')!
223
- .dispatchEvent(new MouseEvent('contextmenu', { bubbles: true }))
224
- expect(menu).toBe(1)
225
- unmount()
226
- })
227
-
228
- // Audit: every multi-word React event-prop in the official component-prop
229
- // list either (a) has an entry in REACT_EVENT_REMAP, OR (b) lowercases
230
- // correctly to the DOM event name. This sweep exercises the most common
231
- // multi-word events from across the family categories (mouse, pointer,
232
- // keyboard, drag, touch, animation, transition, media, composition, form)
233
- // to lock in that lowercasing alone is the right rule for all of them.
234
- // If a new React event-prop is added in a future release with a non-trivial
235
- // mismatch, this sweep won't catch it — but the runtime test for the
236
- // specific event will. Keep this sweep as the structural audit.
237
- const lowercaseSweep: ReadonlyArray<{ prop: string; event: string }> = [
238
- { prop: 'onKeyDown', event: 'keydown' },
239
- { prop: 'onKeyUp', event: 'keyup' },
240
- { prop: 'onMouseDown', event: 'mousedown' },
241
- { prop: 'onMouseUp', event: 'mouseup' },
242
- { prop: 'onMouseEnter', event: 'mouseenter' },
243
- { prop: 'onMouseLeave', event: 'mouseleave' },
244
- { prop: 'onMouseMove', event: 'mousemove' },
245
- { prop: 'onMouseOut', event: 'mouseout' },
246
- { prop: 'onMouseOver', event: 'mouseover' },
247
- { prop: 'onPointerDown', event: 'pointerdown' },
248
- { prop: 'onPointerMove', event: 'pointermove' },
249
- { prop: 'onPointerUp', event: 'pointerup' },
250
- { prop: 'onPointerCancel', event: 'pointercancel' },
251
- { prop: 'onPointerEnter', event: 'pointerenter' },
252
- { prop: 'onPointerLeave', event: 'pointerleave' },
253
- { prop: 'onPointerOver', event: 'pointerover' },
254
- { prop: 'onPointerOut', event: 'pointerout' },
255
- { prop: 'onGotPointerCapture', event: 'gotpointercapture' },
256
- { prop: 'onLostPointerCapture', event: 'lostpointercapture' },
257
- { prop: 'onDragStart', event: 'dragstart' },
258
- { prop: 'onDragEnd', event: 'dragend' },
259
- { prop: 'onDragEnter', event: 'dragenter' },
260
- { prop: 'onDragLeave', event: 'dragleave' },
261
- { prop: 'onDragOver', event: 'dragover' },
262
- { prop: 'onTouchStart', event: 'touchstart' },
263
- { prop: 'onTouchEnd', event: 'touchend' },
264
- { prop: 'onTouchMove', event: 'touchmove' },
265
- { prop: 'onTouchCancel', event: 'touchcancel' },
266
- { prop: 'onAnimationStart', event: 'animationstart' },
267
- { prop: 'onAnimationEnd', event: 'animationend' },
268
- { prop: 'onAnimationIteration', event: 'animationiteration' },
269
- { prop: 'onTransitionEnd', event: 'transitionend' },
270
- { prop: 'onCompositionStart', event: 'compositionstart' },
271
- { prop: 'onCompositionEnd', event: 'compositionend' },
272
- { prop: 'onCompositionUpdate', event: 'compositionupdate' },
273
- { prop: 'onContextMenu', event: 'contextmenu' },
274
- { prop: 'onBeforeInput', event: 'beforeinput' },
275
- { prop: 'onTimeUpdate', event: 'timeupdate' },
276
- { prop: 'onVolumeChange', event: 'volumechange' },
277
- { prop: 'onCanPlayThrough', event: 'canplaythrough' },
278
- { prop: 'onLoadedData', event: 'loadeddata' },
279
- { prop: 'onLoadedMetadata', event: 'loadedmetadata' },
280
- { prop: 'onLoadStart', event: 'loadstart' },
281
- { prop: 'onRateChange', event: 'ratechange' },
282
- { prop: 'onDurationChange', event: 'durationchange' },
283
- ]
284
- for (const { prop, event } of lowercaseSweep) {
285
- it(`${prop} → ${event} (lowercase, no remap)`, () => {
286
- let fired = 0
287
- const handler = () => {
288
- fired++
289
- }
290
- const { container, unmount } = compileAndMount(
291
- `<div><button id="b" ${prop}={handler}>x</button></div>`,
292
- { handler },
293
- )
294
- container
295
- .querySelector<HTMLButtonElement>('#b')!
296
- .dispatchEvent(new Event(event, { bubbles: true }))
297
- expect(fired).toBe(1)
298
- unmount()
299
- })
300
- }
301
- })
@@ -1,94 +0,0 @@
1
- import { h } from '@pyreon/core'
2
- import * as reactivity from '@pyreon/reactivity'
3
- import * as runtimeDom from '@pyreon/runtime-dom'
4
- import { mountInBrowser } from '@pyreon/test-utils/browser'
5
- import type { MountInBrowserResult } from '@pyreon/test-utils/browser'
6
- import { transformJSX } from '../../jsx'
7
-
8
- /**
9
- * Compiler-runtime test harness.
10
- *
11
- * Bridges the gap between the compiler's unit tests (which assert on the
12
- * generated source string) and the e2e tests (which exercise full apps).
13
- * The unit tests in `jsx.test.ts` proved each of PR #352's 4 silent
14
- * compiler bugs produced syntactically valid output; nothing in the unit
15
- * suite caught that the output was *behaviorally* wrong. This layer
16
- * compiles a JSX snippet through the real compiler, drops it into a real
17
- * Chromium DOM, and asserts observable behavior — events fire, signals
18
- * propagate, props reflect on the right DOM channel.
19
- *
20
- * ## How it works
21
- *
22
- * 1. Wrap the snippet as `function App(props) { return ${jsxExpr} }`
23
- * so the compiler emits a `function` declaration with a return.
24
- * 2. `transformJSX(source)` produces JS that imports specific helpers
25
- * (`_tpl`, `_bind`, `_bindDirect`, `_bindText`, etc.) from
26
- * `@pyreon/runtime-dom`.
27
- * 3. Strip the import statement and the `export` keyword.
28
- * 4. Build a `new Function(...args, code)` whose parameter list is the
29
- * union of every runtime-dom export plus the keys of the test's
30
- * `context` object (signals, event handlers). The compiled code uses
31
- * those names directly and `new Function` resolves them via the
32
- * closure-like parameter binding.
33
- * 5. Invoke the factory to get the `App` component back, then render
34
- * via `h(App)` + `mountInBrowser`.
35
- *
36
- * ## Why not Vite / esbuild bundling
37
- *
38
- * The previous bundle-level treeshake tests (in `flow`, `runtime-dom`,
39
- * `styler`) use `vite.build()` — that's the right tool for asserting
40
- * tree-shaking but each invocation costs 100-500ms. This harness needs
41
- * to scale to ~50 tests in Phase B2; the `new Function` path runs in
42
- * single-digit ms per test.
43
- *
44
- * ## Caveats
45
- *
46
- * - Snippets must be self-contained JSX expressions (no external imports).
47
- * - All non-runtime-dom symbols (signals, handlers, components) must be
48
- * passed in via the `context` parameter.
49
- * - The compiler is invoked in JS-fallback mode if the native binary isn't
50
- * available — same as the rest of the test suite.
51
- */
52
- export function compileAndMount(
53
- jsxExpr: string,
54
- context: Record<string, unknown> = {},
55
- ): MountInBrowserResult {
56
- // Wrap as a function so the compiler emits a return statement.
57
- const source = `export function App(props) { return ${jsxExpr} }`
58
- const compiled = transformJSX(source, 'compile-runtime-test.tsx').code
59
-
60
- // Strip ALL `@pyreon/*` imports — we inject the symbols by name
61
- // through `new Function` parameters instead. Both runtime-dom and
62
- // reactivity exports are unioned in below, so any compiler-emitted
63
- // import (`_tpl`, `_bind`, `_bindDirect`, `_bindText`, `_applyProps`,
64
- // etc.) resolves through the parameter binding. Strip the `export`
65
- // keyword so `App` is in factory scope.
66
- const code = compiled
67
- .replace(/^\s*import\s*\{[^}]+\}\s*from\s*["']@pyreon\/[^"']+["'];?\s*$/gm, '')
68
- .replace(/export\s+function\s+App/, 'function App')
69
-
70
- // Union of runtime-dom + reactivity exports + test-supplied context,
71
- // fed into the factory's parameter list. The compiled code uses these
72
- // names directly (e.g. `_tpl(...)`, `_bind(...)`, `sig.set(...)`) —
73
- // `new Function` binds them via closure-equivalent parameter resolution.
74
- // Same-name overrides resolve in declaration order: later wins. We put
75
- // user-supplied context LAST so a test can shadow a runtime export if
76
- // it ever needs to (rare).
77
- const runtimeKeys = Object.keys(runtimeDom)
78
- const reactivityKeys = Object.keys(reactivity).filter((k) => !runtimeKeys.includes(k))
79
- const contextKeys = Object.keys(context).filter(
80
- (k) => !runtimeKeys.includes(k) && !reactivityKeys.includes(k),
81
- )
82
- const allKeys = [...runtimeKeys, ...reactivityKeys, ...contextKeys]
83
- const allValues = [
84
- ...runtimeKeys.map((k) => (runtimeDom as Record<string, unknown>)[k]),
85
- ...reactivityKeys.map((k) => (reactivity as Record<string, unknown>)[k]),
86
- ...contextKeys.map((k) => context[k]),
87
- ]
88
-
89
- // eslint-disable-next-line no-new-func
90
- const factory = new Function(...allKeys, `${code}\nreturn App`)
91
- const App = factory(...allValues) as (props: object) => unknown
92
-
93
- return mountInBrowser(h(App as never, {}) as never)
94
- }
@@ -1,121 +0,0 @@
1
- // @vitest-environment happy-dom
2
- /// <reference lib="dom" />
3
- import { signal } from '@pyreon/reactivity'
4
- import { describe, expect, it } from 'vitest'
5
- import { flush } from '@pyreon/test-utils/browser'
6
- import { compileAndMount } from './harness'
7
-
8
- /**
9
- * Compiler-runtime regression tests for the 4 bug shapes shipped in PR #352.
10
- *
11
- * Each test was bisect-verifiable when the corresponding compiler fix was
12
- * reverted. They are the proof-of-approach for the harness — small,
13
- * focused, observable-behavior assertions against compiled JSX in real
14
- * Chromium. Phase B2 expands this set to ~50 representative shapes.
15
- */
16
-
17
- describe('compiler-runtime — PR #352 bug shapes (real Chromium)', () => {
18
- // ── Bug 1: event-name casing ────────────────────────────────────────
19
- // Before #352 the compiler emitted `__ev_keyDown` (camelCase) instead
20
- // of `__ev_keydown`. The browser dispatches lowercase event names
21
- // exclusively, so multi-word handlers never fired. With the fix in
22
- // place the keydown event correctly invokes the handler.
23
- it('multi-word event names are lowercased so browser dispatch hits them', async () => {
24
- let pressedKey = ''
25
- const handleKey = (e: KeyboardEvent) => {
26
- pressedKey = e.key
27
- }
28
- const { container, unmount } = compileAndMount(
29
- `<div><input id="ev" onKeyDown={handleKey} /></div>`,
30
- { handleKey },
31
- )
32
- const input = container.querySelector<HTMLInputElement>('#ev')!
33
- input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
34
- expect(pressedKey).toBe('Enter')
35
- unmount()
36
- })
37
-
38
- // ── Bug 2: signal-method auto-call ──────────────────────────────────
39
- // Before #352 the compiler auto-called bare signal references in JSX
40
- // event handlers, rewriting `input.set(x)` to `input().set(x)`. Click
41
- // handlers calling `signal.set(...)` silently failed (TypeError on
42
- // string `.set`). With the fix in place the click correctly invokes
43
- // the setter.
44
- it('signal.method() in event handlers is NOT double-called', async () => {
45
- const count = signal(0)
46
- const { container, unmount } = compileAndMount(
47
- `<div><button id="b" onClick={() => count.set(count() + 1)}>+</button></div>`,
48
- { count },
49
- )
50
- const btn = container.querySelector<HTMLButtonElement>('#b')!
51
- btn.click()
52
- btn.click()
53
- btn.click()
54
- expect(count()).toBe(3)
55
- unmount()
56
- })
57
-
58
- // ── Bug 3: JSX text/expression whitespace ───────────────────────────
59
- // Before #352 same-line whitespace adjacent to expressions was
60
- // stripped: `<p>doubled: {x}</p>` rendered "doubled:0" not
61
- // "doubled: 0". With the fix in place (React/Babel
62
- // cleanJSXElementLiteralChild algorithm) the trailing space survives.
63
- it('same-line whitespace before an expression is preserved', async () => {
64
- const x = signal(7)
65
- const { container, unmount } = compileAndMount(
66
- `<div><p id="p">doubled: {x()}</p></div>`,
67
- { x },
68
- )
69
- const p = container.querySelector('#p')!
70
- expect(p.textContent).toBe('doubled: 7')
71
- x.set(42)
72
- await flush()
73
- expect(p.textContent).toBe('doubled: 42')
74
- unmount()
75
- })
76
-
77
- // ── Bug 4a: DOM-property assignment (value) ─────────────────────────
78
- // Before #352 the compiler emitted `setAttribute("value", v)` for
79
- // input value. The HTML attribute and the live `.value` property
80
- // diverge after the user types — clearing the signal then only reset
81
- // the attribute, leaving stale text. With the fix in place the
82
- // compiler emits `el.value = v` so the live property reflects.
83
- it('input value uses the .value property (not setAttribute)', async () => {
84
- const text = signal('hello')
85
- const { container, unmount } = compileAndMount(
86
- `<div><input id="t" value={() => text()} /></div>`,
87
- { text },
88
- )
89
- const input = container.querySelector<HTMLInputElement>('#t')!
90
- expect(input.value).toBe('hello')
91
- text.set('world')
92
- await flush()
93
- expect(input.value).toBe('world')
94
- text.set('')
95
- await flush()
96
- expect(input.value).toBe('')
97
- unmount()
98
- })
99
-
100
- // ── Bug 4b: DOM-property assignment (checked) ───────────────────────
101
- // Before #352 the compiler used `setAttribute("checked", ...)` for
102
- // checkbox state. Presence of the attribute means "checked" in HTML
103
- // regardless of value, so toggling a signal didn't uncheck the box.
104
- // With the fix in place the compiler emits `el.checked = v`.
105
- it('checkbox checked uses the .checked property (not setAttribute)', async () => {
106
- const done = signal(true)
107
- const { container, unmount } = compileAndMount(
108
- `<div><input id="c" type="checkbox" checked={() => done()} /></div>`,
109
- { done },
110
- )
111
- const cb = container.querySelector<HTMLInputElement>('#c')!
112
- expect(cb.checked).toBe(true)
113
- done.set(false)
114
- await flush()
115
- expect(cb.checked).toBe(false)
116
- done.set(true)
117
- await flush()
118
- expect(cb.checked).toBe(true)
119
- unmount()
120
- })
121
- })
@@ -1,81 +0,0 @@
1
- // @vitest-environment happy-dom
2
- /// <reference lib="dom" />
3
- import { signal } from '@pyreon/reactivity'
4
- import { describe, expect, it } from 'vitest'
5
- import { flush } from '@pyreon/test-utils/browser'
6
- import { compileAndMount } from './harness'
7
-
8
- /**
9
- * Compiler-runtime tests — reactive props inlining.
10
- *
11
- * The compiler auto-detects `const` variables derived from `props.*` /
12
- * `splitProps` results and inlines them at JSX use sites for
13
- * fine-grained reactivity. `const x = props.y ?? 'default'; return
14
- * <div>{x}</div>` compiles to `_bind(() => { t.data = (props.y ?? 'default') })`.
15
- *
16
- * This file pins the contract: the inlined expression really IS reactive
17
- * at the call site — changing the underlying signal updates the DOM
18
- * without re-rendering the component.
19
- */
20
-
21
- describe('compiler-runtime — reactive props inlining', () => {
22
- it('text content from a signal updates reactively', async () => {
23
- const value = signal('initial')
24
- const { container, unmount } = compileAndMount(
25
- `<div><p id="p">{value()}</p></div>`,
26
- { value },
27
- )
28
- expect(container.querySelector('#p')!.textContent).toBe('initial')
29
- value.set('updated')
30
- await flush()
31
- expect(container.querySelector('#p')!.textContent).toBe('updated')
32
- unmount()
33
- })
34
-
35
- it('class attribute updates reactively', async () => {
36
- const cls = signal('a')
37
- const { container, unmount } = compileAndMount(
38
- `<div><span id="s" class={cls()}>x</span></div>`,
39
- { cls },
40
- )
41
- expect(container.querySelector('#s')!.className).toBe('a')
42
- cls.set('b c')
43
- await flush()
44
- expect(container.querySelector('#s')!.className).toBe('b c')
45
- unmount()
46
- })
47
-
48
- it('expression with multiple signals tracks all dependencies', async () => {
49
- const a = signal('hello')
50
- const b = signal('world')
51
- const { container, unmount } = compileAndMount(
52
- `<div><p id="p">{a() + ' ' + b()}</p></div>`,
53
- { a, b },
54
- )
55
- expect(container.querySelector('#p')!.textContent).toBe('hello world')
56
- a.set('hi')
57
- await flush()
58
- expect(container.querySelector('#p')!.textContent).toBe('hi world')
59
- b.set('there')
60
- await flush()
61
- expect(container.querySelector('#p')!.textContent).toBe('hi there')
62
- unmount()
63
- })
64
-
65
- it('nested signal access in template literal updates reactively', async () => {
66
- const name = signal('Alice')
67
- const count = signal(3)
68
- const { container, unmount } = compileAndMount(
69
- `<div><p id="p">{` + '`${name()} has ${count()} items`' + `}</p></div>`,
70
- { name, count },
71
- )
72
- expect(container.querySelector('#p')!.textContent).toBe('Alice has 3 items')
73
- count.set(7)
74
- await flush()
75
- expect(container.querySelector('#p')!.textContent).toBe('Alice has 7 items')
76
- name.set('Bob')
77
- await flush()
78
- expect(container.querySelector('#p')!.textContent).toBe('Bob has 7 items')
79
- unmount()
80
- })
81
- })