@pyreon/compiler 0.14.0 → 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/lib/analysis/index.js.html +1 -1
- package/lib/index.js +225 -11
- package/lib/types/index.d.ts +13 -1
- package/package.json +10 -2
- package/src/event-names.ts +65 -0
- package/src/jsx.ts +140 -7
- package/src/pyreon-intercept.ts +226 -2
- package/src/tests/detector-tag-consistency.test.ts +3 -0
- package/src/tests/jsx.test.ts +236 -4
- package/src/tests/native-equivalence.test.ts +77 -0
- package/src/tests/pyreon-intercept.test.ts +155 -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/lib/index.js.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
/// <reference lib="dom" />
|
|
3
|
+
import { computed, 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 — signal patterns in JSX.
|
|
10
|
+
*
|
|
11
|
+
* The #352 signal-method auto-call bug surfaced because the compiler
|
|
12
|
+
* couldn't tell `signal.set(x)` (call on the signal as object) from
|
|
13
|
+
* `signal()` (call the signal to read). The fix added scope-aware
|
|
14
|
+
* detection. This file pins down the matrix: bare reference, function
|
|
15
|
+
* call, member call, accessor wrapper, computed — in different positions.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
describe('compiler-runtime — signals', () => {
|
|
19
|
+
it('signal in text position is reactive', async () => {
|
|
20
|
+
const name = signal('alice')
|
|
21
|
+
const { container, unmount } = compileAndMount(
|
|
22
|
+
`<div><span id="s">{name()}</span></div>`,
|
|
23
|
+
{ name },
|
|
24
|
+
)
|
|
25
|
+
expect(container.querySelector('#s')!.textContent).toBe('alice')
|
|
26
|
+
name.set('bob')
|
|
27
|
+
await flush()
|
|
28
|
+
expect(container.querySelector('#s')!.textContent).toBe('bob')
|
|
29
|
+
unmount()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('signal.method() in event handler does not auto-call signal', () => {
|
|
33
|
+
const x = signal(0)
|
|
34
|
+
const { container, unmount } = compileAndMount(
|
|
35
|
+
`<div><button id="b" onClick={() => x.set(99)}>set</button></div>`,
|
|
36
|
+
{ x },
|
|
37
|
+
)
|
|
38
|
+
container.querySelector<HTMLButtonElement>('#b')!.click()
|
|
39
|
+
expect(x()).toBe(99)
|
|
40
|
+
unmount()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('signal.update() in event handler does not auto-call signal', () => {
|
|
44
|
+
const x = signal(10)
|
|
45
|
+
const { container, unmount } = compileAndMount(
|
|
46
|
+
`<div><button id="b" onClick={() => x.update((v) => v * 2)}>x2</button></div>`,
|
|
47
|
+
{ x },
|
|
48
|
+
)
|
|
49
|
+
const btn = container.querySelector<HTMLButtonElement>('#b')!
|
|
50
|
+
btn.click()
|
|
51
|
+
btn.click()
|
|
52
|
+
expect(x()).toBe(40)
|
|
53
|
+
unmount()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('signal.peek() in event handler does not auto-call signal', () => {
|
|
57
|
+
const x = signal(7)
|
|
58
|
+
const out = { value: 0 }
|
|
59
|
+
const { container, unmount } = compileAndMount(
|
|
60
|
+
`<div><button id="b" onClick={() => { out.value = x.peek() }}>read</button></div>`,
|
|
61
|
+
{ x, out },
|
|
62
|
+
)
|
|
63
|
+
container.querySelector<HTMLButtonElement>('#b')!.click()
|
|
64
|
+
expect(out.value).toBe(7)
|
|
65
|
+
unmount()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('computed value reflected in DOM updates when source changes', async () => {
|
|
69
|
+
const a = signal(2)
|
|
70
|
+
const b = signal(3)
|
|
71
|
+
const sum = computed(() => a() + b())
|
|
72
|
+
const { container, unmount } = compileAndMount(
|
|
73
|
+
`<div><span id="s">{sum()}</span></div>`,
|
|
74
|
+
{ sum },
|
|
75
|
+
)
|
|
76
|
+
expect(container.querySelector('#s')!.textContent).toBe('5')
|
|
77
|
+
a.set(10)
|
|
78
|
+
await flush()
|
|
79
|
+
expect(container.querySelector('#s')!.textContent).toBe('13')
|
|
80
|
+
unmount()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('explicit accessor wrapper preserves reactivity', async () => {
|
|
84
|
+
const x = signal('hi')
|
|
85
|
+
const { container, unmount } = compileAndMount(
|
|
86
|
+
`<div><span id="s">{() => x()}</span></div>`,
|
|
87
|
+
{ x },
|
|
88
|
+
)
|
|
89
|
+
expect(container.querySelector('#s')!.textContent).toBe('hi')
|
|
90
|
+
x.set('hey')
|
|
91
|
+
await flush()
|
|
92
|
+
expect(container.querySelector('#s')!.textContent).toBe('hey')
|
|
93
|
+
unmount()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('signal in attribute position is reactive', async () => {
|
|
97
|
+
const cls = signal('a')
|
|
98
|
+
const { container, unmount } = compileAndMount(
|
|
99
|
+
`<div><span id="s" class={cls()}>x</span></div>`,
|
|
100
|
+
{ cls },
|
|
101
|
+
)
|
|
102
|
+
expect(container.querySelector('#s')!.className).toBe('a')
|
|
103
|
+
cls.set('b')
|
|
104
|
+
await flush()
|
|
105
|
+
expect(container.querySelector('#s')!.className).toBe('b')
|
|
106
|
+
unmount()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('multiple signals on the same element track independently', async () => {
|
|
110
|
+
const txt = signal('hello')
|
|
111
|
+
const cls = signal('a')
|
|
112
|
+
const { container, unmount } = compileAndMount(
|
|
113
|
+
`<div><span id="s" class={cls()}>{txt()}</span></div>`,
|
|
114
|
+
{ txt, cls },
|
|
115
|
+
)
|
|
116
|
+
const span = container.querySelector('#s')!
|
|
117
|
+
expect(span.textContent).toBe('hello')
|
|
118
|
+
expect(span.className).toBe('a')
|
|
119
|
+
txt.set('world')
|
|
120
|
+
await flush()
|
|
121
|
+
expect(span.textContent).toBe('world')
|
|
122
|
+
expect(span.className).toBe('a')
|
|
123
|
+
cls.set('b')
|
|
124
|
+
await flush()
|
|
125
|
+
expect(span.textContent).toBe('world')
|
|
126
|
+
expect(span.className).toBe('b')
|
|
127
|
+
unmount()
|
|
128
|
+
})
|
|
129
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
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 — JSX text/expression whitespace handling.
|
|
10
|
+
*
|
|
11
|
+
* The #352 whitespace bug stripped same-line spaces adjacent to
|
|
12
|
+
* expressions: `<p>doubled: {x}</p>` rendered "doubled:0" instead of
|
|
13
|
+
* "doubled: 0". The fix implements React/Babel's
|
|
14
|
+
* `cleanJSXElementLiteralChild` algorithm. This file pins down the
|
|
15
|
+
* matrix: same-line text±expression, multi-line text, fragments,
|
|
16
|
+
* leading/trailing/internal whitespace.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
describe('compiler-runtime — JSX whitespace', () => {
|
|
20
|
+
it('preserves trailing space before expression on same line', async () => {
|
|
21
|
+
const x = signal(7)
|
|
22
|
+
const { container, unmount } = compileAndMount(
|
|
23
|
+
`<div><p id="p">doubled: {x()}</p></div>`,
|
|
24
|
+
{ x },
|
|
25
|
+
)
|
|
26
|
+
expect(container.querySelector('#p')!.textContent).toBe('doubled: 7')
|
|
27
|
+
unmount()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// Regression: when an expression sits BETWEEN static text segments
|
|
31
|
+
// on the same line, the compiler used to append the dynamic text
|
|
32
|
+
// node to the parent's children AFTER all static text via
|
|
33
|
+
// `appendChild`. Whitespace was preserved correctly (#352), but
|
|
34
|
+
// positioning was wrong:
|
|
35
|
+
// `<p>{x()} remaining</p>` → template `<p> remaining</p>` +
|
|
36
|
+
// appended text → renders as " remaining3" instead of "3 remaining".
|
|
37
|
+
// Fix: extend `analyzeChildren.useMixed` to fire whenever ≥2 of
|
|
38
|
+
// {element, text, expression} are present (not just element+nonElement).
|
|
39
|
+
// Then placeholder-based positional mounting puts the dynamic text in
|
|
40
|
+
// the right slot via `replaceChild`.
|
|
41
|
+
it('preserves leading space after expression on same line', async () => {
|
|
42
|
+
const x = signal(3)
|
|
43
|
+
const { container, unmount } = compileAndMount(
|
|
44
|
+
`<div><p id="p">{x()} remaining</p></div>`,
|
|
45
|
+
{ x },
|
|
46
|
+
)
|
|
47
|
+
expect(container.querySelector('#p')!.textContent).toBe('3 remaining')
|
|
48
|
+
unmount()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('preserves spaces on BOTH sides of expression', async () => {
|
|
52
|
+
const x = signal('cat')
|
|
53
|
+
const { container, unmount } = compileAndMount(
|
|
54
|
+
`<div><p id="p">a {x()} b</p></div>`,
|
|
55
|
+
{ x },
|
|
56
|
+
)
|
|
57
|
+
expect(container.querySelector('#p')!.textContent).toBe('a cat b')
|
|
58
|
+
unmount()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('multi-line JSX with indentation collapses correctly', async () => {
|
|
62
|
+
const x = signal('inner')
|
|
63
|
+
// Multi-line JSX expression. Whitespace inside the JSX literal between
|
|
64
|
+
// <p> and {x()} is treated by React/Babel cleanJSX as "indentation"
|
|
65
|
+
// and collapses; same for between {x()} and </p>.
|
|
66
|
+
const { container, unmount } = compileAndMount(
|
|
67
|
+
`<div>
|
|
68
|
+
<p id="p">
|
|
69
|
+
{x()}
|
|
70
|
+
</p>
|
|
71
|
+
</div>`,
|
|
72
|
+
{ x },
|
|
73
|
+
)
|
|
74
|
+
expect(container.querySelector('#p')!.textContent?.trim()).toBe('inner')
|
|
75
|
+
unmount()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('reactive text updates without losing surrounding whitespace', async () => {
|
|
79
|
+
const x = signal(0)
|
|
80
|
+
const { container, unmount } = compileAndMount(
|
|
81
|
+
`<div><p id="p">count: {x()} items</p></div>`,
|
|
82
|
+
{ x },
|
|
83
|
+
)
|
|
84
|
+
expect(container.querySelector('#p')!.textContent).toBe('count: 0 items')
|
|
85
|
+
x.set(42)
|
|
86
|
+
await flush()
|
|
87
|
+
expect(container.querySelector('#p')!.textContent).toBe('count: 42 items')
|
|
88
|
+
unmount()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('reactive text at end of paragraph updates correctly', async () => {
|
|
92
|
+
// The expression-at-END shape works because the dynamic text node is
|
|
93
|
+
// appended to the parent — which happens to match the source order
|
|
94
|
+
// when there's no text after.
|
|
95
|
+
const x = signal(0)
|
|
96
|
+
const { container, unmount } = compileAndMount(
|
|
97
|
+
`<div><p id="p">count: {x()}</p></div>`,
|
|
98
|
+
{ x },
|
|
99
|
+
)
|
|
100
|
+
expect(container.querySelector('#p')!.textContent).toBe('count: 0')
|
|
101
|
+
x.set(42)
|
|
102
|
+
await flush()
|
|
103
|
+
expect(container.querySelector('#p')!.textContent).toBe('count: 42')
|
|
104
|
+
unmount()
|
|
105
|
+
})
|
|
106
|
+
})
|