@barefootjs/test 0.16.0 → 0.17.1

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.
@@ -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.16.0",
3
+ "version": "0.17.1",
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.16.0"
42
+ "@barefootjs/jsx": "0.17.1"
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