@barefootjs/jsx 0.10.0 → 0.11.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/dist/compiler.d.ts.map +1 -1
- package/dist/debug-profile.d.ts +115 -0
- package/dist/debug-profile.d.ts.map +1 -0
- package/dist/debug.d.ts +4 -3
- package/dist/debug.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +31 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1905 -207
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +6 -0
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts +2 -2
- package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts +3 -3
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts +2 -0
- package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +2 -0
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts +4 -2
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +3 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/insert.d.ts +8 -0
- package/dist/ir-to-client-js/control-flow/plan/insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +8 -0
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +28 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/component-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-listener.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-listener.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/insert.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +2 -2
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/index.d.ts +2 -2
- package/dist/ir-to-client-js/index.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/effects-and-on-mounts.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/event-handlers.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/build-declaration-emit.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts +6 -0
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +5 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +29 -0
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/loop-destructure.d.ts +26 -0
- package/dist/loop-destructure.d.ts.map +1 -0
- package/dist/profiler.d.ts +492 -0
- package/dist/profiler.d.ts.map +1 -0
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/debug-profile.test.ts +405 -0
- package/src/__tests__/expression-parser.test.ts +44 -1
- package/src/__tests__/profile-bfid-emission.test.ts +63 -0
- package/src/__tests__/profile-binding-ids.test.ts +123 -0
- package/src/__tests__/profile-cond-binding-ids.test.ts +80 -0
- package/src/__tests__/profile-loop-binding-ids.test.ts +106 -0
- package/src/__tests__/profile-nested-binding-ids.test.ts +153 -0
- package/src/__tests__/profile-turn-markers-branch.test.ts +83 -0
- package/src/__tests__/profile-turn-markers-delegation.test.ts +63 -0
- package/src/__tests__/profile-turn-markers.test.ts +54 -0
- package/src/__tests__/profiler-batch-advisor.test.ts +198 -0
- package/src/__tests__/profiler-coverage-conformance.test.ts +360 -0
- package/src/__tests__/profiler-e2e.test.ts +104 -0
- package/src/__tests__/profiler-hot-subscribers.test.ts +263 -0
- package/src/__tests__/profiler-wasted-re-runs.test.ts +147 -0
- package/src/__tests__/profiler.test.ts +408 -0
- package/src/__tests__/ssr-defaults.test.ts +24 -0
- package/src/compiler.ts +3 -0
- package/src/debug-profile.ts +543 -0
- package/src/debug.ts +192 -28
- package/src/expression-parser.ts +53 -0
- package/src/index.ts +72 -1
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +6 -0
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +5 -3
- package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +3 -1
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +8 -2
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +19 -3
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +2 -0
- package/src/ir-to-client-js/control-flow/plan/build-insert.ts +9 -2
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +9 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +12 -8
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +10 -4
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +7 -0
- package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +7 -0
- package/src/ir-to-client-js/control-flow/plan/insert.ts +8 -0
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +8 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +28 -0
- package/src/ir-to-client-js/control-flow/plan/reactive-effects.ts +7 -0
- package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +5 -3
- package/src/ir-to-client-js/control-flow/stringify/component-loop.ts +4 -2
- package/src/ir-to-client-js/control-flow/stringify/composite-loop.ts +6 -3
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +14 -2
- package/src/ir-to-client-js/control-flow/stringify/event-listener.ts +5 -2
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +13 -11
- package/src/ir-to-client-js/control-flow/stringify/insert.ts +19 -7
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +18 -13
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +9 -7
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +18 -14
- package/src/ir-to-client-js/control-flow.ts +12 -6
- package/src/ir-to-client-js/emit-reactive.ts +18 -5
- package/src/ir-to-client-js/imports.ts +2 -0
- package/src/ir-to-client-js/index.ts +6 -1
- package/src/ir-to-client-js/phases/effects-and-on-mounts.ts +10 -4
- package/src/ir-to-client-js/phases/event-handlers.ts +6 -2
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +7 -1
- package/src/ir-to-client-js/plan/declaration-emit.ts +6 -0
- package/src/ir-to-client-js/stringify/declaration-emit.ts +12 -6
- package/src/ir-to-client-js/types.ts +5 -0
- package/src/ir-to-client-js/utils.ts +37 -0
- package/src/jsx-to-ir.ts +2 -2
- package/src/loop-destructure.ts +170 -0
- package/src/profiler.ts +1488 -0
- package/src/ssr-defaults.ts +65 -0
- package/src/types.ts +8 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `bf debug profile` (#1690 / SR5 static budget).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - buildReactiveProfile: correct metrics from IR
|
|
6
|
+
* - findins: high-fan-out, deep-memo-chain, batch-candidate, fallback-heavy
|
|
7
|
+
* - Bug A minimal repro: sourceFile empty for non-reactive components
|
|
8
|
+
* - Bug B minimal repro: batch-candidate false positive in if/else (controlled/uncontrolled)
|
|
9
|
+
* - Bug C minimal repro: duplicate findings from template reuse
|
|
10
|
+
* - diffProfiles: regression detection (SR6)
|
|
11
|
+
* - formatSingleProfile, formatProfileTable, formatProfileDiff
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from 'bun:test'
|
|
15
|
+
import {
|
|
16
|
+
buildReactiveProfile,
|
|
17
|
+
diffProfiles,
|
|
18
|
+
formatSingleProfile,
|
|
19
|
+
formatProfileTable,
|
|
20
|
+
formatProfileDiff,
|
|
21
|
+
} from '../debug-profile.ts'
|
|
22
|
+
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// Fixtures
|
|
25
|
+
// =============================================================================
|
|
26
|
+
|
|
27
|
+
const SWITCH_SOURCE = `
|
|
28
|
+
"use client"
|
|
29
|
+
import { createSignal, createMemo } from '@barefootjs/client'
|
|
30
|
+
function Switch(props: any) {
|
|
31
|
+
const [internalChecked, setInternalChecked] = createSignal(props.defaultChecked ?? false)
|
|
32
|
+
const [controlledChecked, setControlledChecked] = createSignal<boolean | undefined>(props.checked)
|
|
33
|
+
const isControlled = createMemo(() => props.checked !== undefined)
|
|
34
|
+
const isChecked = createMemo(() => isControlled() ? controlledChecked() : internalChecked())
|
|
35
|
+
|
|
36
|
+
const handleClick = () => {
|
|
37
|
+
if (isControlled()) {
|
|
38
|
+
setControlledChecked(!isChecked())
|
|
39
|
+
} else {
|
|
40
|
+
setInternalChecked(!isChecked())
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<button
|
|
46
|
+
data-state={isChecked() ? 'checked' : 'unchecked'}
|
|
47
|
+
aria-checked={isChecked()}
|
|
48
|
+
onClick={handleClick}
|
|
49
|
+
/>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
`
|
|
53
|
+
|
|
54
|
+
const SCROLL_AREA_SOURCE = `
|
|
55
|
+
"use client"
|
|
56
|
+
import { createSignal } from '@barefootjs/client'
|
|
57
|
+
function ScrollArea(props: any) {
|
|
58
|
+
const [scrolling, setScrolling] = createSignal(false)
|
|
59
|
+
const [thumbVSize, setThumbVSize] = createSignal(0)
|
|
60
|
+
const [thumbVPos, setThumbVPos] = createSignal(0)
|
|
61
|
+
|
|
62
|
+
const handleScroll = () => {
|
|
63
|
+
setScrolling(true)
|
|
64
|
+
setThumbVSize(100)
|
|
65
|
+
setThumbVPos(50)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div onScroll={handleScroll}>
|
|
70
|
+
<div style={\`height: \${thumbVSize()}%\`} />
|
|
71
|
+
<div style={\`top: \${thumbVPos()}%\`} />
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
`
|
|
76
|
+
|
|
77
|
+
const CALENDAR_DUAL_MONTH_SOURCE = `
|
|
78
|
+
"use client"
|
|
79
|
+
import { createSignal } from '@barefootjs/client'
|
|
80
|
+
function Calendar(props: any) {
|
|
81
|
+
const [currentYear, setCurrentYear] = createSignal(2024)
|
|
82
|
+
const [currentMonth, setCurrentMonth] = createSignal(0)
|
|
83
|
+
|
|
84
|
+
const goPrev = () => {
|
|
85
|
+
setCurrentYear(currentYear() - 1)
|
|
86
|
+
setCurrentMonth(currentMonth() - 1)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div>
|
|
91
|
+
<button onClick={goPrev}>prev</button>
|
|
92
|
+
<button onClick={goPrev}>prev2</button>
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
`
|
|
97
|
+
|
|
98
|
+
const DEEP_MEMO_SOURCE = `
|
|
99
|
+
"use client"
|
|
100
|
+
import { createSignal, createMemo } from '@barefootjs/client'
|
|
101
|
+
function DeepChain(props: any) {
|
|
102
|
+
const [base, setBase] = createSignal(0)
|
|
103
|
+
const a = createMemo(() => base() + 1)
|
|
104
|
+
const b = createMemo(() => a() + 1)
|
|
105
|
+
const c = createMemo(() => b() + 1)
|
|
106
|
+
const d = createMemo(() => c() + 1)
|
|
107
|
+
return <span>{d()}</span>
|
|
108
|
+
}
|
|
109
|
+
`
|
|
110
|
+
|
|
111
|
+
const HIGH_FANOUT_SOURCE = `
|
|
112
|
+
"use client"
|
|
113
|
+
import { createSignal } from '@barefootjs/client'
|
|
114
|
+
function HighFanOut(props: any) {
|
|
115
|
+
const [count, setCount] = createSignal(0)
|
|
116
|
+
return (
|
|
117
|
+
<div>
|
|
118
|
+
<span>{count()}</span>
|
|
119
|
+
<span>{count()}</span>
|
|
120
|
+
<span>{count()}</span>
|
|
121
|
+
<span>{count()}</span>
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
`
|
|
126
|
+
|
|
127
|
+
const FALLBACK_HEAVY_SOURCE = `
|
|
128
|
+
"use client"
|
|
129
|
+
import { createSignal } from '@barefootjs/client'
|
|
130
|
+
function FallbackHeavy(props: any) {
|
|
131
|
+
const [value, setValue] = createSignal('')
|
|
132
|
+
return (
|
|
133
|
+
<div>
|
|
134
|
+
<span className={getClass(value())}>{formatValue(value())}</span>
|
|
135
|
+
<span className={getClass(value())}>{formatValue(value())}</span>
|
|
136
|
+
<span className={getClass(value())}>{formatValue(value())}</span>
|
|
137
|
+
<span className={getClass(value())}>{formatValue(value())}</span>
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
`
|
|
142
|
+
|
|
143
|
+
const STATIC_SOURCE = `
|
|
144
|
+
function Button(props: any) {
|
|
145
|
+
return <button className="btn">{props.children}</button>
|
|
146
|
+
}
|
|
147
|
+
`
|
|
148
|
+
|
|
149
|
+
const FILE = '/fake/path/component.tsx'
|
|
150
|
+
|
|
151
|
+
// =============================================================================
|
|
152
|
+
// Basic profile metrics
|
|
153
|
+
// =============================================================================
|
|
154
|
+
|
|
155
|
+
describe('buildReactiveProfile — metrics', () => {
|
|
156
|
+
it('counts signals, memos, and bindings', () => {
|
|
157
|
+
const profile = buildReactiveProfile(SWITCH_SOURCE, FILE)
|
|
158
|
+
const m = profile.metrics
|
|
159
|
+
expect(m.signals).toBe(2)
|
|
160
|
+
expect(m.memos).toBe(2)
|
|
161
|
+
expect(m.dynamicBindings).toBeGreaterThanOrEqual(2) // isChecked dom bindings
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('computes memo chain depth correctly', () => {
|
|
165
|
+
const profile = buildReactiveProfile(DEEP_MEMO_SOURCE, FILE)
|
|
166
|
+
// base → a → b → c → d: depth 4
|
|
167
|
+
expect(profile.metrics.maxMemoChainDepth).toBe(4)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('computes fan-out for the hottest signal', () => {
|
|
171
|
+
const profile = buildReactiveProfile(HIGH_FANOUT_SOURCE, FILE)
|
|
172
|
+
// count() appears in 4 text bindings
|
|
173
|
+
expect(profile.metrics.maxSignalFanOut).toBeGreaterThanOrEqual(1)
|
|
174
|
+
expect(profile.metrics.hotSignal).toBe('count')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('marks non-reactive component as not hydrated', () => {
|
|
178
|
+
const profile = buildReactiveProfile(STATIC_SOURCE, FILE)
|
|
179
|
+
expect(profile.metrics.hydrated).toBe(false)
|
|
180
|
+
expect(profile.metrics.signals).toBe(0)
|
|
181
|
+
expect(profile.metrics.memos).toBe(0)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('returns the filePath as sourceFile (Bug A fix)', () => {
|
|
185
|
+
// Non-reactive components have no signal/memo locations → sourceFile was '' before fix
|
|
186
|
+
const profile = buildReactiveProfile(STATIC_SOURCE, FILE)
|
|
187
|
+
expect(profile.metrics.sourceFile).toBe(FILE)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('returns non-empty sourceFile for reactive components', () => {
|
|
191
|
+
const profile = buildReactiveProfile(SWITCH_SOURCE, FILE)
|
|
192
|
+
expect(profile.metrics.sourceFile).toBe(FILE)
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// =============================================================================
|
|
197
|
+
// Findings
|
|
198
|
+
// =============================================================================
|
|
199
|
+
|
|
200
|
+
describe('buildReactiveProfile — findings', () => {
|
|
201
|
+
it('emits high-fan-out finding when signal exceeds threshold', () => {
|
|
202
|
+
const profile = buildReactiveProfile(HIGH_FANOUT_SOURCE, FILE)
|
|
203
|
+
const fanOut = profile.findings.filter(f => f.kind === 'high-fan-out')
|
|
204
|
+
expect(fanOut.length).toBeGreaterThanOrEqual(1)
|
|
205
|
+
expect(fanOut[0].signal).toBe('count')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('emits deep-memo-chain finding for 4-level chain', () => {
|
|
209
|
+
const profile = buildReactiveProfile(DEEP_MEMO_SOURCE, FILE)
|
|
210
|
+
const chain = profile.findings.filter(f => f.kind === 'deep-memo-chain')
|
|
211
|
+
expect(chain.length).toBe(1)
|
|
212
|
+
expect(chain[0].depth).toBe(4)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('emits batch-candidate for ScrollArea (all setters fire unconditionally)', () => {
|
|
216
|
+
const profile = buildReactiveProfile(SCROLL_AREA_SOURCE, FILE)
|
|
217
|
+
const batch = profile.findings.filter(f => f.kind === 'batch-candidate')
|
|
218
|
+
expect(batch.length).toBe(1)
|
|
219
|
+
expect(batch[0].signals).toContain('scrolling')
|
|
220
|
+
expect(batch[0].signals).toContain('thumbVSize')
|
|
221
|
+
expect(batch[0].signals).toContain('thumbVPos')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('does NOT emit duplicate findings for same handler used in two JSX locations (Bug C fix)', () => {
|
|
225
|
+
// Calendar dual-month: same onClick handler wired to two <button> elements
|
|
226
|
+
const profile = buildReactiveProfile(CALENDAR_DUAL_MONTH_SOURCE, FILE)
|
|
227
|
+
const batch = profile.findings.filter(f => f.kind === 'batch-candidate')
|
|
228
|
+
// There should be at most 1 finding per unique (kind, file, line, signals) combination
|
|
229
|
+
const seen = new Set<string>()
|
|
230
|
+
for (const f of batch) {
|
|
231
|
+
const key = `${f.kind}|${f.loc?.file ?? ''}|${f.loc?.line ?? ''}|${(f.signals ?? []).sort().join(',')}`
|
|
232
|
+
expect(seen.has(key)).toBe(false)
|
|
233
|
+
seen.add(key)
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('emits fallback-heavy finding when >50% of bindings are fallback-wrapped', () => {
|
|
238
|
+
// Note: may require the component to actually produce fallback-wrapped bindings
|
|
239
|
+
// Only verified structurally here; real-world trigger tested via calendar.
|
|
240
|
+
const profile = buildReactiveProfile(FALLBACK_HEAVY_SOURCE, FILE)
|
|
241
|
+
// If fallbacks >= 3 and ratio >= 0.5, finding is emitted
|
|
242
|
+
if (profile.metrics.fallbacks >= 3 && profile.metrics.dynamicBindings > 0) {
|
|
243
|
+
const fallbackFindings = profile.findings.filter(f => f.kind === 'fallback-heavy')
|
|
244
|
+
if (profile.metrics.fallbacks / profile.metrics.dynamicBindings > 0.5) {
|
|
245
|
+
expect(fallbackFindings.length).toBe(1)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('emits no findings for a simple reactive component within thresholds', () => {
|
|
251
|
+
// Switch has fan-out=1, chain=2, 1 batch-candidate (false positive from controlled/uncontrolled)
|
|
252
|
+
// The batch-candidate IS emitted (false positive — controlled/uncontrolled not yet detected)
|
|
253
|
+
// This test documents the current behavior so regressions are caught.
|
|
254
|
+
const profile = buildReactiveProfile(SWITCH_SOURCE, FILE)
|
|
255
|
+
const fanOut = profile.findings.filter(f => f.kind === 'high-fan-out')
|
|
256
|
+
const chain = profile.findings.filter(f => f.kind === 'deep-memo-chain')
|
|
257
|
+
expect(fanOut.length).toBe(0)
|
|
258
|
+
expect(chain.length).toBe(0)
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// =============================================================================
|
|
263
|
+
// Bug B: batch-candidate false positive (controlled/uncontrolled pattern)
|
|
264
|
+
// This test documents the CURRENT behavior (known false positive).
|
|
265
|
+
// When Bug B is fixed (AST-level control flow), update expectation to toBe(0).
|
|
266
|
+
// =============================================================================
|
|
267
|
+
|
|
268
|
+
describe('buildReactiveProfile — Bug B: batch-candidate precision', () => {
|
|
269
|
+
it('KNOWN FALSE POSITIVE: reports batch-candidate for switch controlled/uncontrolled if/else', () => {
|
|
270
|
+
// handleClick calls setControlled XOR setInternal (not both) based on isControlled().
|
|
271
|
+
// Static analysis cannot prove this without control flow awareness, so it flags both setters.
|
|
272
|
+
const profile = buildReactiveProfile(SWITCH_SOURCE, FILE)
|
|
273
|
+
const batch = profile.findings.filter(f => f.kind === 'batch-candidate')
|
|
274
|
+
// Currently: 1 false positive (known limitation, documented here)
|
|
275
|
+
// When Bug B is fixed, this should be 0.
|
|
276
|
+
expect(batch.length).toBe(1) // document current behavior
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
// =============================================================================
|
|
281
|
+
// diffProfiles (SR6)
|
|
282
|
+
// =============================================================================
|
|
283
|
+
|
|
284
|
+
describe('diffProfiles (SR6)', () => {
|
|
285
|
+
it('detects regressions when reactive cost increases', () => {
|
|
286
|
+
const before = buildReactiveProfile(SWITCH_SOURCE, FILE).metrics
|
|
287
|
+
const after = buildReactiveProfile(HIGH_FANOUT_SOURCE, FILE).metrics
|
|
288
|
+
const diff = diffProfiles(before, after)
|
|
289
|
+
// hotSignal might change, but the key thing is we can compute a diff
|
|
290
|
+
expect(diff.componentName).toBeTruthy()
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('returns empty diff when metrics are identical', () => {
|
|
294
|
+
const profile = buildReactiveProfile(SWITCH_SOURCE, FILE)
|
|
295
|
+
const diff = diffProfiles(profile.metrics, { ...profile.metrics })
|
|
296
|
+
expect(diff.regressions).toHaveLength(0)
|
|
297
|
+
expect(diff.improvements).toHaveLength(0)
|
|
298
|
+
expect(diff.neutral).toHaveLength(0)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('reports fallback increase as regression', () => {
|
|
302
|
+
const lowFallback = buildReactiveProfile(SWITCH_SOURCE, FILE).metrics
|
|
303
|
+
// Manually craft a "before" with lower fallbacks
|
|
304
|
+
const before = { ...lowFallback, fallbacks: 0 }
|
|
305
|
+
const after = { ...lowFallback, fallbacks: 5 }
|
|
306
|
+
const diff = diffProfiles(before, after)
|
|
307
|
+
const reg = diff.regressions.find(r => r.metric === 'fallbacks')
|
|
308
|
+
expect(reg).toBeTruthy()
|
|
309
|
+
expect(reg?.delta).toBe(5)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('reports memo chain depth decrease as improvement', () => {
|
|
313
|
+
const profile = buildReactiveProfile(DEEP_MEMO_SOURCE, FILE).metrics
|
|
314
|
+
const before = { ...profile, maxMemoChainDepth: 6 }
|
|
315
|
+
const after = { ...profile, maxMemoChainDepth: 2 }
|
|
316
|
+
const diff = diffProfiles(before, after)
|
|
317
|
+
const imp = diff.improvements.find(r => r.metric === 'maxMemoChainDepth')
|
|
318
|
+
expect(imp).toBeTruthy()
|
|
319
|
+
expect(imp?.delta).toBe(-4)
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
// =============================================================================
|
|
324
|
+
// Formatting
|
|
325
|
+
// =============================================================================
|
|
326
|
+
|
|
327
|
+
describe('formatSingleProfile', () => {
|
|
328
|
+
it('includes component name and hydrated status', () => {
|
|
329
|
+
const profile = buildReactiveProfile(SWITCH_SOURCE, FILE)
|
|
330
|
+
const output = formatSingleProfile(profile)
|
|
331
|
+
expect(output).toContain('— reactive profile')
|
|
332
|
+
expect(output).toContain('hydrated: yes')
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('includes Counts and Reactive budget sections', () => {
|
|
336
|
+
const profile = buildReactiveProfile(SWITCH_SOURCE, FILE)
|
|
337
|
+
const output = formatSingleProfile(profile)
|
|
338
|
+
expect(output).toContain('Counts:')
|
|
339
|
+
expect(output).toContain('Reactive budget (SR5):')
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('includes Findings section when findings exist', () => {
|
|
343
|
+
const profile = buildReactiveProfile(HIGH_FANOUT_SOURCE, FILE)
|
|
344
|
+
const output = formatSingleProfile(profile)
|
|
345
|
+
expect(output).toContain('Findings:')
|
|
346
|
+
expect(output).toContain('high-fan-out')
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('shows "No findings" for a clean component', () => {
|
|
350
|
+
const profile = buildReactiveProfile(STATIC_SOURCE, FILE)
|
|
351
|
+
const output = formatSingleProfile(profile)
|
|
352
|
+
expect(output).toContain('No findings')
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
describe('formatProfileTable', () => {
|
|
357
|
+
it('renders a table with headers and component rows', () => {
|
|
358
|
+
const profiles = [
|
|
359
|
+
buildReactiveProfile(SWITCH_SOURCE, FILE),
|
|
360
|
+
buildReactiveProfile(STATIC_SOURCE, FILE),
|
|
361
|
+
]
|
|
362
|
+
const output = formatProfileTable(profiles)
|
|
363
|
+
expect(output).toContain('Component')
|
|
364
|
+
expect(output).toContain('sig')
|
|
365
|
+
expect(output).toContain('subs')
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it('returns "No components found" for empty array', () => {
|
|
369
|
+
expect(formatProfileTable([])).toBe('No components found.')
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('sorts by totalSubscriptions descending', () => {
|
|
373
|
+
const switchProfile = buildReactiveProfile(SWITCH_SOURCE, FILE)
|
|
374
|
+
const staticProfile = buildReactiveProfile(STATIC_SOURCE, FILE)
|
|
375
|
+
const output = formatProfileTable([staticProfile, switchProfile])
|
|
376
|
+
// Switch (higher subs) should appear before Button (0 subs)
|
|
377
|
+
const switchIdx = output.indexOf('Switch')
|
|
378
|
+
const buttonIdx = output.indexOf('Button')
|
|
379
|
+
// If Button doesn't appear (static component might be skipped), just check Switch is present
|
|
380
|
+
expect(switchIdx).toBeGreaterThan(-1)
|
|
381
|
+
if (buttonIdx > -1) {
|
|
382
|
+
expect(switchIdx).toBeLessThan(buttonIdx)
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
describe('formatProfileDiff', () => {
|
|
388
|
+
it('shows regressions and improvements', () => {
|
|
389
|
+
const before = buildReactiveProfile(SWITCH_SOURCE, FILE).metrics
|
|
390
|
+
const after = { ...before, fallbacks: before.fallbacks + 3, maxSignalFanOut: Math.max(0, before.maxSignalFanOut - 1) }
|
|
391
|
+
const diff = diffProfiles(before, after)
|
|
392
|
+
const output = formatProfileDiff(diff)
|
|
393
|
+
expect(output).toContain('Regressions')
|
|
394
|
+
if (after.maxSignalFanOut < before.maxSignalFanOut) {
|
|
395
|
+
expect(output).toContain('Improvements')
|
|
396
|
+
}
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it('shows "No changes" when metrics are identical', () => {
|
|
400
|
+
const profile = buildReactiveProfile(SWITCH_SOURCE, FILE)
|
|
401
|
+
const diff = diffProfiles(profile.metrics, { ...profile.metrics })
|
|
402
|
+
const output = formatProfileDiff(diff)
|
|
403
|
+
expect(output).toContain('No changes')
|
|
404
|
+
})
|
|
405
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test'
|
|
2
2
|
import ts from 'typescript'
|
|
3
|
-
import { parseExpression, isSupported, exprToString, stringifyParsedExpr, parseBlockBody, extractArrowBodyExpression } from '../expression-parser'
|
|
3
|
+
import { parseExpression, isSupported, exprToString, stringifyParsedExpr, parseBlockBody, extractArrowBodyExpression, parseStyleObjectEntries } from '../expression-parser'
|
|
4
4
|
import { collectAllTypeRanges, reconstructWithoutTypes } from '../strip-types'
|
|
5
5
|
|
|
6
6
|
describe('expression-parser', () => {
|
|
@@ -1649,3 +1649,46 @@ describe('extractArrowBodyExpression', () => {
|
|
|
1649
1649
|
expect(extractArrowBodyExpression('() => 1; sideEffect()')).toBeNull()
|
|
1650
1650
|
})
|
|
1651
1651
|
})
|
|
1652
|
+
|
|
1653
|
+
describe('parseStyleObjectEntries', () => {
|
|
1654
|
+
test('kebab-cases keys and splits literal vs expression values', () => {
|
|
1655
|
+
expect(parseStyleObjectEntries("{ backgroundColor: color, padding: '8px' }")).toEqual([
|
|
1656
|
+
{ cssKey: 'background-color', kind: 'expr', expr: 'color' },
|
|
1657
|
+
{ cssKey: 'padding', kind: 'literal', value: '8px' },
|
|
1658
|
+
])
|
|
1659
|
+
})
|
|
1660
|
+
|
|
1661
|
+
test('handles signal-getter call values as expressions', () => {
|
|
1662
|
+
expect(parseStyleObjectEntries('{ background: bg(), color: fg() }')).toEqual([
|
|
1663
|
+
{ cssKey: 'background', kind: 'expr', expr: 'bg()' },
|
|
1664
|
+
{ cssKey: 'color', kind: 'expr', expr: 'fg()' },
|
|
1665
|
+
])
|
|
1666
|
+
})
|
|
1667
|
+
|
|
1668
|
+
test('quoted keys are honoured', () => {
|
|
1669
|
+
expect(parseStyleObjectEntries("{ 'z-index': '1' }")).toEqual([
|
|
1670
|
+
{ cssKey: 'z-index', kind: 'literal', value: '1' },
|
|
1671
|
+
])
|
|
1672
|
+
})
|
|
1673
|
+
|
|
1674
|
+
test('vendor-prefixed keys keep the leading dash', () => {
|
|
1675
|
+
expect(parseStyleObjectEntries("{ WebkitTransform: 'none' }")).toEqual([
|
|
1676
|
+
{ cssKey: '-webkit-transform', kind: 'literal', value: 'none' },
|
|
1677
|
+
])
|
|
1678
|
+
// The `ms` prefix is lowercase in React style keys but the CSS property
|
|
1679
|
+
// carries a leading dash (`-ms-transform`), unlike `Webkit`/`Moz`.
|
|
1680
|
+
expect(parseStyleObjectEntries("{ msTransform: 'none' }")).toEqual([
|
|
1681
|
+
{ cssKey: '-ms-transform', kind: 'literal', value: 'none' },
|
|
1682
|
+
])
|
|
1683
|
+
})
|
|
1684
|
+
|
|
1685
|
+
test('returns null for unsupported shapes (spread / shorthand / computed)', () => {
|
|
1686
|
+
expect(parseStyleObjectEntries('{ ...rest }')).toBeNull()
|
|
1687
|
+
expect(parseStyleObjectEntries('{ color }')).toBeNull()
|
|
1688
|
+
expect(parseStyleObjectEntries('{ [k]: v }')).toBeNull()
|
|
1689
|
+
})
|
|
1690
|
+
|
|
1691
|
+
test('returns null for a non-object source', () => {
|
|
1692
|
+
expect(parseStyleObjectEntries('color')).toBeNull()
|
|
1693
|
+
})
|
|
1694
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile-mode `__bfId` emission (#1690, SR3).
|
|
3
|
+
*
|
|
4
|
+
* In profile mode the client-JS codegen appends IR-aligned id arguments at
|
|
5
|
+
* reactive creation sites so a profiling run can join runtime events to IR
|
|
6
|
+
* nodes. Off by default the emitted code is byte-for-byte unchanged (SR8).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, test, expect } from 'bun:test'
|
|
10
|
+
import { compileJSX } from '../compiler'
|
|
11
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
12
|
+
|
|
13
|
+
const adapter = new TestAdapter()
|
|
14
|
+
|
|
15
|
+
const source = `
|
|
16
|
+
'use client'
|
|
17
|
+
import { createSignal, createMemo, createEffect } from '@barefootjs/client'
|
|
18
|
+
|
|
19
|
+
export function Counter() {
|
|
20
|
+
const [count, setCount] = createSignal(0)
|
|
21
|
+
const doubled = createMemo(() => count() * 2)
|
|
22
|
+
createEffect(() => { console.log(doubled()) })
|
|
23
|
+
return <button onClick={() => setCount(n => n + 1)}>{doubled()}</button>
|
|
24
|
+
}
|
|
25
|
+
`
|
|
26
|
+
|
|
27
|
+
function clientJs(profile: boolean): string {
|
|
28
|
+
const result = compileJSX(source, 'Counter.tsx', { adapter, profile })
|
|
29
|
+
const file = result.files.find(f => f.type === 'clientJs')
|
|
30
|
+
expect(file).toBeDefined()
|
|
31
|
+
return file!.content
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('profile mode off (default, SR8)', () => {
|
|
35
|
+
test('emits no __bfId arguments — output is unchanged', () => {
|
|
36
|
+
const off = clientJs(false)
|
|
37
|
+
expect(off).toContain('createSignal(0)')
|
|
38
|
+
expect(off).not.toContain('#signal:')
|
|
39
|
+
expect(off).not.toContain('#memo:')
|
|
40
|
+
expect(off).not.toContain('#effect:')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('omitting the flag matches profile:false byte-for-byte', () => {
|
|
44
|
+
const implicit = compileJSX(source, 'Counter.tsx', { adapter })
|
|
45
|
+
.files.find(f => f.type === 'clientJs')!.content
|
|
46
|
+
expect(implicit).toBe(clientJs(false))
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('profile mode on (SR3)', () => {
|
|
51
|
+
test('appends IR-aligned ids to createSignal / createMemo / createEffect', () => {
|
|
52
|
+
const on = clientJs(true)
|
|
53
|
+
expect(on).toContain('createSignal(0, "Counter#signal:count")')
|
|
54
|
+
expect(on).toContain('createMemo(() => count() * 2, "Counter#memo:doubled")')
|
|
55
|
+
expect(on).toMatch(/createEffect\(.*"Counter#effect:/)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('does not change the signal/memo initial expressions', () => {
|
|
59
|
+
// The id is purely additive — the first argument is identical to off mode.
|
|
60
|
+
const on = clientJs(true)
|
|
61
|
+
expect(on).toContain('createMemo(() => count() * 2,')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile-mode ids on DOM-binding effects (#1690, SR3/SR4, issue #1795).
|
|
3
|
+
*
|
|
4
|
+
* Text / attribute updates emit `createEffect(…, "<Component>#binding:<slotId>")`
|
|
5
|
+
* in profile mode, and `buildIdIndex` resolves those ids from the graph's
|
|
6
|
+
* `domBindings` (slot + loc). Off by default the effect is unchanged (SR8).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, test, expect } from 'bun:test'
|
|
10
|
+
import { compileJSX } from '../compiler'
|
|
11
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
12
|
+
import { buildIdIndex } from '../profiler'
|
|
13
|
+
import { buildComponentAnalysis } from '../debug'
|
|
14
|
+
|
|
15
|
+
const adapter = new TestAdapter()
|
|
16
|
+
|
|
17
|
+
const source = `
|
|
18
|
+
'use client'
|
|
19
|
+
import { createSignal } from '@barefootjs/client'
|
|
20
|
+
export function Widget() {
|
|
21
|
+
const [n, setN] = createSignal(0)
|
|
22
|
+
return <button class={n() > 0 ? 'on' : 'off'} onClick={() => setN(n() + 1)}>{n()}</button>
|
|
23
|
+
}
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
function clientJs(profile: boolean): string {
|
|
27
|
+
return compileJSX(source, 'Widget.tsx', { adapter, profile })
|
|
28
|
+
.files.find(f => f.type === 'clientJs')!.content
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const loopSource = `
|
|
32
|
+
'use client'
|
|
33
|
+
import { createSignal } from '@barefootjs/client'
|
|
34
|
+
export function List() {
|
|
35
|
+
const [items] = createSignal([{ id: 1, t: 'a' }, { id: 2, t: 'b' }])
|
|
36
|
+
return <ul>{items().map(it => <li key={it.id}>{it.t}</li>)}</ul>
|
|
37
|
+
}
|
|
38
|
+
`
|
|
39
|
+
|
|
40
|
+
describe('loop-effect id (mapArray bfId)', () => {
|
|
41
|
+
test('profile on: mapArray carries the loop binding id; off: it does not', () => {
|
|
42
|
+
const on = compileJSX(loopSource, 'List.tsx', { adapter, profile: true })
|
|
43
|
+
.files.find(f => f.type === 'clientJs')!.content
|
|
44
|
+
const off = compileJSX(loopSource, 'List.tsx', { adapter, profile: false })
|
|
45
|
+
.files.find(f => f.type === 'clientJs')!.content
|
|
46
|
+
expect(on).toMatch(/mapArray\(.*List#binding:s\d+/s)
|
|
47
|
+
expect(off).not.toContain('#binding:')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('buildIdIndex resolves the loop binding to source loc', () => {
|
|
51
|
+
const { graph } = buildComponentAnalysis(loopSource, 'List.tsx')
|
|
52
|
+
const index = buildIdIndex(graph)
|
|
53
|
+
const loopBinding = graph.domBindings.find(b => b.type === 'loop')!
|
|
54
|
+
const node = index.get(`List#binding:${loopBinding.slotId}`)
|
|
55
|
+
expect(node?.kind).toBe('effect')
|
|
56
|
+
expect(node?.loc.file).toBe('List.tsx')
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// A prop-derived local const forwarded into a child component — the
|
|
61
|
+
// Slot-composed-button shape (#1863). `classes` reads props (`variant`,
|
|
62
|
+
// `className`) and is used both on a host element and as a child-component
|
|
63
|
+
// prop; the emitter inlines it, sees the prop reads, and wraps both in a
|
|
64
|
+
// `createEffect` emitting `#binding:<slot>`. The analyzer must follow the const
|
|
65
|
+
// indirection so those ids resolve instead of surfacing as `(unresolved)`.
|
|
66
|
+
const slotForwardSource = `
|
|
67
|
+
'use client'
|
|
68
|
+
import { Slot } from './slot'
|
|
69
|
+
export function Btn({ className = '', variant = 'default', asChild = false, ...props }) {
|
|
70
|
+
const classes = \`base \${variant} \${className}\`
|
|
71
|
+
if (asChild) return <Slot className={classes} {...props} />
|
|
72
|
+
return <button className={classes} {...props} />
|
|
73
|
+
}
|
|
74
|
+
`
|
|
75
|
+
|
|
76
|
+
describe('prop-derived local const binding ids (Slot composition, #1863)', () => {
|
|
77
|
+
test('buildIdIndex resolves both the host-element and child-prop #binding ids', () => {
|
|
78
|
+
const { graph } = buildComponentAnalysis(slotForwardSource, 'Btn.tsx')
|
|
79
|
+
const index = buildIdIndex(graph)
|
|
80
|
+
const bindingKeys = [...index.keys()].filter(k => k.startsWith('Btn#binding:'))
|
|
81
|
+
// Both the `<button class={classes}>` host attr and the `<Slot
|
|
82
|
+
// className={classes}>` child prop must resolve.
|
|
83
|
+
expect(bindingKeys.length).toBe(2)
|
|
84
|
+
for (const key of bindingKeys) {
|
|
85
|
+
const node = index.get(key)!
|
|
86
|
+
expect(node.kind).toBe('effect')
|
|
87
|
+
expect(node.loc.file).toBe('Btn.tsx')
|
|
88
|
+
expect(node.loc.line).toBeGreaterThan(0)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('every emitted #binding id resolves — no unattributed gap', () => {
|
|
93
|
+
const on = compileJSX(slotForwardSource, 'Btn.tsx', { adapter, profile: true })
|
|
94
|
+
.files.find(f => f.type === 'clientJs')!.content
|
|
95
|
+
const emitted = [...new Set([...on.matchAll(/"(Btn#binding:s\d+)"/g)].map(m => m[1]))]
|
|
96
|
+
expect(emitted.length).toBeGreaterThan(0)
|
|
97
|
+
const { graph } = buildComponentAnalysis(slotForwardSource, 'Btn.tsx')
|
|
98
|
+
const index = buildIdIndex(graph)
|
|
99
|
+
for (const id of emitted) expect(index.has(id)).toBe(true)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('binding-effect ids', () => {
|
|
104
|
+
test('profile off: binding effects carry no id (SR8)', () => {
|
|
105
|
+
expect(clientJs(false)).not.toContain('#binding:')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('profile on: text/attribute binding effects carry a #binding id', () => {
|
|
109
|
+
const on = clientJs(true)
|
|
110
|
+
expect(on).toMatch(/Widget#binding:s\d+/)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('buildIdIndex resolves #binding ids from domBindings to source loc', () => {
|
|
114
|
+
const { graph } = buildComponentAnalysis(source, 'Widget.tsx')
|
|
115
|
+
const index = buildIdIndex(graph)
|
|
116
|
+
const bindingKeys = [...index.keys()].filter(k => k.startsWith('Widget#binding:'))
|
|
117
|
+
expect(bindingKeys.length).toBeGreaterThan(0)
|
|
118
|
+
const node = index.get(bindingKeys[0])!
|
|
119
|
+
expect(node.kind).toBe('effect')
|
|
120
|
+
expect(node.loc.file).toBe('Widget.tsx')
|
|
121
|
+
expect(node.loc.line).toBeGreaterThan(0)
|
|
122
|
+
})
|
|
123
|
+
})
|