@barefootjs/test 0.1.3 → 0.3.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.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Recursively transforms each IRNode variant into a TestNode.
5
5
  */
6
- import type { IRNode } from '@barefootjs/jsx';
6
+ import type { IRNode, IRMetadata } from '@barefootjs/jsx';
7
7
  import { TestNode } from './test-node';
8
- export declare function irNodeToTestNode(node: IRNode, constantMap?: Map<string, string>): TestNode;
8
+ export declare function irNodeToTestNode(node: IRNode, constantMap?: Map<string, string>, metadata?: IRMetadata): TestNode;
9
9
  //# sourceMappingURL=ir-to-test-node.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ir-to-test-node.d.ts","sourceRoot":"","sources":["../src/ir-to-test-node.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,MAAM,EAYP,MAAM,iBAAiB,CAAA;AAKxB,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAEtC,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,QAAQ,CAG1F"}
1
+ {"version":3,"file":"ir-to-test-node.d.ts","sourceRoot":"","sources":["../src/ir-to-test-node.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,MAAM,EAYN,UAAU,EACX,MAAM,iBAAiB,CAAA;AAMxB,OAAO,EAAE,QAAQ,EAAqB,MAAM,aAAa,CAAA;AAQzD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,CAAC,EAAE,UAAU,GAAG,QAAQ,CAajH"}
@@ -3,6 +3,10 @@
3
3
  *
4
4
  * Produced by converting the compiler IR into a flat, assertion-friendly shape.
5
5
  */
6
+ export interface EventHandler {
7
+ setters: string[];
8
+ via: string[];
9
+ }
6
10
  export interface TestNodeData {
7
11
  tag: string | null;
8
12
  type: 'element' | 'text' | 'expression' | 'conditional' | 'loop' | 'component' | 'fragment';
@@ -14,6 +18,7 @@ export interface TestNodeData {
14
18
  aria: Record<string, string>;
15
19
  dataState: string | null;
16
20
  events: string[];
21
+ handlers: Partial<Record<string, EventHandler>>;
17
22
  reactive: boolean;
18
23
  componentName: string | null;
19
24
  }
@@ -33,8 +38,14 @@ export declare class TestNode implements TestNodeData {
33
38
  aria: Record<string, string>;
34
39
  dataState: string | null;
35
40
  events: string[];
41
+ handlers: Partial<Record<string, EventHandler>>;
36
42
  reactive: boolean;
37
43
  componentName: string | null;
44
+ get onClick(): EventHandler | undefined;
45
+ get onInput(): EventHandler | undefined;
46
+ get onChange(): EventHandler | undefined;
47
+ get onSubmit(): EventHandler | undefined;
48
+ on(event: string): EventHandler | null;
38
49
  constructor(data: TestNodeData);
39
50
  /** Return the first descendant (or self) matching the query. */
40
51
  find(query: TestNodeQuery): TestNode | null;
@@ -1 +1 @@
1
- {"version":3,"file":"test-node.d.ts","sourceRoot":"","sources":["../src/test-node.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,YAAY,GAAG,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,UAAU,CAAA;IAC3F,QAAQ,EAAE,QAAQ,EAAE,CAAA;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAA;IAC9C,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;IACjB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,qBAAa,QAAS,YAAW,YAAY;IAC3C,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,CAAA;IAC1B,QAAQ,EAAE,QAAQ,EAAE,CAAA;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAA;IAC9C,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;IACjB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAE5B,YAAY,IAAI,EAAE,YAAY,EAa7B;IAED,gEAAgE;IAChE,IAAI,CAAC,KAAK,EAAE,aAAa,GAAG,QAAQ,GAAG,IAAI,CAO1C;IAED,4DAA4D;IAC5D,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,QAAQ,EAAE,CAIxC;IAED,wEAAwE;IACxE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAOxC;IAED,OAAO,CAAC,OAAO;IAOf,OAAO,CAAC,cAAc;CAMvB"}
1
+ {"version":3,"file":"test-node.d.ts","sourceRoot":"","sources":["../src/test-node.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,GAAG,EAAE,MAAM,EAAE,CAAA;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,YAAY,GAAG,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,UAAU,CAAA;IAC3F,QAAQ,EAAE,QAAQ,EAAE,CAAA;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAA;IAC9C,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAA;IAC/C,QAAQ,EAAE,OAAO,CAAA;IACjB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,qBAAa,QAAS,YAAW,YAAY;IAC3C,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,CAAA;IAC1B,QAAQ,EAAE,QAAQ,EAAE,CAAA;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAA;IAC9C,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAA;IAC/C,QAAQ,EAAE,OAAO,CAAA;IACjB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAE5B,IAAI,OAAO,IAAI,YAAY,GAAG,SAAS,CAA+B;IACtE,IAAI,OAAO,IAAI,YAAY,GAAG,SAAS,CAA+B;IACtE,IAAI,QAAQ,IAAI,YAAY,GAAG,SAAS,CAAgC;IACxE,IAAI,QAAQ,IAAI,YAAY,GAAG,SAAS,CAAgC;IAExE,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAErC;IAED,YAAY,IAAI,EAAE,YAAY,EAc7B;IAED,gEAAgE;IAChE,IAAI,CAAC,KAAK,EAAE,aAAa,GAAG,QAAQ,GAAG,IAAI,CAO1C;IAED,4DAA4D;IAC5D,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,QAAQ,EAAE,CAIxC;IAED,wEAAwE;IACxE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAOxC;IAED,OAAO,CAAC,OAAO;IAOf,OAAO,CAAC,cAAc;CAMvB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/test",
3
- "version": "0.1.3",
3
+ "version": "0.3.0",
4
4
  "description": "Test utilities for BarefootJS - IR-based component testing without a browser",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -39,7 +39,7 @@
39
39
  "directory": "packages/test"
40
40
  },
41
41
  "dependencies": {
42
- "@barefootjs/jsx": "0.1.3"
42
+ "@barefootjs/jsx": "0.3.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "typescript": "^5.0.0"
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@ export { renderToTest } from './render'
6
6
  export type { TestResult } from './render'
7
7
 
8
8
  export { TestNode } from './test-node'
9
- export type { TestNodeData, TestNodeQuery } from './test-node'
9
+ export type { TestNodeData, TestNodeQuery, EventHandler } from './test-node'
10
10
 
11
11
  export { toStructure } from './structure'
12
12
 
@@ -17,42 +17,60 @@ import type {
17
17
  IRIfStatement,
18
18
  IRProvider,
19
19
  IRAsync,
20
+ IRMetadata,
20
21
  } from '@barefootjs/jsx'
22
+ import { resolveSetters, buildLocalFunctionSetterMap, type SetterRef, type FnSetterResolution } from '@barefootjs/jsx'
21
23
 
22
24
  type IRAttribute = IRElement['attrs'][number]
23
25
  type AttrValue = IRAttribute['value']
24
26
  type TemplateAttr = Extract<AttrValue, { kind: 'template' }>
25
- import { TestNode } from './test-node'
27
+ import { TestNode, type EventHandler } from './test-node'
26
28
 
27
- export function irNodeToTestNode(node: IRNode, constantMap?: Map<string, string>): TestNode {
29
+ interface ConvertContext {
30
+ cmap: Map<string, string>
31
+ setterToSignal: Map<string, string>
32
+ fnSetters: Map<string, FnSetterResolution[]>
33
+ }
34
+
35
+ export function irNodeToTestNode(node: IRNode, constantMap?: Map<string, string>, metadata?: IRMetadata): TestNode {
28
36
  const cmap = constantMap ?? new Map<string, string>()
29
- return convert(node, cmap)
37
+ const setterToSignal = new Map<string, string>()
38
+ const fnSetters = new Map<string, FnSetterResolution[]>()
39
+ if (metadata) {
40
+ for (const s of metadata.signals) {
41
+ if (s.setter) setterToSignal.set(s.setter, s.getter)
42
+ }
43
+ for (const [k, v] of buildLocalFunctionSetterMap(metadata, setterToSignal)) {
44
+ fnSetters.set(k, v)
45
+ }
46
+ }
47
+ return convert(node, { cmap, setterToSignal, fnSetters })
30
48
  }
31
49
 
32
- function convert(node: IRNode, cmap: Map<string, string>): TestNode {
50
+ function convert(node: IRNode, ctx: ConvertContext): TestNode {
33
51
  switch (node.type) {
34
52
  case 'element':
35
- return convertElement(node, cmap)
53
+ return convertElement(node, ctx)
36
54
  case 'text':
37
55
  return convertText(node)
38
56
  case 'expression':
39
57
  return convertExpression(node)
40
58
  case 'conditional':
41
- return convertConditional(node, cmap)
59
+ return convertConditional(node, ctx)
42
60
  case 'loop':
43
- return convertLoop(node, cmap)
61
+ return convertLoop(node, ctx)
44
62
  case 'component':
45
- return convertComponent(node, cmap)
63
+ return convertComponent(node, ctx)
46
64
  case 'fragment':
47
- return convertFragment(node, cmap)
65
+ return convertFragment(node, ctx)
48
66
  case 'slot':
49
67
  return convertSlot(node)
50
68
  case 'if-statement':
51
- return convertIfStatement(node, cmap)
69
+ return convertIfStatement(node, ctx)
52
70
  case 'provider':
53
- return convertProvider(node, cmap)
71
+ return convertProvider(node, ctx)
54
72
  case 'async':
55
- return convertAsync(node, cmap)
73
+ return convertAsync(node, ctx)
56
74
  default: {
57
75
  const _exhaustive: never = node
58
76
  throw new Error(`Unhandled IR node type: ${(_exhaustive as IRNode).type}`)
@@ -64,7 +82,7 @@ function convert(node: IRNode, cmap: Map<string, string>): TestNode {
64
82
  // Element
65
83
  // ---------------------------------------------------------------------------
66
84
 
67
- function convertElement(node: IRElement, cmap: Map<string, string>): TestNode {
85
+ function convertElement(node: IRElement, ctx: ConvertContext): TestNode {
68
86
  const props: Record<string, string | boolean | null> = {}
69
87
  const aria: Record<string, string> = {}
70
88
  let role: string | null = null
@@ -77,8 +95,8 @@ function convertElement(node: IRElement, cmap: Map<string, string>): TestNode {
77
95
  if (attr.name === 'className' || attr.name === 'class') {
78
96
  if (typeof value === 'string') {
79
97
  const isDynamic = attr.value.kind === 'expression' || attr.value.kind === 'template' || attr.value.kind === 'spread'
80
- if (isDynamic && cmap.size > 0) {
81
- const resolved = resolveClassValue(value, cmap)
98
+ if (isDynamic && ctx.cmap.size > 0) {
99
+ const resolved = resolveClassValue(value, ctx.cmap)
82
100
  if (resolved !== null) {
83
101
  classes = resolved.split(/\s+/).filter(Boolean)
84
102
  continue
@@ -110,7 +128,12 @@ function convertElement(node: IRElement, cmap: Map<string, string>): TestNode {
110
128
  }
111
129
 
112
130
  const events = node.events.map(e => e.name)
113
- const children = node.children.map(c => convert(c, cmap))
131
+ const handlers: Record<string, EventHandler> = {}
132
+ for (const event of node.events) {
133
+ const refs = resolveSetters(event.handler, ctx.setterToSignal, ctx.fnSetters)
134
+ handlers[event.name] = refsToHandler(refs)
135
+ }
136
+ const children = node.children.map(c => convert(c, ctx))
114
137
 
115
138
  return new TestNode({
116
139
  tag: node.tag,
@@ -123,6 +146,7 @@ function convertElement(node: IRElement, cmap: Map<string, string>): TestNode {
123
146
  aria,
124
147
  dataState,
125
148
  events,
149
+ handlers,
126
150
  reactive: false,
127
151
  componentName: null,
128
152
  })
@@ -144,6 +168,7 @@ function convertText(node: IRText): TestNode {
144
168
  aria: {},
145
169
  dataState: null,
146
170
  events: [],
171
+ handlers: {},
147
172
  reactive: false,
148
173
  componentName: null,
149
174
  })
@@ -165,6 +190,7 @@ function convertExpression(node: IRExpression): TestNode {
165
190
  aria: {},
166
191
  dataState: null,
167
192
  events: [],
193
+ handlers: {},
168
194
  reactive: node.reactive,
169
195
  componentName: null,
170
196
  })
@@ -174,10 +200,10 @@ function convertExpression(node: IRExpression): TestNode {
174
200
  // Conditional
175
201
  // ---------------------------------------------------------------------------
176
202
 
177
- function convertConditional(node: IRConditional, cmap: Map<string, string>): TestNode {
178
- const children: TestNode[] = [convert(node.whenTrue, cmap)]
203
+ function convertConditional(node: IRConditional, ctx: ConvertContext): TestNode {
204
+ const children: TestNode[] = [convert(node.whenTrue, ctx)]
179
205
  if (node.whenFalse) {
180
- children.push(convert(node.whenFalse, cmap))
206
+ children.push(convert(node.whenFalse, ctx))
181
207
  }
182
208
 
183
209
  return new TestNode({
@@ -191,6 +217,7 @@ function convertConditional(node: IRConditional, cmap: Map<string, string>): Tes
191
217
  aria: {},
192
218
  dataState: null,
193
219
  events: [],
220
+ handlers: {},
194
221
  reactive: node.reactive,
195
222
  componentName: null,
196
223
  })
@@ -200,8 +227,8 @@ function convertConditional(node: IRConditional, cmap: Map<string, string>): Tes
200
227
  // Loop
201
228
  // ---------------------------------------------------------------------------
202
229
 
203
- function convertLoop(node: IRLoop, cmap: Map<string, string>): TestNode {
204
- const children = node.children.map(c => convert(c, cmap))
230
+ function convertLoop(node: IRLoop, ctx: ConvertContext): TestNode {
231
+ const children = node.children.map(c => convert(c, ctx))
205
232
 
206
233
  return new TestNode({
207
234
  tag: null,
@@ -214,6 +241,7 @@ function convertLoop(node: IRLoop, cmap: Map<string, string>): TestNode {
214
241
  aria: {},
215
242
  dataState: null,
216
243
  events: [],
244
+ handlers: {},
217
245
  reactive: false,
218
246
  componentName: null,
219
247
  })
@@ -223,8 +251,10 @@ function convertLoop(node: IRLoop, cmap: Map<string, string>): TestNode {
223
251
  // Component
224
252
  // ---------------------------------------------------------------------------
225
253
 
226
- function convertComponent(node: IRComponent, cmap: Map<string, string>): TestNode {
254
+ function convertComponent(node: IRComponent, ctx: ConvertContext): TestNode {
227
255
  const props: Record<string, string | boolean | null> = {}
256
+ const events: string[] = []
257
+ const handlers: Record<string, EventHandler> = {}
228
258
  for (const prop of node.props) {
229
259
  switch (prop.value.kind) {
230
260
  case 'literal':
@@ -245,9 +275,22 @@ function convertComponent(node: IRComponent, cmap: Map<string, string>): TestNod
245
275
  props[prop.name] = null
246
276
  break
247
277
  }
278
+
279
+ // Component callback props that look like event handlers
280
+ // (`<Button onClick={...}>`). The parent IR sees these as props, but for
281
+ // wiring they behave like events: when the callback fires, which setters
282
+ // run. Keyed by the DOM-style event name (`onClick` -> `click`) so the
283
+ // shorthand getters and `on()` work the same as for native elements.
284
+ if (/^on[A-Z]/.test(prop.name) && prop.value.kind === 'expression') {
285
+ const eventName = prop.name.charAt(2).toLowerCase() + prop.name.slice(3)
286
+ events.push(eventName)
287
+ handlers[eventName] = refsToHandler(
288
+ resolveSetters(prop.value.expr, ctx.setterToSignal, ctx.fnSetters),
289
+ )
290
+ }
248
291
  }
249
292
 
250
- const children = node.children.map(c => convert(c, cmap))
293
+ const children = node.children.map(c => convert(c, ctx))
251
294
 
252
295
  return new TestNode({
253
296
  tag: null,
@@ -259,7 +302,8 @@ function convertComponent(node: IRComponent, cmap: Map<string, string>): TestNod
259
302
  role: null,
260
303
  aria: {},
261
304
  dataState: null,
262
- events: [],
305
+ events,
306
+ handlers,
263
307
  reactive: false,
264
308
  componentName: node.name,
265
309
  })
@@ -269,8 +313,8 @@ function convertComponent(node: IRComponent, cmap: Map<string, string>): TestNod
269
313
  // Fragment
270
314
  // ---------------------------------------------------------------------------
271
315
 
272
- function convertFragment(node: IRFragment, cmap: Map<string, string>): TestNode {
273
- const children = node.children.map(c => convert(c, cmap))
316
+ function convertFragment(node: IRFragment, ctx: ConvertContext): TestNode {
317
+ const children = node.children.map(c => convert(c, ctx))
274
318
 
275
319
  return new TestNode({
276
320
  tag: null,
@@ -283,6 +327,7 @@ function convertFragment(node: IRFragment, cmap: Map<string, string>): TestNode
283
327
  aria: {},
284
328
  dataState: null,
285
329
  events: [],
330
+ handlers: {},
286
331
  reactive: false,
287
332
  componentName: null,
288
333
  })
@@ -304,6 +349,7 @@ function convertSlot(_node: IRSlot): TestNode {
304
349
  aria: {},
305
350
  dataState: null,
306
351
  events: [],
352
+ handlers: {},
307
353
  reactive: false,
308
354
  componentName: null,
309
355
  })
@@ -313,10 +359,10 @@ function convertSlot(_node: IRSlot): TestNode {
313
359
  // IfStatement
314
360
  // ---------------------------------------------------------------------------
315
361
 
316
- function convertIfStatement(node: IRIfStatement, cmap: Map<string, string>): TestNode {
317
- const children: TestNode[] = [convert(node.consequent, cmap)]
362
+ function convertIfStatement(node: IRIfStatement, ctx: ConvertContext): TestNode {
363
+ const children: TestNode[] = [convert(node.consequent, ctx)]
318
364
  if (node.alternate) {
319
- children.push(convert(node.alternate, cmap))
365
+ children.push(convert(node.alternate, ctx))
320
366
  }
321
367
 
322
368
  return new TestNode({
@@ -330,6 +376,7 @@ function convertIfStatement(node: IRIfStatement, cmap: Map<string, string>): Tes
330
376
  aria: {},
331
377
  dataState: null,
332
378
  events: [],
379
+ handlers: {},
333
380
  reactive: false,
334
381
  componentName: null,
335
382
  })
@@ -339,9 +386,9 @@ function convertIfStatement(node: IRIfStatement, cmap: Map<string, string>): Tes
339
386
  // Provider
340
387
  // ---------------------------------------------------------------------------
341
388
 
342
- function convertProvider(node: IRProvider, cmap: Map<string, string>): TestNode {
389
+ function convertProvider(node: IRProvider, ctx: ConvertContext): TestNode {
343
390
  // Transparent — just pass through children
344
- const children = node.children.map(c => convert(c, cmap))
391
+ const children = node.children.map(c => convert(c, ctx))
345
392
 
346
393
  if (children.length === 1) return children[0]
347
394
 
@@ -356,6 +403,7 @@ function convertProvider(node: IRProvider, cmap: Map<string, string>): TestNode
356
403
  aria: {},
357
404
  dataState: null,
358
405
  events: [],
406
+ handlers: {},
359
407
  reactive: false,
360
408
  componentName: null,
361
409
  })
@@ -365,9 +413,9 @@ function convertProvider(node: IRProvider, cmap: Map<string, string>): TestNode
365
413
  // Async
366
414
  // ---------------------------------------------------------------------------
367
415
 
368
- function convertAsync(node: IRAsync, cmap: Map<string, string>): TestNode {
416
+ function convertAsync(node: IRAsync, ctx: ConvertContext): TestNode {
369
417
  // Represent the resolved content as a fragment; tests assert on final state.
370
- const children = node.children.map(c => convert(c, cmap))
418
+ const children = node.children.map(c => convert(c, ctx))
371
419
 
372
420
  return new TestNode({
373
421
  tag: null,
@@ -380,6 +428,7 @@ function convertAsync(node: IRAsync, cmap: Map<string, string>): TestNode {
380
428
  aria: {},
381
429
  dataState: null,
382
430
  events: [],
431
+ handlers: {},
383
432
  reactive: false,
384
433
  componentName: null,
385
434
  })
@@ -389,6 +438,18 @@ function convertAsync(node: IRAsync, cmap: Map<string, string>): TestNode {
389
438
  // Constant-based class value resolution
390
439
  // ---------------------------------------------------------------------------
391
440
 
441
+ function refsToHandler(refs: SetterRef[]): EventHandler {
442
+ const setters: string[] = []
443
+ const via: string[] = []
444
+ for (const ref of refs) {
445
+ if (!setters.includes(ref.setter)) setters.push(ref.setter)
446
+ for (const v of ref.via ?? []) {
447
+ if (!via.includes(v)) via.push(v)
448
+ }
449
+ }
450
+ return { setters, via }
451
+ }
452
+
392
453
  function resolveClassValue(value: string, cmap: Map<string, string>): string | null {
393
454
  // Simple identifier lookup
394
455
  if (cmap.has(value)) {
package/src/render.ts CHANGED
@@ -52,7 +52,7 @@ export function renderToTest(source: string, filePath: string, componentName?: s
52
52
 
53
53
  const metadata = buildMetadata(ctx)
54
54
  const constantMap = resolveConstants(metadata.localConstants)
55
- const root = irNodeToTestNode(ir, constantMap)
55
+ const root = irNodeToTestNode(ir, constantMap, metadata)
56
56
  const signals = metadata.signals.map(s => s.getter)
57
57
  const memos = metadata.memos.map(m => m.name)
58
58
  const effects = metadata.effects.length
@@ -119,6 +119,7 @@ function emptyResult(
119
119
  aria: {},
120
120
  dataState: null,
121
121
  events: [],
122
+ handlers: {},
122
123
  reactive: false,
123
124
  componentName: null,
124
125
  })
package/src/test-node.ts CHANGED
@@ -4,6 +4,11 @@
4
4
  * Produced by converting the compiler IR into a flat, assertion-friendly shape.
5
5
  */
6
6
 
7
+ export interface EventHandler {
8
+ setters: string[]
9
+ via: string[]
10
+ }
11
+
7
12
  export interface TestNodeData {
8
13
  tag: string | null
9
14
  type: 'element' | 'text' | 'expression' | 'conditional' | 'loop' | 'component' | 'fragment'
@@ -15,6 +20,7 @@ export interface TestNodeData {
15
20
  aria: Record<string, string>
16
21
  dataState: string | null
17
22
  events: string[]
23
+ handlers: Partial<Record<string, EventHandler>>
18
24
  reactive: boolean
19
25
  componentName: string | null
20
26
  }
@@ -36,9 +42,19 @@ export class TestNode implements TestNodeData {
36
42
  aria: Record<string, string>
37
43
  dataState: string | null
38
44
  events: string[]
45
+ handlers: Partial<Record<string, EventHandler>>
39
46
  reactive: boolean
40
47
  componentName: string | null
41
48
 
49
+ get onClick(): EventHandler | undefined { return this.handlers.click }
50
+ get onInput(): EventHandler | undefined { return this.handlers.input }
51
+ get onChange(): EventHandler | undefined { return this.handlers.change }
52
+ get onSubmit(): EventHandler | undefined { return this.handlers.submit }
53
+
54
+ on(event: string): EventHandler | null {
55
+ return this.handlers[event] ?? null
56
+ }
57
+
42
58
  constructor(data: TestNodeData) {
43
59
  this.tag = data.tag
44
60
  this.type = data.type
@@ -50,6 +66,7 @@ export class TestNode implements TestNodeData {
50
66
  this.aria = data.aria
51
67
  this.dataState = data.dataState
52
68
  this.events = data.events
69
+ this.handlers = data.handlers
53
70
  this.reactive = data.reactive
54
71
  this.componentName = data.componentName
55
72
  }