@barefootjs/test 0.17.0 → 0.18.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.js CHANGED
@@ -187348,6 +187348,7 @@ function tsNodeToParsedExpr(node) {
187348
187348
  }
187349
187349
  var CALLBACK_METHODS = new Set([
187350
187350
  "filter",
187351
+ "map",
187351
187352
  "every",
187352
187353
  "some",
187353
187354
  "find",
@@ -187436,6 +187437,7 @@ function convertNode(node, raw) {
187436
187437
  if (callee.property === "flat") {
187437
187438
  const depthNode = node.arguments[0];
187438
187439
  let flatDepth;
187440
+ let depthExpr;
187439
187441
  if (depthNode === undefined) {
187440
187442
  flatDepth = 1;
187441
187443
  } else if (import_typescript.default.isIdentifier(depthNode) && depthNode.text === "Infinity") {
@@ -187448,16 +187450,23 @@ function convertNode(node, raw) {
187448
187450
  n = -Number(depthNode.operand.text);
187449
187451
  }
187450
187452
  if (n === undefined || Number.isNaN(n)) {
187451
- return {
187452
- kind: "unsupported",
187453
- raw,
187454
- reason: `\`.flat(depth)\` needs a literal integer or \`Infinity\` depth — a computed depth can't be resolved at template time. Use a literal depth, or pre-compute the value before the template.`
187455
- };
187453
+ const parsedDepth = convertNode(depthNode, raw);
187454
+ if (checkSupport(parsedDepth).supported) {
187455
+ depthExpr = parsedDepth;
187456
+ flatDepth = 1;
187457
+ } else {
187458
+ return {
187459
+ kind: "unsupported",
187460
+ raw,
187461
+ reason: `\`.flat(depth)\` needs a literal integer, \`Infinity\`, or a supported dynamic depth expression — this depth can't be resolved. Use a literal depth, a supported expression (prop/signal/arithmetic), or pre-compute the value before the template.`
187462
+ };
187463
+ }
187464
+ } else {
187465
+ const truncated = Math.trunc(n);
187466
+ flatDepth = truncated < 0 ? 0 : truncated;
187456
187467
  }
187457
- const truncated = Math.trunc(n);
187458
- flatDepth = truncated < 0 ? 0 : truncated;
187459
187468
  }
187460
- return { kind: "array-method", method: "flat", object: callee.object, args: [], flatDepth };
187469
+ return depthExpr !== undefined ? { kind: "array-method", method: "flat", object: callee.object, args: [], flatDepth, depthExpr } : { kind: "array-method", method: "flat", object: callee.object, args: [], flatDepth };
187461
187470
  }
187462
187471
  if (callee.property === "toLowerCase") {
187463
187472
  return { kind: "array-method", method: "toLowerCase", object: callee.object, args };
@@ -188002,6 +188011,8 @@ function validateRestUsage(expr, restName, excludedTopKeys) {
188002
188011
  walk(e.object);
188003
188012
  for (const a of e.args)
188004
188013
  walk(a);
188014
+ if (e.method === "flat" && e.depthExpr)
188015
+ walk(e.depthExpr);
188005
188016
  return;
188006
188017
  case "literal":
188007
188018
  case "unsupported":
@@ -188087,6 +188098,8 @@ function collectIdentifiers(expr, out) {
188087
188098
  case "array-method":
188088
188099
  collectIdentifiers(expr.object, out);
188089
188100
  expr.args.forEach((e) => collectIdentifiers(e, out));
188101
+ if (expr.method === "flat" && expr.depthExpr)
188102
+ collectIdentifiers(expr.depthExpr, out);
188090
188103
  return;
188091
188104
  case "literal":
188092
188105
  case "regex":
@@ -188144,7 +188157,14 @@ function substituteDestructuredFields(expr, fieldMap, syntheticParam, restName)
188144
188157
  return { kind: "array-literal", elements: e.elements.map(walk) };
188145
188158
  case "array-method":
188146
188159
  if (e.method === "flat") {
188147
- return { kind: "array-method", method: "flat", object: walk(e.object), args: [], flatDepth: e.flatDepth };
188160
+ return {
188161
+ kind: "array-method",
188162
+ method: "flat",
188163
+ object: walk(e.object),
188164
+ args: [],
188165
+ flatDepth: e.flatDepth,
188166
+ ...e.depthExpr ? { depthExpr: walk(e.depthExpr) } : {}
188167
+ };
188148
188168
  }
188149
188169
  return { kind: "array-method", method: e.method, object: walk(e.object), args: e.args.map(walk) };
188150
188170
  case "literal":
@@ -188241,6 +188261,11 @@ function checkSupport(expr) {
188241
188261
  if (!argSupport.supported)
188242
188262
  return argSupport;
188243
188263
  }
188264
+ if (expr.method === "flat" && expr.depthExpr) {
188265
+ const depthSupport = checkSupport(expr.depthExpr);
188266
+ if (!depthSupport.supported)
188267
+ return depthSupport;
188268
+ }
188244
188269
  return { supported: true, level: "L2" };
188245
188270
  }
188246
188271
  case "call": {
@@ -188340,6 +188365,9 @@ function checkSupport(expr) {
188340
188365
  const leftSupport = checkSupport(expr.left);
188341
188366
  if (!leftSupport.supported)
188342
188367
  return leftSupport;
188368
+ if (expr.op === "??" && expr.right.kind === "object-literal" && expr.right.properties.length === 0) {
188369
+ return { supported: true, level: "L4" };
188370
+ }
188343
188371
  const rightSupport = checkSupport(expr.right);
188344
188372
  if (!rightSupport.supported)
188345
188373
  return rightSupport;
@@ -188534,7 +188562,10 @@ function usesPerPath(name, expr) {
188534
188562
  case "array-literal":
188535
188563
  return sum(e.elements);
188536
188564
  case "array-method":
188537
- return add(walk(e.object), e.method === "flat" ? { min: 0, max: 0 } : sum(e.args));
188565
+ if (e.method === "flat") {
188566
+ return add(walk(e.object), e.depthExpr ? walk(e.depthExpr) : { min: 0, max: 0 });
188567
+ }
188568
+ return add(walk(e.object), sum(e.args));
188538
188569
  case "object-literal":
188539
188570
  return sum(e.properties.map((p) => p.value));
188540
188571
  case "arrow":
@@ -188589,7 +188620,14 @@ function inlineBinding(expr, name, value) {
188589
188620
  return { kind: "array-literal", elements: e.elements.map((el) => walk(el, enclosing)) };
188590
188621
  case "array-method":
188591
188622
  if (e.method === "flat") {
188592
- return { kind: "array-method", method: "flat", object: walk(e.object, enclosing), args: [], flatDepth: e.flatDepth };
188623
+ return {
188624
+ kind: "array-method",
188625
+ method: "flat",
188626
+ object: walk(e.object, enclosing),
188627
+ args: [],
188628
+ flatDepth: e.flatDepth,
188629
+ ...e.depthExpr ? { depthExpr: walk(e.depthExpr, enclosing) } : {}
188630
+ };
188593
188631
  }
188594
188632
  return { kind: "array-method", method: e.method, object: walk(e.object, enclosing), args: e.args.map((a) => walk(a, enclosing)) };
188595
188633
  case "object-literal":
@@ -188725,6 +188763,9 @@ function stringifyParsedExpr(expr) {
188725
188763
  return `[${expr.elements.map(stringifyParsedExpr).join(", ")}]`;
188726
188764
  case "array-method":
188727
188765
  if (expr.method === "flat") {
188766
+ if (expr.depthExpr) {
188767
+ return `${stringifyParsedExpr(expr.object)}.flat(${stringifyParsedExpr(expr.depthExpr)})`;
188768
+ }
188728
188769
  const d = expr.flatDepth;
188729
188770
  const depthSrc = d === "infinity" ? "Infinity" : String(d);
188730
188771
  return `${stringifyParsedExpr(expr.object)}.flat(${d === 1 ? "" : depthSrc})`;
@@ -189329,6 +189370,15 @@ function createAnalyzerContext(sourceFile, filePath) {
189329
189370
  checker: null,
189330
189371
  componentBodyBlock: null,
189331
189372
  getJS(node) {
189373
+ let ownSourceFile;
189374
+ try {
189375
+ ownSourceFile = node.getSourceFile();
189376
+ } catch {
189377
+ ownSourceFile = undefined;
189378
+ }
189379
+ if (ownSourceFile && ownSourceFile !== sourceFile) {
189380
+ return node.getText(ownSourceFile);
189381
+ }
189332
189382
  return reconstructWithoutTypes(node, sourceFile, this.typeExcludeRanges);
189333
189383
  }
189334
189384
  };
@@ -191305,6 +191355,7 @@ function extractProps(param, ctx) {
191305
191355
  loc: getSourceLocation(param, ctx.sourceFile, ctx.filePath),
191306
191356
  hasIgnoreDirective: ignored
191307
191357
  };
191358
+ const memberTypes = param.type ? collectMemberTypes(param.type, ctx) : null;
191308
191359
  for (const element of param.name.elements) {
191309
191360
  if (import_typescript8.default.isBindingElement(element) && import_typescript8.default.isIdentifier(element.name)) {
191310
191361
  const localName = element.name.text;
@@ -191313,10 +191364,12 @@ function extractProps(param, ctx) {
191313
191364
  ctx.restPropsName = localName;
191314
191365
  continue;
191315
191366
  }
191367
+ const sourcePropName = element.propertyName && import_typescript8.default.isIdentifier(element.propertyName) ? element.propertyName.text : localName;
191368
+ const resolvedType = memberTypes?.get(sourcePropName) ?? { kind: "unknown", raw: "unknown" };
191316
191369
  const defaultContainsArrow = element.initializer ? nodeContainsArrow(element.initializer) : false;
191317
191370
  ctx.propsParams.push({
191318
191371
  name: localName,
191319
- type: { kind: "unknown", raw: "unknown" },
191372
+ type: resolvedType,
191320
191373
  optional: !!element.initializer,
191321
191374
  defaultValue,
191322
191375
  defaultContainsArrow: defaultContainsArrow || undefined
@@ -191377,6 +191430,37 @@ function collectKeysFromMembers(members, ctx) {
191377
191430
  }
191378
191431
  return keys;
191379
191432
  }
191433
+ function collectMemberTypes(typeNode, ctx) {
191434
+ const isResolvablePrimitive = (info) => info.kind === "primitive" && (info.primitive === "string" || info.primitive === "number" || info.primitive === "boolean");
191435
+ const fromMembers = (members) => {
191436
+ const map = new Map;
191437
+ for (const member of members) {
191438
+ if (import_typescript8.default.isPropertySignature(member) && member.name && member.type && !member.questionToken) {
191439
+ const info = typeNodeToTypeInfo(member.type, ctx.sourceFile);
191440
+ if (info && isResolvablePrimitive(info)) {
191441
+ map.set(member.name.getText(ctx.sourceFile), info);
191442
+ }
191443
+ }
191444
+ }
191445
+ return map;
191446
+ };
191447
+ if (import_typescript8.default.isTypeLiteralNode(typeNode)) {
191448
+ return fromMembers(typeNode.members);
191449
+ }
191450
+ if (import_typescript8.default.isTypeReferenceNode(typeNode)) {
191451
+ const typeName = typeNode.typeName.getText(ctx.sourceFile);
191452
+ const typeDecl = findTypeDeclaration(typeName, ctx.sourceFile);
191453
+ if (!typeDecl)
191454
+ return null;
191455
+ if (import_typescript8.default.isInterfaceDeclaration(typeDecl)) {
191456
+ return fromMembers(typeDecl.members);
191457
+ }
191458
+ if (import_typescript8.default.isTypeAliasDeclaration(typeDecl) && import_typescript8.default.isTypeLiteralNode(typeDecl.type)) {
191459
+ return fromMembers(typeDecl.type.members);
191460
+ }
191461
+ }
191462
+ return null;
191463
+ }
191380
191464
  function extractPropsFromType(typeNode, ctx) {
191381
191465
  if (import_typescript8.default.isTypeLiteralNode(typeNode)) {
191382
191466
  extractPropsFromTypeMembers(typeNode.members, ctx);
@@ -192033,6 +192117,38 @@ var AttrValueOf = {
192033
192117
  }
192034
192118
  };
192035
192119
 
192120
+ // ../jsx/src/module-exports.ts
192121
+ function formatParamWithType(p) {
192122
+ const rest = p.isRest ? "..." : "";
192123
+ const optional = p.optional ? "?" : "";
192124
+ const typeAnnotation = p.type?.raw && p.type.raw !== "unknown" ? `: ${p.type.raw}` : "";
192125
+ const defaultPart = p.defaultValue !== undefined ? ` = ${p.defaultValue}` : "";
192126
+ return `${rest}${p.name}${optional}${typeAnnotation}${defaultPart}`;
192127
+ }
192128
+ function findReachableNames(primaryRefs, declarations) {
192129
+ const allNames = new Set(declarations.map((d) => d.name));
192130
+ const bodyMap = new Map(declarations.map((d) => [d.name, d.body]));
192131
+ const reachable = new Set;
192132
+ const queue = [];
192133
+ for (const name of allNames) {
192134
+ if (new RegExp(`\\b${name}\\b`).test(primaryRefs)) {
192135
+ reachable.add(name);
192136
+ queue.push(name);
192137
+ }
192138
+ }
192139
+ while (queue.length > 0) {
192140
+ const current = queue.shift();
192141
+ const body = bodyMap.get(current) || "";
192142
+ for (const name of allNames) {
192143
+ if (!reachable.has(name) && new RegExp(`\\b${name}\\b`).test(body)) {
192144
+ reachable.add(name);
192145
+ queue.push(name);
192146
+ }
192147
+ }
192148
+ }
192149
+ return reachable;
192150
+ }
192151
+
192036
192152
  // ../jsx/src/builtins.ts
192037
192153
  var CLIENT_BUILTIN_SOURCE = "@barefootjs/client";
192038
192154
  function isClientBuiltinName(name) {
@@ -193140,6 +193256,7 @@ function transformExpression(node, ctx) {
193140
193256
  return transformExpressionInner(expr, ctx, node, isClientOnly);
193141
193257
  }
193142
193258
  function transformExpressionInner(expr, ctx, node, isClientOnly) {
193259
+ expr = tryDesugarInterleaveTaggedTemplate(expr, ctx);
193143
193260
  checkBareSignalOrMemoIdentifier(expr, ctx);
193144
193261
  if (import_typescript11.default.isIdentifier(expr)) {
193145
193262
  const jsxNode = ctx.analyzer.jsxConstants.get(expr.text);
@@ -193681,11 +193798,10 @@ function isIteratorShapeCall(node) {
193681
193798
  return { array: node.expression.expression, shape: name };
193682
193799
  }
193683
193800
  function extractSortComparator(callback, _method, ctx) {
193684
- const unsupported = () => {
193685
- const raw = ctx.getJS(callback);
193686
- return {
193687
- result: null,
193688
- unsupportedReason: `Sort comparator '${raw}' is not a supported shape. Accepted:
193801
+ const outerRaw = ctx.getJS(callback);
193802
+ const unsupported = () => ({
193803
+ result: null,
193804
+ unsupportedReason: `Sort comparator '${outerRaw}' is not a supported shape. Accepted:
193689
193805
  ` + ` (a, b) => a - b
193690
193806
  ` + ` (a, b) => a.field - b.field
193691
193807
  ` + ` (a, b) => a.localeCompare(b)
@@ -193693,15 +193809,25 @@ function extractSortComparator(callback, _method, ctx) {
193693
193809
  ` + ` (a, b) => a.field > b.field ? 1 : a.field < b.field ? -1 : 0
193694
193810
  ` + ` any of the above '||'-chained for multi-key tie-breaks
193695
193811
  ` + `(reverse the operands for descending order).`
193696
- };
193697
- };
193698
- if (!import_typescript11.default.isArrowFunction(callback) && !import_typescript11.default.isFunctionExpression(callback)) {
193812
+ });
193813
+ let resolvedNode = callback;
193814
+ if (import_typescript11.default.isIdentifier(callback)) {
193815
+ const resolved = resolveSortComparatorIdentifier(callback.text, ctx);
193816
+ if (!resolved) {
193817
+ return {
193818
+ result: null,
193819
+ unsupportedReason: `Sort comparator '${outerRaw}' could not be resolved to a local function — ` + `declare it in the same file or inline it.`
193820
+ };
193821
+ }
193822
+ resolvedNode = resolved;
193823
+ }
193824
+ if (!import_typescript11.default.isArrowFunction(resolvedNode) && !import_typescript11.default.isFunctionExpression(resolvedNode)) {
193699
193825
  return {
193700
193826
  result: null,
193701
193827
  unsupportedReason: "Sort comparator must be an arrow function or function expression"
193702
193828
  };
193703
193829
  }
193704
- const arrow = tsNodeToParsedExpr(callback);
193830
+ const arrow = tsNodeToParsedExpr(resolvedNode);
193705
193831
  if (arrow.kind !== "arrow" || arrow.params.length !== 2)
193706
193832
  return unsupported();
193707
193833
  if (sortComparatorFromArrow(arrow) === null)
@@ -193715,6 +193841,21 @@ function extractSortComparator(callback, _method, ctx) {
193715
193841
  }
193716
193842
  };
193717
193843
  }
193844
+ function resolveSortComparatorIdentifier(name, ctx) {
193845
+ const constInfo = findLocalConst(name, ctx);
193846
+ const fnInfo = findLocalFunction(name, ctx);
193847
+ if (constInfo && fnInfo)
193848
+ return null;
193849
+ if (constInfo) {
193850
+ const ast = parseConstInitializer(constInfo);
193851
+ return ast && (import_typescript11.default.isArrowFunction(ast) || import_typescript11.default.isFunctionExpression(ast)) ? ast : null;
193852
+ }
193853
+ if (fnInfo) {
193854
+ const ast = parseFunctionInfoAsExpr(fnInfo);
193855
+ return ast && (import_typescript11.default.isArrowFunction(ast) || import_typescript11.default.isFunctionExpression(ast)) ? ast : null;
193856
+ }
193857
+ return null;
193858
+ }
193718
193859
  function extractFilterPredicate(callback, ctx) {
193719
193860
  if (!import_typescript11.default.isArrowFunction(callback))
193720
193861
  return { result: null };
@@ -193787,7 +193928,7 @@ function extractLoopParamBindings(pattern) {
193787
193928
  const appendDotAccess = (prefix, key) => {
193788
193929
  return isIdent(key) ? `${prefix}.${key}` : `${prefix}[${JSON.stringify(key)}]`;
193789
193930
  };
193790
- const walk = (p, prefix) => {
193931
+ const walk = (p, prefix, segments) => {
193791
193932
  if (unsupported)
193792
193933
  return;
193793
193934
  if (import_typescript11.default.isArrayBindingPattern(p)) {
@@ -193804,15 +193945,17 @@ function extractLoopParamBindings(pattern) {
193804
193945
  bindings.push({
193805
193946
  name: el.name.text,
193806
193947
  path: prefix,
193807
- rest: { kind: "array", from: index }
193948
+ rest: { kind: "array", from: index },
193949
+ segments
193808
193950
  });
193809
193951
  return;
193810
193952
  }
193811
193953
  const path = `${prefix}[${index}]`;
193954
+ const nextSegments = [...segments, { kind: "index", index }];
193812
193955
  if (import_typescript11.default.isIdentifier(el.name)) {
193813
- bindings.push({ name: el.name.text, path });
193956
+ bindings.push({ name: el.name.text, path, segments: nextSegments });
193814
193957
  } else {
193815
- walk(el.name, path);
193958
+ walk(el.name, path, nextSegments);
193816
193959
  }
193817
193960
  }
193818
193961
  return;
@@ -193829,7 +193972,8 @@ function extractLoopParamBindings(pattern) {
193829
193972
  bindings.push({
193830
193973
  name: el.name.text,
193831
193974
  path: prefix,
193832
- rest: { kind: "object", exclude: collectedKeys }
193975
+ rest: { kind: "object", exclude: collectedKeys },
193976
+ segments
193833
193977
  });
193834
193978
  return;
193835
193979
  }
@@ -193852,17 +193996,19 @@ function extractLoopParamBindings(pattern) {
193852
193996
  unsupported = true;
193853
193997
  return;
193854
193998
  }
193855
- collectedKeys.push({ key: keyText, isIdent: isIdent(keyText) });
193999
+ const keyIsIdent = isIdent(keyText);
194000
+ collectedKeys.push({ key: keyText, isIdent: keyIsIdent });
193856
194001
  const path = appendDotAccess(prefix, keyText);
194002
+ const nextSegments = [...segments, { kind: "field", key: keyText, isIdent: keyIsIdent }];
193857
194003
  if (import_typescript11.default.isIdentifier(el.name)) {
193858
- bindings.push({ name: el.name.text, path });
194004
+ bindings.push({ name: el.name.text, path, segments: nextSegments });
193859
194005
  } else {
193860
- walk(el.name, path);
194006
+ walk(el.name, path, nextSegments);
193861
194007
  }
193862
194008
  }
193863
194009
  };
193864
194010
  if (import_typescript11.default.isArrayBindingPattern(pattern) || import_typescript11.default.isObjectBindingPattern(pattern)) {
193865
- walk(pattern, "");
194011
+ walk(pattern, "", []);
193866
194012
  if (unsupported)
193867
194013
  return { unsupported: true };
193868
194014
  return bindings;
@@ -194631,6 +194777,7 @@ function getAttributeValue(attr, ctx) {
194631
194777
  expr = branchInit;
194632
194778
  }
194633
194779
  }
194780
+ expr = tryDesugarInterleaveTaggedTemplate(expr, ctx);
194634
194781
  if (import_typescript11.default.isAwaitExpression(expr)) {
194635
194782
  ctx.analyzer.errors.push(createError(ErrorCodes.STAGE_AWAIT_IN_TEMPLATE, getSourceLocation(expr, ctx.sourceFile, ctx.filePath)));
194636
194783
  return AttrValueOf.expression("undefined");
@@ -194766,6 +194913,14 @@ function findLocalConst(name, ctx) {
194766
194913
  const pool = fnScoped.length > 0 ? fnScoped : matches;
194767
194914
  return pool[pool.length - 1];
194768
194915
  }
194916
+ function findLocalFunction(name, ctx) {
194917
+ const matches = ctx.analyzer.localFunctions.filter((f) => f.name === name);
194918
+ if (matches.length === 0)
194919
+ return;
194920
+ const fnScoped = matches.filter((f) => !f.isModule);
194921
+ const pool = fnScoped.length > 0 ? fnScoped : matches;
194922
+ return pool[pool.length - 1];
194923
+ }
194769
194924
  function isDynamicTagLocal(name, ctx) {
194770
194925
  if (!hasDynamicTagBinding(name, ctx.sourceFile))
194771
194926
  return false;
@@ -194860,6 +195015,147 @@ function parseConstInitializerImpl(c) {
194860
195015
  function astText(node) {
194861
195016
  return node.getText(node.getSourceFile());
194862
195017
  }
195018
+ var functionInfoExprCache = new WeakMap;
195019
+ function parseFunctionInfoAsExpr(fn) {
195020
+ const cached = functionInfoExprCache.get(fn);
195021
+ if (cached !== undefined)
195022
+ return cached;
195023
+ const result = parseFunctionInfoAsExprImpl(fn);
195024
+ functionInfoExprCache.set(fn, result);
195025
+ return result;
195026
+ }
195027
+ function parseFunctionInfoAsExprImpl(fn) {
195028
+ if (!fn.body)
195029
+ return null;
195030
+ const params = fn.typedParams !== undefined ? fn.typedParams : fn.params.map(formatParamWithType).join(", ");
195031
+ const body = fn.typedBody ?? fn.body;
195032
+ const wrapped = `const __bf_resolve_fn__ = function(${params}) ${body}`;
195033
+ const sf = import_typescript11.default.createSourceFile("__bf_resolve_fn.ts", wrapped, import_typescript11.default.ScriptTarget.Latest, true, import_typescript11.default.ScriptKind.TS);
195034
+ const stmt = sf.statements[0];
195035
+ if (!stmt || !import_typescript11.default.isVariableStatement(stmt))
195036
+ return null;
195037
+ const decl = stmt.declarationList.declarations[0];
195038
+ if (!decl?.initializer)
195039
+ return null;
195040
+ return decl.initializer;
195041
+ }
195042
+ function tryDesugarInterleaveTaggedTemplate(expr, ctx) {
195043
+ if (!import_typescript11.default.isTaggedTemplateExpression(expr))
195044
+ return expr;
195045
+ if (!import_typescript11.default.isIdentifier(expr.tag))
195046
+ return expr;
195047
+ const resolvedTag = resolveInterleaveTagIdentifier(expr.tag.text, ctx);
195048
+ if (!resolvedTag)
195049
+ return expr;
195050
+ if (!isInterleaveTagFunction(resolvedTag))
195051
+ return expr;
195052
+ const rewritten = buildUntaggedTemplateLiteral(expr, ctx);
195053
+ return rewritten ?? expr;
195054
+ }
195055
+ function resolveInterleaveTagIdentifier(name, ctx) {
195056
+ const constInfo = findLocalConst(name, ctx);
195057
+ const fnInfo = findLocalFunction(name, ctx);
195058
+ if (constInfo && fnInfo)
195059
+ return null;
195060
+ if (constInfo) {
195061
+ const ast = parseConstInitializer(constInfo);
195062
+ return ast && (import_typescript11.default.isArrowFunction(ast) || import_typescript11.default.isFunctionExpression(ast)) ? ast : null;
195063
+ }
195064
+ if (fnInfo) {
195065
+ const ast = parseFunctionInfoAsExpr(fnInfo);
195066
+ return ast && (import_typescript11.default.isArrowFunction(ast) || import_typescript11.default.isFunctionExpression(ast)) ? ast : null;
195067
+ }
195068
+ return null;
195069
+ }
195070
+ function isInterleaveTagFunction(fn) {
195071
+ if (!import_typescript11.default.isArrowFunction(fn) && !import_typescript11.default.isFunctionExpression(fn))
195072
+ return false;
195073
+ if (fn.parameters.length !== 2)
195074
+ return false;
195075
+ const [partsParam, argsParam] = fn.parameters;
195076
+ if (!import_typescript11.default.isIdentifier(partsParam.name) || partsParam.dotDotDotToken)
195077
+ return false;
195078
+ if (!import_typescript11.default.isIdentifier(argsParam.name) || !argsParam.dotDotDotToken)
195079
+ return false;
195080
+ const parsed = tsNodeToParsedExpr(fn);
195081
+ if (parsed.kind !== "arrow")
195082
+ return false;
195083
+ return isInterleaveReduceCall(parsed.body, partsParam.name.text, argsParam.name.text);
195084
+ }
195085
+ function isInterleaveReduceCall(body, partsName, argsName) {
195086
+ if (body.kind !== "call" || body.args.length !== 2)
195087
+ return false;
195088
+ const { callee, args } = body;
195089
+ if (callee.kind !== "member" || callee.computed || callee.property !== "reduce")
195090
+ return false;
195091
+ if (callee.object.kind !== "identifier" || callee.object.name !== partsName)
195092
+ return false;
195093
+ const [callback, init] = args;
195094
+ if (init.kind !== "literal" || init.literalType !== "string" || init.value !== "")
195095
+ return false;
195096
+ if (callback.kind !== "arrow" || callback.params.length !== 3)
195097
+ return false;
195098
+ const [acc, p, i2] = callback.params;
195099
+ return isInterleaveReduceCallbackBody(callback.body, acc, p, i2, argsName);
195100
+ }
195101
+ function isInterleaveReduceCallbackBody(body, acc, p, i2, argsName) {
195102
+ if (body.kind !== "binary" || body.op !== "+")
195103
+ return false;
195104
+ const { left, right } = body;
195105
+ if (left.kind !== "binary" || left.op !== "+")
195106
+ return false;
195107
+ if (left.left.kind !== "identifier" || left.left.name !== acc)
195108
+ return false;
195109
+ if (left.right.kind !== "identifier" || left.right.name !== p)
195110
+ return false;
195111
+ return isInterleaveSpanExpr(right, i2, argsName);
195112
+ }
195113
+ function isInterleaveSpanExpr(expr, i2, argsName) {
195114
+ let inner = expr;
195115
+ if (inner.kind === "call" && inner.args.length === 1 && inner.callee.kind === "identifier" && inner.callee.name === "String") {
195116
+ inner = inner.args[0];
195117
+ }
195118
+ if (inner.kind !== "logical" || inner.op !== "??")
195119
+ return false;
195120
+ if (inner.right.kind !== "literal" || inner.right.literalType !== "string" || inner.right.value !== "") {
195121
+ return false;
195122
+ }
195123
+ const idx = inner.left;
195124
+ if (idx.kind !== "index-access")
195125
+ return false;
195126
+ if (idx.object.kind !== "identifier" || idx.object.name !== argsName)
195127
+ return false;
195128
+ if (idx.index.kind !== "identifier" || idx.index.name !== i2)
195129
+ return false;
195130
+ return true;
195131
+ }
195132
+ function buildUntaggedTemplateLiteral(node, ctx) {
195133
+ const template = node.template;
195134
+ let text;
195135
+ if (import_typescript11.default.isNoSubstitutionTemplateLiteral(template)) {
195136
+ text = "`" + (template.rawText ?? template.text) + "`";
195137
+ } else {
195138
+ let body = template.head.rawText ?? template.head.text;
195139
+ for (const span of template.templateSpans) {
195140
+ const spanText = ctx.getJS(span.expression);
195141
+ body += "${(" + spanText + ") ?? ''}";
195142
+ body += span.literal.rawText ?? span.literal.text;
195143
+ }
195144
+ text = "`" + body + "`";
195145
+ }
195146
+ const wrapped = `const __bf_resolve_tagged__ = (${text})`;
195147
+ const sf = import_typescript11.default.createSourceFile("__bf_resolve_tagged.tsx", wrapped, import_typescript11.default.ScriptTarget.Latest, true, import_typescript11.default.ScriptKind.TSX);
195148
+ const stmt = sf.statements[0];
195149
+ if (!stmt || !import_typescript11.default.isVariableStatement(stmt))
195150
+ return null;
195151
+ const decl = stmt.declarationList.declarations[0];
195152
+ if (!decl?.initializer)
195153
+ return null;
195154
+ const result = import_typescript11.default.isParenthesizedExpression(decl.initializer) ? decl.initializer.expression : decl.initializer;
195155
+ if (!import_typescript11.default.isTemplateExpression(result) && !import_typescript11.default.isNoSubstitutionTemplateLiteral(result))
195156
+ return null;
195157
+ return result;
195158
+ }
194863
195159
  function parseTernary(expr, ctx) {
194864
195160
  const whenTrueValue = getStringValue(expr.whenTrue);
194865
195161
  const whenFalseValue = getStringValue(expr.whenFalse);
@@ -195335,6 +195631,18 @@ var import_typescript12 = __toESM(require_typescript(), 1);
195335
195631
 
195336
195632
  // ../jsx/src/relocate.ts
195337
195633
  var import_typescript13 = __toESM(require_typescript(), 1);
195634
+
195635
+ // ../jsx/src/lowering-registry.ts
195636
+ var plugins = [];
195637
+ function registerLoweringPlugin(plugin) {
195638
+ const existing = plugins.findIndex((p) => p.name === plugin.name);
195639
+ if (existing >= 0)
195640
+ plugins[existing] = plugin;
195641
+ else
195642
+ plugins.push(plugin);
195643
+ }
195644
+
195645
+ // ../jsx/src/relocate.ts
195338
195646
  var REGISTRY_SAFE_BINDING_KINDS = new Set([
195339
195647
  "global",
195340
195648
  "module-import",
@@ -195464,6 +195772,9 @@ var JS_BUILTINS = new Set([
195464
195772
  var ENV_SIGNAL_CLIENT_FACTORY = {
195465
195773
  search: "createSearchParams"
195466
195774
  };
195775
+ var ENV_SIGNAL_READERS = new Map([
195776
+ ["search", { key: "search", canonicalName: "searchParams", methods: new Set(["get"]) }]
195777
+ ]);
195467
195778
  function queryHrefLocalNames(metadata) {
195468
195779
  const names = new Set;
195469
195780
  for (const imp of metadata.imports) {
@@ -195629,38 +195940,6 @@ class SourceMapGenerator {
195629
195940
  }
195630
195941
  }
195631
195942
 
195632
- // ../jsx/src/module-exports.ts
195633
- function formatParamWithType(p) {
195634
- const rest = p.isRest ? "..." : "";
195635
- const optional = p.optional ? "?" : "";
195636
- const typeAnnotation = p.type?.raw && p.type.raw !== "unknown" ? `: ${p.type.raw}` : "";
195637
- const defaultPart = p.defaultValue !== undefined ? ` = ${p.defaultValue}` : "";
195638
- return `${rest}${p.name}${optional}${typeAnnotation}${defaultPart}`;
195639
- }
195640
- function findReachableNames(primaryRefs, declarations) {
195641
- const allNames = new Set(declarations.map((d) => d.name));
195642
- const bodyMap = new Map(declarations.map((d) => [d.name, d.body]));
195643
- const reachable = new Set;
195644
- const queue = [];
195645
- for (const name of allNames) {
195646
- if (new RegExp(`\\b${name}\\b`).test(primaryRefs)) {
195647
- reachable.add(name);
195648
- queue.push(name);
195649
- }
195650
- }
195651
- while (queue.length > 0) {
195652
- const current = queue.shift();
195653
- const body = bodyMap.get(current) || "";
195654
- for (const name of allNames) {
195655
- if (!reachable.has(name) && new RegExp(`\\b${name}\\b`).test(body)) {
195656
- reachable.add(name);
195657
- queue.push(name);
195658
- }
195659
- }
195660
- }
195661
- return reachable;
195662
- }
195663
-
195664
195943
  // ../jsx/src/preprocess-inline-jsx-callbacks.ts
195665
195944
  var import_typescript15 = __toESM(require_typescript(), 1);
195666
195945
 
@@ -195668,9 +195947,12 @@ var import_typescript15 = __toESM(require_typescript(), 1);
195668
195947
  var import_typescript16 = __toESM(require_typescript(), 1);
195669
195948
  var UNRESOLVED = Symbol("unresolved");
195670
195949
  var NO_RETURN = Symbol("no-return");
195950
+
195951
+ // ../jsx/src/augment-inherited-props.ts
195952
+ var import_typescript17 = __toESM(require_typescript(), 1);
195671
195953
  // ../jsx/src/shared-program.ts
195672
195954
  init_path();
195673
- var import_typescript17 = __toESM(require_typescript(), 1);
195955
+ var import_typescript18 = __toESM(require_typescript(), 1);
195674
195956
  // ../jsx/src/adapters/interface.ts
195675
195957
  class BaseAdapter {
195676
195958
  renderChildren(children) {
@@ -196156,15 +196438,6 @@ function isOmitBranch(node) {
196156
196438
  }
196157
196439
  return false;
196158
196440
  }
196159
- // ../jsx/src/lowering-registry.ts
196160
- var plugins = [];
196161
- function registerLoweringPlugin(plugin) {
196162
- const existing = plugins.findIndex((p) => p.name === plugin.name);
196163
- if (existing >= 0)
196164
- plugins[existing] = plugin;
196165
- else
196166
- plugins.push(plugin);
196167
- }
196168
196441
  // ../jsx/src/builtin-lowering-plugins.ts
196169
196442
  var queryHrefPlugin = {
196170
196443
  name: "queryHref",
@@ -196184,9 +196457,9 @@ function registerBuiltinLoweringPlugins() {
196184
196457
  registerLoweringPlugin(plugin);
196185
196458
  }
196186
196459
  // ../jsx/src/combine-client-js.ts
196187
- var import_typescript18 = __toESM(require_typescript(), 1);
196188
- // ../jsx/src/debug.ts
196189
196460
  var import_typescript19 = __toESM(require_typescript(), 1);
196461
+ // ../jsx/src/debug.ts
196462
+ var import_typescript20 = __toESM(require_typescript(), 1);
196190
196463
  function escapeForIdBoundary(name) {
196191
196464
  return name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
196192
196465
  }
@@ -196278,8 +196551,6 @@ function resolveSetters(handler, setterToSignal, fnSetters) {
196278
196551
  return refs;
196279
196552
  }
196280
196553
  // ../jsx/src/profiler.ts
196281
- var import_typescript20 = __toESM(require_typescript(), 1);
196282
- // ../jsx/src/augment-inherited-props.ts
196283
196554
  var import_typescript21 = __toESM(require_typescript(), 1);
196284
196555
 
196285
196556
  // ../jsx/src/index.ts
@@ -196378,6 +196649,7 @@ function irNodeToTestNode(node, constantMap, metadata) {
196378
196649
  const cmap = constantMap ?? new Map;
196379
196650
  const setterToSignal = new Map;
196380
196651
  const fnSetters = new Map;
196652
+ const defaults = new Map;
196381
196653
  if (metadata) {
196382
196654
  for (const s of metadata.signals) {
196383
196655
  if (s.setter)
@@ -196386,8 +196658,24 @@ function irNodeToTestNode(node, constantMap, metadata) {
196386
196658
  for (const [k, v] of buildLocalFunctionSetterMap(metadata, setterToSignal)) {
196387
196659
  fnSetters.set(k, v);
196388
196660
  }
196661
+ for (const p of metadata.propsParams) {
196662
+ const lit = p.defaultValue !== undefined ? parseLiteralDefault(p.defaultValue) : null;
196663
+ if (lit !== null)
196664
+ defaults.set(p.name, lit);
196665
+ }
196389
196666
  }
196390
- return convert(node, { cmap, setterToSignal, fnSetters });
196667
+ return convert(node, { cmap, setterToSignal, fnSetters, defaults });
196668
+ }
196669
+ function parseLiteralDefault(js) {
196670
+ const v = js.trim();
196671
+ if (v.startsWith("'") && v.endsWith("'") && !v.slice(1, -1).includes("'") || v.startsWith('"') && v.endsWith('"') && !v.slice(1, -1).includes('"')) {
196672
+ return v.slice(1, -1);
196673
+ }
196674
+ if (/^-?\d+(\.\d+)?$/.test(v))
196675
+ return v;
196676
+ if (v === "true" || v === "false")
196677
+ return v;
196678
+ return null;
196391
196679
  }
196392
196680
  function convert(node, ctx) {
196393
196681
  switch (node.type) {
@@ -196396,7 +196684,7 @@ function convert(node, ctx) {
196396
196684
  case "text":
196397
196685
  return convertText(node);
196398
196686
  case "expression":
196399
- return convertExpression(node);
196687
+ return convertExpression(node, ctx);
196400
196688
  case "conditional":
196401
196689
  return convertConditional(node, ctx);
196402
196690
  case "loop":
@@ -196426,19 +196714,9 @@ function convertElement(node, ctx) {
196426
196714
  let dataState = null;
196427
196715
  let classes = [];
196428
196716
  for (const attr of node.attrs) {
196429
- const value = resolveAttrValue(attr);
196717
+ const value = resolveAttrValue(attr, ctx);
196430
196718
  if (attr.name === "className" || attr.name === "class") {
196431
- if (typeof value === "string") {
196432
- const isDynamic = attr.value.kind === "expression" || attr.value.kind === "template" || attr.value.kind === "spread";
196433
- if (isDynamic && ctx.cmap.size > 0) {
196434
- const resolved = resolveClassValue(value, ctx.cmap);
196435
- if (resolved !== null) {
196436
- classes = resolved.split(/\s+/).filter(Boolean);
196437
- continue;
196438
- }
196439
- }
196440
- classes = value.split(/\s+/).filter(Boolean);
196441
- }
196719
+ classes = collectClassTokens(attr.value, value, ctx);
196442
196720
  continue;
196443
196721
  }
196444
196722
  if (attr.name === "role") {
@@ -196501,12 +196779,13 @@ function convertText(node) {
196501
196779
  componentName: null
196502
196780
  });
196503
196781
  }
196504
- function convertExpression(node) {
196782
+ function convertExpression(node, ctx) {
196783
+ const text = ctx.defaults.get(node.expr.trim()) ?? node.expr;
196505
196784
  return new TestNode({
196506
196785
  tag: null,
196507
196786
  type: "expression",
196508
196787
  children: [],
196509
- text: node.expr,
196788
+ text,
196510
196789
  props: {},
196511
196790
  classes: [],
196512
196791
  role: null,
@@ -196571,7 +196850,7 @@ function convertComponent(node, ctx) {
196571
196850
  props[prop.name] = prop.value.expr;
196572
196851
  break;
196573
196852
  case "template":
196574
- props[prop.name] = resolveTemplateAttr(prop.value);
196853
+ props[prop.name] = resolveTemplateAttr(prop.value, ctx);
196575
196854
  break;
196576
196855
  case "boolean-shorthand":
196577
196856
  case "boolean-attr":
@@ -196711,41 +196990,69 @@ function refsToHandler(refs) {
196711
196990
  }
196712
196991
  return { setters, via };
196713
196992
  }
196714
- function resolveClassValue(value, cmap) {
196715
- if (cmap.has(value)) {
196716
- return cmap.get(value);
196993
+ function splitClassTokens(value) {
196994
+ return value.split(/\s+/).filter((t) => t && !t.includes("${"));
196995
+ }
196996
+ function collectClassTokens(attrValue, resolved, ctx) {
196997
+ if (attrValue.kind === "template") {
196998
+ const joined = attrValue.parts.map((part) => {
196999
+ if (part.type === "string")
197000
+ return substituteInterpolations(part.value, ctx);
197001
+ if (part.type === "ternary")
197002
+ return `${part.whenTrue} ${part.whenFalse}`;
197003
+ return Object.values(part.cases).join(" ");
197004
+ }).join("");
197005
+ return splitClassTokens(joined);
197006
+ }
197007
+ if (typeof resolved !== "string")
197008
+ return [];
197009
+ const isDynamic = attrValue.kind === "expression" || attrValue.kind === "spread";
197010
+ if (isDynamic) {
197011
+ const value = resolveClassValue(resolved, ctx);
197012
+ if (value !== null)
197013
+ return splitClassTokens(value);
197014
+ }
197015
+ return splitClassTokens(resolved);
197016
+ }
197017
+ function substituteInterpolations(value, ctx) {
197018
+ return value.replace(/\$\{([^}]+)\}/g, (raw, expr) => {
197019
+ const trimmed = expr.trim();
197020
+ return ctx.cmap.get(trimmed) ?? ctx.defaults.get(trimmed) ?? raw;
197021
+ });
197022
+ }
197023
+ function resolveClassValue(value, ctx) {
197024
+ if (ctx.cmap.has(value)) {
197025
+ return ctx.cmap.get(value);
197026
+ }
197027
+ if (ctx.defaults.has(value)) {
197028
+ return ctx.defaults.get(value);
196717
197029
  }
196718
197030
  if (value.startsWith("`") && value.endsWith("`")) {
196719
- const inner = value.slice(1, -1);
196720
- return inner.replace(/\$\{([^}]+)\}/g, (_, expr) => {
196721
- const trimmed = expr.trim();
196722
- return cmap.get(trimmed) ?? "";
196723
- });
197031
+ return substituteInterpolations(value.slice(1, -1), ctx);
196724
197032
  }
196725
197033
  return null;
196726
197034
  }
196727
- function resolveAttrValue(attr) {
197035
+ function resolveAttrValue(attr, ctx) {
196728
197036
  switch (attr.value.kind) {
196729
197037
  case "boolean-attr":
196730
197038
  return true;
196731
197039
  case "literal":
196732
197040
  return attr.value.value;
196733
197041
  case "expression":
196734
- return attr.value.expr;
196735
197042
  case "spread":
196736
- return attr.value.expr;
197043
+ return ctx.defaults.get(attr.value.expr.trim()) ?? attr.value.expr;
196737
197044
  case "template":
196738
- return resolveTemplateAttr(attr.value);
197045
+ return resolveTemplateAttr(attr.value, ctx);
196739
197046
  case "boolean-shorthand":
196740
197047
  return true;
196741
197048
  case "jsx-children":
196742
197049
  return null;
196743
197050
  }
196744
197051
  }
196745
- function resolveTemplateAttr(tl) {
197052
+ function resolveTemplateAttr(tl, ctx) {
196746
197053
  return tl.parts.map((part) => {
196747
197054
  if (part.type === "string")
196748
- return part.value;
197055
+ return substituteInterpolations(part.value, ctx);
196749
197056
  if (part.type === "ternary")
196750
197057
  return `{${part.condition}}`;
196751
197058
  return Object.values(part.cases).join(" ");
@@ -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,EAYN,UAAU,EACX,MAAM,iBAAiB,CAAA;AAMxB,OAAO,EAAE,QAAQ,EAAqB,MAAM,gBAAgB,CAAA;AAQ5D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,CAAC,EAAE,UAAU,GAAG,QAAQ,CAajH"}
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,gBAAgB,CAAA;AAU5D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,CAAC,EAAE,UAAU,GAAG,QAAQ,CAuBjH"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/test",
3
- "version": "0.17.0",
3
+ "version": "0.18.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.17.0"
42
+ "@barefootjs/jsx": "0.18.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "typescript": "^5.0.0"
@@ -30,12 +30,15 @@ interface ConvertContext {
30
30
  cmap: Map<string, string>
31
31
  setterToSignal: Map<string, string>
32
32
  fnSetters: Map<string, FnSetterResolution[]>
33
+ /** Prop name → literal destructure-default value (`{ size = 'md' }` → `size: 'md'`). */
34
+ defaults: Map<string, string>
33
35
  }
34
36
 
35
37
  export function irNodeToTestNode(node: IRNode, constantMap?: Map<string, string>, metadata?: IRMetadata): TestNode {
36
38
  const cmap = constantMap ?? new Map<string, string>()
37
39
  const setterToSignal = new Map<string, string>()
38
40
  const fnSetters = new Map<string, FnSetterResolution[]>()
41
+ const defaults = new Map<string, string>()
39
42
  if (metadata) {
40
43
  for (const s of metadata.signals) {
41
44
  if (s.setter) setterToSignal.set(s.setter, s.getter)
@@ -43,8 +46,35 @@ export function irNodeToTestNode(node: IRNode, constantMap?: Map<string, string>
43
46
  for (const [k, v] of buildLocalFunctionSetterMap(metadata, setterToSignal)) {
44
47
  fnSetters.set(k, v)
45
48
  }
49
+ // renderToTest models the component compiled with NO incoming props,
50
+ // so a literal destructure default IS the statically-known value of
51
+ // that prop (#2069). Non-literal defaults (arrows, objects, computed
52
+ // expressions) stay unresolved — surfacing a stale expression string
53
+ // would be worse than the raw reference.
54
+ for (const p of metadata.propsParams) {
55
+ const lit = p.defaultValue !== undefined ? parseLiteralDefault(p.defaultValue) : null
56
+ if (lit !== null) defaults.set(p.name, lit)
57
+ }
46
58
  }
47
- return convert(node, { cmap, setterToSignal, fnSetters })
59
+ return convert(node, { cmap, setterToSignal, fnSetters, defaults })
60
+ }
61
+
62
+ /**
63
+ * Parse a `ParamInfo.defaultValue` JS-text into a plain string when it is
64
+ * a self-contained literal: quoted string, number, or boolean. Returns
65
+ * null for anything else.
66
+ */
67
+ function parseLiteralDefault(js: string): string | null {
68
+ const v = js.trim()
69
+ if (
70
+ (v.startsWith("'") && v.endsWith("'") && !v.slice(1, -1).includes("'")) ||
71
+ (v.startsWith('"') && v.endsWith('"') && !v.slice(1, -1).includes('"'))
72
+ ) {
73
+ return v.slice(1, -1)
74
+ }
75
+ if (/^-?\d+(\.\d+)?$/.test(v)) return v
76
+ if (v === 'true' || v === 'false') return v
77
+ return null
48
78
  }
49
79
 
50
80
  function convert(node: IRNode, ctx: ConvertContext): TestNode {
@@ -54,7 +84,7 @@ function convert(node: IRNode, ctx: ConvertContext): TestNode {
54
84
  case 'text':
55
85
  return convertText(node)
56
86
  case 'expression':
57
- return convertExpression(node)
87
+ return convertExpression(node, ctx)
58
88
  case 'conditional':
59
89
  return convertConditional(node, ctx)
60
90
  case 'loop':
@@ -90,20 +120,10 @@ function convertElement(node: IRElement, ctx: ConvertContext): TestNode {
90
120
  let classes: string[] = []
91
121
 
92
122
  for (const attr of node.attrs) {
93
- const value = resolveAttrValue(attr)
123
+ const value = resolveAttrValue(attr, ctx)
94
124
 
95
125
  if (attr.name === 'className' || attr.name === 'class') {
96
- if (typeof value === 'string') {
97
- const isDynamic = attr.value.kind === 'expression' || attr.value.kind === 'template' || attr.value.kind === 'spread'
98
- if (isDynamic && ctx.cmap.size > 0) {
99
- const resolved = resolveClassValue(value, ctx.cmap)
100
- if (resolved !== null) {
101
- classes = resolved.split(/\s+/).filter(Boolean)
102
- continue
103
- }
104
- }
105
- classes = value.split(/\s+/).filter(Boolean)
106
- }
126
+ classes = collectClassTokens(attr.value, value, ctx)
107
127
  continue
108
128
  }
109
129
 
@@ -178,12 +198,20 @@ function convertText(node: IRText): TestNode {
178
198
  // Expression
179
199
  // ---------------------------------------------------------------------------
180
200
 
181
- function convertExpression(node: IRExpression): TestNode {
201
+ function convertExpression(node: IRExpression, ctx: ConvertContext): TestNode {
202
+ // A bare reference to a defaulted prop (`<div>{label}</div>` with
203
+ // `{ label = 'Hello' }`) resolves to its literal default, so
204
+ // `findByText('Hello')` sees the zero-props render (#2069). Prop refs
205
+ // are flagged reactive (they update on parent re-render), so this
206
+ // keys off defaults-map membership, not the reactive flag: signal /
207
+ // memo reads are call expressions (`count()`), never bare identifiers,
208
+ // and keep their source text — wiring is the assertion surface there.
209
+ const text = ctx.defaults.get(node.expr.trim()) ?? node.expr
182
210
  return new TestNode({
183
211
  tag: null,
184
212
  type: 'expression',
185
213
  children: [],
186
- text: node.expr,
214
+ text,
187
215
  props: {},
188
216
  classes: [],
189
217
  role: null,
@@ -265,7 +293,7 @@ function convertComponent(node: IRComponent, ctx: ConvertContext): TestNode {
265
293
  props[prop.name] = prop.value.expr
266
294
  break
267
295
  case 'template':
268
- props[prop.name] = resolveTemplateAttr(prop.value)
296
+ props[prop.name] = resolveTemplateAttr(prop.value, ctx)
269
297
  break
270
298
  case 'boolean-shorthand':
271
299
  case 'boolean-attr':
@@ -450,19 +478,77 @@ function refsToHandler(refs: SetterRef[]): EventHandler {
450
478
  return { setters, via }
451
479
  }
452
480
 
453
- function resolveClassValue(value: string, cmap: Map<string, string>): string | null {
454
- // Simple identifier lookup
455
- if (cmap.has(value)) {
456
- return cmap.get(value)!
481
+ /**
482
+ * Split a resolved className value into tokens, dropping any span that
483
+ * still carries an unresolved runtime interpolation (`${className}`,
484
+ * `foo-${x}`). Those are dynamic passthroughs the IR can't evaluate —
485
+ * they aren't real class tokens, and leaking them verbatim pollutes
486
+ * `.classes` for exact-match assertions.
487
+ */
488
+ function splitClassTokens(value: string): string[] {
489
+ return value.split(/\s+/).filter(t => t && !t.includes('${'))
490
+ }
491
+
492
+ /**
493
+ * Resolve a className attribute value into its class tokens.
494
+ *
495
+ * For a structured template attr the parts are walked directly so class
496
+ * collection keeps union semantics per part kind:
497
+ * - `lookup` (`${MAP[KEY]}`) → every case's tokens (PR #2000)
498
+ * - `ternary` (`cond ? 'on' : 'off'`) → both branches' tokens,
499
+ * matching the intermediate-const `valueBranches` union (#525)
500
+ * - `string` spans → literal text, with `${ident}` interpolations
501
+ * substituted from resolved consts / literal prop defaults
502
+ * For expression attrs the value resolves through local consts (cmap)
503
+ * and literal prop defaults. Anything still carrying a `${...}`
504
+ * interpolation is dropped by `splitClassTokens`.
505
+ */
506
+ function collectClassTokens(attrValue: AttrValue, resolved: string | boolean | null, ctx: ConvertContext): string[] {
507
+ if (attrValue.kind === 'template') {
508
+ const joined = attrValue.parts
509
+ .map(part => {
510
+ if (part.type === 'string') return substituteInterpolations(part.value, ctx)
511
+ if (part.type === 'ternary') return `${part.whenTrue} ${part.whenFalse}`
512
+ return Object.values(part.cases).join(' ')
513
+ })
514
+ .join('')
515
+ return splitClassTokens(joined)
516
+ }
517
+
518
+ if (typeof resolved !== 'string') return []
519
+
520
+ const isDynamic = attrValue.kind === 'expression' || attrValue.kind === 'spread'
521
+ if (isDynamic) {
522
+ const value = resolveClassValue(resolved, ctx)
523
+ if (value !== null) return splitClassTokens(value)
524
+ }
525
+ return splitClassTokens(resolved)
526
+ }
527
+
528
+ /**
529
+ * Replace `${ident}` interpolations with a resolved const or a literal
530
+ * prop default; unresolvable spans are kept verbatim so the caller's
531
+ * token filter can drop them.
532
+ */
533
+ function substituteInterpolations(value: string, ctx: ConvertContext): string {
534
+ return value.replace(/\$\{([^}]+)\}/g, (raw, expr) => {
535
+ const trimmed = expr.trim()
536
+ return ctx.cmap.get(trimmed) ?? ctx.defaults.get(trimmed) ?? raw
537
+ })
538
+ }
539
+
540
+ function resolveClassValue(value: string, ctx: ConvertContext): string | null {
541
+ // Simple identifier lookup: a local const or a literal prop default.
542
+ if (ctx.cmap.has(value)) {
543
+ return ctx.cmap.get(value)!
544
+ }
545
+ if (ctx.defaults.has(value)) {
546
+ return ctx.defaults.get(value)!
457
547
  }
458
548
 
459
549
  // Template literal: `...${var}...`
460
550
  if (value.startsWith('`') && value.endsWith('`')) {
461
- const inner = value.slice(1, -1)
462
- return inner.replace(/\$\{([^}]+)\}/g, (_, expr) => {
463
- const trimmed = expr.trim()
464
- return cmap.get(trimmed) ?? ''
465
- })
551
+ return substituteInterpolations(value.slice(1, -1), ctx)
466
552
  }
467
553
 
468
554
  return null
@@ -472,18 +558,22 @@ function resolveClassValue(value: string, cmap: Map<string, string>): string | n
472
558
  // Attribute value resolution
473
559
  // ---------------------------------------------------------------------------
474
560
 
475
- function resolveAttrValue(attr: IRAttribute): string | boolean | null {
561
+ function resolveAttrValue(attr: IRAttribute, ctx: ConvertContext): string | boolean | null {
476
562
  switch (attr.value.kind) {
477
563
  case 'boolean-attr':
478
564
  return true
479
565
  case 'literal':
480
566
  return attr.value.value
481
567
  case 'expression':
482
- return attr.value.expr
483
568
  case 'spread':
484
- return attr.value.expr
569
+ // A bare reference to a defaulted prop (`type={type}` with
570
+ // `{ type = 'button' }`) resolves to its literal default —
571
+ // renderToTest models the zero-props render, where the default
572
+ // IS the value (#2069). Anything non-bare keeps the expression
573
+ // text (the wiring-visible representation).
574
+ return ctx.defaults.get(attr.value.expr.trim()) ?? attr.value.expr
485
575
  case 'template':
486
- return resolveTemplateAttr(attr.value)
576
+ return resolveTemplateAttr(attr.value, ctx)
487
577
  case 'boolean-shorthand':
488
578
  return true
489
579
  case 'jsx-children':
@@ -491,10 +581,12 @@ function resolveAttrValue(attr: IRAttribute): string | boolean | null {
491
581
  }
492
582
  }
493
583
 
494
- function resolveTemplateAttr(tl: TemplateAttr): string {
584
+ function resolveTemplateAttr(tl: TemplateAttr, ctx: ConvertContext): string {
495
585
  return tl.parts
496
586
  .map(part => {
497
- if (part.type === 'string') return part.value
587
+ // `${ident}` interpolations against a literal prop default
588
+ // resolve like local consts do (#2069).
589
+ if (part.type === 'string') return substituteInterpolations(part.value, ctx)
498
590
  if (part.type === 'ternary') return `{${part.condition}}`
499
591
  // `lookup` (`Record<T, string>[key]`) — the test framework
500
592
  // doesn't render against a specific key, so concatenate every