@usefragments/core 1.10.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-MZ4SW3TP.js → chunk-3IOWHECM.js} +8 -1
- package/dist/{chunk-MZ4SW3TP.js.map → chunk-3IOWHECM.js.map} +1 -1
- package/dist/chunk-7ULGH74M.js +66 -0
- package/dist/chunk-7ULGH74M.js.map +1 -0
- package/dist/{chunk-XN3LSDPY.js → chunk-BMPYIUZE.js} +612 -344
- package/dist/chunk-BMPYIUZE.js.map +1 -0
- package/dist/{chunk-YF65VYRY.js → chunk-ML5S6QNU.js} +191 -169
- package/dist/chunk-ML5S6QNU.js.map +1 -0
- package/dist/{chunk-3LLRNCPX.js → chunk-MZ2FS7U4.js} +1 -1
- package/dist/chunk-MZ2FS7U4.js.map +1 -0
- package/dist/chunk-PWIJMOI4.js +202 -0
- package/dist/chunk-PWIJMOI4.js.map +1 -0
- package/dist/chunk-RYFULE43.js +578 -0
- package/dist/chunk-RYFULE43.js.map +1 -0
- package/dist/codes/index.d.ts +2 -2
- package/dist/codes/index.js +3 -2
- package/dist/compiled-types/index.d.ts +327 -2
- package/dist/compiled-types/index.js +1 -1
- package/dist/generate/index.d.ts +3 -2
- package/dist/{governance-CLk_wkP9.d.ts → governance-hOPXGbbs.d.ts} +474 -515
- package/dist/governance-telemetry.d.ts +6 -0
- package/dist/governance-telemetry.js +1 -1
- package/dist/{index-_sxhUNqx.d.ts → index-C8bcXVav.d.ts} +448 -448
- package/dist/index.d.ts +3280 -1305
- package/dist/index.js +2414 -350
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +228 -0
- package/dist/manifest.js +24 -0
- package/dist/manifest.js.map +1 -0
- package/dist/preview/index.js +45 -1
- package/dist/preview/index.js.map +1 -1
- package/dist/preview-runtime.d.ts +1 -2
- package/dist/preview-runtime.js +150 -14
- package/dist/preview-runtime.js.map +1 -1
- package/dist/react-types.d.ts +1 -2
- package/dist/registry.d.ts +1412 -203
- package/dist/registry.js +30 -3
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +3 -2
- package/dist/storyAdapter.d.ts +1 -2
- package/dist/storyAdapter.js +11 -49
- package/dist/storyAdapter.js.map +1 -1
- package/dist/test-utils.d.ts +3 -2
- package/dist/topology/index.d.ts +1 -1
- package/dist/topology/index.js +1 -1
- package/package.json +8 -2
- package/src/__tests__/contract-parser.test.ts +318 -277
- package/src/__tests__/preview-runtime-hook.test.tsx +315 -0
- package/src/__tests__/preview-runtime.test.tsx +30 -8
- package/src/__tests__/schema.test.ts +191 -14
- package/src/analysis-plan/analysis-plan-v1.test.ts +320 -0
- package/src/analysis-plan/coverage.ts +181 -0
- package/src/analysis-plan/digest.ts +141 -0
- package/src/analysis-plan/index.ts +34 -0
- package/src/analysis-plan/types.ts +207 -0
- package/src/approved-contract-tokens.test.ts +39 -0
- package/src/approved-contract-tokens.ts +18 -0
- package/src/codes/__tests__/codes.test.ts +13 -0
- package/src/codes/codes.ts +40 -0
- package/src/compiled-types/index.ts +640 -39
- package/src/compiled-types/parse.test.ts +145 -4
- package/src/component-contract.ts +95 -53
- package/src/composition.ts +7 -13
- package/src/constants.ts +3 -6
- package/src/contract/hash.test.ts +20 -0
- package/src/contract/hash.ts +66 -9
- package/src/contract/index.ts +24 -1
- package/src/contract/manifest.test.ts +94 -0
- package/src/contract/manifest.ts +68 -0
- package/src/contract/preimage.test.ts +219 -1
- package/src/contract/preimage.ts +326 -6
- package/src/contract/stamp.test.ts +3 -0
- package/src/contract/stamp.ts +1 -1
- package/src/contract-parser.ts +54 -30
- package/src/defineFragment.test.ts +476 -91
- package/src/defineFragment.ts +204 -114
- package/src/domain-ids.test.ts +35 -0
- package/src/domain-ids.ts +61 -0
- package/src/evaluation/evaluate.test.ts +522 -0
- package/src/evaluation/evaluate.ts +690 -0
- package/src/evaluation/evaluation-v2-receipt-v1.test.ts +772 -0
- package/src/evaluation/index.ts +58 -0
- package/src/evaluation/receipt.ts +753 -0
- package/src/evaluation/types.ts +406 -0
- package/src/facts/builders.ts +2 -0
- package/src/facts/compile.ts +29 -6
- package/src/facts/fact-index.ts +13 -3
- package/src/facts/fact-integrity-v1.test.ts +172 -0
- package/src/facts/facts.test.ts +15 -0
- package/src/facts/ids.ts +46 -3
- package/src/facts/index.ts +14 -1
- package/src/facts/integrity.ts +134 -0
- package/src/facts/types.ts +36 -0
- package/src/governance-integrity.test.ts +1 -0
- package/src/governance-integrity.ts +5 -3
- package/src/governance-telemetry.ts +8 -0
- package/src/governance.ts +70 -8
- package/src/index.ts +230 -37
- package/src/preview/validation.test.ts +62 -0
- package/src/preview/validation.ts +48 -2
- package/src/preview-runtime.tsx +227 -20
- package/src/registry-install-plan.ts +200 -109
- package/src/registry-shards.test.ts +263 -0
- package/src/registry.ts +237 -0
- package/src/repository-binding.test.ts +50 -0
- package/src/repository-binding.ts +96 -0
- package/src/rules/families.test.ts +36 -0
- package/src/rules/finding.ts +7 -2
- package/src/rules/index.ts +17 -1
- package/src/rules/rule-config.test.ts +66 -0
- package/src/rules/rule-config.ts +73 -0
- package/src/rules/rules.test.ts +26 -0
- package/src/rules/tokens-css-vars-must-be-defined.test.ts +51 -2
- package/src/rules/tokens-css-vars-must-be-defined.ts +34 -1
- package/src/schema.ts +293 -113
- package/src/schemas/index.ts +1 -1
- package/src/storyAdapter.test.ts +68 -12
- package/src/storyAdapter.ts +44 -75
- package/src/topology/resolve-area.ts +1 -1
- package/src/types.ts +258 -40
- package/dist/chunk-3LLRNCPX.js.map +0 -1
- package/dist/chunk-RANPUC6C.js +0 -72
- package/dist/chunk-RANPUC6C.js.map +0 -1
- package/dist/chunk-XN3LSDPY.js.map +0 -1
- package/dist/chunk-YF65VYRY.js.map +0 -1
- package/src/fragment-types.ts +0 -214
- package/src/react-create-element.test.ts +0 -22
- package/src/react-create-element.ts +0 -12
- package/src/storyFilters.test.ts +0 -350
- package/src/storyFilters.ts +0 -253
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { describe, it, expect } from
|
|
2
|
-
import { parseComponentContract, isContractFile } from
|
|
3
|
-
import { componentContractSchema } from
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseComponentContract, isContractFile } from "../contract-parser.js";
|
|
3
|
+
import { componentContractSchema } from "../component-contract.js";
|
|
4
4
|
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
// Helpers
|
|
@@ -8,42 +8,42 @@ import { componentContractSchema } from '../component-contract.js';
|
|
|
8
8
|
|
|
9
9
|
function makeContract(overrides: Record<string, unknown> = {}): string {
|
|
10
10
|
return JSON.stringify({
|
|
11
|
-
$schema:
|
|
12
|
-
name:
|
|
13
|
-
description:
|
|
14
|
-
category:
|
|
15
|
-
sourcePath:
|
|
16
|
-
exportName:
|
|
11
|
+
$schema: "https://usefragments.com/schemas/contract.v1.json",
|
|
12
|
+
name: "Button",
|
|
13
|
+
description: "A clickable button component",
|
|
14
|
+
category: "actions",
|
|
15
|
+
sourcePath: "src/components/Button/index.tsx",
|
|
16
|
+
exportName: "Button",
|
|
17
17
|
propsSummary: [
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
"variant: primary|secondary|ghost (default: primary)",
|
|
19
|
+
"size: sm|md|lg (default: md)",
|
|
20
|
+
"disabled: boolean (default: false)",
|
|
21
21
|
],
|
|
22
22
|
props: {
|
|
23
23
|
variant: {
|
|
24
|
-
type:
|
|
25
|
-
values: [
|
|
26
|
-
default:
|
|
27
|
-
description:
|
|
24
|
+
type: "enum",
|
|
25
|
+
values: ["primary", "secondary", "ghost"],
|
|
26
|
+
default: "primary",
|
|
27
|
+
description: "Visual style variant",
|
|
28
28
|
},
|
|
29
29
|
size: {
|
|
30
|
-
type:
|
|
31
|
-
values: [
|
|
32
|
-
default:
|
|
33
|
-
description:
|
|
30
|
+
type: "enum",
|
|
31
|
+
values: ["sm", "md", "lg"],
|
|
32
|
+
default: "md",
|
|
33
|
+
description: "Button size",
|
|
34
34
|
},
|
|
35
35
|
disabled: {
|
|
36
|
-
type:
|
|
36
|
+
type: "boolean",
|
|
37
37
|
default: false,
|
|
38
|
-
description:
|
|
38
|
+
description: "Whether the button is disabled",
|
|
39
39
|
},
|
|
40
40
|
},
|
|
41
41
|
usage: {
|
|
42
|
-
when: [
|
|
43
|
-
whenNot: [
|
|
42
|
+
when: ["User needs to trigger an action"],
|
|
43
|
+
whenNot: ["Navigation — use Link instead"],
|
|
44
44
|
},
|
|
45
45
|
provenance: {
|
|
46
|
-
source:
|
|
46
|
+
source: "manual",
|
|
47
47
|
verified: false,
|
|
48
48
|
},
|
|
49
49
|
...overrides,
|
|
@@ -54,17 +54,17 @@ function makeContract(overrides: Record<string, unknown> = {}): string {
|
|
|
54
54
|
// isContractFile
|
|
55
55
|
// ---------------------------------------------------------------------------
|
|
56
56
|
|
|
57
|
-
describe(
|
|
58
|
-
it(
|
|
59
|
-
expect(isContractFile(
|
|
60
|
-
expect(isContractFile(
|
|
57
|
+
describe("isContractFile", () => {
|
|
58
|
+
it("detects .contract.json extension", () => {
|
|
59
|
+
expect(isContractFile("Button.contract.json")).toBe(true);
|
|
60
|
+
expect(isContractFile("src/components/Card.contract.json")).toBe(true);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
it(
|
|
64
|
-
expect(isContractFile(
|
|
65
|
-
expect(isContractFile(
|
|
66
|
-
expect(isContractFile(
|
|
67
|
-
expect(isContractFile(
|
|
63
|
+
it("rejects non-contract files", () => {
|
|
64
|
+
expect(isContractFile("Button.fragment.tsx")).toBe(false);
|
|
65
|
+
expect(isContractFile("contract.json")).toBe(false);
|
|
66
|
+
expect(isContractFile("button.json")).toBe(false);
|
|
67
|
+
expect(isContractFile("button.contract.ts")).toBe(false);
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
70
|
|
|
@@ -72,311 +72,352 @@ describe('isContractFile', () => {
|
|
|
72
72
|
// Schema validation
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
74
|
|
|
75
|
-
describe(
|
|
75
|
+
describe("componentContractSchema", () => {
|
|
76
76
|
const minimalValid = {
|
|
77
|
-
$schema:
|
|
78
|
-
name:
|
|
79
|
-
description:
|
|
80
|
-
category:
|
|
81
|
-
sourcePath:
|
|
82
|
-
exportName:
|
|
77
|
+
$schema: "https://usefragments.com/schemas/contract.v1.json",
|
|
78
|
+
name: "Button",
|
|
79
|
+
description: "A button",
|
|
80
|
+
category: "actions",
|
|
81
|
+
sourcePath: "src/Button.tsx",
|
|
82
|
+
exportName: "Button",
|
|
83
83
|
propsSummary: [],
|
|
84
84
|
props: {},
|
|
85
|
-
usage: { when: [
|
|
86
|
-
provenance: { source:
|
|
85
|
+
usage: { when: ["click"], whenNot: ["nav"] },
|
|
86
|
+
provenance: { source: "manual", verified: false },
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
-
it(
|
|
89
|
+
it("validates a minimal contract", () => {
|
|
90
90
|
const result = componentContractSchema.safeParse(minimalValid);
|
|
91
91
|
expect(result.success).toBe(true);
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
it(
|
|
95
|
-
const data = { name:
|
|
94
|
+
it("rejects contract missing required fields", () => {
|
|
95
|
+
const data = { name: "Button" };
|
|
96
96
|
const result = componentContractSchema.safeParse(data);
|
|
97
97
|
expect(result.success).toBe(false);
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
it(
|
|
100
|
+
it("rejects contract missing $schema", () => {
|
|
101
101
|
const { $schema: _, ...noSchema } = minimalValid;
|
|
102
102
|
const result = componentContractSchema.safeParse(noSchema);
|
|
103
103
|
expect(result.success).toBe(false);
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
it(
|
|
106
|
+
it("rejects contract missing sourcePath", () => {
|
|
107
107
|
const { sourcePath: _, ...noSource } = minimalValid;
|
|
108
108
|
const result = componentContractSchema.safeParse(noSource);
|
|
109
109
|
expect(result.success).toBe(false);
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
-
it(
|
|
112
|
+
it("rejects contract missing exportName", () => {
|
|
113
113
|
const { exportName: _, ...noExport } = minimalValid;
|
|
114
114
|
const result = componentContractSchema.safeParse(noExport);
|
|
115
115
|
expect(result.success).toBe(false);
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
-
it(
|
|
118
|
+
it("rejects contract missing provenance", () => {
|
|
119
119
|
const { provenance: _, ...noProv } = minimalValid;
|
|
120
120
|
const result = componentContractSchema.safeParse(noProv);
|
|
121
121
|
expect(result.success).toBe(false);
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
it(
|
|
125
|
-
const result = componentContractSchema.safeParse({ ...minimalValid, status:
|
|
124
|
+
it("rejects contract with invalid status", () => {
|
|
125
|
+
const result = componentContractSchema.safeParse({ ...minimalValid, status: "invalid-status" });
|
|
126
126
|
expect(result.success).toBe(false);
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
-
it(
|
|
130
|
-
for (const status of [
|
|
129
|
+
it("accepts all valid status values", () => {
|
|
130
|
+
for (const status of ["stable", "beta", "deprecated", "experimental"]) {
|
|
131
131
|
const result = componentContractSchema.safeParse({ ...minimalValid, status });
|
|
132
132
|
expect(result.success).toBe(true);
|
|
133
133
|
}
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
-
it(
|
|
137
|
-
for (const framework of [
|
|
136
|
+
it("accepts all valid framework values", () => {
|
|
137
|
+
for (const framework of ["react", "vue", "svelte", "web-components", "angular"]) {
|
|
138
138
|
const result = componentContractSchema.safeParse({ ...minimalValid, framework });
|
|
139
139
|
expect(result.success).toBe(true);
|
|
140
140
|
}
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
it(
|
|
143
|
+
it("rejects invalid relationship type", () => {
|
|
144
144
|
const result = componentContractSchema.safeParse({
|
|
145
145
|
...minimalValid,
|
|
146
|
-
relations: [{ component:
|
|
146
|
+
relations: [{ component: "Link", relationship: "replaces", note: "test" }],
|
|
147
147
|
});
|
|
148
148
|
expect(result.success).toBe(false);
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
-
it(
|
|
152
|
-
for (const relationship of [
|
|
151
|
+
it("accepts all valid relationship types", () => {
|
|
152
|
+
for (const relationship of ["alternative", "parent", "child", "sibling"]) {
|
|
153
153
|
const result = componentContractSchema.safeParse({
|
|
154
154
|
...minimalValid,
|
|
155
|
-
relations: [{ component:
|
|
155
|
+
relations: [{ component: "Link", relationship, note: "test" }],
|
|
156
156
|
});
|
|
157
157
|
expect(result.success).toBe(true);
|
|
158
158
|
}
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
-
it(
|
|
161
|
+
it("validates ai metadata", () => {
|
|
162
162
|
const result = componentContractSchema.safeParse({
|
|
163
163
|
...minimalValid,
|
|
164
164
|
ai: {
|
|
165
|
-
compositionPattern:
|
|
166
|
-
subComponents: [
|
|
167
|
-
requiredChildren: [
|
|
168
|
-
commonPatterns: [
|
|
165
|
+
compositionPattern: "compound",
|
|
166
|
+
subComponents: ["Header", "Body", "Footer"],
|
|
167
|
+
requiredChildren: ["Body"],
|
|
168
|
+
commonPatterns: ["<Card><Card.Header /><Card.Body /></Card>"],
|
|
169
169
|
},
|
|
170
170
|
});
|
|
171
171
|
expect(result.success).toBe(true);
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
-
it(
|
|
174
|
+
it("validates preview config", () => {
|
|
175
175
|
const result = componentContractSchema.safeParse({
|
|
176
176
|
...minimalValid,
|
|
177
177
|
preview: {
|
|
178
|
-
setupModule:
|
|
179
|
-
wrapperModule:
|
|
180
|
-
wrapperExport:
|
|
181
|
-
css: [
|
|
182
|
-
theme:
|
|
178
|
+
setupModule: "./preview-setup.ts",
|
|
179
|
+
wrapperModule: "./ThemeProvider.tsx",
|
|
180
|
+
wrapperExport: "ThemeProvider",
|
|
181
|
+
css: ["./globals.css"],
|
|
182
|
+
theme: "dark",
|
|
183
183
|
},
|
|
184
184
|
});
|
|
185
185
|
expect(result.success).toBe(true);
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
-
it(
|
|
188
|
+
it("validates provenance with all fields", () => {
|
|
189
189
|
const result = componentContractSchema.safeParse({
|
|
190
190
|
...minimalValid,
|
|
191
191
|
provenance: {
|
|
192
|
-
source:
|
|
192
|
+
source: "extracted",
|
|
193
193
|
verified: true,
|
|
194
|
-
frameworkSupport:
|
|
195
|
-
sourceHash:
|
|
196
|
-
extractedAt:
|
|
194
|
+
frameworkSupport: "native",
|
|
195
|
+
sourceHash: "abc123",
|
|
196
|
+
extractedAt: "2026-03-13T00:00:00Z",
|
|
197
197
|
},
|
|
198
198
|
});
|
|
199
199
|
expect(result.success).toBe(true);
|
|
200
200
|
});
|
|
201
|
+
|
|
202
|
+
it("accepts only explicit positive performance budgets (A08-02)", () => {
|
|
203
|
+
for (const performanceBudget of [0, -1, Number.POSITIVE_INFINITY]) {
|
|
204
|
+
const result = componentContractSchema.safeParse({
|
|
205
|
+
...minimalValid,
|
|
206
|
+
contract: { performanceBudget },
|
|
207
|
+
});
|
|
208
|
+
expect(result.success).toBe(false);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
expect(
|
|
212
|
+
componentContractSchema.safeParse({
|
|
213
|
+
...minimalValid,
|
|
214
|
+
contract: { performanceBudget: 1 },
|
|
215
|
+
}).success
|
|
216
|
+
).toBe(true);
|
|
217
|
+
expect(componentContractSchema.safeParse(minimalValid).success).toBe(true);
|
|
218
|
+
});
|
|
201
219
|
});
|
|
202
220
|
|
|
203
221
|
// ---------------------------------------------------------------------------
|
|
204
222
|
// parseComponentContract
|
|
205
223
|
// ---------------------------------------------------------------------------
|
|
206
224
|
|
|
207
|
-
describe(
|
|
208
|
-
describe(
|
|
209
|
-
it(
|
|
225
|
+
describe("parseComponentContract", () => {
|
|
226
|
+
describe("basic parsing", () => {
|
|
227
|
+
it("parses a minimal contract", () => {
|
|
210
228
|
const content = makeContract();
|
|
211
|
-
const result = parseComponentContract(content,
|
|
229
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
212
230
|
|
|
213
|
-
expect(result.meta.name).toBe(
|
|
214
|
-
expect(result.meta.description).toBe(
|
|
215
|
-
expect(result.meta.category).toBe(
|
|
216
|
-
expect(result.filePath).toBe(
|
|
231
|
+
expect(result.meta.name).toBe("Button");
|
|
232
|
+
expect(result.meta.description).toBe("A clickable button component");
|
|
233
|
+
expect(result.meta.category).toBe("actions");
|
|
234
|
+
expect(result.filePath).toBe("Button.contract.json");
|
|
217
235
|
});
|
|
218
236
|
|
|
219
|
-
it(
|
|
237
|
+
it("maps props correctly", () => {
|
|
220
238
|
const content = makeContract();
|
|
221
|
-
const result = parseComponentContract(content,
|
|
239
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
222
240
|
|
|
223
241
|
expect(Object.keys(result.props)).toHaveLength(3);
|
|
224
|
-
expect(result.props.variant.type).toBe(
|
|
225
|
-
expect(result.props.variant.values).toEqual([
|
|
226
|
-
expect(result.props.variant.default).toBe(
|
|
227
|
-
expect(result.props.variant.description).toBe(
|
|
242
|
+
expect(result.props.variant.type).toBe("enum");
|
|
243
|
+
expect(result.props.variant.values).toEqual(["primary", "secondary", "ghost"]);
|
|
244
|
+
expect(result.props.variant.default).toBe("primary");
|
|
245
|
+
expect(result.props.variant.description).toBe("Visual style variant");
|
|
228
246
|
});
|
|
229
247
|
|
|
230
|
-
it(
|
|
248
|
+
it("maps usage correctly", () => {
|
|
231
249
|
const content = makeContract();
|
|
232
|
-
const result = parseComponentContract(content,
|
|
250
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
233
251
|
|
|
234
|
-
expect(result.usage.when).toEqual([
|
|
235
|
-
expect(result.usage.whenNot).toEqual([
|
|
252
|
+
expect(result.usage.when).toEqual(["User needs to trigger an action"]);
|
|
253
|
+
expect(result.usage.whenNot).toEqual(["Navigation — use Link instead"]);
|
|
236
254
|
});
|
|
237
255
|
});
|
|
238
256
|
|
|
239
|
-
describe(
|
|
240
|
-
it(
|
|
241
|
-
const content = makeContract({ tags: [
|
|
242
|
-
const result = parseComponentContract(content,
|
|
257
|
+
describe("optional fields", () => {
|
|
258
|
+
it("handles tags", () => {
|
|
259
|
+
const content = makeContract({ tags: ["interactive", "form"] });
|
|
260
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
243
261
|
|
|
244
|
-
expect(result.meta.tags).toEqual([
|
|
262
|
+
expect(result.meta.tags).toEqual(["interactive", "form"]);
|
|
245
263
|
});
|
|
246
264
|
|
|
247
|
-
it(
|
|
248
|
-
const content = makeContract({ status:
|
|
249
|
-
const result = parseComponentContract(content,
|
|
265
|
+
it("handles status", () => {
|
|
266
|
+
const content = makeContract({ status: "stable" });
|
|
267
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
250
268
|
|
|
251
|
-
expect(result.meta.status).toBe(
|
|
269
|
+
expect(result.meta.status).toBe("stable");
|
|
252
270
|
});
|
|
253
271
|
|
|
254
|
-
it(
|
|
272
|
+
it("handles guidelines and accessibility in usage", () => {
|
|
255
273
|
const content = makeContract({
|
|
256
274
|
usage: {
|
|
257
|
-
when: [
|
|
258
|
-
whenNot: [
|
|
259
|
-
guidelines: [
|
|
275
|
+
when: ["Action needed"],
|
|
276
|
+
whenNot: ["No action"],
|
|
277
|
+
guidelines: ["Always provide a label"],
|
|
260
278
|
accessibility: ['Must have role="button"'],
|
|
261
279
|
},
|
|
262
280
|
});
|
|
263
|
-
const result = parseComponentContract(content,
|
|
281
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
264
282
|
|
|
265
|
-
expect(result.usage.guidelines).toEqual([
|
|
283
|
+
expect(result.usage.guidelines).toEqual(["Always provide a label"]);
|
|
266
284
|
expect(result.usage.accessibility).toEqual(['Must have role="button"']);
|
|
267
285
|
});
|
|
268
286
|
|
|
269
|
-
it(
|
|
287
|
+
it("preserves every contract field plus preview and tokens (A08-01)", () => {
|
|
270
288
|
const content = makeContract({
|
|
271
289
|
contract: {
|
|
272
|
-
propsSummary: [
|
|
273
|
-
scenarioTags: [
|
|
274
|
-
a11yRules: [
|
|
275
|
-
bans: [{ pattern:
|
|
290
|
+
propsSummary: ["variant: primary|secondary|ghost (default: primary)"],
|
|
291
|
+
scenarioTags: ["form", "action"],
|
|
292
|
+
a11yRules: ["no-empty-buttons"],
|
|
293
|
+
bans: [{ pattern: "onClick without type", message: "Always specify button type" }],
|
|
294
|
+
performanceBudget: 1024,
|
|
295
|
+
compoundChildren: {
|
|
296
|
+
Icon: { required: true, accepts: ["svg"], description: "Leading icon" },
|
|
297
|
+
},
|
|
298
|
+
canonicalUsage: ['<Button variant="primary" />'],
|
|
299
|
+
},
|
|
300
|
+
preview: {
|
|
301
|
+
setupModule: "./preview-setup.ts",
|
|
302
|
+
wrapperModule: "./ThemeProvider.tsx",
|
|
303
|
+
wrapperExport: "ThemeProvider",
|
|
304
|
+
css: ["./globals.css"],
|
|
305
|
+
theme: "dark",
|
|
276
306
|
},
|
|
307
|
+
tokens: ["--ds-color-primary", "--ds-space-4"],
|
|
277
308
|
});
|
|
278
|
-
const result = parseComponentContract(content,
|
|
279
|
-
|
|
280
|
-
expect(result.contract).
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
309
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
310
|
+
|
|
311
|
+
expect(result.contract).toEqual({
|
|
312
|
+
propsSummary: ["variant: primary|secondary|ghost (default: primary)"],
|
|
313
|
+
scenarioTags: ["form", "action"],
|
|
314
|
+
a11yRules: ["no-empty-buttons"],
|
|
315
|
+
bans: [{ pattern: "onClick without type", message: "Always specify button type" }],
|
|
316
|
+
performanceBudget: 1024,
|
|
317
|
+
compoundChildren: {
|
|
318
|
+
Icon: { required: true, accepts: ["svg"], description: "Leading icon" },
|
|
319
|
+
},
|
|
320
|
+
canonicalUsage: ['<Button variant="primary" />'],
|
|
321
|
+
});
|
|
322
|
+
expect(result.preview).toEqual({
|
|
323
|
+
setupModule: "./preview-setup.ts",
|
|
324
|
+
wrapperModule: "./ThemeProvider.tsx",
|
|
325
|
+
wrapperExport: "ThemeProvider",
|
|
326
|
+
css: ["./globals.css"],
|
|
327
|
+
theme: "dark",
|
|
328
|
+
});
|
|
329
|
+
expect(result.tokens).toEqual(["--ds-color-primary", "--ds-space-4"]);
|
|
284
330
|
});
|
|
285
331
|
|
|
286
|
-
it(
|
|
287
|
-
const
|
|
288
|
-
tokens: ['--ds-color-primary', '--ds-space-4'],
|
|
289
|
-
});
|
|
332
|
+
it("leaves an absent performance budget absent (A08-02)", () => {
|
|
333
|
+
const result = parseComponentContract(makeContract(), "Button.contract.json");
|
|
290
334
|
|
|
291
|
-
|
|
292
|
-
// Just verify it parses without error
|
|
293
|
-
const result = parseComponentContract(content, 'Button.contract.json');
|
|
294
|
-
expect(result.meta.name).toBe('Button');
|
|
335
|
+
expect(result.contract).not.toHaveProperty("performanceBudget");
|
|
295
336
|
});
|
|
296
337
|
});
|
|
297
338
|
|
|
298
|
-
describe(
|
|
299
|
-
it(
|
|
339
|
+
describe("examples → variants mapping", () => {
|
|
340
|
+
it("maps examples to variants", () => {
|
|
300
341
|
const content = makeContract({
|
|
301
342
|
examples: [
|
|
302
343
|
{
|
|
303
|
-
name:
|
|
304
|
-
description:
|
|
344
|
+
name: "Primary",
|
|
345
|
+
description: "Primary button style",
|
|
305
346
|
code: '<Button variant="primary">Click me</Button>',
|
|
306
347
|
},
|
|
307
348
|
{
|
|
308
|
-
name:
|
|
309
|
-
description:
|
|
349
|
+
name: "With icon",
|
|
350
|
+
description: "Button with leading icon",
|
|
310
351
|
code: '<Button><Icon name="plus" /> Add</Button>',
|
|
311
|
-
args: { variant:
|
|
352
|
+
args: { variant: "secondary" },
|
|
312
353
|
},
|
|
313
354
|
],
|
|
314
355
|
});
|
|
315
|
-
const result = parseComponentContract(content,
|
|
356
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
316
357
|
|
|
317
358
|
expect(result.variants).toHaveLength(2);
|
|
318
|
-
expect(result.variants[0].name).toBe(
|
|
359
|
+
expect(result.variants[0].name).toBe("Primary");
|
|
319
360
|
expect(result.variants[0].code).toBe('<Button variant="primary">Click me</Button>');
|
|
320
|
-
expect(result.variants[1].args).toEqual({ variant:
|
|
361
|
+
expect(result.variants[1].args).toEqual({ variant: "secondary" });
|
|
321
362
|
});
|
|
322
363
|
|
|
323
|
-
it(
|
|
364
|
+
it("returns empty variants when no examples", () => {
|
|
324
365
|
const content = makeContract();
|
|
325
|
-
const result = parseComponentContract(content,
|
|
366
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
326
367
|
|
|
327
368
|
expect(result.variants).toEqual([]);
|
|
328
369
|
});
|
|
329
370
|
});
|
|
330
371
|
|
|
331
|
-
describe(
|
|
332
|
-
it(
|
|
372
|
+
describe("relations mapping", () => {
|
|
373
|
+
it("maps all relationship types", () => {
|
|
333
374
|
const content = makeContract({
|
|
334
375
|
relations: [
|
|
335
|
-
{ component:
|
|
336
|
-
{ component:
|
|
337
|
-
{ component:
|
|
338
|
-
{ component:
|
|
376
|
+
{ component: "Link", relationship: "alternative", note: "For navigation" },
|
|
377
|
+
{ component: "Form", relationship: "parent", note: "Used inside forms" },
|
|
378
|
+
{ component: "Icon", relationship: "child", note: "Can contain icons" },
|
|
379
|
+
{ component: "Input", relationship: "sibling", note: "Often paired" },
|
|
339
380
|
],
|
|
340
381
|
});
|
|
341
|
-
const result = parseComponentContract(content,
|
|
382
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
342
383
|
|
|
343
384
|
expect(result.relations).toHaveLength(4);
|
|
344
|
-
expect(result.relations![0].relationship).toBe(
|
|
345
|
-
expect(result.relations![1].relationship).toBe(
|
|
346
|
-
expect(result.relations![2].relationship).toBe(
|
|
347
|
-
expect(result.relations![3].relationship).toBe(
|
|
385
|
+
expect(result.relations![0].relationship).toBe("alternative");
|
|
386
|
+
expect(result.relations![1].relationship).toBe("parent");
|
|
387
|
+
expect(result.relations![2].relationship).toBe("child");
|
|
388
|
+
expect(result.relations![3].relationship).toBe("sibling");
|
|
348
389
|
});
|
|
349
390
|
});
|
|
350
391
|
|
|
351
|
-
describe(
|
|
352
|
-
it(
|
|
392
|
+
describe("figma mappings", () => {
|
|
393
|
+
it("maps figma nodeUrl to meta.figma", () => {
|
|
353
394
|
const content = makeContract({
|
|
354
395
|
figma: {
|
|
355
|
-
nodeUrl:
|
|
396
|
+
nodeUrl: "https://www.figma.com/file/abc123/Design?node-id=1-2",
|
|
356
397
|
},
|
|
357
398
|
});
|
|
358
|
-
const result = parseComponentContract(content,
|
|
399
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
359
400
|
|
|
360
|
-
expect(result.meta.figma).toBe(
|
|
401
|
+
expect(result.meta.figma).toBe("https://www.figma.com/file/abc123/Design?node-id=1-2");
|
|
361
402
|
});
|
|
362
403
|
|
|
363
|
-
it(
|
|
404
|
+
it("maps figma propMappings to meta.figmaProps", () => {
|
|
364
405
|
const content = makeContract({
|
|
365
406
|
figma: {
|
|
366
407
|
propMappings: {
|
|
367
408
|
variant: {
|
|
368
|
-
type:
|
|
369
|
-
figmaProperty:
|
|
370
|
-
values: { primary:
|
|
409
|
+
type: "enum",
|
|
410
|
+
figmaProperty: "Style",
|
|
411
|
+
values: { primary: "Primary", secondary: "Secondary" },
|
|
371
412
|
},
|
|
372
413
|
disabled: {
|
|
373
|
-
type:
|
|
374
|
-
figmaProperty:
|
|
414
|
+
type: "boolean",
|
|
415
|
+
figmaProperty: "Disabled",
|
|
375
416
|
},
|
|
376
417
|
},
|
|
377
418
|
},
|
|
378
419
|
});
|
|
379
|
-
const result = parseComponentContract(content,
|
|
420
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
380
421
|
|
|
381
422
|
expect(result.meta.figmaProps).toBeDefined();
|
|
382
423
|
const mappings = result.meta.figmaProps as Record<string, unknown>;
|
|
@@ -384,231 +425,231 @@ describe('parseComponentContract', () => {
|
|
|
384
425
|
});
|
|
385
426
|
});
|
|
386
427
|
|
|
387
|
-
describe(
|
|
388
|
-
it(
|
|
428
|
+
describe("prop constraints", () => {
|
|
429
|
+
it("preserves prop constraints", () => {
|
|
389
430
|
const content = makeContract({
|
|
390
431
|
props: {
|
|
391
432
|
maxLength: {
|
|
392
|
-
type:
|
|
393
|
-
description:
|
|
394
|
-
constraints: [
|
|
433
|
+
type: "number",
|
|
434
|
+
description: "Maximum character length",
|
|
435
|
+
constraints: ["Must be positive", "Max 1000"],
|
|
395
436
|
},
|
|
396
437
|
},
|
|
397
438
|
});
|
|
398
|
-
const result = parseComponentContract(content,
|
|
439
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
399
440
|
|
|
400
|
-
expect(result.props.maxLength.constraints).toEqual([
|
|
441
|
+
expect(result.props.maxLength.constraints).toEqual(["Must be positive", "Max 1000"]);
|
|
401
442
|
});
|
|
402
443
|
|
|
403
|
-
it(
|
|
444
|
+
it("preserves required flag", () => {
|
|
404
445
|
const content = makeContract({
|
|
405
446
|
props: {
|
|
406
447
|
label: {
|
|
407
|
-
type:
|
|
408
|
-
description:
|
|
448
|
+
type: "string",
|
|
449
|
+
description: "Button label",
|
|
409
450
|
required: true,
|
|
410
451
|
},
|
|
411
452
|
},
|
|
412
453
|
});
|
|
413
|
-
const result = parseComponentContract(content,
|
|
454
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
414
455
|
|
|
415
456
|
expect(result.props.label.required).toBe(true);
|
|
416
457
|
});
|
|
417
458
|
});
|
|
418
459
|
|
|
419
|
-
describe(
|
|
420
|
-
it(
|
|
421
|
-
expect(() => parseComponentContract(
|
|
460
|
+
describe("error handling", () => {
|
|
461
|
+
it("throws on invalid JSON", () => {
|
|
462
|
+
expect(() => parseComponentContract("not json", "test.contract.json")).toThrow();
|
|
422
463
|
});
|
|
423
464
|
|
|
424
|
-
it(
|
|
425
|
-
const content = JSON.stringify({ name:
|
|
426
|
-
expect(() => parseComponentContract(content,
|
|
465
|
+
it("throws on missing required fields", () => {
|
|
466
|
+
const content = JSON.stringify({ name: "Button" });
|
|
467
|
+
expect(() => parseComponentContract(content, "test.contract.json")).toThrow();
|
|
427
468
|
});
|
|
428
469
|
|
|
429
|
-
it(
|
|
470
|
+
it("throws on invalid prop type structure", () => {
|
|
430
471
|
const content = JSON.stringify({
|
|
431
|
-
name:
|
|
432
|
-
description:
|
|
433
|
-
category:
|
|
472
|
+
name: "Button",
|
|
473
|
+
description: "A button",
|
|
474
|
+
category: "actions",
|
|
434
475
|
props: {
|
|
435
|
-
variant:
|
|
476
|
+
variant: "string", // Should be an object, not a string
|
|
436
477
|
},
|
|
437
478
|
usage: { when: [], whenNot: [] },
|
|
438
479
|
});
|
|
439
|
-
expect(() => parseComponentContract(content,
|
|
480
|
+
expect(() => parseComponentContract(content, "test.contract.json")).toThrow();
|
|
440
481
|
});
|
|
441
482
|
});
|
|
442
483
|
|
|
443
|
-
describe(
|
|
444
|
-
it(
|
|
484
|
+
describe("CompiledFragment compatibility", () => {
|
|
485
|
+
it("output has all required CompiledFragment fields", () => {
|
|
445
486
|
const content = makeContract();
|
|
446
|
-
const result = parseComponentContract(content,
|
|
487
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
447
488
|
|
|
448
489
|
// Required fields
|
|
449
|
-
expect(result).toHaveProperty(
|
|
450
|
-
expect(result).toHaveProperty(
|
|
451
|
-
expect(result).toHaveProperty(
|
|
452
|
-
expect(result).toHaveProperty(
|
|
453
|
-
expect(result).toHaveProperty(
|
|
490
|
+
expect(result).toHaveProperty("filePath");
|
|
491
|
+
expect(result).toHaveProperty("meta");
|
|
492
|
+
expect(result).toHaveProperty("usage");
|
|
493
|
+
expect(result).toHaveProperty("props");
|
|
494
|
+
expect(result).toHaveProperty("variants");
|
|
454
495
|
|
|
455
496
|
// Meta required fields
|
|
456
|
-
expect(result.meta).toHaveProperty(
|
|
457
|
-
expect(result.meta).toHaveProperty(
|
|
458
|
-
expect(result.meta).toHaveProperty(
|
|
497
|
+
expect(result.meta).toHaveProperty("name");
|
|
498
|
+
expect(result.meta).toHaveProperty("description");
|
|
499
|
+
expect(result.meta).toHaveProperty("category");
|
|
459
500
|
|
|
460
501
|
// Usage required fields
|
|
461
|
-
expect(result.usage).toHaveProperty(
|
|
462
|
-
expect(result.usage).toHaveProperty(
|
|
502
|
+
expect(result.usage).toHaveProperty("when");
|
|
503
|
+
expect(result.usage).toHaveProperty("whenNot");
|
|
463
504
|
});
|
|
464
505
|
});
|
|
465
506
|
|
|
466
|
-
describe(
|
|
467
|
-
it(
|
|
507
|
+
describe("real-world contracts", () => {
|
|
508
|
+
it("parses a Vue component contract", () => {
|
|
468
509
|
const content = JSON.stringify({
|
|
469
|
-
$schema:
|
|
470
|
-
name:
|
|
471
|
-
description:
|
|
472
|
-
category:
|
|
473
|
-
framework:
|
|
474
|
-
status:
|
|
475
|
-
tags: [
|
|
476
|
-
sourcePath:
|
|
477
|
-
exportName:
|
|
478
|
-
propsSummary: [
|
|
510
|
+
$schema: "https://usefragments.com/schemas/contract.v1.json",
|
|
511
|
+
name: "VButton",
|
|
512
|
+
description: "Vuetify button component",
|
|
513
|
+
category: "actions",
|
|
514
|
+
framework: "vue",
|
|
515
|
+
status: "stable",
|
|
516
|
+
tags: ["vuetify", "material"],
|
|
517
|
+
sourcePath: "src/VButton.vue",
|
|
518
|
+
exportName: "VButton",
|
|
519
|
+
propsSummary: ["color: string", "outlined: boolean", "block: boolean"],
|
|
479
520
|
props: {
|
|
480
|
-
color: { type:
|
|
481
|
-
outlined: { type:
|
|
482
|
-
block: { type:
|
|
521
|
+
color: { type: "string", description: "Button color", default: "primary" },
|
|
522
|
+
outlined: { type: "boolean", description: "Outlined style", default: false },
|
|
523
|
+
block: { type: "boolean", description: "Full width", default: false },
|
|
483
524
|
},
|
|
484
525
|
usage: {
|
|
485
|
-
when: [
|
|
486
|
-
whenNot: [
|
|
526
|
+
when: ["User action needed"],
|
|
527
|
+
whenNot: ["Navigation — use v-link"],
|
|
487
528
|
},
|
|
488
529
|
examples: [
|
|
489
530
|
{
|
|
490
|
-
name:
|
|
491
|
-
description:
|
|
531
|
+
name: "Default",
|
|
532
|
+
description: "Default button",
|
|
492
533
|
code: '<v-btn color="primary">Click</v-btn>',
|
|
493
534
|
},
|
|
494
535
|
],
|
|
495
|
-
provenance: { source:
|
|
536
|
+
provenance: { source: "manual", verified: false },
|
|
496
537
|
});
|
|
497
538
|
|
|
498
|
-
const result = parseComponentContract(content,
|
|
539
|
+
const result = parseComponentContract(content, "VButton.contract.json");
|
|
499
540
|
|
|
500
|
-
expect(result.meta.name).toBe(
|
|
501
|
-
expect(result.meta.tags).toContain(
|
|
502
|
-
expect(result.framework).toBe(
|
|
541
|
+
expect(result.meta.name).toBe("VButton");
|
|
542
|
+
expect(result.meta.tags).toContain("vuetify");
|
|
543
|
+
expect(result.framework).toBe("vue");
|
|
503
544
|
expect(result.variants).toHaveLength(1);
|
|
504
|
-
expect(result.variants[0].code).toContain(
|
|
505
|
-
expect(result.provenance?.source).toBe(
|
|
545
|
+
expect(result.variants[0].code).toContain("<v-btn");
|
|
546
|
+
expect(result.provenance?.source).toBe("manual");
|
|
506
547
|
});
|
|
507
548
|
|
|
508
|
-
it(
|
|
549
|
+
it("parses a Web Components contract", () => {
|
|
509
550
|
const content = JSON.stringify({
|
|
510
|
-
$schema:
|
|
511
|
-
name:
|
|
512
|
-
description:
|
|
513
|
-
category:
|
|
514
|
-
framework:
|
|
515
|
-
sourcePath:
|
|
516
|
-
exportName:
|
|
517
|
-
propsSummary: [
|
|
551
|
+
$schema: "https://usefragments.com/schemas/contract.v1.json",
|
|
552
|
+
name: "MyButton",
|
|
553
|
+
description: "Custom element button",
|
|
554
|
+
category: "actions",
|
|
555
|
+
framework: "web-components",
|
|
556
|
+
sourcePath: "src/my-button.ts",
|
|
557
|
+
exportName: "MyButton",
|
|
558
|
+
propsSummary: ["variant: primary|secondary"],
|
|
518
559
|
props: {
|
|
519
560
|
variant: {
|
|
520
|
-
type:
|
|
521
|
-
description:
|
|
522
|
-
values: [
|
|
561
|
+
type: "string",
|
|
562
|
+
description: "Visual variant",
|
|
563
|
+
values: ["primary", "secondary"],
|
|
523
564
|
},
|
|
524
565
|
},
|
|
525
566
|
usage: {
|
|
526
|
-
when: [
|
|
527
|
-
whenNot: [
|
|
567
|
+
when: ["Need a custom button"],
|
|
568
|
+
whenNot: ["Use native <button> when possible"],
|
|
528
569
|
},
|
|
529
|
-
provenance: { source:
|
|
570
|
+
provenance: { source: "manual", verified: false },
|
|
530
571
|
});
|
|
531
572
|
|
|
532
|
-
const result = parseComponentContract(content,
|
|
533
|
-
expect(result.meta.name).toBe(
|
|
534
|
-
expect(result.framework).toBe(
|
|
573
|
+
const result = parseComponentContract(content, "MyButton.contract.json");
|
|
574
|
+
expect(result.meta.name).toBe("MyButton");
|
|
575
|
+
expect(result.framework).toBe("web-components");
|
|
535
576
|
});
|
|
536
577
|
|
|
537
|
-
it(
|
|
578
|
+
it("parses a contract with many props", () => {
|
|
538
579
|
const props: Record<string, unknown> = {};
|
|
539
580
|
const summaries: string[] = [];
|
|
540
581
|
for (let i = 0; i < 50; i++) {
|
|
541
582
|
props[`prop${i}`] = {
|
|
542
|
-
type:
|
|
583
|
+
type: "string",
|
|
543
584
|
description: `Property ${i}`,
|
|
544
585
|
};
|
|
545
586
|
summaries.push(`prop${i}: string`);
|
|
546
587
|
}
|
|
547
588
|
|
|
548
589
|
const content = JSON.stringify({
|
|
549
|
-
$schema:
|
|
550
|
-
name:
|
|
551
|
-
description:
|
|
552
|
-
category:
|
|
553
|
-
sourcePath:
|
|
554
|
-
exportName:
|
|
590
|
+
$schema: "https://usefragments.com/schemas/contract.v1.json",
|
|
591
|
+
name: "ComplexComponent",
|
|
592
|
+
description: "Component with many props",
|
|
593
|
+
category: "complex",
|
|
594
|
+
sourcePath: "src/Complex.tsx",
|
|
595
|
+
exportName: "ComplexComponent",
|
|
555
596
|
propsSummary: summaries,
|
|
556
597
|
props,
|
|
557
|
-
usage: { when: [
|
|
558
|
-
provenance: { source:
|
|
598
|
+
usage: { when: ["Complex use case"], whenNot: [] },
|
|
599
|
+
provenance: { source: "manual", verified: false },
|
|
559
600
|
});
|
|
560
601
|
|
|
561
|
-
const result = parseComponentContract(content,
|
|
602
|
+
const result = parseComponentContract(content, "Complex.contract.json");
|
|
562
603
|
expect(Object.keys(result.props)).toHaveLength(50);
|
|
563
604
|
expect(result.propsSummary).toHaveLength(50);
|
|
564
605
|
});
|
|
565
606
|
});
|
|
566
607
|
|
|
567
|
-
describe(
|
|
568
|
-
it(
|
|
608
|
+
describe("new V2 fields", () => {
|
|
609
|
+
it("maps propsSummary to top-level and contract", () => {
|
|
569
610
|
const content = makeContract();
|
|
570
|
-
const result = parseComponentContract(content,
|
|
611
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
571
612
|
|
|
572
613
|
expect(result.propsSummary).toHaveLength(3);
|
|
573
614
|
expect(result.contract?.propsSummary).toHaveLength(3);
|
|
574
615
|
});
|
|
575
616
|
|
|
576
|
-
it(
|
|
617
|
+
it("maps provenance", () => {
|
|
577
618
|
const content = makeContract();
|
|
578
|
-
const result = parseComponentContract(content,
|
|
619
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
579
620
|
|
|
580
|
-
expect(result.provenance?.source).toBe(
|
|
621
|
+
expect(result.provenance?.source).toBe("manual");
|
|
581
622
|
expect(result.provenance?.verified).toBe(false);
|
|
582
623
|
});
|
|
583
624
|
|
|
584
|
-
it(
|
|
625
|
+
it("maps sourcePath and exportName", () => {
|
|
585
626
|
const content = makeContract();
|
|
586
|
-
const result = parseComponentContract(content,
|
|
627
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
587
628
|
|
|
588
|
-
expect(result.sourcePath).toBe(
|
|
589
|
-
expect(result.exportName).toBe(
|
|
629
|
+
expect(result.sourcePath).toBe("src/components/Button/index.tsx");
|
|
630
|
+
expect(result.exportName).toBe("Button");
|
|
590
631
|
});
|
|
591
632
|
|
|
592
|
-
it(
|
|
633
|
+
it("maps ai metadata", () => {
|
|
593
634
|
const content = makeContract({
|
|
594
635
|
ai: {
|
|
595
|
-
compositionPattern:
|
|
596
|
-
subComponents: [
|
|
597
|
-
requiredChildren: [
|
|
598
|
-
commonPatterns: [
|
|
636
|
+
compositionPattern: "compound",
|
|
637
|
+
subComponents: ["Header", "Body"],
|
|
638
|
+
requiredChildren: ["Body"],
|
|
639
|
+
commonPatterns: ["<Card><Card.Body>content</Card.Body></Card>"],
|
|
599
640
|
},
|
|
600
641
|
});
|
|
601
|
-
const result = parseComponentContract(content,
|
|
642
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
602
643
|
|
|
603
|
-
expect(result.ai?.compositionPattern).toBe(
|
|
604
|
-
expect(result.ai?.subComponents).toEqual([
|
|
644
|
+
expect(result.ai?.compositionPattern).toBe("compound");
|
|
645
|
+
expect(result.ai?.subComponents).toEqual(["Header", "Body"]);
|
|
605
646
|
});
|
|
606
647
|
|
|
607
|
-
it(
|
|
608
|
-
const content = makeContract({ framework:
|
|
609
|
-
const result = parseComponentContract(content,
|
|
648
|
+
it("maps framework", () => {
|
|
649
|
+
const content = makeContract({ framework: "react" });
|
|
650
|
+
const result = parseComponentContract(content, "Button.contract.json");
|
|
610
651
|
|
|
611
|
-
expect(result.framework).toBe(
|
|
652
|
+
expect(result.framework).toBe("react");
|
|
612
653
|
});
|
|
613
654
|
});
|
|
614
655
|
});
|