inkbridge 0.1.0-beta.2 → 0.1.0-beta.21
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/README.md +108 -25
- package/bin/inkbridge.mjs +354 -83
- package/code.js +40 -11802
- package/manifest.json +1 -0
- package/package.json +74 -23
- package/scanner/adapter-utils-regression.ts +159 -0
- package/scanner/aspect-percent-position-regression.ts +237 -0
- package/scanner/aspect-ratio-regression.ts +90 -0
- package/scanner/blob-placement-regression.ts +2 -2
- package/scanner/block-cache-regression.ts +195 -0
- package/scanner/bundle-size-regression.ts +50 -0
- package/scanner/child-sizing-matrix-regression.ts +303 -0
- package/scanner/cli.ts +342 -13
- package/scanner/component-scanner.ts +2108 -174
- package/scanner/component-sections-regression.ts +198 -0
- package/scanner/compound-classes-lookup-regression.ts +163 -0
- package/scanner/css-token-reader-regression.ts +7 -6
- package/scanner/css-token-reader.ts +152 -31
- package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
- package/scanner/cva-master-icon-regression.ts +315 -0
- package/scanner/data-attr-prop-alias-regression.ts +129 -0
- package/scanner/explicit-size-root-regression.ts +102 -0
- package/scanner/font-family-extract-regression.ts +113 -0
- package/scanner/font-style-resolver-regression.ts +1 -1
- package/scanner/framework-adapter-shadcn-regression.ts +480 -0
- package/scanner/full-width-matrix-regression.ts +338 -0
- package/scanner/grid-cols-extraction-regression.ts +110 -0
- package/scanner/image-src-collector-regression.ts +204 -0
- package/scanner/inline-flex-regression.ts +235 -0
- package/scanner/input-range-regression.ts +217 -0
- package/scanner/instance-rendering-regression.ts +224 -0
- package/scanner/jsx-prop-unresolved-regression.ts +178 -0
- package/scanner/jsx-text-regression.ts +178 -0
- package/scanner/layout-alignment-regression.ts +108 -0
- package/scanner/layout-flex-regression.ts +90 -0
- package/scanner/layout-mode-regression.ts +71 -0
- package/scanner/layout-sizing-regression.ts +227 -0
- package/scanner/layout-spacing-regression.ts +135 -0
- package/scanner/local-const-className-regression.ts +331 -0
- package/scanner/percent-position-regression.ts +105 -0
- package/scanner/provider-cascade-regression.ts +224 -0
- package/scanner/provider-flatten-regression.ts +235 -0
- package/scanner/radial-gradient-regression.ts +1 -1
- package/scanner/render-prop-parser-regression.ts +161 -0
- package/scanner/ring-utility-regression.ts +153 -0
- package/scanner/sandbox-spread-regression.ts +125 -0
- package/scanner/selection-pressed-regression.ts +241 -0
- package/scanner/size-full-normalization-regression.ts +127 -0
- package/scanner/state-classification-regression.ts +175 -0
- package/scanner/story-diagnostics-regression.ts +216 -0
- package/scanner/story-dimensioning-regression.ts +298 -0
- package/scanner/story-render-strategy-regression.ts +205 -0
- package/scanner/stretch-to-parent-width-regression.ts +147 -0
- package/scanner/svg-fill-parent-regression.ts +98 -0
- package/scanner/svg-group-inheritance-regression.ts +166 -0
- package/scanner/svg-marker-inline-regression.ts +211 -0
- package/scanner/svg-marker-regression.ts +116 -0
- package/scanner/tailwind-parser.ts +46 -4
- package/scanner/text-resize-matrix-regression.ts +173 -0
- package/scanner/transform-math-regression.ts +1 -1
- package/scanner/types.ts +26 -2
- package/src/cache/frame-cache.ts +150 -0
- package/src/cache/index.ts +2 -0
- package/src/{component-defs.ts → components/component-defs.ts} +25 -10
- package/src/{component-gen.ts → components/component-gen.ts} +43 -116
- package/src/components/component-instance.ts +386 -0
- package/src/components/component-library.ts +44 -0
- package/src/components/component-lookup.ts +161 -0
- package/src/components/index.ts +7 -0
- package/src/components/scanner-types.ts +39 -0
- package/src/components/symbol-instance-policy.ts +312 -0
- package/src/design-system/block-cache.ts +130 -0
- package/src/design-system/component-sections.ts +107 -0
- package/src/design-system/cva-inference.ts +187 -0
- package/src/design-system/cva-master.ts +427 -0
- package/src/design-system/cva-utils.ts +29 -0
- package/src/design-system/design-system.ts +334 -0
- package/src/design-system/frame-stabilizers.ts +191 -0
- package/src/design-system/frame-utils.ts +46 -0
- package/src/design-system/generated-node.ts +84 -0
- package/src/design-system/icon-rendering.ts +229 -0
- package/src/design-system/index.ts +13 -0
- package/src/design-system/instance-rendering.ts +307 -0
- package/src/design-system/master-shared.ts +133 -0
- package/src/design-system/node-helpers.ts +237 -0
- package/src/design-system/node-variants.ts +196 -0
- package/src/design-system/non-cva-master.ts +104 -0
- package/src/design-system/portal-handling.ts +138 -0
- package/src/design-system/preview-builder.ts +738 -0
- package/src/{render-context.ts → design-system/render-context.ts} +32 -6
- package/src/design-system/render-prop-parser.ts +50 -0
- package/src/design-system/responsive-resolver.ts +180 -0
- package/src/design-system/selectable-state.ts +157 -0
- package/src/design-system/state-master.ts +267 -0
- package/src/design-system/state-utils.ts +15 -0
- package/src/design-system/story-builder-context.ts +40 -0
- package/src/design-system/story-builder.ts +1322 -0
- package/src/design-system/story-diagnostics.ts +80 -0
- package/src/design-system/story-dimensioning.ts +272 -0
- package/src/design-system/story-frames.ts +400 -0
- package/src/design-system/story-instance.ts +333 -0
- package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
- package/src/design-system/story-render-strategy.ts +150 -0
- package/src/design-system/story-tree-search.ts +110 -0
- package/src/design-system/symbol-fallback.ts +89 -0
- package/src/design-system/symbol-source.ts +172 -0
- package/src/design-system/table-helpers.ts +56 -0
- package/src/design-system/tag-predicates.ts +99 -0
- package/src/design-system/theme-context.ts +52 -0
- package/src/design-system/typography.ts +100 -0
- package/src/design-system/ui-builder.ts +2676 -0
- package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
- package/src/effects/icon-builder.ts +1074 -0
- package/src/effects/index.ts +5 -0
- package/src/effects/portal-panel.ts +369 -0
- package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
- package/src/framework-adapters/index.ts +47 -0
- package/src/framework-adapters/shadcn.ts +541 -0
- package/src/{github.ts → github/github.ts} +46 -21
- package/src/github/index.ts +1 -0
- package/src/layout/deferred-layout.ts +1556 -0
- package/src/layout/index.ts +24 -0
- package/src/layout/layout-parser.ts +375 -0
- package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
- package/src/layout/parser/alignment.ts +54 -0
- package/src/layout/parser/flex.ts +59 -0
- package/src/layout/parser/index.ts +65 -0
- package/src/layout/parser/ir.ts +80 -0
- package/src/layout/parser/layout-mode.ts +57 -0
- package/src/layout/parser/sizing.ts +241 -0
- package/src/layout/parser/spacing-scale.ts +78 -0
- package/src/layout/parser/spacing.ts +134 -0
- package/src/layout/ring-utils.ts +120 -0
- package/src/layout/size-utils.ts +143 -0
- package/src/layout/text-resize-decision.ts +51 -0
- package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
- package/src/main.ts +444 -162
- package/src/{config.ts → plugin/config.ts} +12 -12
- package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
- package/src/plugin/image-src-collector.ts +52 -0
- package/src/plugin/index.ts +3 -0
- package/src/plugin/packs/index.ts +2 -0
- package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
- package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
- package/src/render-engine-version.ts +2 -0
- package/src/tailwind/adapter-utils.ts +137 -0
- package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
- package/src/tailwind/index.ts +8 -0
- package/src/tailwind/jsx-utils.ts +319 -0
- package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
- package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
- package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
- package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
- package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
- package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
- package/src/text/index.ts +4 -0
- package/src/{inline-text.ts → text/inline-text.ts} +13 -13
- package/src/{text-builder.ts → text/text-builder.ts} +24 -7
- package/src/{text-line.ts → text/text-line.ts} +2 -2
- package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
- package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
- package/src/{colors.ts → tokens/colors.ts} +13 -6
- package/src/tokens/index.ts +6 -0
- package/src/{token-source.ts → tokens/token-source.ts} +4 -1
- package/src/{tokens.ts → tokens/tokens.ts} +116 -20
- package/src/{variables.ts → tokens/variables.ts} +447 -102
- package/templates/patch-tokens-route.ts +25 -6
- package/templates/scan-components-route.ts +26 -5
- package/ui.html +485 -37
- package/src/component-lookup.ts +0 -82
- package/src/design-system.ts +0 -59
- package/src/icon-builder.ts +0 -607
- package/src/layout-parser.ts +0 -667
- package/src/story-builder.ts +0 -1706
- package/src/ui-builder.ts +0 -1996
- /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
- /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
- /package/src/{transform-math.ts → tailwind/transform-math.ts} +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { parseRenderPropToSyntheticInstance } from '../src/design-system/render-prop-parser';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Regression: parseRenderPropToSyntheticInstance turns a Storybook
|
|
6
|
+
* `render="<Foo bar=\"x\">y</Foo>"` string literal into a synthetic
|
|
7
|
+
* ComponentInstance the renderer can treat like a real scanner output.
|
|
8
|
+
* Stories that declare their JSX as a literal string instead of a render
|
|
9
|
+
* function depend on this — if the parser drifts, those stories silently
|
|
10
|
+
* render as nothing.
|
|
11
|
+
*
|
|
12
|
+
* Extracted from `src/design-system/story-builder.ts` into
|
|
13
|
+
* `src/design-system/render-prop-parser.ts`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Defensive: non-string / empty / falsy inputs return null without throwing
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
assert.equal(parseRenderPropToSyntheticInstance(null), null, 'null returns null');
|
|
21
|
+
assert.equal(parseRenderPropToSyntheticInstance(undefined), null, 'undefined returns null');
|
|
22
|
+
assert.equal(parseRenderPropToSyntheticInstance(42), null, 'number returns null');
|
|
23
|
+
assert.equal(parseRenderPropToSyntheticInstance({}), null, 'object returns null');
|
|
24
|
+
assert.equal(parseRenderPropToSyntheticInstance(''), null, 'empty string returns null');
|
|
25
|
+
assert.equal(parseRenderPropToSyntheticInstance(' '), null, 'whitespace-only string returns null');
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Tagname extraction
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
const out = parseRenderPropToSyntheticInstance('<Button>Click me</Button>');
|
|
33
|
+
assert.ok(out, 'simple tag must parse');
|
|
34
|
+
assert.equal(out.componentName, 'Button', 'componentName captures the opening tag');
|
|
35
|
+
assert.equal(out.children, 'Click me', 'inner text becomes children');
|
|
36
|
+
assert.deepEqual(out.props, {}, 'no attrs → empty props');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Component naming preserved (PascalCase, hyphens, dots — namespaced
|
|
40
|
+
// components like `Form.Item` are captured as written).
|
|
41
|
+
for (const tag of ['Button', 'PrimaryButton', 'form-item', 'Form.Item', 'My_Custom_123']) {
|
|
42
|
+
const out = parseRenderPropToSyntheticInstance(`<${tag}>x</${tag}>`);
|
|
43
|
+
assert.ok(out, `tag "${tag}" must parse`);
|
|
44
|
+
assert.equal(out.componentName, tag, `tag "${tag}" preserved verbatim`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Attribute extraction
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
// Single attr
|
|
52
|
+
{
|
|
53
|
+
const out = parseRenderPropToSyntheticInstance('<Button variant="primary">Go</Button>');
|
|
54
|
+
assert.ok(out);
|
|
55
|
+
assert.deepEqual(out.props, { variant: 'primary' }, 'single attr extracted');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Multiple attrs
|
|
59
|
+
{
|
|
60
|
+
const out = parseRenderPropToSyntheticInstance(
|
|
61
|
+
'<Button variant="primary" size="lg" disabled="true">Submit</Button>',
|
|
62
|
+
);
|
|
63
|
+
assert.ok(out);
|
|
64
|
+
assert.deepEqual(
|
|
65
|
+
out.props,
|
|
66
|
+
{ variant: 'primary', size: 'lg', disabled: 'true' },
|
|
67
|
+
'all attrs extracted',
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Attr names support letters, digits, underscore, hyphen, colon
|
|
72
|
+
{
|
|
73
|
+
const out = parseRenderPropToSyntheticInstance(
|
|
74
|
+
'<Foo aria-label="x" data-test="y" my_attr="z" ns:attr="w">x</Foo>',
|
|
75
|
+
);
|
|
76
|
+
assert.ok(out);
|
|
77
|
+
assert.deepEqual(out.props, {
|
|
78
|
+
'aria-label': 'x',
|
|
79
|
+
'data-test': 'y',
|
|
80
|
+
my_attr: 'z',
|
|
81
|
+
'ns:attr': 'w',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Boolean shorthand (no =) is ignored — non-goal documented in the module.
|
|
86
|
+
{
|
|
87
|
+
const out = parseRenderPropToSyntheticInstance('<Button disabled>x</Button>');
|
|
88
|
+
assert.ok(out);
|
|
89
|
+
assert.deepEqual(out.props, {}, 'boolean shorthand attrs are deliberately ignored');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Empty attr values are kept (designers may want to detect their presence).
|
|
93
|
+
{
|
|
94
|
+
const out = parseRenderPropToSyntheticInstance('<Foo bar="">x</Foo>');
|
|
95
|
+
assert.ok(out);
|
|
96
|
+
assert.deepEqual(out.props, { bar: '' }, 'empty-string attr value preserved');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Children handling
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
// Empty children → undefined (not empty string) so the renderer can treat
|
|
104
|
+
// it as "no content" rather than "explicit empty text".
|
|
105
|
+
{
|
|
106
|
+
const out = parseRenderPropToSyntheticInstance('<Button></Button>');
|
|
107
|
+
assert.ok(out);
|
|
108
|
+
assert.equal(out.children, undefined, 'empty children become undefined, not ""');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Whitespace-only children → undefined (trimmed before falsy check).
|
|
112
|
+
{
|
|
113
|
+
const out = parseRenderPropToSyntheticInstance('<Button> </Button>');
|
|
114
|
+
assert.ok(out);
|
|
115
|
+
assert.equal(out.children, undefined, 'whitespace-only children become undefined');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Inner content can contain markup (regex is non-greedy) — first matching
|
|
119
|
+
// closing tag wins.
|
|
120
|
+
{
|
|
121
|
+
const out = parseRenderPropToSyntheticInstance(
|
|
122
|
+
'<Card><span>Inner</span></Card>',
|
|
123
|
+
);
|
|
124
|
+
assert.ok(out);
|
|
125
|
+
assert.equal(out.componentName, 'Card');
|
|
126
|
+
assert.equal(out.children, '<span>Inner</span>', 'nested markup kept as raw text');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Invalid inputs return null (no partial matches)
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
// Self-closing tag isn't matched — non-goal documented in module.
|
|
134
|
+
assert.equal(
|
|
135
|
+
parseRenderPropToSyntheticInstance('<Button />'),
|
|
136
|
+
null,
|
|
137
|
+
'self-closing tag not supported — parser stays strict',
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Mismatched tags return null (backref \1 enforces equality).
|
|
141
|
+
assert.equal(
|
|
142
|
+
parseRenderPropToSyntheticInstance('<Button>x</Span>'),
|
|
143
|
+
null,
|
|
144
|
+
'mismatched closing tag returns null',
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Just a string without tags returns null.
|
|
148
|
+
assert.equal(
|
|
149
|
+
parseRenderPropToSyntheticInstance('hello world'),
|
|
150
|
+
null,
|
|
151
|
+
'plain text (no tags) returns null',
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Truncated / unclosed tag returns null.
|
|
155
|
+
assert.equal(
|
|
156
|
+
parseRenderPropToSyntheticInstance('<Button>missing close'),
|
|
157
|
+
null,
|
|
158
|
+
'unclosed tag returns null',
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
console.log('render-prop-parser-regression: PASS');
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
import { parseRingWidth, parseRingColor, getRingInfoFromClasses } from '../src/layout/ring-utils';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Regression: `applyTailwindStylesToFrame` in tailwind.ts now delegates
|
|
7
|
+
* ring rendering to `applyRingEffect` (overlay-frame approach in
|
|
8
|
+
* `layout/ring-utils.ts`) instead of pushing a Figma DROP_SHADOW with
|
|
9
|
+
* `spread > 0`.
|
|
10
|
+
*
|
|
11
|
+
* Why this matters: Figma surfaces
|
|
12
|
+
* "The 'spread' parameter is not supported when frames or components
|
|
13
|
+
* have no visible fills"
|
|
14
|
+
* for every transparent frame that gets a spread-based ring — i.e. every
|
|
15
|
+
* structural layout frame in the design system. The console fills with
|
|
16
|
+
* the warning for the duration of a build, and the workaround
|
|
17
|
+
* `clipsContent = true` from a prior round (for a different Figma spread
|
|
18
|
+
* constraint about clipping) is now redundant: the overlay path doesn't
|
|
19
|
+
* touch effects or fills.
|
|
20
|
+
*
|
|
21
|
+
* This file locks the ring-utils parser inputs that `applyRingEffect`
|
|
22
|
+
* relies on. If a future refactor re-introduces an inline DROP_SHADOW
|
|
23
|
+
* path or weakens the parser, these assertions catch it.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const colorGroup: Record<string, string> = {
|
|
27
|
+
ring: '#3b82f6',
|
|
28
|
+
primary: '#1d4ed8',
|
|
29
|
+
destructive: '#ef4444',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// parseRingWidth — width-only utilities.
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
assert.equal(parseRingWidth('ring'), 3, 'bare `ring` → default 3px');
|
|
37
|
+
assert.equal(parseRingWidth('ring-2'), 2, '`ring-2` → 2px');
|
|
38
|
+
assert.equal(parseRingWidth('ring-0'), 0, '`ring-0` → 0px (caller must filter zero)');
|
|
39
|
+
assert.equal(parseRingWidth('ring-[3px]'), 3, '`ring-[3px]` → 3px arbitrary');
|
|
40
|
+
assert.equal(parseRingWidth('ring-[1rem]'), 16, '`ring-[1rem]` → 16px (1rem = 16px)');
|
|
41
|
+
assert.equal(parseRingWidth('ring-inset'), null, '`ring-inset` is a modifier, not a width');
|
|
42
|
+
assert.equal(parseRingWidth('ring-offset-2'), null, '`ring-offset-N` is a separate utility');
|
|
43
|
+
assert.equal(parseRingWidth('ring-primary'), null, 'color utilities do not parse as width');
|
|
44
|
+
assert.equal(parseRingWidth('hover:ring-2'), null, 'variant-prefixed classes return null');
|
|
45
|
+
assert.equal(parseRingWidth('border-2'), null, 'unrelated utilities return null');
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// parseRingColor — color and color/opacity utilities.
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
const color = parseRingColor('ring-primary', colorGroup);
|
|
53
|
+
assert.ok(color, '`ring-primary` resolves to a color');
|
|
54
|
+
assert.equal(color!.r, parseInt('1d', 16) / 255, 'primary R');
|
|
55
|
+
assert.equal(color!.g, parseInt('4e', 16) / 255, 'primary G');
|
|
56
|
+
assert.equal(color!.b, parseInt('d8', 16) / 255, 'primary B');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
const color = parseRingColor('ring-primary/50', colorGroup);
|
|
61
|
+
assert.ok(color, '`ring-primary/50` resolves with opacity');
|
|
62
|
+
assert.equal(color!.a, 0.5, 'opacity 50 → 0.5');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
const color = parseRingColor('ring-destructive', colorGroup);
|
|
67
|
+
assert.ok(color, '`ring-destructive` resolves');
|
|
68
|
+
assert.equal(color!.r, parseInt('ef', 16) / 255, 'destructive R');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
assert.equal(parseRingColor('ring-2', colorGroup), null, 'width is not a color');
|
|
72
|
+
assert.equal(parseRingColor('ring', colorGroup), null, 'bare `ring` is width-only, not a color');
|
|
73
|
+
assert.equal(parseRingColor('ring-inset', colorGroup), null, '`ring-inset` is a modifier');
|
|
74
|
+
assert.equal(parseRingColor('ring-offset-primary', colorGroup), null, '`ring-offset-*` is a separate utility');
|
|
75
|
+
assert.equal(parseRingColor('ring-unknown', colorGroup), null, 'unknown token returns null');
|
|
76
|
+
assert.equal(parseRingColor('ring-[3px]', colorGroup), null, 'arbitrary widths are not colors');
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// getRingInfoFromClasses — the helper applyRingEffect calls.
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
{
|
|
83
|
+
const ring = getRingInfoFromClasses(['ring-2', 'ring-primary'], colorGroup);
|
|
84
|
+
assert.ok(ring, 'width + color → ring');
|
|
85
|
+
assert.equal(ring!.width, 2, 'width 2');
|
|
86
|
+
assert.equal(ring!.color.r, parseInt('1d', 16) / 255, 'primary R');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
// Bare `ring` defaults to 3px and uses the `ring` token color when no
|
|
91
|
+
// explicit color class is present.
|
|
92
|
+
const ring = getRingInfoFromClasses(['ring'], colorGroup);
|
|
93
|
+
assert.ok(ring, 'bare ring resolves with token fallback');
|
|
94
|
+
assert.equal(ring!.width, 3, 'default 3px');
|
|
95
|
+
assert.equal(ring!.color.r, parseInt('3b', 16) / 255, 'ring token R');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
{
|
|
99
|
+
// Color WITHOUT an explicit width is a CSS no-op — Tailwind's
|
|
100
|
+
// `ring-COLOR` sets `--tw-ring-color` but the ring stays
|
|
101
|
+
// invisible until a width utility is added. shadcn's
|
|
102
|
+
// invalid-input pattern relies on this:
|
|
103
|
+
// `aria-invalid:ring-destructive/20 focus-visible:ring-[3px]`
|
|
104
|
+
// → tinted ring when invalid, only visible (3px) when focused.
|
|
105
|
+
// Previously this helper defaulted to width 3 when ANY ring color
|
|
106
|
+
// was present — so the State Matrix `error` variant rendered with
|
|
107
|
+
// a doubled red ring outside the destructive border instead of
|
|
108
|
+
// just the border. Now: color-only → null (no ring).
|
|
109
|
+
const ring = getRingInfoFromClasses(['ring-primary'], colorGroup);
|
|
110
|
+
assert.equal(ring, null, 'color without width → null (no ring)');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
{
|
|
114
|
+
// Same contract for the shadcn invalid-input pattern: tinted color,
|
|
115
|
+
// no width, no visible ring.
|
|
116
|
+
const ring = getRingInfoFromClasses(['ring-destructive', 'border-destructive'], colorGroup);
|
|
117
|
+
assert.equal(ring, null, 'color + non-ring class without width → null');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
{
|
|
121
|
+
// Color + explicit width DOES render the ring.
|
|
122
|
+
const ring = getRingInfoFromClasses(['ring-destructive', 'ring-[3px]'], colorGroup);
|
|
123
|
+
assert.ok(ring, 'color + arbitrary width → ring renders');
|
|
124
|
+
assert.equal(ring!.width, 3, '3px from `ring-[3px]`');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
{
|
|
128
|
+
// No ring classes → null.
|
|
129
|
+
const ring = getRingInfoFromClasses(['border-2', 'rounded-md'], colorGroup);
|
|
130
|
+
assert.equal(ring, null, 'no ring classes → null');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
{
|
|
134
|
+
// ring-0 explicitly disables — width <= 0 returns null.
|
|
135
|
+
const ring = getRingInfoFromClasses(['ring-0'], colorGroup);
|
|
136
|
+
assert.equal(ring, null, '`ring-0` → null (no effect)');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
{
|
|
140
|
+
// Color group without `ring` falls back to `primary`.
|
|
141
|
+
const minimalGroup: Record<string, string> = { primary: '#000000' };
|
|
142
|
+
const ring = getRingInfoFromClasses(['ring'], minimalGroup);
|
|
143
|
+
assert.ok(ring, 'falls back to primary when ring token is absent');
|
|
144
|
+
assert.equal(ring!.color.r, 0, 'fallback color R');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
{
|
|
148
|
+
// Empty color group → null (no fallback available).
|
|
149
|
+
const ring = getRingInfoFromClasses(['ring'], {});
|
|
150
|
+
assert.equal(ring, null, 'no color group → null');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.log('ring-utility-regression: PASS (parseRingWidth + parseRingColor + getRingInfoFromClasses)');
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { Project, SyntaxKind, type Node } from 'ts-morph';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
// Regression: the Figma plugin sandbox (where code.js runs) chokes on
|
|
11
|
+
// value-side **spread** in function calls and array literals — that is
|
|
12
|
+
// the form documented in .ai/behaviour.md and the one that bit Slider
|
|
13
|
+
// (Math.min(...arr) silently threw and the whole render bailed). Object
|
|
14
|
+
// spread (esbuild down-compiles to Object.assign at target=es2017) and
|
|
15
|
+
// function rest parameters / destructuring rest are well-supported by
|
|
16
|
+
// the sandbox's engine and are NOT flagged here.
|
|
17
|
+
//
|
|
18
|
+
// We walk the AST and flag the dangerous form only:
|
|
19
|
+
//
|
|
20
|
+
// DANGEROUS (flagged):
|
|
21
|
+
// - SpreadElement (call args / array literal: fn(...x), [...x])
|
|
22
|
+
//
|
|
23
|
+
// SAFE (not flagged):
|
|
24
|
+
// - SpreadAssignment (object literal spread: { ...obj, k: v })
|
|
25
|
+
// - Parameter+rest (function rest param: function(...args))
|
|
26
|
+
// - BindingElement+rest (destructuring rest: const [a, ...rest] = arr)
|
|
27
|
+
// - Type-position rest (TS types are erased at compile time)
|
|
28
|
+
//
|
|
29
|
+
// Replacements when a flag fires (mirroring patches already shipped in
|
|
30
|
+
// the codebase):
|
|
31
|
+
// Math.min(...arr) / Math.max(...arr) → manual for-loop computing min/max
|
|
32
|
+
// arr.push(...other) → for (let i = 0; i < other.length; i++) arr.push(other[i])
|
|
33
|
+
// [...set] / [...arr, item] → Array.from(set) / arr.concat([item])
|
|
34
|
+
// const [a, ...rest] = arr → arr.slice(1)
|
|
35
|
+
// function (...args) → explicit array param
|
|
36
|
+
|
|
37
|
+
const SRC_DIR = path.resolve(__dirname, '..', 'src');
|
|
38
|
+
|
|
39
|
+
function gatherSrcFiles(dir: string, out: string[]): void {
|
|
40
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
41
|
+
const full = path.join(dir, entry.name);
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
gatherSrcFiles(full, out);
|
|
44
|
+
} else if (entry.isFile() && entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {
|
|
45
|
+
out.push(full);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const files: string[] = [];
|
|
51
|
+
gatherSrcFiles(SRC_DIR, files);
|
|
52
|
+
|
|
53
|
+
assert.ok(files.length > 0, 'expected to find .ts files under tools/figma-plugin/src/');
|
|
54
|
+
|
|
55
|
+
const project = new Project({
|
|
56
|
+
tsConfigFilePath: path.resolve(__dirname, '..', 'tsconfig.json'),
|
|
57
|
+
skipAddingFilesFromTsConfig: true,
|
|
58
|
+
});
|
|
59
|
+
for (const f of files) project.addSourceFileAtPath(f);
|
|
60
|
+
|
|
61
|
+
type Violation = { file: string; line: number; col: number; kind: string; snippet: string };
|
|
62
|
+
const violations: Violation[] = [];
|
|
63
|
+
|
|
64
|
+
function isInTypeContext(node: Node): boolean {
|
|
65
|
+
// Walk up; if we cross into a TypeNode / TypeReference / TypeLiteral
|
|
66
|
+
// (anything erased at compile time), the spread is type-only and safe.
|
|
67
|
+
let parent = node.getParent();
|
|
68
|
+
while (parent) {
|
|
69
|
+
const k = parent.getKind();
|
|
70
|
+
if (
|
|
71
|
+
k === SyntaxKind.TypeReference
|
|
72
|
+
|| k === SyntaxKind.TypeLiteral
|
|
73
|
+
|| k === SyntaxKind.IntersectionType
|
|
74
|
+
|| k === SyntaxKind.UnionType
|
|
75
|
+
|| k === SyntaxKind.TupleType
|
|
76
|
+
|| k === SyntaxKind.FunctionType
|
|
77
|
+
|| k === SyntaxKind.ConstructorType
|
|
78
|
+
|| k === SyntaxKind.TypeAliasDeclaration
|
|
79
|
+
|| k === SyntaxKind.InterfaceDeclaration
|
|
80
|
+
|| k === SyntaxKind.RestType
|
|
81
|
+
|| k === SyntaxKind.NamedTupleMember
|
|
82
|
+
) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
parent = parent.getParent();
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const sf of project.getSourceFiles()) {
|
|
91
|
+
const relative = path.relative(SRC_DIR, sf.getFilePath());
|
|
92
|
+
|
|
93
|
+
sf.forEachDescendant((node) => {
|
|
94
|
+
const k = node.getKind();
|
|
95
|
+
// DANGEROUS: SpreadElement in a call or array literal.
|
|
96
|
+
if (k === SyntaxKind.SpreadElement) {
|
|
97
|
+
if (isInTypeContext(node)) return;
|
|
98
|
+
const { line, column } = sf.getLineAndColumnAtPos(node.getStart());
|
|
99
|
+
violations.push({
|
|
100
|
+
file: relative,
|
|
101
|
+
line,
|
|
102
|
+
col: column,
|
|
103
|
+
kind: 'SpreadElement',
|
|
104
|
+
snippet: node.getText().slice(0, 80),
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// SpreadAssignment (object-literal spread), Parameter(rest), and
|
|
109
|
+
// BindingElement(rest) are INTENTIONALLY NOT flagged — they all
|
|
110
|
+
// compile to forms the sandbox engine supports. Keep an eye on
|
|
111
|
+
// this if the build target ever changes.
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (violations.length > 0) {
|
|
116
|
+
console.error('sandbox-spread-regression: FAIL — dangerous spread/rest leaked into src/');
|
|
117
|
+
for (const v of violations) {
|
|
118
|
+
console.error(` ${v.file}:${v.line}:${v.col} ${v.kind} ${v.snippet}`);
|
|
119
|
+
}
|
|
120
|
+
console.error('');
|
|
121
|
+
console.error('Replace each with Array.from / .concat / .slice / manual loop. See header of this file.');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(`sandbox-spread-regression: PASS (scanned ${files.length} files, 0 dangerous spread/rest in src/)`);
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { ComponentScanner } from './component-scanner';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Regression: a group element with a static `defaultValue` (or `value`)
|
|
7
|
+
* array/string must inject `defaultPressed="true"` on each child whose
|
|
8
|
+
* own `value` prop matches one of the group's values.
|
|
9
|
+
*
|
|
10
|
+
* The variant engine's `data-pressed` alias then activates
|
|
11
|
+
* `data-[pressed]:*` classes at render time, so e.g. ToggleGroup's
|
|
12
|
+
* selected item visibly toggles its `data-[pressed]:bg-accent` style.
|
|
13
|
+
*
|
|
14
|
+
* Statically models base-ui's ToggleGroup/RadioGroup runtime behaviour,
|
|
15
|
+
* which compares the group's value(s) against each item's value and
|
|
16
|
+
* pushes `data-pressed`/`data-checked` to the matching items. The Context
|
|
17
|
+
* cascade only carries variant/size — selection state lives on the group
|
|
18
|
+
* primitive's own `defaultValue` prop.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
interface JsxTextNode { type: 'text'; content: string }
|
|
22
|
+
interface JsxElementNode {
|
|
23
|
+
type: 'element';
|
|
24
|
+
tagName?: string;
|
|
25
|
+
props?: Record<string, string>;
|
|
26
|
+
children?: JsxNodeLike[];
|
|
27
|
+
}
|
|
28
|
+
type JsxNodeLike = JsxTextNode | JsxElementNode;
|
|
29
|
+
|
|
30
|
+
interface TestScannerView {
|
|
31
|
+
project: import('ts-morph').Project;
|
|
32
|
+
extractComponentJsxTree: (
|
|
33
|
+
sourceFile: import('ts-morph').SourceFile,
|
|
34
|
+
componentName: string
|
|
35
|
+
) => JsxNodeLike | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeScanner(): TestScannerView {
|
|
39
|
+
const scanner = new ComponentScanner({
|
|
40
|
+
componentPaths: [],
|
|
41
|
+
filePattern: '*.tsx',
|
|
42
|
+
exclude: [],
|
|
43
|
+
});
|
|
44
|
+
return scanner as unknown as TestScannerView;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function fixturePath(relative: string): string {
|
|
48
|
+
return path.resolve(process.cwd(), 'tools/figma-plugin/scanner/__fixtures__', relative);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function findElementsByTag(
|
|
52
|
+
node: JsxNodeLike | null | undefined,
|
|
53
|
+
tagName: string,
|
|
54
|
+
out: JsxElementNode[] = []
|
|
55
|
+
): JsxElementNode[] {
|
|
56
|
+
if (!node || node.type !== 'element') return out;
|
|
57
|
+
if (node.tagName === tagName) out.push(node);
|
|
58
|
+
if (node.children) for (const c of node.children) findElementsByTag(c, tagName, out);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const scanner = makeScanner();
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Case 1: array `defaultValue` — match injects defaultPressed on the
|
|
66
|
+
// matching item, leaves others untouched.
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
{
|
|
69
|
+
const file = scanner.project.createSourceFile(
|
|
70
|
+
fixturePath('toggle-group-array-default.tsx'),
|
|
71
|
+
`
|
|
72
|
+
function Story() {
|
|
73
|
+
return (
|
|
74
|
+
<div defaultValue={["center"]}>
|
|
75
|
+
<button value="left" />
|
|
76
|
+
<button value="center" />
|
|
77
|
+
<button value="right" />
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
export { Story };
|
|
82
|
+
`,
|
|
83
|
+
{ overwrite: true }
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const tree = scanner.extractComponentJsxTree(file, 'Story');
|
|
87
|
+
assert.ok(tree && tree.type === 'element', 'Story tree must build');
|
|
88
|
+
const buttons = findElementsByTag(tree, 'button');
|
|
89
|
+
assert.equal(buttons.length, 3, `expected 3 buttons, got ${buttons.length}`);
|
|
90
|
+
assert.equal(buttons[0].props?.defaultPressed, undefined, 'left: not selected, no injection');
|
|
91
|
+
assert.equal(buttons[1].props?.defaultPressed, 'true', 'center: matches, defaultPressed=true');
|
|
92
|
+
assert.equal(buttons[2].props?.defaultPressed, undefined, 'right: not selected, no injection');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Case 2: multi-value `defaultValue` array — all matching children pressed.
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
{
|
|
99
|
+
const file = scanner.project.createSourceFile(
|
|
100
|
+
fixturePath('toggle-group-multi-default.tsx'),
|
|
101
|
+
`
|
|
102
|
+
function Story() {
|
|
103
|
+
return (
|
|
104
|
+
<div defaultValue={["bold", "italic"]}>
|
|
105
|
+
<span value="bold" />
|
|
106
|
+
<span value="italic" />
|
|
107
|
+
<span value="underline" />
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
export { Story };
|
|
112
|
+
`,
|
|
113
|
+
{ overwrite: true }
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const tree = scanner.extractComponentJsxTree(file, 'Story');
|
|
117
|
+
assert.ok(tree && tree.type === 'element', 'Story tree must build');
|
|
118
|
+
const spans = findElementsByTag(tree, 'span');
|
|
119
|
+
assert.equal(spans.length, 3, `expected 3 spans, got ${spans.length}`);
|
|
120
|
+
assert.equal(spans[0].props?.defaultPressed, 'true', 'bold: matches');
|
|
121
|
+
assert.equal(spans[1].props?.defaultPressed, 'true', 'italic: matches');
|
|
122
|
+
assert.equal(spans[2].props?.defaultPressed, undefined, 'underline: no match');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Case 3: scalar `defaultValue` string — single match works.
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
{
|
|
129
|
+
const file = scanner.project.createSourceFile(
|
|
130
|
+
fixturePath('toggle-group-scalar-default.tsx'),
|
|
131
|
+
`
|
|
132
|
+
function Story() {
|
|
133
|
+
return (
|
|
134
|
+
<ul defaultValue="b">
|
|
135
|
+
<li value="a" />
|
|
136
|
+
<li value="b" />
|
|
137
|
+
<li value="c" />
|
|
138
|
+
</ul>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
export { Story };
|
|
142
|
+
`,
|
|
143
|
+
{ overwrite: true }
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const tree = scanner.extractComponentJsxTree(file, 'Story');
|
|
147
|
+
assert.ok(tree && tree.type === 'element', 'Story tree must build');
|
|
148
|
+
const items = findElementsByTag(tree, 'li');
|
|
149
|
+
assert.equal(items[0].props?.defaultPressed, undefined, 'a: no match');
|
|
150
|
+
assert.equal(items[1].props?.defaultPressed, 'true', 'b: scalar string match');
|
|
151
|
+
assert.equal(items[2].props?.defaultPressed, undefined, 'c: no match');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Case 4: `value` (not `defaultValue`) also works — controlled-mode in
|
|
156
|
+
// the runtime maps to the same selection-match logic statically.
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
{
|
|
159
|
+
const file = scanner.project.createSourceFile(
|
|
160
|
+
fixturePath('toggle-group-value-default.tsx'),
|
|
161
|
+
`
|
|
162
|
+
function Story() {
|
|
163
|
+
return (
|
|
164
|
+
<section value={["x"]}>
|
|
165
|
+
<article value="x" />
|
|
166
|
+
<article value="y" />
|
|
167
|
+
</section>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
export { Story };
|
|
171
|
+
`,
|
|
172
|
+
{ overwrite: true }
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const tree = scanner.extractComponentJsxTree(file, 'Story');
|
|
176
|
+
assert.ok(tree && tree.type === 'element', 'Story tree must build');
|
|
177
|
+
const articles = findElementsByTag(tree, 'article');
|
|
178
|
+
assert.equal(articles[0].props?.defaultPressed, 'true', 'first: matches "value" prop');
|
|
179
|
+
assert.equal(articles[1].props?.defaultPressed, undefined, 'second: no match');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Case 5: explicit `defaultPressed` on the child wins — selection-match
|
|
184
|
+
// MUST NOT override a deliberate consumer setting.
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
{
|
|
187
|
+
const file = scanner.project.createSourceFile(
|
|
188
|
+
fixturePath('toggle-group-explicit-pressed.tsx'),
|
|
189
|
+
`
|
|
190
|
+
function Story() {
|
|
191
|
+
return (
|
|
192
|
+
<div defaultValue={["a"]}>
|
|
193
|
+
<button value="a" defaultPressed={false} />
|
|
194
|
+
<button value="b" />
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
export { Story };
|
|
199
|
+
`,
|
|
200
|
+
{ overwrite: true }
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const tree = scanner.extractComponentJsxTree(file, 'Story');
|
|
204
|
+
assert.ok(tree && tree.type === 'element', 'Story tree must build');
|
|
205
|
+
const buttons = findElementsByTag(tree, 'button');
|
|
206
|
+
assert.equal(
|
|
207
|
+
String(buttons[0].props?.defaultPressed),
|
|
208
|
+
'false',
|
|
209
|
+
'explicit defaultPressed=false stays — selection-match must not override',
|
|
210
|
+
);
|
|
211
|
+
assert.equal(buttons[1].props?.defaultPressed, undefined, 'second: no match, no injection');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Case 6: no defaultValue/value on parent — no injection on any child.
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
{
|
|
218
|
+
const file = scanner.project.createSourceFile(
|
|
219
|
+
fixturePath('no-default-value.tsx'),
|
|
220
|
+
`
|
|
221
|
+
function Story() {
|
|
222
|
+
return (
|
|
223
|
+
<div>
|
|
224
|
+
<button value="a" />
|
|
225
|
+
<button value="b" />
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
export { Story };
|
|
230
|
+
`,
|
|
231
|
+
{ overwrite: true }
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const tree = scanner.extractComponentJsxTree(file, 'Story');
|
|
235
|
+
assert.ok(tree && tree.type === 'element', 'Story tree must build');
|
|
236
|
+
const buttons = findElementsByTag(tree, 'button');
|
|
237
|
+
assert.equal(buttons[0].props?.defaultPressed, undefined);
|
|
238
|
+
assert.equal(buttons[1].props?.defaultPressed, undefined);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log('selection-pressed-regression: PASS (6 cases)');
|