@fragments-sdk/cli 0.2.2
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/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +4783 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4FDQSGKX.js +786 -0
- package/dist/chunk-4FDQSGKX.js.map +1 -0
- package/dist/chunk-7H2MMGYG.js +369 -0
- package/dist/chunk-7H2MMGYG.js.map +1 -0
- package/dist/chunk-BSCG3IP7.js +619 -0
- package/dist/chunk-BSCG3IP7.js.map +1 -0
- package/dist/chunk-LY2CFFPY.js +898 -0
- package/dist/chunk-LY2CFFPY.js.map +1 -0
- package/dist/chunk-MUZ6CM66.js +6636 -0
- package/dist/chunk-MUZ6CM66.js.map +1 -0
- package/dist/chunk-OAENNG3G.js +1489 -0
- package/dist/chunk-OAENNG3G.js.map +1 -0
- package/dist/chunk-XHNKNI6J.js +235 -0
- package/dist/chunk-XHNKNI6J.js.map +1 -0
- package/dist/core-DWKLGY4N.js +68 -0
- package/dist/core-DWKLGY4N.js.map +1 -0
- package/dist/generate-4LQNJ7SX.js +249 -0
- package/dist/generate-4LQNJ7SX.js.map +1 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-EMVI47QG.js +416 -0
- package/dist/init-EMVI47QG.js.map +1 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +1117 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/scan-4YPRF7FV.js +12 -0
- package/dist/scan-4YPRF7FV.js.map +1 -0
- package/dist/service-QSZMZJBJ.js +208 -0
- package/dist/service-QSZMZJBJ.js.map +1 -0
- package/dist/static-viewer-MIPGZ4Z7.js +12 -0
- package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
- package/dist/test-SQ5ZHXWU.js +1067 -0
- package/dist/test-SQ5ZHXWU.js.map +1 -0
- package/dist/tokens-HSGMYK64.js +173 -0
- package/dist/tokens-HSGMYK64.js.map +1 -0
- package/dist/viewer-YRF4SQE4.js +11101 -0
- package/dist/viewer-YRF4SQE4.js.map +1 -0
- package/package.json +107 -0
- package/src/ai.ts +266 -0
- package/src/analyze.ts +265 -0
- package/src/bin.ts +916 -0
- package/src/build.ts +248 -0
- package/src/commands/a11y.ts +302 -0
- package/src/commands/add.ts +313 -0
- package/src/commands/audit.ts +195 -0
- package/src/commands/baseline.ts +221 -0
- package/src/commands/build.ts +144 -0
- package/src/commands/compare.ts +337 -0
- package/src/commands/context.ts +107 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/enhance.ts +858 -0
- package/src/commands/generate.ts +391 -0
- package/src/commands/init.ts +531 -0
- package/src/commands/link/figma.ts +645 -0
- package/src/commands/link/index.ts +10 -0
- package/src/commands/link/storybook.ts +267 -0
- package/src/commands/list.ts +49 -0
- package/src/commands/metrics.ts +114 -0
- package/src/commands/reset.ts +242 -0
- package/src/commands/scan.ts +537 -0
- package/src/commands/storygen.ts +207 -0
- package/src/commands/tokens.ts +251 -0
- package/src/commands/validate.ts +93 -0
- package/src/commands/verify.ts +215 -0
- package/src/core/composition.test.ts +262 -0
- package/src/core/composition.ts +255 -0
- package/src/core/config.ts +84 -0
- package/src/core/constants.ts +111 -0
- package/src/core/context.ts +380 -0
- package/src/core/defineSegment.ts +137 -0
- package/src/core/discovery.ts +337 -0
- package/src/core/figma.ts +263 -0
- package/src/core/fragment-types.ts +214 -0
- package/src/core/generators/context.ts +389 -0
- package/src/core/generators/index.ts +23 -0
- package/src/core/generators/registry.ts +364 -0
- package/src/core/generators/typescript-extractor.ts +374 -0
- package/src/core/importAnalyzer.ts +217 -0
- package/src/core/index.ts +149 -0
- package/src/core/loader.ts +155 -0
- package/src/core/node.ts +63 -0
- package/src/core/parser.ts +551 -0
- package/src/core/previewLoader.ts +172 -0
- package/src/core/schema/fragment.schema.json +189 -0
- package/src/core/schema/registry.schema.json +137 -0
- package/src/core/schema.ts +182 -0
- package/src/core/storyAdapter.test.ts +571 -0
- package/src/core/storyAdapter.ts +761 -0
- package/src/core/token-types.ts +287 -0
- package/src/core/types.ts +754 -0
- package/src/diff.ts +323 -0
- package/src/index.ts +43 -0
- package/src/mcp/__tests__/projectFields.test.ts +130 -0
- package/src/mcp/bin.ts +36 -0
- package/src/mcp/index.ts +8 -0
- package/src/mcp/server.ts +1310 -0
- package/src/mcp/utils.ts +54 -0
- package/src/mcp-bin.ts +36 -0
- package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
- package/src/migrate/__tests__/args/args.test.ts +452 -0
- package/src/migrate/__tests__/meta/meta.test.ts +198 -0
- package/src/migrate/__tests__/stories/stories.test.ts +278 -0
- package/src/migrate/__tests__/utils/utils.test.ts +371 -0
- package/src/migrate/__tests__/values/values.test.ts +303 -0
- package/src/migrate/bin.ts +108 -0
- package/src/migrate/converter.ts +658 -0
- package/src/migrate/detect.ts +196 -0
- package/src/migrate/index.ts +45 -0
- package/src/migrate/migrate.ts +163 -0
- package/src/migrate/parser.ts +1136 -0
- package/src/migrate/report.ts +624 -0
- package/src/migrate/types.ts +169 -0
- package/src/screenshot.ts +249 -0
- package/src/service/__tests__/ast-utils.test.ts +426 -0
- package/src/service/__tests__/enhance-scanner.test.ts +200 -0
- package/src/service/__tests__/figma/figma.test.ts +652 -0
- package/src/service/__tests__/metrics-store.test.ts +409 -0
- package/src/service/__tests__/patch-generator.test.ts +186 -0
- package/src/service/__tests__/props-extractor.test.ts +365 -0
- package/src/service/__tests__/token-registry.test.ts +267 -0
- package/src/service/analytics.ts +659 -0
- package/src/service/ast-utils.ts +444 -0
- package/src/service/browser-pool.ts +339 -0
- package/src/service/capture.ts +267 -0
- package/src/service/diff.ts +279 -0
- package/src/service/enhance/aggregator.ts +489 -0
- package/src/service/enhance/cache.ts +275 -0
- package/src/service/enhance/codebase-scanner.ts +357 -0
- package/src/service/enhance/context-generator.ts +529 -0
- package/src/service/enhance/doc-extractor.ts +523 -0
- package/src/service/enhance/index.ts +131 -0
- package/src/service/enhance/props-extractor.ts +665 -0
- package/src/service/enhance/scanner.ts +445 -0
- package/src/service/enhance/storybook-parser.ts +552 -0
- package/src/service/enhance/types.ts +346 -0
- package/src/service/enhance/variant-renderer.ts +479 -0
- package/src/service/figma.ts +1008 -0
- package/src/service/index.ts +249 -0
- package/src/service/metrics-store.ts +333 -0
- package/src/service/patch-generator.ts +349 -0
- package/src/service/report.ts +854 -0
- package/src/service/storage.ts +401 -0
- package/src/service/token-fixes.ts +281 -0
- package/src/service/token-parser.ts +504 -0
- package/src/service/token-registry.ts +721 -0
- package/src/service/utils.ts +172 -0
- package/src/setup.ts +241 -0
- package/src/shared/command-wrapper.ts +81 -0
- package/src/shared/dev-server-client.ts +199 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/segment-loader.ts +59 -0
- package/src/shared/types.ts +147 -0
- package/src/static-viewer.ts +715 -0
- package/src/test/discovery.ts +172 -0
- package/src/test/index.ts +281 -0
- package/src/test/reporters/console.ts +194 -0
- package/src/test/reporters/json.ts +190 -0
- package/src/test/reporters/junit.ts +186 -0
- package/src/test/runner.ts +598 -0
- package/src/test/types.ts +245 -0
- package/src/test/watch.ts +200 -0
- package/src/validators.ts +152 -0
- package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
- package/src/viewer/__tests__/render-utils.test.ts +232 -0
- package/src/viewer/__tests__/style-utils.test.ts +404 -0
- package/src/viewer/bin.ts +86 -0
- package/src/viewer/cli/health.ts +256 -0
- package/src/viewer/cli/index.ts +33 -0
- package/src/viewer/cli/scan.ts +124 -0
- package/src/viewer/cli/utils.ts +174 -0
- package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
- package/src/viewer/components/ActionCapture.tsx +172 -0
- package/src/viewer/components/ActionsPanel.tsx +371 -0
- package/src/viewer/components/App.tsx +638 -0
- package/src/viewer/components/BottomPanel.tsx +224 -0
- package/src/viewer/components/CodePanel.tsx +589 -0
- package/src/viewer/components/CommandPalette.tsx +336 -0
- package/src/viewer/components/ComponentGraph.tsx +394 -0
- package/src/viewer/components/ComponentHeader.tsx +85 -0
- package/src/viewer/components/ContractPanel.tsx +234 -0
- package/src/viewer/components/ErrorBoundary.tsx +85 -0
- package/src/viewer/components/FigmaEmbed.tsx +231 -0
- package/src/viewer/components/FragmentEditor.tsx +485 -0
- package/src/viewer/components/HealthDashboard.tsx +452 -0
- package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
- package/src/viewer/components/Icons.tsx +417 -0
- package/src/viewer/components/InteractionsPanel.tsx +720 -0
- package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
- package/src/viewer/components/IsolatedRender.tsx +111 -0
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
- package/src/viewer/components/LandingPage.tsx +441 -0
- package/src/viewer/components/Layout.tsx +22 -0
- package/src/viewer/components/LeftSidebar.tsx +391 -0
- package/src/viewer/components/MultiViewportPreview.tsx +429 -0
- package/src/viewer/components/PreviewArea.tsx +404 -0
- package/src/viewer/components/PreviewFrameHost.tsx +310 -0
- package/src/viewer/components/PreviewPane.tsx +150 -0
- package/src/viewer/components/PreviewToolbar.tsx +176 -0
- package/src/viewer/components/PropsEditor.tsx +512 -0
- package/src/viewer/components/PropsTable.tsx +98 -0
- package/src/viewer/components/RelationsSection.tsx +57 -0
- package/src/viewer/components/ResizablePanel.tsx +328 -0
- package/src/viewer/components/RightSidebar.tsx +118 -0
- package/src/viewer/components/ScreenshotButton.tsx +90 -0
- package/src/viewer/components/Sidebar.tsx +169 -0
- package/src/viewer/components/SkeletonLoader.tsx +156 -0
- package/src/viewer/components/StoryRenderer.tsx +128 -0
- package/src/viewer/components/ThemeProvider.tsx +96 -0
- package/src/viewer/components/Toast.tsx +67 -0
- package/src/viewer/components/TokenStylePanel.tsx +708 -0
- package/src/viewer/components/UsageSection.tsx +95 -0
- package/src/viewer/components/VariantMatrix.tsx +350 -0
- package/src/viewer/components/VariantRenderer.tsx +131 -0
- package/src/viewer/components/VariantTabs.tsx +84 -0
- package/src/viewer/components/ViewportSelector.tsx +165 -0
- package/src/viewer/components/_future/CreatePage.tsx +836 -0
- package/src/viewer/composition-renderer.ts +381 -0
- package/src/viewer/constants/index.ts +1 -0
- package/src/viewer/constants/ui.ts +185 -0
- package/src/viewer/entry.tsx +299 -0
- package/src/viewer/hooks/index.ts +2 -0
- package/src/viewer/hooks/useA11yCache.ts +383 -0
- package/src/viewer/hooks/useA11yService.ts +498 -0
- package/src/viewer/hooks/useActions.ts +138 -0
- package/src/viewer/hooks/useAppState.ts +124 -0
- package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
- package/src/viewer/hooks/useHmrStatus.ts +109 -0
- package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
- package/src/viewer/hooks/usePreviewBridge.ts +347 -0
- package/src/viewer/hooks/useScrollSpy.ts +78 -0
- package/src/viewer/hooks/useUrlState.ts +330 -0
- package/src/viewer/hooks/useViewSettings.ts +125 -0
- package/src/viewer/index.html +28 -0
- package/src/viewer/index.ts +14 -0
- package/src/viewer/intelligence/healthReport.ts +505 -0
- package/src/viewer/intelligence/styleDrift.ts +340 -0
- package/src/viewer/intelligence/usageScanner.ts +309 -0
- package/src/viewer/jsx-parser.ts +485 -0
- package/src/viewer/postcss.config.js +6 -0
- package/src/viewer/preview-frame-entry.tsx +25 -0
- package/src/viewer/preview-frame.html +109 -0
- package/src/viewer/render-template.html +68 -0
- package/src/viewer/render-utils.ts +170 -0
- package/src/viewer/server.ts +276 -0
- package/src/viewer/style-utils.ts +414 -0
- package/src/viewer/styles/globals.css +355 -0
- package/src/viewer/tailwind.config.js +37 -0
- package/src/viewer/types/a11y.ts +197 -0
- package/src/viewer/utils/a11y-fixes.ts +471 -0
- package/src/viewer/utils/actionExport.ts +372 -0
- package/src/viewer/utils/colorSchemes.ts +201 -0
- package/src/viewer/utils/detectRelationships.ts +256 -0
- package/src/viewer/vite-plugin.ts +2143 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { mkdir, rm, readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import {
|
|
6
|
+
MetricsStore,
|
|
7
|
+
createMetricsStore,
|
|
8
|
+
type ComplianceSnapshot,
|
|
9
|
+
} from "../metrics-store.js";
|
|
10
|
+
|
|
11
|
+
describe("metrics-store", () => {
|
|
12
|
+
let testDir: string;
|
|
13
|
+
let store: MetricsStore;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
// Create a unique temp directory for each test
|
|
17
|
+
testDir = join(tmpdir(), `metrics-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
18
|
+
await mkdir(testDir, { recursive: true });
|
|
19
|
+
store = createMetricsStore(testDir);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
// Clean up temp directory
|
|
24
|
+
try {
|
|
25
|
+
await rm(testDir, { recursive: true, force: true });
|
|
26
|
+
} catch {
|
|
27
|
+
// Ignore cleanup errors
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("saveSnapshot", () => {
|
|
32
|
+
it("saves a compliance snapshot to disk", async () => {
|
|
33
|
+
const snapshot: ComplianceSnapshot = {
|
|
34
|
+
timestamp: new Date().toISOString(),
|
|
35
|
+
component: "Button",
|
|
36
|
+
compliance: 85.5,
|
|
37
|
+
violations: 3,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const filepath = await store.saveSnapshot(snapshot);
|
|
41
|
+
|
|
42
|
+
expect(filepath).toContain("Button.json");
|
|
43
|
+
|
|
44
|
+
// Verify file exists by reading it
|
|
45
|
+
const content = await readFile(filepath, "utf-8");
|
|
46
|
+
const snapshots = JSON.parse(content);
|
|
47
|
+
expect(snapshots.length).toBe(1);
|
|
48
|
+
expect(snapshots[0].component).toBe("Button");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("appends snapshots to existing file for same day/component", async () => {
|
|
52
|
+
const timestamp = new Date().toISOString();
|
|
53
|
+
|
|
54
|
+
const snapshot1: ComplianceSnapshot = {
|
|
55
|
+
timestamp,
|
|
56
|
+
component: "Button",
|
|
57
|
+
compliance: 80,
|
|
58
|
+
violations: 5,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const snapshot2: ComplianceSnapshot = {
|
|
62
|
+
timestamp,
|
|
63
|
+
component: "Button",
|
|
64
|
+
compliance: 85,
|
|
65
|
+
violations: 3,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const filepath1 = await store.saveSnapshot(snapshot1);
|
|
69
|
+
const filepath2 = await store.saveSnapshot(snapshot2);
|
|
70
|
+
|
|
71
|
+
// Both should write to same file
|
|
72
|
+
expect(filepath1).toBe(filepath2);
|
|
73
|
+
|
|
74
|
+
// File should contain both snapshots
|
|
75
|
+
const content = await readFile(filepath2, "utf-8");
|
|
76
|
+
const snapshots = JSON.parse(content);
|
|
77
|
+
expect(snapshots).toHaveLength(2);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("includes violation details when provided", async () => {
|
|
81
|
+
const snapshot: ComplianceSnapshot = {
|
|
82
|
+
timestamp: new Date().toISOString(),
|
|
83
|
+
component: "Card",
|
|
84
|
+
compliance: 75,
|
|
85
|
+
violations: 2,
|
|
86
|
+
violationDetails: [
|
|
87
|
+
{ property: "backgroundColor", issue: "Hardcoded value", severity: "error" },
|
|
88
|
+
{ property: "padding", issue: "Non-token value", severity: "warning" },
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const filepath = await store.saveSnapshot(snapshot);
|
|
93
|
+
const content = await readFile(filepath, "utf-8");
|
|
94
|
+
const snapshots = JSON.parse(content);
|
|
95
|
+
|
|
96
|
+
expect(snapshots[0].violationDetails).toHaveLength(2);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("includes git metadata when provided", async () => {
|
|
100
|
+
const snapshot: ComplianceSnapshot = {
|
|
101
|
+
timestamp: new Date().toISOString(),
|
|
102
|
+
component: "Button",
|
|
103
|
+
compliance: 90,
|
|
104
|
+
violations: 1,
|
|
105
|
+
commitHash: "abc123",
|
|
106
|
+
branch: "feature/new-button",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const filepath = await store.saveSnapshot(snapshot);
|
|
110
|
+
const content = await readFile(filepath, "utf-8");
|
|
111
|
+
const snapshots = JSON.parse(content);
|
|
112
|
+
|
|
113
|
+
expect(snapshots[0].commitHash).toBe("abc123");
|
|
114
|
+
expect(snapshots[0].branch).toBe("feature/new-button");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("getHistory", () => {
|
|
119
|
+
it("returns empty array when no metrics exist", async () => {
|
|
120
|
+
const history = await store.getHistory("Button");
|
|
121
|
+
expect(history).toHaveLength(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("returns snapshots for a specific component", async () => {
|
|
125
|
+
const now = new Date();
|
|
126
|
+
|
|
127
|
+
await store.saveSnapshot({
|
|
128
|
+
timestamp: now.toISOString(),
|
|
129
|
+
component: "Button",
|
|
130
|
+
compliance: 85,
|
|
131
|
+
violations: 2,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await store.saveSnapshot({
|
|
135
|
+
timestamp: now.toISOString(),
|
|
136
|
+
component: "Card",
|
|
137
|
+
compliance: 90,
|
|
138
|
+
violations: 1,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const history = await store.getHistory("Button");
|
|
142
|
+
|
|
143
|
+
expect(history).toHaveLength(1);
|
|
144
|
+
expect(history[0].component).toBe("Button");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("filters by date range", async () => {
|
|
148
|
+
const now = new Date();
|
|
149
|
+
const thirtyDaysAgo = new Date(now);
|
|
150
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 35);
|
|
151
|
+
|
|
152
|
+
// Save a recent snapshot
|
|
153
|
+
await store.saveSnapshot({
|
|
154
|
+
timestamp: now.toISOString(),
|
|
155
|
+
component: "Button",
|
|
156
|
+
compliance: 85,
|
|
157
|
+
violations: 2,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Default is 30 days, so 35 days ago would be filtered out
|
|
161
|
+
const history = await store.getHistory("Button", { days: 30 });
|
|
162
|
+
|
|
163
|
+
expect(history).toHaveLength(1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("returns all components when component is 'all'", async () => {
|
|
167
|
+
const now = new Date();
|
|
168
|
+
|
|
169
|
+
await store.saveSnapshot({
|
|
170
|
+
timestamp: now.toISOString(),
|
|
171
|
+
component: "Button",
|
|
172
|
+
compliance: 85,
|
|
173
|
+
violations: 2,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await store.saveSnapshot({
|
|
177
|
+
timestamp: now.toISOString(),
|
|
178
|
+
component: "Card",
|
|
179
|
+
compliance: 90,
|
|
180
|
+
violations: 1,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const history = await store.getHistory("all");
|
|
184
|
+
|
|
185
|
+
expect(history).toHaveLength(2);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("sorts snapshots by timestamp", async () => {
|
|
189
|
+
const now = new Date();
|
|
190
|
+
const earlier = new Date(now);
|
|
191
|
+
earlier.setHours(earlier.getHours() - 1);
|
|
192
|
+
|
|
193
|
+
await store.saveSnapshot({
|
|
194
|
+
timestamp: now.toISOString(),
|
|
195
|
+
component: "Button",
|
|
196
|
+
compliance: 90,
|
|
197
|
+
violations: 1,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await store.saveSnapshot({
|
|
201
|
+
timestamp: earlier.toISOString(),
|
|
202
|
+
component: "Button",
|
|
203
|
+
compliance: 80,
|
|
204
|
+
violations: 3,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const history = await store.getHistory("Button");
|
|
208
|
+
|
|
209
|
+
expect(history).toHaveLength(2);
|
|
210
|
+
expect(history[0].compliance).toBe(80); // Earlier first
|
|
211
|
+
expect(history[1].compliance).toBe(90); // Later second
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("getSystemHistory", () => {
|
|
216
|
+
it("returns all component snapshots", async () => {
|
|
217
|
+
const now = new Date();
|
|
218
|
+
|
|
219
|
+
await store.saveSnapshot({
|
|
220
|
+
timestamp: now.toISOString(),
|
|
221
|
+
component: "Button",
|
|
222
|
+
compliance: 85,
|
|
223
|
+
violations: 2,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
await store.saveSnapshot({
|
|
227
|
+
timestamp: now.toISOString(),
|
|
228
|
+
component: "Card",
|
|
229
|
+
compliance: 90,
|
|
230
|
+
violations: 1,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const history = await store.getSystemHistory();
|
|
234
|
+
|
|
235
|
+
expect(history).toHaveLength(2);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("getTrend", () => {
|
|
240
|
+
it("returns trend data grouped by day", async () => {
|
|
241
|
+
const now = new Date();
|
|
242
|
+
|
|
243
|
+
await store.saveSnapshot({
|
|
244
|
+
timestamp: now.toISOString(),
|
|
245
|
+
component: "Button",
|
|
246
|
+
compliance: 85,
|
|
247
|
+
violations: 2,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const trend = await store.getTrend("Button", { groupBy: "day" });
|
|
251
|
+
|
|
252
|
+
expect(trend.component).toBe("Button");
|
|
253
|
+
expect(trend.period).toBe("day");
|
|
254
|
+
expect(trend.dataPoints.length).toBeGreaterThanOrEqual(1);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("calculates improving trend", async () => {
|
|
258
|
+
const now = new Date();
|
|
259
|
+
const yesterday = new Date(now);
|
|
260
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
261
|
+
|
|
262
|
+
// Earlier: lower compliance
|
|
263
|
+
await store.saveSnapshot({
|
|
264
|
+
timestamp: yesterday.toISOString(),
|
|
265
|
+
component: "Button",
|
|
266
|
+
compliance: 70,
|
|
267
|
+
violations: 5,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Now: higher compliance
|
|
271
|
+
await store.saveSnapshot({
|
|
272
|
+
timestamp: now.toISOString(),
|
|
273
|
+
component: "Button",
|
|
274
|
+
compliance: 95,
|
|
275
|
+
violations: 1,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const trend = await store.getTrend("Button");
|
|
279
|
+
|
|
280
|
+
// With significant improvement, trend should be improving
|
|
281
|
+
expect(trend.trend).toBe("improving");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("calculates declining trend", async () => {
|
|
285
|
+
const now = new Date();
|
|
286
|
+
const yesterday = new Date(now);
|
|
287
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
288
|
+
|
|
289
|
+
// Earlier: higher compliance
|
|
290
|
+
await store.saveSnapshot({
|
|
291
|
+
timestamp: yesterday.toISOString(),
|
|
292
|
+
component: "Button",
|
|
293
|
+
compliance: 95,
|
|
294
|
+
violations: 1,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Now: lower compliance
|
|
298
|
+
await store.saveSnapshot({
|
|
299
|
+
timestamp: now.toISOString(),
|
|
300
|
+
component: "Button",
|
|
301
|
+
compliance: 70,
|
|
302
|
+
violations: 5,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const trend = await store.getTrend("Button");
|
|
306
|
+
|
|
307
|
+
expect(trend.trend).toBe("declining");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("calculates stable trend for small changes", async () => {
|
|
311
|
+
const now = new Date();
|
|
312
|
+
|
|
313
|
+
await store.saveSnapshot({
|
|
314
|
+
timestamp: now.toISOString(),
|
|
315
|
+
component: "Button",
|
|
316
|
+
compliance: 85,
|
|
317
|
+
violations: 2,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const trend = await store.getTrend("Button");
|
|
321
|
+
|
|
322
|
+
// With only one data point, should be stable
|
|
323
|
+
expect(trend.trend).toBe("stable");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("calculates average compliance", async () => {
|
|
327
|
+
const now = new Date();
|
|
328
|
+
|
|
329
|
+
await store.saveSnapshot({
|
|
330
|
+
timestamp: now.toISOString(),
|
|
331
|
+
component: "Button",
|
|
332
|
+
compliance: 80,
|
|
333
|
+
violations: 3,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
await store.saveSnapshot({
|
|
337
|
+
timestamp: now.toISOString(),
|
|
338
|
+
component: "Button",
|
|
339
|
+
compliance: 90,
|
|
340
|
+
violations: 1,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const trend = await store.getTrend("Button");
|
|
344
|
+
|
|
345
|
+
expect(trend.averageCompliance).toBe(85);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe("generateSparkline", () => {
|
|
350
|
+
it("generates ASCII sparkline from data points", () => {
|
|
351
|
+
const dataPoints = [
|
|
352
|
+
{ compliance: 60 },
|
|
353
|
+
{ compliance: 70 },
|
|
354
|
+
{ compliance: 80 },
|
|
355
|
+
{ compliance: 90 },
|
|
356
|
+
{ compliance: 100 },
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
const sparkline = store.generateSparkline(dataPoints);
|
|
360
|
+
|
|
361
|
+
expect(sparkline.length).toBe(5);
|
|
362
|
+
expect(sparkline).toMatch(/[▁▂▃▄▅▆▇█ ]+/);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("handles empty data points", () => {
|
|
366
|
+
const sparkline = store.generateSparkline([]);
|
|
367
|
+
expect(sparkline).toBe("─");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("handles single data point", () => {
|
|
371
|
+
const sparkline = store.generateSparkline([{ compliance: 80 }]);
|
|
372
|
+
expect(sparkline.length).toBe(1);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("handles flat data (all same values)", () => {
|
|
376
|
+
const dataPoints = [
|
|
377
|
+
{ compliance: 80 },
|
|
378
|
+
{ compliance: 80 },
|
|
379
|
+
{ compliance: 80 },
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
const sparkline = store.generateSparkline(dataPoints);
|
|
383
|
+
|
|
384
|
+
// All same value, should be middle character
|
|
385
|
+
expect(sparkline.length).toBe(3);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe("cleanup", () => {
|
|
390
|
+
it("deletes files older than retention period", async () => {
|
|
391
|
+
// This test is tricky because we can't easily create old files
|
|
392
|
+
// Just verify the method runs without error
|
|
393
|
+
const deleted = await store.cleanup(90);
|
|
394
|
+
expect(typeof deleted).toBe("number");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("returns count of deleted files", async () => {
|
|
398
|
+
const deleted = await store.cleanup(90);
|
|
399
|
+
expect(deleted).toBe(0); // No files to delete in fresh test dir
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe("createMetricsStore", () => {
|
|
404
|
+
it("creates a MetricsStore instance", () => {
|
|
405
|
+
const store = createMetricsStore("/some/path");
|
|
406
|
+
expect(store).toBeInstanceOf(MetricsStore);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import {
|
|
6
|
+
generateTokenPatches,
|
|
7
|
+
generateCSSInJSPatches,
|
|
8
|
+
type Patch,
|
|
9
|
+
} from "../patch-generator.js";
|
|
10
|
+
import {
|
|
11
|
+
TokenRegistryManager,
|
|
12
|
+
createTokenRegistry,
|
|
13
|
+
} from "../token-registry.js";
|
|
14
|
+
import type { TokenConfig } from "../../core/index.js";
|
|
15
|
+
|
|
16
|
+
describe("patch-generator", () => {
|
|
17
|
+
let testDir: string;
|
|
18
|
+
let registry: TokenRegistryManager;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
testDir = join(tmpdir(), `patch-gen-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
22
|
+
await mkdir(testDir, { recursive: true });
|
|
23
|
+
registry = createTokenRegistry();
|
|
24
|
+
|
|
25
|
+
// Create token files for testing
|
|
26
|
+
const cssTokens = `
|
|
27
|
+
:root {
|
|
28
|
+
--color-primary: #ff0000;
|
|
29
|
+
--color-secondary: #0000ff;
|
|
30
|
+
--spacing-sm: 8px;
|
|
31
|
+
--spacing-md: 16px;
|
|
32
|
+
--spacing-lg: 24px;
|
|
33
|
+
}
|
|
34
|
+
`;
|
|
35
|
+
await writeFile(join(testDir, "tokens.css"), cssTokens);
|
|
36
|
+
|
|
37
|
+
const config: TokenConfig = {
|
|
38
|
+
include: ["tokens.css"],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
await registry.initialize(config, testDir);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(async () => {
|
|
45
|
+
registry.clear();
|
|
46
|
+
try {
|
|
47
|
+
await rm(testDir, { recursive: true, force: true });
|
|
48
|
+
} catch {
|
|
49
|
+
// Ignore cleanup errors
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("generateTokenPatches", () => {
|
|
54
|
+
it("generates patches for hardcoded color values", () => {
|
|
55
|
+
const styleDiffs = [
|
|
56
|
+
{ property: "backgroundColor", figma: "#ff0000", rendered: "#ff0000", match: true },
|
|
57
|
+
{ property: "color", figma: "#0000ff", rendered: "#0000ff", match: true },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const result = generateTokenPatches("Button", styleDiffs, registry, {
|
|
61
|
+
sourceFile: "src/Button.tsx",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(result.summary).toBeDefined();
|
|
65
|
+
expect(result.fixableCount).toBeGreaterThanOrEqual(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns empty patches when all values already use tokens", () => {
|
|
69
|
+
const styleDiffs = [
|
|
70
|
+
{ property: "backgroundColor", figma: "var(--color-primary)", rendered: "var(--color-primary)", match: true },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const result = generateTokenPatches("Button", styleDiffs, registry);
|
|
74
|
+
|
|
75
|
+
// All values are already tokens
|
|
76
|
+
expect(result.summary).toContain("compliance");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("identifies unfixable values without matching tokens", () => {
|
|
80
|
+
const styleDiffs = [
|
|
81
|
+
{ property: "backgroundColor", figma: "#abc123", rendered: "#abc123", match: true },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const result = generateTokenPatches("Button", styleDiffs, registry);
|
|
85
|
+
|
|
86
|
+
expect(result.unfixableCount).toBeGreaterThanOrEqual(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("includes summary with fix counts", () => {
|
|
90
|
+
const styleDiffs = [
|
|
91
|
+
{ property: "padding", figma: "16px", rendered: "16px", match: true },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const result = generateTokenPatches("Card", styleDiffs, registry);
|
|
95
|
+
|
|
96
|
+
expect(result.summary).toBeDefined();
|
|
97
|
+
expect(typeof result.fixableCount).toBe("number");
|
|
98
|
+
expect(typeof result.unfixableCount).toBe("number");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("generateCSSInJSPatches", () => {
|
|
103
|
+
it("generates patches for inline style values", () => {
|
|
104
|
+
const source = `
|
|
105
|
+
function Button() {
|
|
106
|
+
return <button style={{ backgroundColor: "#ff0000", padding: "16px" }}>Click</button>;
|
|
107
|
+
}
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
const fixes = [
|
|
111
|
+
{ property: "backgroundColor", currentValue: "#ff0000", tokenName: "color-primary" },
|
|
112
|
+
{ property: "padding", currentValue: "16px", tokenName: "spacing-md" },
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const patches = generateCSSInJSPatches(source, fixes, { fileName: "Button.tsx" });
|
|
116
|
+
|
|
117
|
+
expect(patches.length).toBeGreaterThanOrEqual(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("generates patches for styled-components template literals", () => {
|
|
121
|
+
const source = `
|
|
122
|
+
const Button = styled.button\`
|
|
123
|
+
background-color: #ff0000;
|
|
124
|
+
padding: 16px;
|
|
125
|
+
\`;
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
const fixes = [
|
|
129
|
+
{ property: "background-color", currentValue: "#ff0000", tokenName: "color-primary" },
|
|
130
|
+
{ property: "padding", currentValue: "16px", tokenName: "spacing-md" },
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
const patches = generateCSSInJSPatches(source, fixes, { fileName: "Button.tsx" });
|
|
134
|
+
|
|
135
|
+
expect(patches.length).toBeGreaterThanOrEqual(0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("returns empty patches for values not found in source", () => {
|
|
139
|
+
const source = `
|
|
140
|
+
function Button() {
|
|
141
|
+
return <button className="btn">Click</button>;
|
|
142
|
+
}
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
const fixes = [
|
|
146
|
+
{ property: "backgroundColor", currentValue: "#123456", tokenName: "color-unknown" },
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const patches = generateCSSInJSPatches(source, fixes, { fileName: "Button.tsx" });
|
|
150
|
+
|
|
151
|
+
expect(patches).toHaveLength(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("handles camelCase property names", () => {
|
|
155
|
+
const source = `
|
|
156
|
+
function Button() {
|
|
157
|
+
return <button style={{ backgroundColor: "red" }}>Click</button>;
|
|
158
|
+
}
|
|
159
|
+
`;
|
|
160
|
+
|
|
161
|
+
const fixes = [
|
|
162
|
+
{ property: "backgroundColor", currentValue: "red", tokenName: "color-error" },
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const patches = generateCSSInJSPatches(source, fixes, { fileName: "Button.tsx" });
|
|
166
|
+
|
|
167
|
+
expect(patches.length).toBeGreaterThanOrEqual(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("handles kebab-case property names in CSS", () => {
|
|
171
|
+
const source = `
|
|
172
|
+
.button {
|
|
173
|
+
background-color: #ff0000;
|
|
174
|
+
}
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
const fixes = [
|
|
178
|
+
{ property: "background-color", currentValue: "#ff0000", tokenName: "color-primary" },
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
const patches = generateCSSInJSPatches(source, fixes, { fileName: "styles.css" });
|
|
182
|
+
|
|
183
|
+
expect(patches.length).toBeGreaterThanOrEqual(0);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|