@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,720 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactions Panel - Storybook-style interaction testing
|
|
3
|
+
*
|
|
4
|
+
* Runs play functions from Storybook stories and displays:
|
|
5
|
+
* - Step-by-step progress
|
|
6
|
+
* - Assertion results (pass/fail)
|
|
7
|
+
* - Error messages with stack traces
|
|
8
|
+
* - Re-run capabilities
|
|
9
|
+
* - Step-through debugging with breakpoints
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
13
|
+
import clsx from "clsx";
|
|
14
|
+
import type { PlayFunction, PlayFunctionContext, SegmentVariant } from "../../core/index.js";
|
|
15
|
+
import {
|
|
16
|
+
PlayIcon,
|
|
17
|
+
CheckIcon,
|
|
18
|
+
XIcon,
|
|
19
|
+
LoadingIcon,
|
|
20
|
+
ChevronDownIcon,
|
|
21
|
+
ChevronRightIcon,
|
|
22
|
+
RefreshIcon,
|
|
23
|
+
BugIcon,
|
|
24
|
+
PauseIcon,
|
|
25
|
+
StepOverIcon,
|
|
26
|
+
ContinueIcon,
|
|
27
|
+
BreakpointIcon,
|
|
28
|
+
BreakpointEmptyIcon,
|
|
29
|
+
} from "./Icons.js";
|
|
30
|
+
|
|
31
|
+
// Step execution state
|
|
32
|
+
interface StepResult {
|
|
33
|
+
name: string;
|
|
34
|
+
status: "pending" | "running" | "passed" | "failed" | "paused";
|
|
35
|
+
error?: string;
|
|
36
|
+
duration?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface InteractionResult {
|
|
40
|
+
status: "idle" | "running" | "passed" | "failed" | "paused";
|
|
41
|
+
steps: StepResult[];
|
|
42
|
+
error?: string;
|
|
43
|
+
duration?: number;
|
|
44
|
+
startTime?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Debug state
|
|
48
|
+
interface DebugState {
|
|
49
|
+
mode: "normal" | "debug";
|
|
50
|
+
isPaused: boolean;
|
|
51
|
+
currentStepIndex: number;
|
|
52
|
+
breakpoints: Set<number>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface InteractionsPanelProps {
|
|
56
|
+
/** The current variant being displayed */
|
|
57
|
+
variant: SegmentVariant | null;
|
|
58
|
+
/** Selector for the preview container element */
|
|
59
|
+
previewSelector?: string;
|
|
60
|
+
/** Key that changes when the preview updates */
|
|
61
|
+
previewKey?: number;
|
|
62
|
+
/** Args currently being used for the preview */
|
|
63
|
+
currentArgs?: Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function InteractionsPanel({
|
|
67
|
+
variant,
|
|
68
|
+
previewSelector = '[data-preview-container="true"]',
|
|
69
|
+
previewKey = 0,
|
|
70
|
+
currentArgs = {},
|
|
71
|
+
}: InteractionsPanelProps) {
|
|
72
|
+
const [result, setResult] = useState<InteractionResult>({
|
|
73
|
+
status: "idle",
|
|
74
|
+
steps: [],
|
|
75
|
+
});
|
|
76
|
+
const [expandedSteps, setExpandedSteps] = useState<Set<number>>(new Set());
|
|
77
|
+
const [debugState, setDebugState] = useState<DebugState>({
|
|
78
|
+
mode: "normal",
|
|
79
|
+
isPaused: false,
|
|
80
|
+
currentStepIndex: -1,
|
|
81
|
+
breakpoints: new Set(),
|
|
82
|
+
});
|
|
83
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
84
|
+
const resumeResolverRef = useRef<(() => void) | null>(null);
|
|
85
|
+
|
|
86
|
+
// Reset state when variant changes
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
setResult({ status: "idle", steps: [] });
|
|
89
|
+
setExpandedSteps(new Set());
|
|
90
|
+
setDebugState((prev) => ({
|
|
91
|
+
...prev,
|
|
92
|
+
isPaused: false,
|
|
93
|
+
currentStepIndex: -1,
|
|
94
|
+
}));
|
|
95
|
+
}, [variant?.name, previewKey]);
|
|
96
|
+
|
|
97
|
+
// Keyboard shortcuts
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
100
|
+
// Only handle if this panel is focused or no specific element is focused
|
|
101
|
+
if (document.activeElement && document.activeElement.tagName !== 'BODY') {
|
|
102
|
+
const isInteractionsPanelFocused = (document.activeElement as HTMLElement).closest('[data-interactions-panel]');
|
|
103
|
+
if (!isInteractionsPanelFocused) return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
switch (e.key) {
|
|
107
|
+
case 'F5':
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
if (debugState.mode === 'debug' && !result.status.includes('running')) {
|
|
110
|
+
runInteractions(true);
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
case 'F8':
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
if (debugState.isPaused) {
|
|
116
|
+
handleResume();
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
case 'F9':
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
if (debugState.currentStepIndex >= 0) {
|
|
122
|
+
toggleBreakpoint(debugState.currentStepIndex);
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
case 'F10':
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
if (debugState.isPaused) {
|
|
128
|
+
handleStepOver();
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
135
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
136
|
+
}, [debugState, result.status]);
|
|
137
|
+
|
|
138
|
+
const hasPlayFunction = variant?.hasPlayFunction && variant?.play;
|
|
139
|
+
|
|
140
|
+
const toggleBreakpoint = (index: number) => {
|
|
141
|
+
setDebugState((prev) => {
|
|
142
|
+
const next = new Set(prev.breakpoints);
|
|
143
|
+
if (next.has(index)) {
|
|
144
|
+
next.delete(index);
|
|
145
|
+
} else {
|
|
146
|
+
next.add(index);
|
|
147
|
+
}
|
|
148
|
+
return { ...prev, breakpoints: next };
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const toggleDebugMode = () => {
|
|
153
|
+
setDebugState((prev) => ({
|
|
154
|
+
...prev,
|
|
155
|
+
mode: prev.mode === 'normal' ? 'debug' : 'normal',
|
|
156
|
+
}));
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const handleResume = () => {
|
|
160
|
+
if (resumeResolverRef.current) {
|
|
161
|
+
resumeResolverRef.current();
|
|
162
|
+
resumeResolverRef.current = null;
|
|
163
|
+
}
|
|
164
|
+
setDebugState((prev) => ({ ...prev, isPaused: false }));
|
|
165
|
+
setResult((prev) => ({ ...prev, status: 'running' }));
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handleStepOver = () => {
|
|
169
|
+
// Mark current step to not pause, then resume
|
|
170
|
+
if (resumeResolverRef.current) {
|
|
171
|
+
resumeResolverRef.current();
|
|
172
|
+
resumeResolverRef.current = null;
|
|
173
|
+
}
|
|
174
|
+
setDebugState((prev) => ({ ...prev, isPaused: false }));
|
|
175
|
+
setResult((prev) => ({ ...prev, status: 'running' }));
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Run the play function
|
|
179
|
+
const runInteractions = useCallback(async (withDebugger = false) => {
|
|
180
|
+
if (!variant?.play) return;
|
|
181
|
+
|
|
182
|
+
// Cancel any existing run
|
|
183
|
+
if (abortControllerRef.current) {
|
|
184
|
+
abortControllerRef.current.abort();
|
|
185
|
+
}
|
|
186
|
+
abortControllerRef.current = new AbortController();
|
|
187
|
+
|
|
188
|
+
const startTime = performance.now();
|
|
189
|
+
const steps: StepResult[] = [];
|
|
190
|
+
const isDebugMode = withDebugger || debugState.mode === 'debug';
|
|
191
|
+
let stepIndex = 0;
|
|
192
|
+
|
|
193
|
+
setResult({
|
|
194
|
+
status: "running",
|
|
195
|
+
steps: [],
|
|
196
|
+
startTime,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
setDebugState((prev) => ({
|
|
200
|
+
...prev,
|
|
201
|
+
isPaused: false,
|
|
202
|
+
currentStepIndex: -1,
|
|
203
|
+
}));
|
|
204
|
+
|
|
205
|
+
// Find the preview container
|
|
206
|
+
const canvasElement = document.querySelector(previewSelector) as HTMLElement;
|
|
207
|
+
if (!canvasElement) {
|
|
208
|
+
setResult({
|
|
209
|
+
status: "failed",
|
|
210
|
+
steps: [],
|
|
211
|
+
error: `Preview container not found: ${previewSelector}`,
|
|
212
|
+
duration: performance.now() - startTime,
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Create the step function that tracks execution
|
|
218
|
+
const step = async (name: string, fn: () => Promise<void>): Promise<void> => {
|
|
219
|
+
const currentIndex = stepIndex++;
|
|
220
|
+
const stepStartTime = performance.now();
|
|
221
|
+
|
|
222
|
+
// Check for breakpoint or debug mode pause
|
|
223
|
+
if (isDebugMode && (debugState.breakpoints.has(currentIndex) || currentIndex === 0)) {
|
|
224
|
+
// Pause at breakpoint
|
|
225
|
+
steps.push({ name, status: "paused" });
|
|
226
|
+
setResult((prev) => ({
|
|
227
|
+
...prev,
|
|
228
|
+
status: "paused",
|
|
229
|
+
steps: [...steps],
|
|
230
|
+
}));
|
|
231
|
+
setDebugState((prev) => ({
|
|
232
|
+
...prev,
|
|
233
|
+
isPaused: true,
|
|
234
|
+
currentStepIndex: currentIndex,
|
|
235
|
+
}));
|
|
236
|
+
|
|
237
|
+
// Wait for resume
|
|
238
|
+
await new Promise<void>((resolve) => {
|
|
239
|
+
resumeResolverRef.current = resolve;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Update to running
|
|
243
|
+
steps[currentIndex] = { name, status: "running" };
|
|
244
|
+
setResult((prev) => ({
|
|
245
|
+
...prev,
|
|
246
|
+
status: "running",
|
|
247
|
+
steps: [...steps],
|
|
248
|
+
}));
|
|
249
|
+
} else {
|
|
250
|
+
// Add step as running
|
|
251
|
+
steps.push({ name, status: "running" });
|
|
252
|
+
setResult((prev) => ({
|
|
253
|
+
...prev,
|
|
254
|
+
steps: [...steps],
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
setDebugState((prev) => ({
|
|
259
|
+
...prev,
|
|
260
|
+
currentStepIndex: currentIndex,
|
|
261
|
+
}));
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
await fn();
|
|
265
|
+
steps[currentIndex] = {
|
|
266
|
+
name,
|
|
267
|
+
status: "passed",
|
|
268
|
+
duration: performance.now() - stepStartTime,
|
|
269
|
+
};
|
|
270
|
+
setResult((prev) => ({
|
|
271
|
+
...prev,
|
|
272
|
+
steps: [...steps],
|
|
273
|
+
}));
|
|
274
|
+
} catch (error) {
|
|
275
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
276
|
+
steps[currentIndex] = {
|
|
277
|
+
name,
|
|
278
|
+
status: "failed",
|
|
279
|
+
error: errorMessage,
|
|
280
|
+
duration: performance.now() - stepStartTime,
|
|
281
|
+
};
|
|
282
|
+
setResult((prev) => ({
|
|
283
|
+
...prev,
|
|
284
|
+
steps: [...steps],
|
|
285
|
+
}));
|
|
286
|
+
throw error; // Re-throw to stop execution
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// In debug mode, pause after each step if stepping
|
|
290
|
+
if (isDebugMode && debugState.breakpoints.has(currentIndex + 1)) {
|
|
291
|
+
setResult((prev) => ({
|
|
292
|
+
...prev,
|
|
293
|
+
status: "paused",
|
|
294
|
+
}));
|
|
295
|
+
setDebugState((prev) => ({
|
|
296
|
+
...prev,
|
|
297
|
+
isPaused: true,
|
|
298
|
+
}));
|
|
299
|
+
|
|
300
|
+
await new Promise<void>((resolve) => {
|
|
301
|
+
resumeResolverRef.current = resolve;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Create the context for the play function
|
|
307
|
+
const context: PlayFunctionContext = {
|
|
308
|
+
canvasElement,
|
|
309
|
+
args: currentArgs,
|
|
310
|
+
step,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
await variant.play(context);
|
|
315
|
+
|
|
316
|
+
// If no explicit steps were defined, the whole play function is one step
|
|
317
|
+
if (steps.length === 0) {
|
|
318
|
+
steps.push({
|
|
319
|
+
name: "Play function",
|
|
320
|
+
status: "passed",
|
|
321
|
+
duration: performance.now() - startTime,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
setResult({
|
|
326
|
+
status: "passed",
|
|
327
|
+
steps: [...steps],
|
|
328
|
+
duration: performance.now() - startTime,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
setDebugState((prev) => ({
|
|
332
|
+
...prev,
|
|
333
|
+
isPaused: false,
|
|
334
|
+
currentStepIndex: -1,
|
|
335
|
+
}));
|
|
336
|
+
} catch (error) {
|
|
337
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
338
|
+
|
|
339
|
+
// If no steps failed explicitly, mark as failed at top level
|
|
340
|
+
if (steps.length === 0 || !steps.some((s) => s.status === "failed")) {
|
|
341
|
+
steps.push({
|
|
342
|
+
name: "Play function",
|
|
343
|
+
status: "failed",
|
|
344
|
+
error: errorMessage,
|
|
345
|
+
duration: performance.now() - startTime,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
setResult({
|
|
350
|
+
status: "failed",
|
|
351
|
+
steps: [...steps],
|
|
352
|
+
error: errorMessage,
|
|
353
|
+
duration: performance.now() - startTime,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Auto-expand failed steps
|
|
357
|
+
const failedIndices = steps
|
|
358
|
+
.map((s, i) => (s.status === "failed" ? i : -1))
|
|
359
|
+
.filter((i) => i >= 0);
|
|
360
|
+
setExpandedSteps(new Set(failedIndices));
|
|
361
|
+
|
|
362
|
+
setDebugState((prev) => ({
|
|
363
|
+
...prev,
|
|
364
|
+
isPaused: false,
|
|
365
|
+
currentStepIndex: -1,
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
}, [variant, previewSelector, currentArgs, debugState.mode, debugState.breakpoints]);
|
|
369
|
+
|
|
370
|
+
const toggleStep = (index: number) => {
|
|
371
|
+
setExpandedSteps((prev) => {
|
|
372
|
+
const next = new Set(prev);
|
|
373
|
+
if (next.has(index)) {
|
|
374
|
+
next.delete(index);
|
|
375
|
+
} else {
|
|
376
|
+
next.add(index);
|
|
377
|
+
}
|
|
378
|
+
return next;
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const formatDuration = (ms?: number) => {
|
|
383
|
+
if (ms === undefined) return "";
|
|
384
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
385
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// No play function available
|
|
389
|
+
if (!hasPlayFunction) {
|
|
390
|
+
return (
|
|
391
|
+
<div className="h-full flex flex-col">
|
|
392
|
+
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
|
393
|
+
<h3 className="font-medium text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
|
394
|
+
<PlayIcon className="w-4 h-4" />
|
|
395
|
+
Interactions
|
|
396
|
+
</h3>
|
|
397
|
+
</div>
|
|
398
|
+
<div className="flex-1 flex items-center justify-center p-8 text-center">
|
|
399
|
+
<div className="max-w-md">
|
|
400
|
+
<div className="w-12 h-12 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center mx-auto mb-4">
|
|
401
|
+
<PlayIcon className="w-6 h-6 text-gray-400" />
|
|
402
|
+
</div>
|
|
403
|
+
<h4 className="font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
404
|
+
No interactions defined
|
|
405
|
+
</h4>
|
|
406
|
+
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
407
|
+
This variant doesn't have a play function. Add a <code className="px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded text-xs">play</code> function to your Storybook story to enable interaction testing.
|
|
408
|
+
</p>
|
|
409
|
+
<div className="mt-4 p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg text-left">
|
|
410
|
+
<pre className="text-xs text-gray-600 dark:text-gray-400 overflow-x-auto">
|
|
411
|
+
{`export const Default = {
|
|
412
|
+
play: async ({ canvasElement, step }) => {
|
|
413
|
+
const canvas = within(canvasElement);
|
|
414
|
+
|
|
415
|
+
await step('Click the button', async () => {
|
|
416
|
+
await userEvent.click(
|
|
417
|
+
canvas.getByRole('button')
|
|
418
|
+
);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
await expect(
|
|
422
|
+
canvas.getByText('Clicked!')
|
|
423
|
+
).toBeInTheDocument();
|
|
424
|
+
}
|
|
425
|
+
};`}
|
|
426
|
+
</pre>
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return (
|
|
435
|
+
<div className="h-full flex flex-col" data-interactions-panel>
|
|
436
|
+
{/* Header */}
|
|
437
|
+
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
|
438
|
+
<h3 className="font-medium text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
|
439
|
+
<PlayIcon className="w-4 h-4" />
|
|
440
|
+
Interactions
|
|
441
|
+
</h3>
|
|
442
|
+
<div className="flex items-center gap-2">
|
|
443
|
+
{result.duration !== undefined && (
|
|
444
|
+
<span className="text-xs text-gray-500 dark:text-gray-400">
|
|
445
|
+
{formatDuration(result.duration)}
|
|
446
|
+
</span>
|
|
447
|
+
)}
|
|
448
|
+
|
|
449
|
+
{/* Debug mode toggle */}
|
|
450
|
+
<button
|
|
451
|
+
onClick={toggleDebugMode}
|
|
452
|
+
className={clsx(
|
|
453
|
+
"p-1.5 rounded-md transition-colors",
|
|
454
|
+
debugState.mode === 'debug'
|
|
455
|
+
? "bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400"
|
|
456
|
+
: "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"
|
|
457
|
+
)}
|
|
458
|
+
title={debugState.mode === 'debug' ? "Exit debug mode" : "Enable debug mode (F5 to run with debugger)"}
|
|
459
|
+
>
|
|
460
|
+
<BugIcon className="w-4 h-4" />
|
|
461
|
+
</button>
|
|
462
|
+
|
|
463
|
+
{/* Debug controls when paused */}
|
|
464
|
+
{debugState.isPaused && (
|
|
465
|
+
<>
|
|
466
|
+
<button
|
|
467
|
+
onClick={handleResume}
|
|
468
|
+
className="p-1.5 text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-900/30 rounded-md transition-colors"
|
|
469
|
+
title="Continue (F8)"
|
|
470
|
+
>
|
|
471
|
+
<ContinueIcon className="w-4 h-4" />
|
|
472
|
+
</button>
|
|
473
|
+
<button
|
|
474
|
+
onClick={handleStepOver}
|
|
475
|
+
className="p-1.5 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/30 rounded-md transition-colors"
|
|
476
|
+
title="Step over (F10)"
|
|
477
|
+
>
|
|
478
|
+
<StepOverIcon className="w-4 h-4" />
|
|
479
|
+
</button>
|
|
480
|
+
</>
|
|
481
|
+
)}
|
|
482
|
+
|
|
483
|
+
<button
|
|
484
|
+
onClick={() => runInteractions(debugState.mode === 'debug')}
|
|
485
|
+
disabled={result.status === "running"}
|
|
486
|
+
className={clsx(
|
|
487
|
+
"flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm font-medium transition-colors",
|
|
488
|
+
result.status === "running" || result.status === "paused"
|
|
489
|
+
? "bg-gray-100 dark:bg-gray-800 text-gray-400 cursor-not-allowed"
|
|
490
|
+
: "bg-blue-600 hover:bg-blue-700 text-white"
|
|
491
|
+
)}
|
|
492
|
+
>
|
|
493
|
+
{result.status === "running" ? (
|
|
494
|
+
<>
|
|
495
|
+
<LoadingIcon className="w-4 h-4 animate-spin" />
|
|
496
|
+
Running...
|
|
497
|
+
</>
|
|
498
|
+
) : result.status === "paused" ? (
|
|
499
|
+
<>
|
|
500
|
+
<PauseIcon className="w-4 h-4" />
|
|
501
|
+
Paused
|
|
502
|
+
</>
|
|
503
|
+
) : result.status === "idle" ? (
|
|
504
|
+
<>
|
|
505
|
+
<PlayIcon className="w-4 h-4" />
|
|
506
|
+
{debugState.mode === 'debug' ? 'Debug' : 'Run'}
|
|
507
|
+
</>
|
|
508
|
+
) : (
|
|
509
|
+
<>
|
|
510
|
+
<RefreshIcon className="w-4 h-4" />
|
|
511
|
+
Rerun
|
|
512
|
+
</>
|
|
513
|
+
)}
|
|
514
|
+
</button>
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
|
|
518
|
+
{/* Results */}
|
|
519
|
+
<div className="flex-1 overflow-y-auto">
|
|
520
|
+
{result.status === "idle" ? (
|
|
521
|
+
<div className="p-8 text-center">
|
|
522
|
+
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
523
|
+
Click "Run" to execute the interaction tests
|
|
524
|
+
</p>
|
|
525
|
+
</div>
|
|
526
|
+
) : (
|
|
527
|
+
<div className="p-4 space-y-2">
|
|
528
|
+
{/* Overall status */}
|
|
529
|
+
<div
|
|
530
|
+
className={clsx(
|
|
531
|
+
"p-3 rounded-lg flex items-center justify-between",
|
|
532
|
+
result.status === "running" && "bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800",
|
|
533
|
+
result.status === "paused" && "bg-orange-50 dark:bg-orange-950/30 border border-orange-200 dark:border-orange-800",
|
|
534
|
+
result.status === "passed" && "bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-800",
|
|
535
|
+
result.status === "failed" && "bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800"
|
|
536
|
+
)}
|
|
537
|
+
>
|
|
538
|
+
<div className="flex items-center gap-2">
|
|
539
|
+
{result.status === "running" && (
|
|
540
|
+
<LoadingIcon className="w-5 h-5 text-blue-600 dark:text-blue-400 animate-spin" />
|
|
541
|
+
)}
|
|
542
|
+
{result.status === "paused" && (
|
|
543
|
+
<PauseIcon className="w-5 h-5 text-orange-600 dark:text-orange-400" />
|
|
544
|
+
)}
|
|
545
|
+
{result.status === "passed" && (
|
|
546
|
+
<CheckIcon className="w-5 h-5 text-green-600 dark:text-green-400" />
|
|
547
|
+
)}
|
|
548
|
+
{result.status === "failed" && (
|
|
549
|
+
<XIcon className="w-5 h-5 text-red-600 dark:text-red-400" />
|
|
550
|
+
)}
|
|
551
|
+
<span
|
|
552
|
+
className={clsx(
|
|
553
|
+
"font-medium",
|
|
554
|
+
result.status === "running" && "text-blue-700 dark:text-blue-300",
|
|
555
|
+
result.status === "paused" && "text-orange-700 dark:text-orange-300",
|
|
556
|
+
result.status === "passed" && "text-green-700 dark:text-green-300",
|
|
557
|
+
result.status === "failed" && "text-red-700 dark:text-red-300"
|
|
558
|
+
)}
|
|
559
|
+
>
|
|
560
|
+
{result.status === "running" && "Running interactions..."}
|
|
561
|
+
{result.status === "paused" && "Paused at breakpoint"}
|
|
562
|
+
{result.status === "passed" && "All interactions passed"}
|
|
563
|
+
{result.status === "failed" && "Interactions failed"}
|
|
564
|
+
</span>
|
|
565
|
+
</div>
|
|
566
|
+
<span className="text-xs text-gray-500 dark:text-gray-400">
|
|
567
|
+
{result.steps.filter((s) => s.status === "passed").length}/{result.steps.length} steps
|
|
568
|
+
</span>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
{/* Steps */}
|
|
572
|
+
{result.steps.length > 0 && (
|
|
573
|
+
<div className="mt-4 space-y-1">
|
|
574
|
+
<div className="flex items-center justify-between mb-2">
|
|
575
|
+
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
|
576
|
+
Steps
|
|
577
|
+
</h4>
|
|
578
|
+
{debugState.mode === 'debug' && (
|
|
579
|
+
<span className="text-xs text-orange-600 dark:text-orange-400">
|
|
580
|
+
Debug mode - click gutter to set breakpoints
|
|
581
|
+
</span>
|
|
582
|
+
)}
|
|
583
|
+
</div>
|
|
584
|
+
{result.steps.map((step, index) => (
|
|
585
|
+
<div
|
|
586
|
+
key={index}
|
|
587
|
+
className={clsx(
|
|
588
|
+
"rounded-lg border transition-colors flex",
|
|
589
|
+
step.status === "pending" && "bg-gray-50 dark:bg-gray-800/50 border-gray-200 dark:border-gray-700",
|
|
590
|
+
step.status === "running" && "bg-blue-50 dark:bg-blue-950/30 border-blue-200 dark:border-blue-800",
|
|
591
|
+
step.status === "paused" && "bg-orange-50 dark:bg-orange-950/30 border-orange-200 dark:border-orange-800",
|
|
592
|
+
step.status === "passed" && "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700",
|
|
593
|
+
step.status === "failed" && "bg-red-50 dark:bg-red-950/30 border-red-200 dark:border-red-800",
|
|
594
|
+
debugState.currentStepIndex === index && "ring-2 ring-orange-400 dark:ring-orange-500"
|
|
595
|
+
)}
|
|
596
|
+
>
|
|
597
|
+
{/* Breakpoint gutter - only show in debug mode */}
|
|
598
|
+
{debugState.mode === 'debug' && (
|
|
599
|
+
<button
|
|
600
|
+
onClick={(e) => {
|
|
601
|
+
e.stopPropagation();
|
|
602
|
+
toggleBreakpoint(index);
|
|
603
|
+
}}
|
|
604
|
+
className="w-6 flex-shrink-0 flex items-center justify-center border-r border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-700/50 transition-colors"
|
|
605
|
+
title={debugState.breakpoints.has(index) ? "Remove breakpoint (F9)" : "Add breakpoint (F9)"}
|
|
606
|
+
>
|
|
607
|
+
{debugState.breakpoints.has(index) ? (
|
|
608
|
+
<BreakpointIcon className="w-3 h-3 text-red-500" />
|
|
609
|
+
) : (
|
|
610
|
+
<BreakpointEmptyIcon className="w-3 h-3 text-gray-300 dark:text-gray-600 hover:text-red-400" />
|
|
611
|
+
)}
|
|
612
|
+
</button>
|
|
613
|
+
)}
|
|
614
|
+
|
|
615
|
+
<div className="flex-1">
|
|
616
|
+
<button
|
|
617
|
+
onClick={() => step.error && toggleStep(index)}
|
|
618
|
+
className={clsx(
|
|
619
|
+
"w-full p-3 flex items-center gap-2 text-left",
|
|
620
|
+
step.error && "cursor-pointer"
|
|
621
|
+
)}
|
|
622
|
+
disabled={!step.error}
|
|
623
|
+
>
|
|
624
|
+
{/* Current step indicator */}
|
|
625
|
+
{debugState.mode === 'debug' && debugState.currentStepIndex === index && (
|
|
626
|
+
<span className="text-orange-500 font-bold">▶</span>
|
|
627
|
+
)}
|
|
628
|
+
|
|
629
|
+
{/* Status icon */}
|
|
630
|
+
{step.status === "pending" && (
|
|
631
|
+
<div className="w-4 h-4 rounded-full border-2 border-gray-300 dark:border-gray-600" />
|
|
632
|
+
)}
|
|
633
|
+
{step.status === "running" && (
|
|
634
|
+
<LoadingIcon className="w-4 h-4 text-blue-600 dark:text-blue-400 animate-spin" />
|
|
635
|
+
)}
|
|
636
|
+
{step.status === "paused" && (
|
|
637
|
+
<PauseIcon className="w-4 h-4 text-orange-600 dark:text-orange-400" />
|
|
638
|
+
)}
|
|
639
|
+
{step.status === "passed" && (
|
|
640
|
+
<CheckIcon className="w-4 h-4 text-green-600 dark:text-green-400" />
|
|
641
|
+
)}
|
|
642
|
+
{step.status === "failed" && (
|
|
643
|
+
<XIcon className="w-4 h-4 text-red-600 dark:text-red-400" />
|
|
644
|
+
)}
|
|
645
|
+
|
|
646
|
+
{/* Step name */}
|
|
647
|
+
<span
|
|
648
|
+
className={clsx(
|
|
649
|
+
"flex-1 text-sm",
|
|
650
|
+
step.status === "pending" && "text-gray-400 dark:text-gray-500",
|
|
651
|
+
step.status === "running" && "text-blue-700 dark:text-blue-300",
|
|
652
|
+
step.status === "paused" && "text-orange-700 dark:text-orange-300",
|
|
653
|
+
step.status === "passed" && "text-gray-700 dark:text-gray-300",
|
|
654
|
+
step.status === "failed" && "text-red-700 dark:text-red-300"
|
|
655
|
+
)}
|
|
656
|
+
>
|
|
657
|
+
{step.name}
|
|
658
|
+
</span>
|
|
659
|
+
|
|
660
|
+
{/* Duration */}
|
|
661
|
+
{step.duration !== undefined && (
|
|
662
|
+
<span className="text-xs text-gray-400 dark:text-gray-500">
|
|
663
|
+
{formatDuration(step.duration)}
|
|
664
|
+
</span>
|
|
665
|
+
)}
|
|
666
|
+
|
|
667
|
+
{/* Expand icon for errors */}
|
|
668
|
+
{step.error && (
|
|
669
|
+
expandedSteps.has(index) ? (
|
|
670
|
+
<ChevronDownIcon className="w-4 h-4 text-gray-400" />
|
|
671
|
+
) : (
|
|
672
|
+
<ChevronRightIcon className="w-4 h-4 text-gray-400" />
|
|
673
|
+
)
|
|
674
|
+
)}
|
|
675
|
+
</button>
|
|
676
|
+
|
|
677
|
+
{/* Error details */}
|
|
678
|
+
{step.error && expandedSteps.has(index) && (
|
|
679
|
+
<div className="px-3 pb-3 pl-9">
|
|
680
|
+
<pre className="text-xs text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-950/50 p-2 rounded overflow-x-auto whitespace-pre-wrap">
|
|
681
|
+
{step.error}
|
|
682
|
+
</pre>
|
|
683
|
+
</div>
|
|
684
|
+
)}
|
|
685
|
+
</div>
|
|
686
|
+
</div>
|
|
687
|
+
))}
|
|
688
|
+
</div>
|
|
689
|
+
)}
|
|
690
|
+
|
|
691
|
+
{/* Keyboard shortcuts help in debug mode */}
|
|
692
|
+
{debugState.mode === 'debug' && (
|
|
693
|
+
<div className="mt-4 p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg text-xs text-gray-500 dark:text-gray-400">
|
|
694
|
+
<div className="font-medium mb-1">Keyboard shortcuts:</div>
|
|
695
|
+
<div className="grid grid-cols-2 gap-1">
|
|
696
|
+
<span><kbd className="px-1 bg-gray-200 dark:bg-gray-700 rounded">F5</kbd> Run with debugger</span>
|
|
697
|
+
<span><kbd className="px-1 bg-gray-200 dark:bg-gray-700 rounded">F8</kbd> Continue</span>
|
|
698
|
+
<span><kbd className="px-1 bg-gray-200 dark:bg-gray-700 rounded">F9</kbd> Toggle breakpoint</span>
|
|
699
|
+
<span><kbd className="px-1 bg-gray-200 dark:bg-gray-700 rounded">F10</kbd> Step over</span>
|
|
700
|
+
</div>
|
|
701
|
+
</div>
|
|
702
|
+
)}
|
|
703
|
+
|
|
704
|
+
{/* Top-level error (if no steps failed) */}
|
|
705
|
+
{result.error && !result.steps.some((s) => s.status === "failed") && (
|
|
706
|
+
<div className="mt-4">
|
|
707
|
+
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">
|
|
708
|
+
Error
|
|
709
|
+
</h4>
|
|
710
|
+
<pre className="text-xs text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-950/50 p-3 rounded overflow-x-auto whitespace-pre-wrap">
|
|
711
|
+
{result.error}
|
|
712
|
+
</pre>
|
|
713
|
+
</div>
|
|
714
|
+
)}
|
|
715
|
+
</div>
|
|
716
|
+
)}
|
|
717
|
+
</div>
|
|
718
|
+
</div>
|
|
719
|
+
);
|
|
720
|
+
}
|