@barefootjs/test 0.2.0 → 0.4.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/index.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  export { renderToTest } from './render';
5
5
  export type { TestResult } from './render';
6
6
  export { TestNode } from './test-node';
7
- export type { TestNodeData, TestNodeQuery } from './test-node';
7
+ export type { TestNodeData, TestNodeQuery, EventHandler } from './test-node';
8
8
  export { toStructure } from './structure';
9
9
  export { resolveConstants } from './resolve-constants';
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AACvC,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAE1C,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE9D,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAEzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AACvC,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAE1C,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE5E,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAEzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA"}
package/dist/index.js CHANGED
@@ -192314,7 +192314,7 @@ function transformExpressionInner(expr, ctx, node, isClientOnly) {
192314
192314
  }
192315
192315
  const ir = transformJsxExpression(expr, ctx, isClientOnly);
192316
192316
  if (ir !== null) {
192317
- if (isClientOnly && ir.type === "conditional") {
192317
+ if ((isClientOnly || shouldAutoDeferReactiveBrand(expr, ctx)) && ir.type === "conditional") {
192318
192318
  ir.clientOnly = true;
192319
192319
  if (!ir.slotId) {
192320
192320
  ir.slotId = generateSlotId(ctx);
@@ -193665,7 +193665,7 @@ function processAttributes(attributes, ctx) {
193665
193665
  value = { ...value, templateExpr: rewritten };
193666
193666
  }
193667
193667
  }
193668
- if (hasLeadingClientDirective(attr.initializer.expression, ctx.sourceFile)) {
193668
+ if (hasLeadingClientDirective(attr.initializer.expression, ctx.sourceFile) || shouldAutoDeferReactiveBrand(attr.initializer.expression, ctx)) {
193669
193669
  clientOnly = true;
193670
193670
  }
193671
193671
  }
@@ -194069,6 +194069,16 @@ function isReactiveExpression(expr, ctx, astNode) {
194069
194069
  }
194070
194070
  return false;
194071
194071
  }
194072
+ function shouldAutoDeferReactiveBrand(expr, ctx) {
194073
+ const checker = ctx.analyzer.checker;
194074
+ if (!checker)
194075
+ return false;
194076
+ if (!containsReactiveExpression(expr, checker))
194077
+ return false;
194078
+ if (isSignalOrMemoReference(ctx.getJS(expr), ctx))
194079
+ return false;
194080
+ return true;
194081
+ }
194072
194082
  function isSignalOrMemoReference(expr, ctx, visited) {
194073
194083
  for (const { pattern } of ctx.patterns.signals) {
194074
194084
  if (pattern.test(expr))
@@ -194638,6 +194648,97 @@ var CLIENT_PACKAGE_SOURCES = new Set([
194638
194648
  "@barefootjs/client",
194639
194649
  "@barefootjs/client/runtime"
194640
194650
  ]);
194651
+ // ../jsx/src/debug.ts
194652
+ function escapeForIdBoundary(name) {
194653
+ return name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
194654
+ }
194655
+ function makeIdCallRegex(name) {
194656
+ return new RegExp(`(?:^|[^\\w$])${escapeForIdBoundary(name)}\\s*\\(`);
194657
+ }
194658
+ function buildLocalFunctionSetterMap(meta, setterToSignal) {
194659
+ const setterPatterns = [...setterToSignal.keys()].map((s) => ({ name: s, re: makeIdCallRegex(s) }));
194660
+ const bodies = new Map;
194661
+ for (const fn of meta.localFunctions)
194662
+ bodies.set(fn.name, fn.body);
194663
+ for (const c of meta.localConstants) {
194664
+ if (c.containsArrow && c.value)
194665
+ bodies.set(c.name, c.value);
194666
+ }
194667
+ const fnNamePatterns = [...bodies.keys()].map((n) => ({ name: n, re: makeIdCallRegex(n) }));
194668
+ const directSetters = new Map;
194669
+ const directCalls = new Map;
194670
+ for (const [name, body] of bodies) {
194671
+ const setters = [];
194672
+ for (const { name: setter, re } of setterPatterns) {
194673
+ if (re.test(body))
194674
+ setters.push(setter);
194675
+ }
194676
+ directSetters.set(name, setters);
194677
+ const calls = [];
194678
+ for (const { name: callee, re } of fnNamePatterns) {
194679
+ if (callee !== name && re.test(body))
194680
+ calls.push(callee);
194681
+ }
194682
+ directCalls.set(name, calls);
194683
+ }
194684
+ const resolve2 = (name, stack) => {
194685
+ const out = [];
194686
+ const seen = new Set;
194687
+ for (const setter of directSetters.get(name) ?? []) {
194688
+ if (!seen.has(setter)) {
194689
+ out.push({ setter, chain: [] });
194690
+ seen.add(setter);
194691
+ }
194692
+ }
194693
+ for (const callee of directCalls.get(name) ?? []) {
194694
+ if (stack.has(callee))
194695
+ continue;
194696
+ const sub = resolve2(callee, new Set([...stack, callee]));
194697
+ for (const r of sub) {
194698
+ if (!seen.has(r.setter)) {
194699
+ out.push({ setter: r.setter, chain: [callee, ...r.chain] });
194700
+ seen.add(r.setter);
194701
+ }
194702
+ }
194703
+ }
194704
+ return out;
194705
+ };
194706
+ const result = new Map;
194707
+ for (const name of bodies.keys()) {
194708
+ const resolved = resolve2(name, new Set([name]));
194709
+ if (resolved.length > 0)
194710
+ result.set(name, resolved);
194711
+ }
194712
+ return result;
194713
+ }
194714
+ function resolveSetters(handler, setterToSignal, fnSetters) {
194715
+ const refs = [];
194716
+ const seen = new Set;
194717
+ const trimmed = handler.trim();
194718
+ for (const [setter, signal] of setterToSignal) {
194719
+ if (trimmed === setter || makeIdCallRegex(setter).test(handler)) {
194720
+ if (!seen.has(setter)) {
194721
+ refs.push({ setter, signal });
194722
+ seen.add(setter);
194723
+ }
194724
+ }
194725
+ }
194726
+ for (const [fnName, resolutions] of fnSetters) {
194727
+ if (trimmed === fnName || makeIdCallRegex(fnName).test(handler)) {
194728
+ for (const r of resolutions) {
194729
+ if (!seen.has(r.setter)) {
194730
+ refs.push({
194731
+ setter: r.setter,
194732
+ signal: setterToSignal.get(r.setter) ?? null,
194733
+ via: [fnName, ...r.chain]
194734
+ });
194735
+ seen.add(r.setter);
194736
+ }
194737
+ }
194738
+ }
194739
+ }
194740
+ return refs;
194741
+ }
194641
194742
  // src/test-node.ts
194642
194743
  class TestNode {
194643
194744
  tag;
@@ -194650,8 +194751,24 @@ class TestNode {
194650
194751
  aria;
194651
194752
  dataState;
194652
194753
  events;
194754
+ handlers;
194653
194755
  reactive;
194654
194756
  componentName;
194757
+ get onClick() {
194758
+ return this.handlers.click;
194759
+ }
194760
+ get onInput() {
194761
+ return this.handlers.input;
194762
+ }
194763
+ get onChange() {
194764
+ return this.handlers.change;
194765
+ }
194766
+ get onSubmit() {
194767
+ return this.handlers.submit;
194768
+ }
194769
+ on(event) {
194770
+ return this.handlers[event];
194771
+ }
194655
194772
  constructor(data) {
194656
194773
  this.tag = data.tag;
194657
194774
  this.type = data.type;
@@ -194663,6 +194780,7 @@ class TestNode {
194663
194780
  this.aria = data.aria;
194664
194781
  this.dataState = data.dataState;
194665
194782
  this.events = data.events;
194783
+ this.handlers = data.handlers;
194666
194784
  this.reactive = data.reactive;
194667
194785
  this.componentName = data.componentName;
194668
194786
  }
@@ -194710,41 +194828,52 @@ class TestNode {
194710
194828
  }
194711
194829
 
194712
194830
  // src/ir-to-test-node.ts
194713
- function irNodeToTestNode(node, constantMap) {
194831
+ function irNodeToTestNode(node, constantMap, metadata) {
194714
194832
  const cmap = constantMap ?? new Map;
194715
- return convert(node, cmap);
194833
+ const setterToSignal = new Map;
194834
+ const fnSetters = new Map;
194835
+ if (metadata) {
194836
+ for (const s of metadata.signals) {
194837
+ if (s.setter)
194838
+ setterToSignal.set(s.setter, s.getter);
194839
+ }
194840
+ for (const [k, v] of buildLocalFunctionSetterMap(metadata, setterToSignal)) {
194841
+ fnSetters.set(k, v);
194842
+ }
194843
+ }
194844
+ return convert(node, { cmap, setterToSignal, fnSetters });
194716
194845
  }
194717
- function convert(node, cmap) {
194846
+ function convert(node, ctx) {
194718
194847
  switch (node.type) {
194719
194848
  case "element":
194720
- return convertElement(node, cmap);
194849
+ return convertElement(node, ctx);
194721
194850
  case "text":
194722
194851
  return convertText(node);
194723
194852
  case "expression":
194724
194853
  return convertExpression(node);
194725
194854
  case "conditional":
194726
- return convertConditional(node, cmap);
194855
+ return convertConditional(node, ctx);
194727
194856
  case "loop":
194728
- return convertLoop(node, cmap);
194857
+ return convertLoop(node, ctx);
194729
194858
  case "component":
194730
- return convertComponent(node, cmap);
194859
+ return convertComponent(node, ctx);
194731
194860
  case "fragment":
194732
- return convertFragment(node, cmap);
194861
+ return convertFragment(node, ctx);
194733
194862
  case "slot":
194734
194863
  return convertSlot(node);
194735
194864
  case "if-statement":
194736
- return convertIfStatement(node, cmap);
194865
+ return convertIfStatement(node, ctx);
194737
194866
  case "provider":
194738
- return convertProvider(node, cmap);
194867
+ return convertProvider(node, ctx);
194739
194868
  case "async":
194740
- return convertAsync(node, cmap);
194869
+ return convertAsync(node, ctx);
194741
194870
  default: {
194742
194871
  const _exhaustive = node;
194743
194872
  throw new Error(`Unhandled IR node type: ${_exhaustive.type}`);
194744
194873
  }
194745
194874
  }
194746
194875
  }
194747
- function convertElement(node, cmap) {
194876
+ function convertElement(node, ctx) {
194748
194877
  const props = {};
194749
194878
  const aria = {};
194750
194879
  let role = null;
@@ -194755,8 +194884,8 @@ function convertElement(node, cmap) {
194755
194884
  if (attr.name === "className" || attr.name === "class") {
194756
194885
  if (typeof value === "string") {
194757
194886
  const isDynamic = attr.value.kind === "expression" || attr.value.kind === "template" || attr.value.kind === "spread";
194758
- if (isDynamic && cmap.size > 0) {
194759
- const resolved = resolveClassValue(value, cmap);
194887
+ if (isDynamic && ctx.cmap.size > 0) {
194888
+ const resolved = resolveClassValue(value, ctx.cmap);
194760
194889
  if (resolved !== null) {
194761
194890
  classes = resolved.split(/\s+/).filter(Boolean);
194762
194891
  continue;
@@ -194787,7 +194916,12 @@ function convertElement(node, cmap) {
194787
194916
  props[attr.name] = value;
194788
194917
  }
194789
194918
  const events = node.events.map((e) => e.name);
194790
- const children = node.children.map((c) => convert(c, cmap));
194919
+ const handlers = {};
194920
+ for (const event of node.events) {
194921
+ const refs = resolveSetters(event.handler, ctx.setterToSignal, ctx.fnSetters);
194922
+ handlers[event.name] = refsToHandler(refs);
194923
+ }
194924
+ const children = node.children.map((c) => convert(c, ctx));
194791
194925
  return new TestNode({
194792
194926
  tag: node.tag,
194793
194927
  type: "element",
@@ -194799,6 +194933,7 @@ function convertElement(node, cmap) {
194799
194933
  aria,
194800
194934
  dataState,
194801
194935
  events,
194936
+ handlers,
194802
194937
  reactive: false,
194803
194938
  componentName: null
194804
194939
  });
@@ -194815,6 +194950,7 @@ function convertText(node) {
194815
194950
  aria: {},
194816
194951
  dataState: null,
194817
194952
  events: [],
194953
+ handlers: {},
194818
194954
  reactive: false,
194819
194955
  componentName: null
194820
194956
  });
@@ -194831,14 +194967,15 @@ function convertExpression(node) {
194831
194967
  aria: {},
194832
194968
  dataState: null,
194833
194969
  events: [],
194970
+ handlers: {},
194834
194971
  reactive: node.reactive,
194835
194972
  componentName: null
194836
194973
  });
194837
194974
  }
194838
- function convertConditional(node, cmap) {
194839
- const children = [convert(node.whenTrue, cmap)];
194975
+ function convertConditional(node, ctx) {
194976
+ const children = [convert(node.whenTrue, ctx)];
194840
194977
  if (node.whenFalse) {
194841
- children.push(convert(node.whenFalse, cmap));
194978
+ children.push(convert(node.whenFalse, ctx));
194842
194979
  }
194843
194980
  return new TestNode({
194844
194981
  tag: null,
@@ -194851,12 +194988,13 @@ function convertConditional(node, cmap) {
194851
194988
  aria: {},
194852
194989
  dataState: null,
194853
194990
  events: [],
194991
+ handlers: {},
194854
194992
  reactive: node.reactive,
194855
194993
  componentName: null
194856
194994
  });
194857
194995
  }
194858
- function convertLoop(node, cmap) {
194859
- const children = node.children.map((c) => convert(c, cmap));
194996
+ function convertLoop(node, ctx) {
194997
+ const children = node.children.map((c) => convert(c, ctx));
194860
194998
  return new TestNode({
194861
194999
  tag: null,
194862
195000
  type: "loop",
@@ -194868,12 +195006,15 @@ function convertLoop(node, cmap) {
194868
195006
  aria: {},
194869
195007
  dataState: null,
194870
195008
  events: [],
195009
+ handlers: {},
194871
195010
  reactive: false,
194872
195011
  componentName: null
194873
195012
  });
194874
195013
  }
194875
- function convertComponent(node, cmap) {
195014
+ function convertComponent(node, ctx) {
194876
195015
  const props = {};
195016
+ const events = [];
195017
+ const handlers = {};
194877
195018
  for (const prop of node.props) {
194878
195019
  switch (prop.value.kind) {
194879
195020
  case "literal":
@@ -194894,8 +195035,13 @@ function convertComponent(node, cmap) {
194894
195035
  props[prop.name] = null;
194895
195036
  break;
194896
195037
  }
195038
+ if (/^on[A-Z]/.test(prop.name) && prop.value.kind === "expression") {
195039
+ const eventName = prop.name.charAt(2).toLowerCase() + prop.name.slice(3);
195040
+ events.push(eventName);
195041
+ handlers[eventName] = refsToHandler(resolveSetters(prop.value.expr, ctx.setterToSignal, ctx.fnSetters));
195042
+ }
194897
195043
  }
194898
- const children = node.children.map((c) => convert(c, cmap));
195044
+ const children = node.children.map((c) => convert(c, ctx));
194899
195045
  return new TestNode({
194900
195046
  tag: null,
194901
195047
  type: "component",
@@ -194906,13 +195052,14 @@ function convertComponent(node, cmap) {
194906
195052
  role: null,
194907
195053
  aria: {},
194908
195054
  dataState: null,
194909
- events: [],
195055
+ events,
195056
+ handlers,
194910
195057
  reactive: false,
194911
195058
  componentName: node.name
194912
195059
  });
194913
195060
  }
194914
- function convertFragment(node, cmap) {
194915
- const children = node.children.map((c) => convert(c, cmap));
195061
+ function convertFragment(node, ctx) {
195062
+ const children = node.children.map((c) => convert(c, ctx));
194916
195063
  return new TestNode({
194917
195064
  tag: null,
194918
195065
  type: "fragment",
@@ -194924,6 +195071,7 @@ function convertFragment(node, cmap) {
194924
195071
  aria: {},
194925
195072
  dataState: null,
194926
195073
  events: [],
195074
+ handlers: {},
194927
195075
  reactive: false,
194928
195076
  componentName: null
194929
195077
  });
@@ -194940,14 +195088,15 @@ function convertSlot(_node) {
194940
195088
  aria: {},
194941
195089
  dataState: null,
194942
195090
  events: [],
195091
+ handlers: {},
194943
195092
  reactive: false,
194944
195093
  componentName: null
194945
195094
  });
194946
195095
  }
194947
- function convertIfStatement(node, cmap) {
194948
- const children = [convert(node.consequent, cmap)];
195096
+ function convertIfStatement(node, ctx) {
195097
+ const children = [convert(node.consequent, ctx)];
194949
195098
  if (node.alternate) {
194950
- children.push(convert(node.alternate, cmap));
195099
+ children.push(convert(node.alternate, ctx));
194951
195100
  }
194952
195101
  return new TestNode({
194953
195102
  tag: null,
@@ -194960,12 +195109,13 @@ function convertIfStatement(node, cmap) {
194960
195109
  aria: {},
194961
195110
  dataState: null,
194962
195111
  events: [],
195112
+ handlers: {},
194963
195113
  reactive: false,
194964
195114
  componentName: null
194965
195115
  });
194966
195116
  }
194967
- function convertProvider(node, cmap) {
194968
- const children = node.children.map((c) => convert(c, cmap));
195117
+ function convertProvider(node, ctx) {
195118
+ const children = node.children.map((c) => convert(c, ctx));
194969
195119
  if (children.length === 1)
194970
195120
  return children[0];
194971
195121
  return new TestNode({
@@ -194979,12 +195129,13 @@ function convertProvider(node, cmap) {
194979
195129
  aria: {},
194980
195130
  dataState: null,
194981
195131
  events: [],
195132
+ handlers: {},
194982
195133
  reactive: false,
194983
195134
  componentName: null
194984
195135
  });
194985
195136
  }
194986
- function convertAsync(node, cmap) {
194987
- const children = node.children.map((c) => convert(c, cmap));
195137
+ function convertAsync(node, ctx) {
195138
+ const children = node.children.map((c) => convert(c, ctx));
194988
195139
  return new TestNode({
194989
195140
  tag: null,
194990
195141
  type: "fragment",
@@ -194996,10 +195147,24 @@ function convertAsync(node, cmap) {
194996
195147
  aria: {},
194997
195148
  dataState: null,
194998
195149
  events: [],
195150
+ handlers: {},
194999
195151
  reactive: false,
195000
195152
  componentName: null
195001
195153
  });
195002
195154
  }
195155
+ function refsToHandler(refs) {
195156
+ const setters = [];
195157
+ const via = [];
195158
+ for (const ref of refs) {
195159
+ if (!setters.includes(ref.setter))
195160
+ setters.push(ref.setter);
195161
+ for (const v of ref.via ?? []) {
195162
+ if (!via.includes(v))
195163
+ via.push(v);
195164
+ }
195165
+ }
195166
+ return { setters, via };
195167
+ }
195003
195168
  function resolveClassValue(value, cmap) {
195004
195169
  if (cmap.has(value)) {
195005
195170
  return cmap.get(value);
@@ -195175,7 +195340,7 @@ function renderToTest(source, filePath, componentName) {
195175
195340
  }
195176
195341
  const metadata = buildMetadata2(ctx);
195177
195342
  const constantMap = resolveConstants(metadata.localConstants);
195178
- const root = irNodeToTestNode(ir, constantMap);
195343
+ const root = irNodeToTestNode(ir, constantMap, metadata);
195179
195344
  const signals = metadata.signals.map((s) => s.getter);
195180
195345
  const memos = metadata.memos.map((m) => m.name);
195181
195346
  const effects = metadata.effects.length;
@@ -195232,6 +195397,7 @@ function emptyResult(componentName, isClient, errors) {
195232
195397
  aria: {},
195233
195398
  dataState: null,
195234
195399
  events: [],
195400
+ handlers: {},
195235
195401
  reactive: false,
195236
195402
  componentName: null
195237
195403
  });
@@ -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,20 @@
3
3
  *
4
4
  * Produced by converting the compiler IR into a flat, assertion-friendly shape.
5
5
  */
6
+ /**
7
+ * Statically-resolved wiring for a single event handler.
8
+ *
9
+ * `setters`/`via` are populated only for handlers that reach raw signal setters
10
+ * declared in the component — directly (`onClick={() => setX(1)}`) or through
11
+ * local helper functions (`via`). Handlers backed by a library's
12
+ * property-access method — e.g. `@barefootjs/form`'s `name.handleInput` or
13
+ * `form.handleSubmit` — register the event but report empty `setters`/`via`,
14
+ * because the setter calls live inside the library, not the component body.
15
+ */
16
+ export interface EventHandler {
17
+ setters: string[];
18
+ via: string[];
19
+ }
6
20
  export interface TestNodeData {
7
21
  tag: string | null;
8
22
  type: 'element' | 'text' | 'expression' | 'conditional' | 'loop' | 'component' | 'fragment';
@@ -14,6 +28,7 @@ export interface TestNodeData {
14
28
  aria: Record<string, string>;
15
29
  dataState: string | null;
16
30
  events: string[];
31
+ handlers: Partial<Record<string, EventHandler>>;
17
32
  reactive: boolean;
18
33
  componentName: string | null;
19
34
  }
@@ -33,8 +48,15 @@ export declare class TestNode implements TestNodeData {
33
48
  aria: Record<string, string>;
34
49
  dataState: string | null;
35
50
  events: string[];
51
+ handlers: Partial<Record<string, EventHandler>>;
36
52
  reactive: boolean;
37
53
  componentName: string | null;
54
+ get onClick(): EventHandler | undefined;
55
+ get onInput(): EventHandler | undefined;
56
+ get onChange(): EventHandler | undefined;
57
+ get onSubmit(): EventHandler | undefined;
58
+ /** Returns the handler wired to `event`, or `undefined` if none — matching the shorthand getters. */
59
+ on(event: string): EventHandler | undefined;
38
60
  constructor(data: TestNodeData);
39
61
  /** Return the first descendant (or self) matching the query. */
40
62
  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;;;;;;;;;GASG;AACH,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,qGAAqG;IACrG,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAE1C;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.2.0",
3
+ "version": "0.4.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.2.0"
42
+ "@barefootjs/jsx": "0.4.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,21 @@
4
4
  * Produced by converting the compiler IR into a flat, assertion-friendly shape.
5
5
  */
6
6
 
7
+ /**
8
+ * Statically-resolved wiring for a single event handler.
9
+ *
10
+ * `setters`/`via` are populated only for handlers that reach raw signal setters
11
+ * declared in the component — directly (`onClick={() => setX(1)}`) or through
12
+ * local helper functions (`via`). Handlers backed by a library's
13
+ * property-access method — e.g. `@barefootjs/form`'s `name.handleInput` or
14
+ * `form.handleSubmit` — register the event but report empty `setters`/`via`,
15
+ * because the setter calls live inside the library, not the component body.
16
+ */
17
+ export interface EventHandler {
18
+ setters: string[]
19
+ via: string[]
20
+ }
21
+
7
22
  export interface TestNodeData {
8
23
  tag: string | null
9
24
  type: 'element' | 'text' | 'expression' | 'conditional' | 'loop' | 'component' | 'fragment'
@@ -15,6 +30,7 @@ export interface TestNodeData {
15
30
  aria: Record<string, string>
16
31
  dataState: string | null
17
32
  events: string[]
33
+ handlers: Partial<Record<string, EventHandler>>
18
34
  reactive: boolean
19
35
  componentName: string | null
20
36
  }
@@ -36,9 +52,20 @@ export class TestNode implements TestNodeData {
36
52
  aria: Record<string, string>
37
53
  dataState: string | null
38
54
  events: string[]
55
+ handlers: Partial<Record<string, EventHandler>>
39
56
  reactive: boolean
40
57
  componentName: string | null
41
58
 
59
+ get onClick(): EventHandler | undefined { return this.handlers.click }
60
+ get onInput(): EventHandler | undefined { return this.handlers.input }
61
+ get onChange(): EventHandler | undefined { return this.handlers.change }
62
+ get onSubmit(): EventHandler | undefined { return this.handlers.submit }
63
+
64
+ /** Returns the handler wired to `event`, or `undefined` if none — matching the shorthand getters. */
65
+ on(event: string): EventHandler | undefined {
66
+ return this.handlers[event]
67
+ }
68
+
42
69
  constructor(data: TestNodeData) {
43
70
  this.tag = data.tag
44
71
  this.type = data.type
@@ -50,6 +77,7 @@ export class TestNode implements TestNodeData {
50
77
  this.aria = data.aria
51
78
  this.dataState = data.dataState
52
79
  this.events = data.events
80
+ this.handlers = data.handlers
53
81
  this.reactive = data.reactive
54
82
  this.componentName = data.componentName
55
83
  }