@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.
- package/dist/index.js +4639 -4181
- package/dist/ir-to-test-node.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/ir-to-test-node.ts +124 -32
|
@@ -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;
|
|
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.
|
|
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.
|
|
42
|
+
"@barefootjs/jsx": "0.17.1"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"typescript": "^5.0.0"
|
package/src/ir-to-test-node.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|