inkbridge 0.1.0-beta.21 → 0.1.0-beta.23
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 +29 -0
- package/code.js +15 -15
- package/manifest.json +1 -2
- package/package.json +40 -22
- package/scanner/border-dash-pattern-regression.ts +163 -0
- package/scanner/child-sizing-matrix-regression.ts +9 -0
- package/scanner/cli.ts +21 -5
- package/scanner/component-scanner.ts +1333 -77
- package/scanner/conditional-map-branch-regression.ts +180 -0
- package/scanner/css-token-reader.ts +66 -5
- package/scanner/dialog-content-gate-regression.ts +195 -0
- package/scanner/expression-evaluator-regression.ts +432 -0
- package/scanner/framework-adapter-shadcn-regression.ts +157 -1
- package/scanner/hidden-check-drift-regression.ts +125 -0
- package/scanner/horizontal-text-shrink-regression.ts +230 -0
- package/scanner/imported-array-map-regression.ts +195 -0
- package/scanner/inline-flex-regression.ts +5 -0
- package/scanner/intrinsic-sizing-regression.ts +333 -0
- package/scanner/portal-class-strip-regression.ts +109 -0
- package/scanner/responsive-hidden-inline-regression.ts +226 -0
- package/scanner/responsive-opt-in-regression.ts +212 -0
- package/scanner/select-root-flatten-regression.ts +314 -0
- package/scanner/space-between-single-child-regression.ts +163 -0
- package/scanner/story-args-resolution-regression.ts +311 -0
- package/scanner/story-dimensioning-regression.ts +76 -1
- package/scanner/style-map.ts +57 -0
- package/scanner/table-column-alignment-regression.ts +355 -0
- package/scanner/ternary-fragment-branch-regression.ts +196 -0
- package/scanner/text-truncate-regression.ts +481 -0
- package/scanner/types.ts +13 -0
- package/src/components/component-gen.ts +21 -38
- package/src/design-system/cva-master.ts +11 -18
- package/src/design-system/design-system.ts +36 -7
- package/src/design-system/frame-stabilizers.ts +109 -12
- package/src/design-system/preview-builder.ts +38 -0
- package/src/design-system/selectable-state.ts +8 -1
- package/src/design-system/story-builder.ts +62 -32
- package/src/design-system/story-dimensioning.ts +14 -3
- package/src/design-system/tag-predicates.ts +8 -0
- package/src/design-system/typography.ts +26 -0
- package/src/design-system/ui-builder.ts +188 -60
- package/src/effects/icon-builder.ts +8 -0
- package/src/framework-adapters/shadcn.ts +113 -0
- package/src/github/github.ts +22 -4
- package/src/layout/index.ts +4 -0
- package/src/layout/intrinsic-applier.ts +105 -0
- package/src/layout/intrinsic-sizing.ts +183 -0
- package/src/layout/layout-parser.ts +36 -0
- package/src/layout/parser/layout-mode.ts +14 -4
- package/src/layout/table-layout.ts +271 -0
- package/src/layout/text-truncate-pass.ts +151 -0
- package/src/layout/width-solver.ts +63 -17
- package/src/main.ts +37 -124
- package/src/plugin/config.ts +21 -0
- package/src/plugin/packs/pack-provider.ts +20 -4
- package/src/plugin/packs/packs.ts +14 -0
- package/src/render-engine-version.ts +1 -1
- package/src/tailwind/jsx-utils.ts +39 -0
- package/src/tailwind/node-ir.ts +8 -1
- package/src/tailwind/responsive-analyzer.ts +57 -3
- package/src/tailwind/tailwind.ts +344 -51
- package/src/text/index.ts +1 -0
- package/src/text/inline-text.ts +112 -12
- package/src/text/text-builder.ts +2 -2
- package/src/text/text-truncate.ts +101 -0
- package/src/tokens/tokens.ts +107 -16
- package/src/tokens/variables.ts +203 -46
- package/templates/scan-components-route.ts +8 -0
- package/ui.html +144 -43
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import { ComponentScanner } from './component-scanner';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Regression: the expression evaluator in `ComponentScanner.resolveExpressionValue`
|
|
8
|
+
* must handle three families of JSX expressions that previously rendered
|
|
9
|
+
* as raw arg values (or empty) in Figma. Each family corresponds to a
|
|
10
|
+
* phase in the rollout:
|
|
11
|
+
*
|
|
12
|
+
* P1. Built-in method calls and global conversions —
|
|
13
|
+
* `trade.txHash.slice(0, 6)`, `Number(value)`, `"x".padStart(...)`,
|
|
14
|
+
* `Math.abs(...)`, `Number.isFinite(...)`.
|
|
15
|
+
*
|
|
16
|
+
* P2. Pure-instance constructors at module scope —
|
|
17
|
+
* `const fmt = new Intl.NumberFormat("en-US", { ... });`
|
|
18
|
+
* followed by `fmt.format(N)` in the JSX body. The evaluator
|
|
19
|
+
* resolves the identifier to the live JS instance and calls
|
|
20
|
+
* `.format()` at scan time. Also covers `new Date(x).toISOString()`.
|
|
21
|
+
*
|
|
22
|
+
* P3. User-defined function evaluation — `formatUsd(trade.price)`
|
|
23
|
+
* where `formatUsd` is a pure arrow function in the same file or
|
|
24
|
+
* in an imported module (mirrors the perps utils pattern).
|
|
25
|
+
*
|
|
26
|
+
* Background: before this lands, the scanner returned the raw first
|
|
27
|
+
* argument for any user-function call and dropped method-chain results
|
|
28
|
+
* entirely. Figma rendered `172800000000` instead of `$172,800,000,000.00`
|
|
29
|
+
* and tx-hash cells collapsed to just the em-dash separator. Fix: make
|
|
30
|
+
* the evaluator a small AST interpreter that handles pure JS constructs
|
|
31
|
+
* found in real consumer code. Adding a new evaluator path? Add a case
|
|
32
|
+
* here too — silent drift between scanner reality and consumer
|
|
33
|
+
* expectations is the recurring bug class this fixture exists to prevent.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
interface JsxNodeLike {
|
|
37
|
+
type: 'element' | 'text';
|
|
38
|
+
tagName?: string;
|
|
39
|
+
content?: string;
|
|
40
|
+
props?: Record<string, unknown>;
|
|
41
|
+
children?: JsxNodeLike[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface StoryShape {
|
|
45
|
+
name: string;
|
|
46
|
+
jsxTree?: JsxNodeLike;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ScannedShape {
|
|
50
|
+
name: string;
|
|
51
|
+
stories?: StoryShape[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findElementByRole(node: JsxNodeLike | undefined | null, role: string): JsxNodeLike | null {
|
|
55
|
+
if (!node || node.type !== 'element') return null;
|
|
56
|
+
if (node.props?.['data-role'] === role) return node;
|
|
57
|
+
for (const child of node.children || []) {
|
|
58
|
+
const found = findElementByRole(child, role);
|
|
59
|
+
if (found) return found;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function collectText(node: JsxNodeLike | undefined | null, out: string[] = []): string[] {
|
|
65
|
+
if (!node) return out;
|
|
66
|
+
if (node.type === 'text') {
|
|
67
|
+
if (typeof node.content === 'string') out.push(node.content);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
for (const child of node.children || []) collectText(child, out);
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const FIXTURE_DIR = path.resolve(
|
|
75
|
+
process.cwd(),
|
|
76
|
+
'tools/figma-plugin/scanner/__fixtures__/expression-evaluator'
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
function writeFixtures(): void {
|
|
80
|
+
fs.mkdirSync(FIXTURE_DIR, { recursive: true });
|
|
81
|
+
|
|
82
|
+
// Shared constants module — exercises the imported-object-literal /
|
|
83
|
+
// imported-array-literal resolution path (P5). Without this, the
|
|
84
|
+
// perps TOKEN_METADATA[mint]?.symbol lookup returned undefined and
|
|
85
|
+
// the symbol display fell back to a 4-char mint slice.
|
|
86
|
+
fs.writeFileSync(
|
|
87
|
+
path.join(FIXTURE_DIR, 'constants.ts'),
|
|
88
|
+
`
|
|
89
|
+
export const TOKEN_LABELS = {
|
|
90
|
+
"So11111111111111111111111111111111111111112": { symbol: "SOL", name: "Solana" },
|
|
91
|
+
"7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs": { symbol: "ETH", name: "Ether" },
|
|
92
|
+
} as const;
|
|
93
|
+
|
|
94
|
+
export const SUPPORTED_MINTS: string[] = [
|
|
95
|
+
"So11111111111111111111111111111111111111112",
|
|
96
|
+
"7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs",
|
|
97
|
+
];
|
|
98
|
+
`,
|
|
99
|
+
'utf-8'
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Shared utils module — the user-function evaluator must follow the
|
|
103
|
+
// import and walk this file's function bodies. Mirrors the perps
|
|
104
|
+
// `~/domains/perps/utils` layout.
|
|
105
|
+
fs.writeFileSync(
|
|
106
|
+
path.join(FIXTURE_DIR, 'utils.ts'),
|
|
107
|
+
`
|
|
108
|
+
export const usdFormatter = new Intl.NumberFormat("en-US", {
|
|
109
|
+
style: "currency",
|
|
110
|
+
currency: "USD",
|
|
111
|
+
minimumFractionDigits: 2,
|
|
112
|
+
maximumFractionDigits: 2,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export const baseUnitsToNumber = (value?: string | null, decimals = 6) => {
|
|
116
|
+
if (value === undefined || value === null) return null;
|
|
117
|
+
const numeric = Number(value);
|
|
118
|
+
if (!Number.isFinite(numeric)) return null;
|
|
119
|
+
return numeric / 10 ** decimals;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const formatUsd = (value?: string | null, fallback = "-") => {
|
|
123
|
+
if (!value && value !== "0") return fallback;
|
|
124
|
+
const numeric = Number(value);
|
|
125
|
+
if (!Number.isFinite(numeric)) return fallback;
|
|
126
|
+
return usdFormatter.format(numeric);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const formatUsdFromBase = (value?: string | null) => {
|
|
130
|
+
const num = baseUnitsToNumber(value, 6);
|
|
131
|
+
return num === null ? "-" : usdFormatter.format(num);
|
|
132
|
+
};
|
|
133
|
+
`,
|
|
134
|
+
'utf-8'
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
fs.writeFileSync(
|
|
138
|
+
path.join(FIXTURE_DIR, 'Receipt.tsx'),
|
|
139
|
+
`
|
|
140
|
+
import { formatUsd, formatUsdFromBase } from "./utils";
|
|
141
|
+
import { TOKEN_LABELS, SUPPORTED_MINTS } from "./constants";
|
|
142
|
+
|
|
143
|
+
export interface Trade {
|
|
144
|
+
txHash: string;
|
|
145
|
+
priceRaw: string;
|
|
146
|
+
feeBaseUnits: string;
|
|
147
|
+
countBig: number;
|
|
148
|
+
mint: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface ReceiptProps {
|
|
152
|
+
trade: Trade;
|
|
153
|
+
// Optional list — when undefined, the component must still treat
|
|
154
|
+
// the visibility gate as falsy and pick the DEFAULT branch.
|
|
155
|
+
activeGroups?: Array<"price" | "market">;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const DEFAULT_ACTIVE: Array<"price" | "market"> = ["price", "market"];
|
|
159
|
+
|
|
160
|
+
// Module-level Intl instance — P2 must follow the identifier back to
|
|
161
|
+
// the constructor and dispatch \`.format()\` against a live instance.
|
|
162
|
+
const inlineUsdFmt = new Intl.NumberFormat("en-US", {
|
|
163
|
+
style: "currency",
|
|
164
|
+
currency: "USD",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
export function Receipt({ trade, activeGroups }: ReceiptProps) {
|
|
168
|
+
// P4: optional-prop guard + Set + Set.has — the DataSourceDetailsDialog
|
|
169
|
+
// visibility-gate pattern. \`activeGroups\` is missing in args, so the
|
|
170
|
+
// \`&&\` must short-circuit to false and the ternary must pick
|
|
171
|
+
// DEFAULT_ACTIVE. \`new Set(...)\` constructs at scan time. \`.has(...)\`
|
|
172
|
+
// dispatches against the live Set prototype.
|
|
173
|
+
const active = new Set(activeGroups && activeGroups.length ? activeGroups : DEFAULT_ACTIVE);
|
|
174
|
+
return (
|
|
175
|
+
<div>
|
|
176
|
+
{/* P1: String.prototype.slice with positive + negative indices */}
|
|
177
|
+
<span data-role="hash-short">
|
|
178
|
+
{trade.txHash.slice(0, 6)}…{trade.txHash.slice(-4)}
|
|
179
|
+
</span>
|
|
180
|
+
{/* P1: Number global + Number method */}
|
|
181
|
+
<span data-role="count-padded">{Number(trade.countBig).toString().padStart(8, "0")}</span>
|
|
182
|
+
{/* P1: Math global */}
|
|
183
|
+
<span data-role="abs-of-neg">{Math.abs(-42)}</span>
|
|
184
|
+
{/* P2: Intl.NumberFormat instance + .format() chain */}
|
|
185
|
+
<span data-role="price-inline">{inlineUsdFmt.format(Number(trade.priceRaw))}</span>
|
|
186
|
+
{/* P3: user-function evaluation, single arg */}
|
|
187
|
+
<span data-role="price-user-fn">{formatUsd(trade.priceRaw)}</span>
|
|
188
|
+
{/* P3: chained user-function evaluation (formatUsdFromBase ->
|
|
189
|
+
baseUnitsToNumber -> usdFormatter.format) */}
|
|
190
|
+
<span data-role="fee-from-base">{formatUsdFromBase(trade.feeBaseUnits)}</span>
|
|
191
|
+
{/* P3: user-function with default-param fallback path */}
|
|
192
|
+
<span data-role="missing-fallback">{formatUsd(null)}</span>
|
|
193
|
+
{/* P3: IIFE — inline arrow invoked immediately to scope a few
|
|
194
|
+
statements inside a single JSX expression. Real-world case:
|
|
195
|
+
perps RecentTradeDetailsDialog Collateral Delta cell. Tests
|
|
196
|
+
if-return, local const binding, template literal, and a
|
|
197
|
+
chained user-fn + Intl call inside an anonymous arrow. */}
|
|
198
|
+
<span data-role="delta-iife">
|
|
199
|
+
{(() => {
|
|
200
|
+
const delta = formatUsdFromBase(trade.priceRaw);
|
|
201
|
+
if (delta === "-") return "-";
|
|
202
|
+
const label = trade.countBig > 0 ? "up" : "down";
|
|
203
|
+
return label + " " + delta;
|
|
204
|
+
})()}
|
|
205
|
+
</span>
|
|
206
|
+
{/* P4: Set membership gate. Both branches must render when the
|
|
207
|
+
Set was built from DEFAULT_ACTIVE (active.has returns true). */}
|
|
208
|
+
{active.has("price") ? <span data-role="set-price-section">price-on</span> : null}
|
|
209
|
+
{active.has("market") ? <span data-role="set-market-section">market-on</span> : null}
|
|
210
|
+
{/* P4: Set negative case — missing key must return false and
|
|
211
|
+
hide the section, not undefined (which would drop on
|
|
212
|
+
JSX-walker fall-through). */}
|
|
213
|
+
{active.has("nonexistent") ? <span data-role="set-missing-section">should-not-render</span> : <span data-role="set-missing-fallback">missing-off</span>}
|
|
214
|
+
{/* P5: simple imported-object lookup (no cast) */}
|
|
215
|
+
<span data-role="symbol-direct">{TOKEN_LABELS[trade.mint]?.symbol ?? "unknown"}</span>
|
|
216
|
+
{/* P5: AsExpression unwrap — \`(trade as any).mint\` must
|
|
217
|
+
evaluate through the cast so the imported-object lookup
|
|
218
|
+
below can read .symbol. Mirrors the perps pattern
|
|
219
|
+
\`TOKEN_METADATA[(position as any).marketMint]?.symbol\`. */}
|
|
220
|
+
<span data-role="symbol-via-cast">{TOKEN_LABELS[(trade as any).mint]?.symbol ?? "unknown"}</span>
|
|
221
|
+
{/* P5: Imported array literal — SUPPORTED_MINTS comes from
|
|
222
|
+
./constants. \`.includes(trade.mint)\` must dispatch
|
|
223
|
+
against the resolved array (not undefined). */}
|
|
224
|
+
<span data-role="mint-supported">{SUPPORTED_MINTS.includes(trade.mint) ? "supported" : "unsupported"}</span>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
`,
|
|
229
|
+
'utf-8'
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
fs.writeFileSync(
|
|
233
|
+
path.join(FIXTURE_DIR, 'Receipt.stories.tsx'),
|
|
234
|
+
`
|
|
235
|
+
import { Receipt } from "./Receipt";
|
|
236
|
+
const meta = { component: Receipt };
|
|
237
|
+
export default meta;
|
|
238
|
+
|
|
239
|
+
export const Default = {
|
|
240
|
+
args: {
|
|
241
|
+
trade: {
|
|
242
|
+
txHash: "5xKx8mP3qN7vYbR2tF6gH4nDcWeZL9aJpKmU8oXyB1sErT3hQ",
|
|
243
|
+
priceRaw: "172800",
|
|
244
|
+
feeBaseUnits: "2500000",
|
|
245
|
+
countBig: 7,
|
|
246
|
+
mint: "So11111111111111111111111111111111111111112",
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
`,
|
|
251
|
+
'utf-8'
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function cleanupFixtures(): void {
|
|
256
|
+
try {
|
|
257
|
+
fs.rmSync(FIXTURE_DIR, { recursive: true, force: true });
|
|
258
|
+
} catch {
|
|
259
|
+
/* ignore */
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function run(): Promise<void> {
|
|
264
|
+
writeFixtures();
|
|
265
|
+
|
|
266
|
+
const scanner = new ComponentScanner({
|
|
267
|
+
componentPaths: [FIXTURE_DIR],
|
|
268
|
+
filePattern: '*.tsx',
|
|
269
|
+
exclude: [],
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const results = (await scanner.scanAll()) as unknown as ScannedShape[];
|
|
273
|
+
const receipt = results.find((r) => r.name === 'Receipt');
|
|
274
|
+
assert.ok(receipt, `must find Receipt; got: ${results.map((r) => r.name).join(', ')}`);
|
|
275
|
+
const story = (receipt.stories || []).find((s) => s.name === 'Default');
|
|
276
|
+
assert.ok(story?.jsxTree, 'Default story must produce a jsxTree');
|
|
277
|
+
|
|
278
|
+
const textForRole = (role: string): string => {
|
|
279
|
+
const el = findElementByRole(story.jsxTree, role);
|
|
280
|
+
assert.ok(el, `expected element with data-role="${role}" in story tree`);
|
|
281
|
+
return collectText(el).join('');
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// P1: built-in string/number method calls + global conversions
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
{
|
|
288
|
+
const hashShort = textForRole('hash-short');
|
|
289
|
+
assert.equal(
|
|
290
|
+
hashShort,
|
|
291
|
+
'5xKx8m…T3hQ',
|
|
292
|
+
`P1 string.slice(start)/slice(-n) must evaluate to the truncated hash; got "${hashShort}"`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
{
|
|
296
|
+
const countPadded = textForRole('count-padded');
|
|
297
|
+
assert.equal(
|
|
298
|
+
countPadded,
|
|
299
|
+
'00000007',
|
|
300
|
+
`P1 Number(...).toString().padStart(...) chain must evaluate; got "${countPadded}"`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
{
|
|
304
|
+
const abs = textForRole('abs-of-neg');
|
|
305
|
+
assert.equal(abs, '42', `P1 Math.abs(-42) must evaluate to "42"; got "${abs}"`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// P2: Intl.NumberFormat module instance + .format()
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
{
|
|
312
|
+
const priceInline = textForRole('price-inline');
|
|
313
|
+
assert.equal(
|
|
314
|
+
priceInline,
|
|
315
|
+
'$172,800.00',
|
|
316
|
+
`P2 inline Intl.NumberFormat instance must format numeric arg; got "${priceInline}"`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// P3: user-function evaluation (single arg, chained, default-param)
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
{
|
|
324
|
+
const priceUserFn = textForRole('price-user-fn');
|
|
325
|
+
assert.equal(
|
|
326
|
+
priceUserFn,
|
|
327
|
+
'$172,800.00',
|
|
328
|
+
`P3 formatUsd(priceRaw) must evaluate via the imported arrow function and Intl chain; got "${priceUserFn}"`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
{
|
|
332
|
+
const feeFromBase = textForRole('fee-from-base');
|
|
333
|
+
assert.equal(
|
|
334
|
+
feeFromBase,
|
|
335
|
+
'$2.50',
|
|
336
|
+
`P3 formatUsdFromBase -> baseUnitsToNumber chain ($2500000 / 10^6 = 2.5) must format to "$2.50"; got "${feeFromBase}"`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
{
|
|
340
|
+
const missingFallback = textForRole('missing-fallback');
|
|
341
|
+
assert.equal(
|
|
342
|
+
missingFallback,
|
|
343
|
+
'-',
|
|
344
|
+
`P3 user-fn must honour the default-param fallback path when arg is null; got "${missingFallback}"`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
{
|
|
348
|
+
const deltaIife = textForRole('delta-iife');
|
|
349
|
+
// 172800 / 10**6 (USD_DECIMALS in baseUnitsToNumber) = 0.1728 → "$0.17"
|
|
350
|
+
// Prefixed with "up " by the IIFE's binary-op + ternary path.
|
|
351
|
+
assert.equal(
|
|
352
|
+
deltaIife,
|
|
353
|
+
'up $0.17',
|
|
354
|
+
`P3 IIFE must interpret inline arrow body (if-return + ternary + string concat + chained user-fn / Intl); got "${deltaIife}"`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
// P4: Set construction + .has() membership gate, plus optional-prop short
|
|
360
|
+
// circuit (activeGroups undefined → ternary picks DEFAULT_ACTIVE).
|
|
361
|
+
//
|
|
362
|
+
// Real-world trigger: perps DataSourceDetailsDialog renders nothing past
|
|
363
|
+
// the header when activeGroups is omitted because the scanner couldn't
|
|
364
|
+
// resolve `new Set(...)` / `.has(...)` and every section's
|
|
365
|
+
// `isXActive ? <Section/> : null` evaluated to undefined.
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
{
|
|
368
|
+
const priceSection = textForRole('set-price-section');
|
|
369
|
+
assert.equal(
|
|
370
|
+
priceSection,
|
|
371
|
+
'price-on',
|
|
372
|
+
`P4 Set.has("price") (from DEFAULT_ACTIVE) must return true → section renders; got "${priceSection}"`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
{
|
|
376
|
+
const marketSection = textForRole('set-market-section');
|
|
377
|
+
assert.equal(
|
|
378
|
+
marketSection,
|
|
379
|
+
'market-on',
|
|
380
|
+
`P4 Set.has("market") (from DEFAULT_ACTIVE) must return true → section renders; got "${marketSection}"`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
{
|
|
384
|
+
const missingFallbackSection = textForRole('set-missing-fallback');
|
|
385
|
+
assert.equal(
|
|
386
|
+
missingFallbackSection,
|
|
387
|
+
'missing-off',
|
|
388
|
+
`P4 Set.has("nonexistent") must return false → ternary picks the fallback span; got "${missingFallbackSection}"`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
// P5: AsExpression unwrap + imported object/array literal resolution.
|
|
394
|
+
// Real-world trigger: perps StopLossModal renders `LONG So11` (4-char
|
|
395
|
+
// mint slice fallback) instead of `LONG SOL` because `TOKEN_METADATA[
|
|
396
|
+
// (position as any).marketMint]?.symbol` couldn't resolve through the
|
|
397
|
+
// type assertion and the imported lookup table.
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
{
|
|
400
|
+
const symbolDirect = textForRole('symbol-direct');
|
|
401
|
+
assert.equal(
|
|
402
|
+
symbolDirect,
|
|
403
|
+
'SOL',
|
|
404
|
+
`P5 imported-object lookup (no cast) must resolve TOKEN_LABELS[trade.mint].symbol; got "${symbolDirect}"`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
{
|
|
408
|
+
const symbolViaCast = textForRole('symbol-via-cast');
|
|
409
|
+
assert.equal(
|
|
410
|
+
symbolViaCast,
|
|
411
|
+
'SOL',
|
|
412
|
+
`P5 (trade as any).mint must unwrap through AsExpression AND TOKEN_LABELS imported object lookup must resolve; got "${symbolViaCast}"`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
{
|
|
416
|
+
const mintSupported = textForRole('mint-supported');
|
|
417
|
+
assert.equal(
|
|
418
|
+
mintSupported,
|
|
419
|
+
'supported',
|
|
420
|
+
`P5 imported array (SUPPORTED_MINTS).includes(trade.mint) must dispatch against the resolved array; got "${mintSupported}"`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
console.log('expression-evaluator-regression: PASS (13 cases — P1 + P2 + P3 + IIFE + P4 Set + P5 cast/imports)');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
run()
|
|
428
|
+
.catch((err) => {
|
|
429
|
+
cleanupFixtures();
|
|
430
|
+
throw err;
|
|
431
|
+
})
|
|
432
|
+
.then(cleanupFixtures);
|
|
@@ -477,4 +477,160 @@ function scrollAreaTree(extraScrollbarClasses: string[] = []): NodeIR {
|
|
|
477
477
|
);
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
-
|
|
480
|
+
// (i3) When the ScrollArea wraps a `<table>`, replace
|
|
481
|
+
// ScrollAreaPrimitive.Root with a plain `<div>` that inherits Root's
|
|
482
|
+
// classes (the consumer's padding from the `className` prop). The
|
|
483
|
+
// synthetic div is `kind: 'element'` so it gets the normal block-
|
|
484
|
+
// stretch-to-parent-width treatment instead of staying HUG-primary
|
|
485
|
+
// like the original `kind: 'component'` Root.
|
|
486
|
+
{
|
|
487
|
+
const tableInsideScrollArea = el({}, [], [
|
|
488
|
+
{
|
|
489
|
+
kind: 'element',
|
|
490
|
+
tagName: 'ScrollAreaPrimitive.Root',
|
|
491
|
+
tagLower: 'div',
|
|
492
|
+
props: {},
|
|
493
|
+
classes: ['relative', 'overflow-hidden', 'px-4', 'sm:pl-3', 'sm:pr-0'],
|
|
494
|
+
children: [
|
|
495
|
+
{
|
|
496
|
+
kind: 'element',
|
|
497
|
+
tagName: 'ScrollAreaPrimitive.Viewport',
|
|
498
|
+
tagLower: 'div',
|
|
499
|
+
props: {},
|
|
500
|
+
classes: ['h-full', 'w-full'],
|
|
501
|
+
children: [{
|
|
502
|
+
kind: 'element',
|
|
503
|
+
tagName: 'table',
|
|
504
|
+
tagLower: 'table',
|
|
505
|
+
props: {},
|
|
506
|
+
classes: ['min-w-full'],
|
|
507
|
+
children: [],
|
|
508
|
+
}],
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
kind: 'element',
|
|
512
|
+
tagName: 'ScrollAreaPrimitive.Scrollbar',
|
|
513
|
+
tagLower: 'div',
|
|
514
|
+
props: {},
|
|
515
|
+
classes: [],
|
|
516
|
+
children: [],
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
} as unknown as NodeIR,
|
|
520
|
+
]);
|
|
521
|
+
const next = applyShadcnAdapter(tableInsideScrollArea);
|
|
522
|
+
const top = (next as { children: NodeIR[] }).children;
|
|
523
|
+
assert.equal(top.length, 1, 'after rewrite the wrapper still has a single direct child');
|
|
524
|
+
const wrapper = top[0] as { tagName?: string; classes?: string[]; children?: NodeIR[] };
|
|
525
|
+
assert.equal(wrapper.tagName, 'div', 'Root is replaced by a plain <div>');
|
|
526
|
+
// The scrollbar-gutter mirror: `sm:pr-0` was paired with `sm:pl-3`,
|
|
527
|
+
// so the strip swaps it for `sm:pr-3`. Other classes are preserved.
|
|
528
|
+
// (Without this, row separators inside the table run flush to the
|
|
529
|
+
// card's right edge because Figma has no scrollbar to absorb the
|
|
530
|
+
// gap that the `pr-0` was reserving in CSS.)
|
|
531
|
+
assert.deepEqual(
|
|
532
|
+
wrapper.classes,
|
|
533
|
+
['relative', 'overflow-hidden', 'px-4', 'sm:pl-3', 'sm:pr-3'],
|
|
534
|
+
"scrollbar-gutter sm:pr-0 mirrored to sm:pr-3 (matching sm:pl-3)",
|
|
535
|
+
);
|
|
536
|
+
assert.equal(wrapper.children?.length, 1, 'Scrollbar dropped, Viewport flattened — only the table survives inside');
|
|
537
|
+
assert.equal(
|
|
538
|
+
(wrapper.children?.[0] as { tagName?: string }).tagName,
|
|
539
|
+
'table',
|
|
540
|
+
'the table sits directly inside the synthetic div',
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// (i4) ScrollArea wrapping non-table content keeps the Root wrapper.
|
|
545
|
+
// Standalone `<ScrollArea>` stories rely on the wrapper for their
|
|
546
|
+
// visual shape; only the table case gets the structural rewrite.
|
|
547
|
+
{
|
|
548
|
+
const nonTableScrollArea = el({}, [], [
|
|
549
|
+
{
|
|
550
|
+
kind: 'element',
|
|
551
|
+
tagName: 'ScrollAreaPrimitive.Root',
|
|
552
|
+
tagLower: 'div',
|
|
553
|
+
props: {},
|
|
554
|
+
classes: ['relative'],
|
|
555
|
+
children: [
|
|
556
|
+
{
|
|
557
|
+
kind: 'element',
|
|
558
|
+
tagName: 'ScrollAreaPrimitive.Viewport',
|
|
559
|
+
tagLower: 'div',
|
|
560
|
+
props: {},
|
|
561
|
+
classes: [],
|
|
562
|
+
children: [el({}, ['p-3'], [])],
|
|
563
|
+
},
|
|
564
|
+
],
|
|
565
|
+
} as unknown as NodeIR,
|
|
566
|
+
]);
|
|
567
|
+
const next = applyShadcnAdapter(nonTableScrollArea);
|
|
568
|
+
const top = (next as { children: NodeIR[] }).children;
|
|
569
|
+
assert.equal(top.length, 1, 'non-table ScrollArea keeps its Root');
|
|
570
|
+
assert.equal(
|
|
571
|
+
(top[0] as { tagName?: string }).tagName,
|
|
572
|
+
'ScrollAreaPrimitive.Root',
|
|
573
|
+
'Root must survive when its subtree does NOT contain a table',
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// (i5) Scrollbar-gutter mirror edge cases.
|
|
578
|
+
// - `pr-0` with no matching `pl-N` in the same variant is left alone
|
|
579
|
+
// (could be a genuine edge-aligned design).
|
|
580
|
+
// - `px-N` shorthand counts as a same-breakpoint `pl-N` source, so
|
|
581
|
+
// `px-4 sm:pr-0` mirrors to `px-4 sm:pr-4`.
|
|
582
|
+
{
|
|
583
|
+
// Edge A: only sm:pr-0, no sm:pl. Leave alone.
|
|
584
|
+
const onlyPrZero = el({}, [], [
|
|
585
|
+
{
|
|
586
|
+
kind: 'element',
|
|
587
|
+
tagName: 'ScrollAreaPrimitive.Root',
|
|
588
|
+
tagLower: 'div',
|
|
589
|
+
props: {},
|
|
590
|
+
classes: ['sm:pr-0'],
|
|
591
|
+
children: [{
|
|
592
|
+
kind: 'element',
|
|
593
|
+
tagName: 'table',
|
|
594
|
+
tagLower: 'table',
|
|
595
|
+
props: {},
|
|
596
|
+
classes: [],
|
|
597
|
+
children: [],
|
|
598
|
+
}],
|
|
599
|
+
} as unknown as NodeIR,
|
|
600
|
+
]);
|
|
601
|
+
const out = applyShadcnAdapter(onlyPrZero);
|
|
602
|
+
const wrapper = (out as { children: NodeIR[] }).children[0] as { classes?: string[] };
|
|
603
|
+
assert.deepEqual(wrapper.classes, ['sm:pr-0'], 'lone sm:pr-0 (no matching pl) is preserved');
|
|
604
|
+
|
|
605
|
+
// Edge B: px-4 counts as pl source — sm:pr-0 mirrors to sm:pr-0… or 4?
|
|
606
|
+
// Note: px-4 has no variant, so it provides the base ('') pl source,
|
|
607
|
+
// not the sm: source. sm:pr-0 doesn't match because no sm:pl.
|
|
608
|
+
// Result: sm:pr-0 left alone, but a *base* `pr-0` (if present) would
|
|
609
|
+
// mirror against px-4.
|
|
610
|
+
const pxWithPrZero = el({}, [], [
|
|
611
|
+
{
|
|
612
|
+
kind: 'element',
|
|
613
|
+
tagName: 'ScrollAreaPrimitive.Root',
|
|
614
|
+
tagLower: 'div',
|
|
615
|
+
props: {},
|
|
616
|
+
classes: ['px-4', 'pr-0'],
|
|
617
|
+
children: [{
|
|
618
|
+
kind: 'element',
|
|
619
|
+
tagName: 'table',
|
|
620
|
+
tagLower: 'table',
|
|
621
|
+
props: {},
|
|
622
|
+
classes: [],
|
|
623
|
+
children: [],
|
|
624
|
+
}],
|
|
625
|
+
} as unknown as NodeIR,
|
|
626
|
+
]);
|
|
627
|
+
const out2 = applyShadcnAdapter(pxWithPrZero);
|
|
628
|
+
const wrapper2 = (out2 as { children: NodeIR[] }).children[0] as { classes?: string[] };
|
|
629
|
+
assert.deepEqual(
|
|
630
|
+
wrapper2.classes,
|
|
631
|
+
['px-4', 'pr-4'],
|
|
632
|
+
'base pr-0 mirrors against base px-4 (px contributes pl in same variant)',
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
console.log('framework-adapter-shadcn-regression: PASS (3 registry + 4 leaf + 4 avatar + 5 progress + 5 slider + 5 scroll-area cases)');
|