@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,598 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test runner - executes play functions in browser via Playwright
|
|
3
|
+
*
|
|
4
|
+
* Playwright is an optional dependency. If not installed, the test command
|
|
5
|
+
* will prompt the user to install it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
TestCase,
|
|
10
|
+
TestResult,
|
|
11
|
+
TestSuite,
|
|
12
|
+
TestRunResult,
|
|
13
|
+
TestError,
|
|
14
|
+
StepResult,
|
|
15
|
+
A11yResult,
|
|
16
|
+
A11yViolation,
|
|
17
|
+
RunnerOptions,
|
|
18
|
+
TestReporter,
|
|
19
|
+
} from './types.js';
|
|
20
|
+
import { groupTestsByComponent } from './discovery.js';
|
|
21
|
+
|
|
22
|
+
// Dynamic playwright types (since it's optional)
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
type Browser = any;
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
type BrowserContext = any;
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
type Page = any;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Browser pool for parallel test execution
|
|
32
|
+
*/
|
|
33
|
+
interface BrowserPool {
|
|
34
|
+
browser: Browser;
|
|
35
|
+
contexts: BrowserContextEntry[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface BrowserContextEntry {
|
|
39
|
+
id: number;
|
|
40
|
+
context: BrowserContext;
|
|
41
|
+
page: Page;
|
|
42
|
+
busy: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Run tests with the given configuration
|
|
47
|
+
*/
|
|
48
|
+
export async function runTests(
|
|
49
|
+
testCases: TestCase[],
|
|
50
|
+
options: RunnerOptions,
|
|
51
|
+
reporters: TestReporter[] = []
|
|
52
|
+
): Promise<TestRunResult> {
|
|
53
|
+
const startTime = new Date();
|
|
54
|
+
const results: TestResult[] = [];
|
|
55
|
+
|
|
56
|
+
// Notify reporters of run start
|
|
57
|
+
for (const reporter of reporters) {
|
|
58
|
+
reporter.onRunStart?.(testCases.length);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Import Playwright dynamically (optional dependency)
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
+
let playwright: any;
|
|
64
|
+
try {
|
|
65
|
+
playwright = await import('playwright');
|
|
66
|
+
} catch {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'Playwright is required for running tests. Install it with: npm install -D playwright'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Launch browser
|
|
73
|
+
const browserType = playwright[options.browser];
|
|
74
|
+
const browser = await browserType.launch({
|
|
75
|
+
headless: options.headless,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Create browser pool for parallel execution
|
|
79
|
+
const pool = await createBrowserPool(browser, options.parallel);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
// Determine server URL
|
|
83
|
+
const serverUrl = options.serverUrl || `http://localhost:${options.port}`;
|
|
84
|
+
|
|
85
|
+
// Run tests with parallelism
|
|
86
|
+
if (options.parallel > 1) {
|
|
87
|
+
await runTestsParallel(
|
|
88
|
+
testCases,
|
|
89
|
+
pool,
|
|
90
|
+
serverUrl,
|
|
91
|
+
options,
|
|
92
|
+
reporters,
|
|
93
|
+
results
|
|
94
|
+
);
|
|
95
|
+
} else {
|
|
96
|
+
await runTestsSequential(
|
|
97
|
+
testCases,
|
|
98
|
+
pool.contexts[0],
|
|
99
|
+
serverUrl,
|
|
100
|
+
options,
|
|
101
|
+
reporters,
|
|
102
|
+
results
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
} finally {
|
|
106
|
+
// Cleanup browser pool
|
|
107
|
+
await cleanupBrowserPool(pool);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const endTime = new Date();
|
|
111
|
+
|
|
112
|
+
// Aggregate results into suites
|
|
113
|
+
const suites = aggregateResults(results);
|
|
114
|
+
|
|
115
|
+
const runResult: TestRunResult = {
|
|
116
|
+
suites,
|
|
117
|
+
totalTests: results.length,
|
|
118
|
+
totalPassed: results.filter((r) => r.status === 'passed').length,
|
|
119
|
+
totalFailed: results.filter((r) => r.status === 'failed').length,
|
|
120
|
+
totalSkipped: results.filter((r) => r.status === 'skipped').length,
|
|
121
|
+
totalDuration: endTime.getTime() - startTime.getTime(),
|
|
122
|
+
startTime,
|
|
123
|
+
endTime,
|
|
124
|
+
totalA11yViolations: options.a11y
|
|
125
|
+
? results.reduce((sum, r) => sum + (r.accessibility?.violations.length || 0), 0)
|
|
126
|
+
: undefined,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Notify reporters of run completion
|
|
130
|
+
for (const reporter of reporters) {
|
|
131
|
+
await reporter.onRunComplete(runResult);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return runResult;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Create a browser pool for parallel execution
|
|
139
|
+
*/
|
|
140
|
+
async function createBrowserPool(
|
|
141
|
+
browser: Browser,
|
|
142
|
+
parallelism: number
|
|
143
|
+
): Promise<BrowserPool> {
|
|
144
|
+
const contexts: BrowserContextEntry[] = [];
|
|
145
|
+
|
|
146
|
+
for (let i = 0; i < parallelism; i++) {
|
|
147
|
+
const context = await browser.newContext({
|
|
148
|
+
viewport: { width: 1280, height: 720 },
|
|
149
|
+
});
|
|
150
|
+
const page = await context.newPage();
|
|
151
|
+
contexts.push({ id: i, context, page, busy: false });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { browser, contexts };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Cleanup browser pool
|
|
159
|
+
*/
|
|
160
|
+
async function cleanupBrowserPool(pool: BrowserPool): Promise<void> {
|
|
161
|
+
for (const entry of pool.contexts) {
|
|
162
|
+
await entry.page.close();
|
|
163
|
+
await entry.context.close();
|
|
164
|
+
}
|
|
165
|
+
await pool.browser.close();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Run tests sequentially
|
|
170
|
+
*/
|
|
171
|
+
async function runTestsSequential(
|
|
172
|
+
testCases: TestCase[],
|
|
173
|
+
ctx: BrowserContextEntry,
|
|
174
|
+
serverUrl: string,
|
|
175
|
+
options: RunnerOptions,
|
|
176
|
+
reporters: TestReporter[],
|
|
177
|
+
results: TestResult[]
|
|
178
|
+
): Promise<void> {
|
|
179
|
+
for (const testCase of testCases) {
|
|
180
|
+
// Notify reporters of test start
|
|
181
|
+
for (const reporter of reporters) {
|
|
182
|
+
reporter.onTestStart?.(testCase);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const result = await runSingleTest(testCase, ctx, serverUrl, options);
|
|
186
|
+
results.push(result);
|
|
187
|
+
|
|
188
|
+
// Notify reporters of test completion
|
|
189
|
+
for (const reporter of reporters) {
|
|
190
|
+
reporter.onTestComplete?.(result);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Bail on first failure if configured
|
|
194
|
+
if (options.bail && result.status === 'failed') {
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Run tests in parallel
|
|
202
|
+
*/
|
|
203
|
+
async function runTestsParallel(
|
|
204
|
+
testCases: TestCase[],
|
|
205
|
+
pool: BrowserPool,
|
|
206
|
+
serverUrl: string,
|
|
207
|
+
options: RunnerOptions,
|
|
208
|
+
reporters: TestReporter[],
|
|
209
|
+
results: TestResult[]
|
|
210
|
+
): Promise<void> {
|
|
211
|
+
const queue = [...testCases];
|
|
212
|
+
const inFlight: Promise<void>[] = [];
|
|
213
|
+
let bailTriggered = false;
|
|
214
|
+
|
|
215
|
+
const acquireContext = (): BrowserContextEntry | null => {
|
|
216
|
+
const available = pool.contexts.find((c) => !c.busy);
|
|
217
|
+
if (available) {
|
|
218
|
+
available.busy = true;
|
|
219
|
+
return available;
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const releaseContext = (ctx: BrowserContextEntry): void => {
|
|
225
|
+
ctx.busy = false;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const runNext = async (): Promise<void> => {
|
|
229
|
+
if (bailTriggered || queue.length === 0) return;
|
|
230
|
+
|
|
231
|
+
const ctx = acquireContext();
|
|
232
|
+
if (!ctx) return;
|
|
233
|
+
|
|
234
|
+
const testCase = queue.shift()!;
|
|
235
|
+
|
|
236
|
+
// Notify reporters of test start
|
|
237
|
+
for (const reporter of reporters) {
|
|
238
|
+
reporter.onTestStart?.(testCase);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const result = await runSingleTest(testCase, ctx, serverUrl, options);
|
|
243
|
+
results.push(result);
|
|
244
|
+
|
|
245
|
+
// Notify reporters of test completion
|
|
246
|
+
for (const reporter of reporters) {
|
|
247
|
+
reporter.onTestComplete?.(result);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check bail condition
|
|
251
|
+
if (options.bail && result.status === 'failed') {
|
|
252
|
+
bailTriggered = true;
|
|
253
|
+
}
|
|
254
|
+
} finally {
|
|
255
|
+
releaseContext(ctx);
|
|
256
|
+
// Start next test if not bailing
|
|
257
|
+
if (!bailTriggered && queue.length > 0) {
|
|
258
|
+
const nextPromise = runNext();
|
|
259
|
+
inFlight.push(nextPromise);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Start initial batch
|
|
265
|
+
const initialBatch = Math.min(pool.contexts.length, queue.length);
|
|
266
|
+
for (let i = 0; i < initialBatch; i++) {
|
|
267
|
+
inFlight.push(runNext());
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Wait for all to complete
|
|
271
|
+
await Promise.all(inFlight);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Run a single test case
|
|
276
|
+
*/
|
|
277
|
+
async function runSingleTest(
|
|
278
|
+
testCase: TestCase,
|
|
279
|
+
ctx: BrowserContextEntry,
|
|
280
|
+
serverUrl: string,
|
|
281
|
+
options: RunnerOptions
|
|
282
|
+
): Promise<TestResult> {
|
|
283
|
+
const { page } = ctx;
|
|
284
|
+
const startTime = performance.now();
|
|
285
|
+
const steps: StepResult[] = [];
|
|
286
|
+
let error: TestError | undefined;
|
|
287
|
+
let status: TestResult['status'] = 'passed';
|
|
288
|
+
let retryAttempt = 0;
|
|
289
|
+
|
|
290
|
+
// Retry logic
|
|
291
|
+
const maxAttempts = options.retries + 1;
|
|
292
|
+
|
|
293
|
+
while (retryAttempt < maxAttempts) {
|
|
294
|
+
try {
|
|
295
|
+
// Navigate to the variant
|
|
296
|
+
const url = buildVariantUrl(serverUrl, testCase);
|
|
297
|
+
await page.goto(url, { waitUntil: 'networkidle' });
|
|
298
|
+
|
|
299
|
+
// Wait for preview container
|
|
300
|
+
await page.waitForSelector('[data-preview-container="true"]', {
|
|
301
|
+
timeout: options.timeout,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Execute play function in browser context
|
|
305
|
+
const playResult = await executePlayFunction(page, testCase, options.timeout);
|
|
306
|
+
|
|
307
|
+
if (playResult.error) {
|
|
308
|
+
error = playResult.error;
|
|
309
|
+
status = 'failed';
|
|
310
|
+
steps.push(...playResult.steps);
|
|
311
|
+
|
|
312
|
+
// Retry if not last attempt
|
|
313
|
+
if (retryAttempt < maxAttempts - 1) {
|
|
314
|
+
retryAttempt++;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
status = 'passed';
|
|
319
|
+
steps.push(...playResult.steps);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
break; // Success, no retry needed
|
|
323
|
+
} catch (e) {
|
|
324
|
+
error = {
|
|
325
|
+
message: e instanceof Error ? e.message : String(e),
|
|
326
|
+
stack: e instanceof Error ? e.stack : undefined,
|
|
327
|
+
};
|
|
328
|
+
status = 'failed';
|
|
329
|
+
|
|
330
|
+
if (retryAttempt < maxAttempts - 1) {
|
|
331
|
+
retryAttempt++;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const duration = performance.now() - startTime;
|
|
339
|
+
|
|
340
|
+
// Run accessibility checks if enabled
|
|
341
|
+
let accessibility: A11yResult | undefined;
|
|
342
|
+
if (options.a11y && status === 'passed') {
|
|
343
|
+
accessibility = await runA11yChecks(page);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Capture screenshot for visual regression if enabled
|
|
347
|
+
let screenshotPath: string | undefined;
|
|
348
|
+
if (options.visual) {
|
|
349
|
+
screenshotPath = await captureScreenshot(page, testCase, options.outputDir);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
testCase,
|
|
354
|
+
status,
|
|
355
|
+
duration,
|
|
356
|
+
steps,
|
|
357
|
+
error,
|
|
358
|
+
accessibility,
|
|
359
|
+
screenshotPath,
|
|
360
|
+
retryAttempt: retryAttempt > 0 ? retryAttempt : undefined,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Build URL for a variant
|
|
366
|
+
*/
|
|
367
|
+
function buildVariantUrl(serverUrl: string, testCase: TestCase): string {
|
|
368
|
+
const params = new URLSearchParams({
|
|
369
|
+
component: testCase.component,
|
|
370
|
+
variant: testCase.variant,
|
|
371
|
+
isolated: 'true',
|
|
372
|
+
});
|
|
373
|
+
return `${serverUrl}?${params.toString()}`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Execute play function in browser context
|
|
378
|
+
*/
|
|
379
|
+
async function executePlayFunction(
|
|
380
|
+
page: Page,
|
|
381
|
+
testCase: TestCase,
|
|
382
|
+
timeout: number
|
|
383
|
+
): Promise<{ steps: StepResult[]; error?: TestError }> {
|
|
384
|
+
const result = await page.evaluate(
|
|
385
|
+
async ({ component, variant, timeout }: { component: string; variant: string; timeout: number }) => {
|
|
386
|
+
// Find the segment definition in the global registry
|
|
387
|
+
const registry = (window as unknown as { __SEGMENTS_REGISTRY__?: Map<string, unknown> })
|
|
388
|
+
.__SEGMENTS_REGISTRY__;
|
|
389
|
+
|
|
390
|
+
if (!registry) {
|
|
391
|
+
return {
|
|
392
|
+
steps: [],
|
|
393
|
+
error: { message: 'Segments registry not found. Make sure the viewer is loaded.' },
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const segment = registry.get(component) as {
|
|
398
|
+
variants?: Array<{
|
|
399
|
+
name: string;
|
|
400
|
+
play?: (ctx: {
|
|
401
|
+
canvasElement: HTMLElement;
|
|
402
|
+
args: Record<string, unknown>;
|
|
403
|
+
step: (name: string, fn: () => Promise<void>) => Promise<void>;
|
|
404
|
+
}) => Promise<void>;
|
|
405
|
+
}>;
|
|
406
|
+
} | undefined;
|
|
407
|
+
|
|
408
|
+
if (!segment) {
|
|
409
|
+
return {
|
|
410
|
+
steps: [],
|
|
411
|
+
error: { message: `Component "${component}" not found in registry` },
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const variantDef = segment.variants?.find((v) => v.name === variant);
|
|
416
|
+
if (!variantDef || !variantDef.play) {
|
|
417
|
+
return {
|
|
418
|
+
steps: [],
|
|
419
|
+
error: { message: `Variant "${variant}" not found or has no play function` },
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Find the preview container
|
|
424
|
+
const canvasElement = document.querySelector('[data-preview-container="true"]') as HTMLElement;
|
|
425
|
+
if (!canvasElement) {
|
|
426
|
+
return {
|
|
427
|
+
steps: [],
|
|
428
|
+
error: { message: 'Preview container not found' },
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Track steps
|
|
433
|
+
const steps: Array<{
|
|
434
|
+
name: string;
|
|
435
|
+
status: 'passed' | 'failed';
|
|
436
|
+
duration: number;
|
|
437
|
+
error?: { message: string };
|
|
438
|
+
}> = [];
|
|
439
|
+
|
|
440
|
+
const step = async (name: string, fn: () => Promise<void>): Promise<void> => {
|
|
441
|
+
const stepStart = performance.now();
|
|
442
|
+
try {
|
|
443
|
+
await fn();
|
|
444
|
+
steps.push({
|
|
445
|
+
name,
|
|
446
|
+
status: 'passed',
|
|
447
|
+
duration: performance.now() - stepStart,
|
|
448
|
+
});
|
|
449
|
+
} catch (e) {
|
|
450
|
+
steps.push({
|
|
451
|
+
name,
|
|
452
|
+
status: 'failed',
|
|
453
|
+
duration: performance.now() - stepStart,
|
|
454
|
+
error: { message: e instanceof Error ? e.message : String(e) },
|
|
455
|
+
});
|
|
456
|
+
throw e;
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// Execute with timeout
|
|
461
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
462
|
+
setTimeout(() => reject(new Error(`Test timed out after ${timeout}ms`)), timeout);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
await Promise.race([
|
|
467
|
+
variantDef.play({
|
|
468
|
+
canvasElement,
|
|
469
|
+
args: {},
|
|
470
|
+
step,
|
|
471
|
+
}),
|
|
472
|
+
timeoutPromise,
|
|
473
|
+
]);
|
|
474
|
+
|
|
475
|
+
// If no explicit steps, add one for the whole play function
|
|
476
|
+
if (steps.length === 0) {
|
|
477
|
+
steps.push({
|
|
478
|
+
name: 'Play function',
|
|
479
|
+
status: 'passed',
|
|
480
|
+
duration: 0,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return { steps, error: undefined };
|
|
485
|
+
} catch (e) {
|
|
486
|
+
// If no steps failed explicitly, add one
|
|
487
|
+
if (!steps.some((s) => s.status === 'failed')) {
|
|
488
|
+
steps.push({
|
|
489
|
+
name: 'Play function',
|
|
490
|
+
status: 'failed',
|
|
491
|
+
duration: 0,
|
|
492
|
+
error: { message: e instanceof Error ? e.message : String(e) },
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
steps,
|
|
498
|
+
error: { message: e instanceof Error ? e.message : String(e) },
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
{ component: testCase.component, variant: testCase.variant, timeout }
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
return result as { steps: StepResult[]; error?: TestError };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Run accessibility checks using axe-core
|
|
510
|
+
*/
|
|
511
|
+
async function runA11yChecks(page: Page): Promise<A11yResult> {
|
|
512
|
+
try {
|
|
513
|
+
// Inject axe-core if not already present
|
|
514
|
+
await page.evaluate(() => {
|
|
515
|
+
if ((window as unknown as { axe?: unknown }).axe) return;
|
|
516
|
+
|
|
517
|
+
return new Promise<void>((resolve, reject) => {
|
|
518
|
+
const script = document.createElement('script');
|
|
519
|
+
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.8.2/axe.min.js';
|
|
520
|
+
script.onload = () => resolve();
|
|
521
|
+
script.onerror = () => reject(new Error('Failed to load axe-core'));
|
|
522
|
+
document.head.appendChild(script);
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Run axe-core analysis
|
|
527
|
+
const results = await page.evaluate(async () => {
|
|
528
|
+
const axe = (window as unknown as { axe: { run: (el: HTMLElement) => Promise<{ violations: unknown[]; passes: unknown[] }> } }).axe;
|
|
529
|
+
const container = document.querySelector('[data-preview-container="true"]') as HTMLElement;
|
|
530
|
+
if (!container) {
|
|
531
|
+
return { violations: [], passes: 0 };
|
|
532
|
+
}
|
|
533
|
+
const axeResults = await axe.run(container);
|
|
534
|
+
return {
|
|
535
|
+
violations: axeResults.violations,
|
|
536
|
+
passes: axeResults.passes.length,
|
|
537
|
+
};
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
violations: results.violations as A11yViolation[],
|
|
542
|
+
passes: results.passes,
|
|
543
|
+
};
|
|
544
|
+
} catch {
|
|
545
|
+
return { violations: [], passes: 0 };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Capture screenshot for visual regression
|
|
551
|
+
*/
|
|
552
|
+
async function captureScreenshot(
|
|
553
|
+
page: Page,
|
|
554
|
+
testCase: TestCase,
|
|
555
|
+
outputDir: string
|
|
556
|
+
): Promise<string> {
|
|
557
|
+
const { mkdir, writeFile } = await import('node:fs/promises');
|
|
558
|
+
const { join } = await import('node:path');
|
|
559
|
+
|
|
560
|
+
const screenshotsDir = join(outputDir, 'screenshots');
|
|
561
|
+
await mkdir(screenshotsDir, { recursive: true });
|
|
562
|
+
|
|
563
|
+
const filename = `${testCase.component}--${testCase.variant}.png`
|
|
564
|
+
.replace(/\s+/g, '-')
|
|
565
|
+
.toLowerCase();
|
|
566
|
+
const filepath = join(screenshotsDir, filename);
|
|
567
|
+
|
|
568
|
+
const container = await page.$('[data-preview-container="true"]');
|
|
569
|
+
if (container) {
|
|
570
|
+
const screenshot = await container.screenshot();
|
|
571
|
+
await writeFile(filepath, screenshot);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return filepath;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Aggregate test results into suites by component
|
|
579
|
+
*/
|
|
580
|
+
function aggregateResults(results: TestResult[]): TestSuite[] {
|
|
581
|
+
const grouped = groupTestsByComponent(results.map((r) => r.testCase));
|
|
582
|
+
const suites: TestSuite[] = [];
|
|
583
|
+
|
|
584
|
+
for (const [componentName, testCases] of grouped) {
|
|
585
|
+
const componentResults = results.filter((r) => r.testCase.component === componentName);
|
|
586
|
+
|
|
587
|
+
suites.push({
|
|
588
|
+
name: componentName,
|
|
589
|
+
tests: componentResults,
|
|
590
|
+
duration: componentResults.reduce((sum, r) => sum + r.duration, 0),
|
|
591
|
+
passed: componentResults.filter((r) => r.status === 'passed').length,
|
|
592
|
+
failed: componentResults.filter((r) => r.status === 'failed').length,
|
|
593
|
+
skipped: componentResults.filter((r) => r.status === 'skipped').length,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return suites;
|
|
598
|
+
}
|