@elabs-ai/components-ai 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/dist/_audio-player-media-chrome-KA5DY54G.js +81 -0
- package/dist/_audio-player-media-chrome-KA5DY54G.js.map +1 -0
- package/dist/_flow-boundary-D63PJ65S.js +186 -0
- package/dist/_flow-boundary-D63PJ65S.js.map +1 -0
- package/dist/_interactive-terminal-xterm-ATJ5EW3G.js +11 -0
- package/dist/_interactive-terminal-xterm-ATJ5EW3G.js.map +1 -0
- package/dist/_persona-rive-RFR2EUWP.js +164 -0
- package/dist/_persona-rive-RFR2EUWP.js.map +1 -0
- package/dist/index.d.ts +4079 -0
- package/dist/index.js +11770 -0
- package/dist/index.js.map +1 -0
- package/package.json +87 -0
- package/src/_audio-player-media-chrome.tsx +146 -0
- package/src/_chat-shell-rail.tsx +18 -0
- package/src/_code-block-theme.test.ts +134 -0
- package/src/_code-block-theme.ts +184 -0
- package/src/_flow-boundary.tsx +237 -0
- package/src/_flow-lazy.ts +34 -0
- package/src/_interactive-terminal-xterm.ts +32 -0
- package/src/_lazy-mermaid.test.ts +101 -0
- package/src/_lazy-mermaid.ts +117 -0
- package/src/_persona-rive.tsx +252 -0
- package/src/_streamdown-i18n.ts +119 -0
- package/src/agent-timeline.stories.tsx +66 -0
- package/src/agent-timeline.test.tsx +74 -0
- package/src/agent-timeline.tsx +106 -0
- package/src/agent.tsx +113 -0
- package/src/agentic-workspace.stories.tsx +1099 -0
- package/src/artifact.stories.tsx +62 -0
- package/src/artifact.test.tsx +112 -0
- package/src/artifact.tsx +167 -0
- package/src/asset-preview.stories.tsx +100 -0
- package/src/asset-preview.test.tsx +126 -0
- package/src/asset-preview.tsx +218 -0
- package/src/attachments.tsx +393 -0
- package/src/audio-player.stories.tsx +99 -0
- package/src/audio-player.tsx +170 -0
- package/src/blocks-ai-composer.stories.tsx +83 -0
- package/src/canvas.stories.tsx +142 -0
- package/src/canvas.tsx +29 -0
- package/src/chain-of-thought.tsx +193 -0
- package/src/chat-greeting.stories.tsx +96 -0
- package/src/chat-greeting.test.tsx +91 -0
- package/src/chat-greeting.tsx +91 -0
- package/src/chat-shell.stories.tsx +123 -0
- package/src/chat-shell.test.tsx +113 -0
- package/src/chat-shell.tsx +97 -0
- package/src/chat.stories.tsx +99 -0
- package/src/checkpoint.tsx +54 -0
- package/src/code-block.stories.tsx +81 -0
- package/src/code-block.test.tsx +191 -0
- package/src/code-block.tsx +673 -0
- package/src/commit.tsx +343 -0
- package/src/composer.stories.tsx +188 -0
- package/src/composer.test.tsx +182 -0
- package/src/composer.tsx +202 -0
- package/src/confirmation.stories.tsx +61 -0
- package/src/confirmation.test.tsx +136 -0
- package/src/confirmation.tsx +273 -0
- package/src/connection.tsx +23 -0
- package/src/context-panel.stories.tsx +137 -0
- package/src/context-panel.test.tsx +198 -0
- package/src/context-panel.tsx +609 -0
- package/src/context.stories.tsx +16 -0
- package/src/context.tsx +363 -0
- package/src/controls.tsx +20 -0
- package/src/conversation.stories.tsx +26 -0
- package/src/conversation.test.tsx +123 -0
- package/src/conversation.tsx +143 -0
- package/src/dark-theme-variant.test.ts +47 -0
- package/src/edge.tsx +33 -0
- package/src/environment-variables.tsx +304 -0
- package/src/file-tree.stories.tsx +55 -0
- package/src/file-tree.test.tsx +101 -0
- package/src/file-tree.tsx +390 -0
- package/src/gallery.stories.tsx +182 -0
- package/src/gallery.test.tsx +150 -0
- package/src/gallery.tsx +737 -0
- package/src/grouped-parts.stories.tsx +104 -0
- package/src/grouped-parts.test.tsx +167 -0
- package/src/grouped-parts.tsx +247 -0
- package/src/image.stories.tsx +67 -0
- package/src/image.test.tsx +105 -0
- package/src/image.tsx +115 -0
- package/src/index.ts +93 -0
- package/src/inline-citation.stories.tsx +91 -0
- package/src/inline-citation.test.tsx +96 -0
- package/src/inline-citation.tsx +314 -0
- package/src/interactive-terminal.stories.tsx +165 -0
- package/src/interactive-terminal.test.tsx +448 -0
- package/src/interactive-terminal.tsx +444 -0
- package/src/jsx-preview.stories.tsx +265 -0
- package/src/jsx-preview.test.tsx +75 -0
- package/src/jsx-preview.tsx +409 -0
- package/src/markdown-view.stories.tsx +57 -0
- package/src/markdown-view.test.tsx +36 -0
- package/src/markdown-view.tsx +113 -0
- package/src/message-edit.stories.tsx +150 -0
- package/src/message-edit.test.tsx +115 -0
- package/src/message-edit.tsx +336 -0
- package/src/message-feedback.stories.tsx +53 -0
- package/src/message-feedback.test.tsx +62 -0
- package/src/message-feedback.tsx +117 -0
- package/src/message-form-spec.ts +349 -0
- package/src/message-form.stories.tsx +182 -0
- package/src/message-form.test.tsx +227 -0
- package/src/message-form.tsx +845 -0
- package/src/message-table-spec.ts +281 -0
- package/src/message-table.stories.tsx +176 -0
- package/src/message-table.test.tsx +144 -0
- package/src/message-table.tsx +366 -0
- package/src/message.stories.tsx +262 -0
- package/src/message.test.tsx +508 -0
- package/src/message.tsx +619 -0
- package/src/mic-selector.tsx +339 -0
- package/src/microcopy.test.tsx +123 -0
- package/src/model-selector.stories.tsx +73 -0
- package/src/model-selector.test.tsx +55 -0
- package/src/model-selector.tsx +225 -0
- package/src/motion-config.tsx +49 -0
- package/src/node.tsx +71 -0
- package/src/open-in-chat.tsx +340 -0
- package/src/package-info.tsx +205 -0
- package/src/panel.tsx +20 -0
- package/src/part-groups.ts +258 -0
- package/src/persona-sources.ts +59 -0
- package/src/persona.stories.tsx +111 -0
- package/src/persona.test.tsx +67 -0
- package/src/persona.tsx +148 -0
- package/src/plan.tsx +135 -0
- package/src/prompt-input.stories.tsx +261 -0
- package/src/prompt-input.test.tsx +392 -0
- package/src/prompt-input.tsx +1668 -0
- package/src/queue.tsx +237 -0
- package/src/reasoning.stories.tsx +81 -0
- package/src/reasoning.test.tsx +50 -0
- package/src/reasoning.tsx +229 -0
- package/src/sandbox.stories.tsx +62 -0
- package/src/sandbox.test.tsx +42 -0
- package/src/sandbox.tsx +149 -0
- package/src/schema-display.tsx +412 -0
- package/src/selection-toolbar.stories.tsx +86 -0
- package/src/selection-toolbar.test.tsx +89 -0
- package/src/selection-toolbar.tsx +192 -0
- package/src/shimmer.stories.tsx +10 -0
- package/src/shimmer.tsx +81 -0
- package/src/snippet.stories.tsx +19 -0
- package/src/snippet.test.tsx +116 -0
- package/src/snippet.tsx +138 -0
- package/src/sources.stories.tsx +34 -0
- package/src/sources.test.tsx +129 -0
- package/src/sources.tsx +93 -0
- package/src/speech-input.tsx +312 -0
- package/src/stack-trace.tsx +479 -0
- package/src/streamdown-i18n.test.tsx +163 -0
- package/src/suggestion.stories.tsx +75 -0
- package/src/suggestion.test.tsx +72 -0
- package/src/suggestion.tsx +120 -0
- package/src/task.stories.tsx +22 -0
- package/src/task.test.tsx +36 -0
- package/src/task.tsx +89 -0
- package/src/templates-ai-assistant.stories.tsx +142 -0
- package/src/terminal.tsx +244 -0
- package/src/test-results.stories.tsx +177 -0
- package/src/test-results.test.tsx +241 -0
- package/src/test-results.tsx +408 -0
- package/src/tool-result-card.stories.tsx +143 -0
- package/src/tool-result-card.test.tsx +72 -0
- package/src/tool-result-card.tsx +86 -0
- package/src/tool.stories.tsx +60 -0
- package/src/tool.test.tsx +63 -0
- package/src/tool.tsx +235 -0
- package/src/toolbar.tsx +44 -0
- package/src/transcription.tsx +118 -0
- package/src/voice-selector.tsx +469 -0
- package/src/web-preview.stories.tsx +67 -0
- package/src/web-preview.test.tsx +117 -0
- package/src/web-preview.tsx +283 -0
package/src/terminal.tsx
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@elabs-ai/components-ui";
|
|
4
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
5
|
+
import Ansi from "ansi-to-react";
|
|
6
|
+
import { CheckIcon, CopyIcon, TerminalIcon, Trash2Icon } from "lucide-react";
|
|
7
|
+
import type { ComponentProps, HTMLAttributes } from "react";
|
|
8
|
+
import {
|
|
9
|
+
createContext,
|
|
10
|
+
useCallback,
|
|
11
|
+
useContext,
|
|
12
|
+
useEffect,
|
|
13
|
+
useMemo,
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
16
|
+
} from "react";
|
|
17
|
+
|
|
18
|
+
interface TerminalContextType {
|
|
19
|
+
output: string;
|
|
20
|
+
isStreaming: boolean;
|
|
21
|
+
autoScroll: boolean;
|
|
22
|
+
onClear?: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const TerminalContext = createContext<TerminalContextType>({
|
|
26
|
+
autoScroll: true,
|
|
27
|
+
isStreaming: false,
|
|
28
|
+
output: "",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type TerminalHeaderProps = HTMLAttributes<HTMLDivElement>;
|
|
32
|
+
|
|
33
|
+
export const TerminalHeader = ({ className, children, ...props }: TerminalHeaderProps) => (
|
|
34
|
+
<div
|
|
35
|
+
className={cn(
|
|
36
|
+
"flex items-center justify-between border-zinc-800 border-b px-4 py-2",
|
|
37
|
+
className,
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
export type TerminalTitleProps = HTMLAttributes<HTMLDivElement>;
|
|
46
|
+
|
|
47
|
+
export const TerminalTitle = ({ className, children, ...props }: TerminalTitleProps) => (
|
|
48
|
+
<div className={cn("flex items-center gap-2 text-sm text-zinc-400", className)} {...props}>
|
|
49
|
+
<TerminalIcon className="size-4" />
|
|
50
|
+
{children ?? "Terminal"}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export type TerminalStatusProps = HTMLAttributes<HTMLDivElement>;
|
|
55
|
+
|
|
56
|
+
export const TerminalStatus = ({ className, children, ...props }: TerminalStatusProps) => {
|
|
57
|
+
const { isStreaming } = useContext(TerminalContext);
|
|
58
|
+
|
|
59
|
+
if (!isStreaming) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className={cn("flex items-center gap-2 text-xs text-zinc-400", className)} {...props}>
|
|
65
|
+
{children}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type TerminalActionsProps = HTMLAttributes<HTMLDivElement>;
|
|
71
|
+
|
|
72
|
+
export const TerminalActions = ({ className, children, ...props }: TerminalActionsProps) => (
|
|
73
|
+
<div className={cn("flex items-center gap-1", className)} {...props}>
|
|
74
|
+
{children}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
export type TerminalCopyButtonProps = ComponentProps<typeof Button> & {
|
|
79
|
+
onCopy?: () => void;
|
|
80
|
+
onError?: (error: Error) => void;
|
|
81
|
+
timeout?: number;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const TerminalCopyButton = ({
|
|
85
|
+
onCopy,
|
|
86
|
+
onError,
|
|
87
|
+
timeout = 2000,
|
|
88
|
+
children,
|
|
89
|
+
className,
|
|
90
|
+
...props
|
|
91
|
+
}: TerminalCopyButtonProps) => {
|
|
92
|
+
const [isCopied, setIsCopied] = useState(false);
|
|
93
|
+
const timeoutRef = useRef<number>(0);
|
|
94
|
+
const { output } = useContext(TerminalContext);
|
|
95
|
+
|
|
96
|
+
const copyToClipboard = useCallback(async () => {
|
|
97
|
+
if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
|
|
98
|
+
onError?.(new Error("Clipboard API not available"));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
await navigator.clipboard.writeText(output);
|
|
104
|
+
setIsCopied(true);
|
|
105
|
+
onCopy?.();
|
|
106
|
+
timeoutRef.current = window.setTimeout(() => setIsCopied(false), timeout);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
onError?.(error as Error);
|
|
109
|
+
}
|
|
110
|
+
}, [output, onCopy, onError, timeout]);
|
|
111
|
+
|
|
112
|
+
useEffect(
|
|
113
|
+
() => () => {
|
|
114
|
+
window.clearTimeout(timeoutRef.current);
|
|
115
|
+
},
|
|
116
|
+
[],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const Icon = isCopied ? CheckIcon : CopyIcon;
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Button
|
|
123
|
+
className={cn(
|
|
124
|
+
"size-7 shrink-0 text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100",
|
|
125
|
+
className,
|
|
126
|
+
)}
|
|
127
|
+
onClick={copyToClipboard}
|
|
128
|
+
size="icon"
|
|
129
|
+
variant="ghost"
|
|
130
|
+
{...props}
|
|
131
|
+
>
|
|
132
|
+
{children ?? <Icon size={14} />}
|
|
133
|
+
</Button>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export type TerminalClearButtonProps = ComponentProps<typeof Button>;
|
|
138
|
+
|
|
139
|
+
export const TerminalClearButton = ({
|
|
140
|
+
children,
|
|
141
|
+
className,
|
|
142
|
+
...props
|
|
143
|
+
}: TerminalClearButtonProps) => {
|
|
144
|
+
const { onClear } = useContext(TerminalContext);
|
|
145
|
+
|
|
146
|
+
if (!onClear) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<Button
|
|
152
|
+
className={cn(
|
|
153
|
+
"size-7 shrink-0 text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100",
|
|
154
|
+
className,
|
|
155
|
+
)}
|
|
156
|
+
onClick={onClear}
|
|
157
|
+
size="icon"
|
|
158
|
+
variant="ghost"
|
|
159
|
+
{...props}
|
|
160
|
+
>
|
|
161
|
+
{children ?? <Trash2Icon size={14} />}
|
|
162
|
+
</Button>
|
|
163
|
+
);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export type TerminalContentProps = HTMLAttributes<HTMLDivElement>;
|
|
167
|
+
|
|
168
|
+
export const TerminalContent = ({ className, children, ...props }: TerminalContentProps) => {
|
|
169
|
+
const { output, isStreaming, autoScroll } = useContext(TerminalContext);
|
|
170
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
171
|
+
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (autoScroll && containerRef.current) {
|
|
174
|
+
containerRef.current.scrollTop = containerRef.current.scrollHeight;
|
|
175
|
+
}
|
|
176
|
+
}, [output, autoScroll]);
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<div
|
|
180
|
+
className={cn("max-h-96 overflow-auto p-4 font-mono text-sm leading-relaxed", className)}
|
|
181
|
+
ref={containerRef}
|
|
182
|
+
{...props}
|
|
183
|
+
>
|
|
184
|
+
{children ?? (
|
|
185
|
+
<pre className="whitespace-pre-wrap break-words">
|
|
186
|
+
<Ansi>{output}</Ansi>
|
|
187
|
+
{isStreaming && (
|
|
188
|
+
<span className="ml-0.5 inline-block h-4 w-2 animate-pulse bg-zinc-100" />
|
|
189
|
+
)}
|
|
190
|
+
</pre>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export type TerminalProps = HTMLAttributes<HTMLDivElement> & {
|
|
197
|
+
output: string;
|
|
198
|
+
isStreaming?: boolean;
|
|
199
|
+
autoScroll?: boolean;
|
|
200
|
+
onClear?: () => void;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export const Terminal = ({
|
|
204
|
+
output,
|
|
205
|
+
isStreaming = false,
|
|
206
|
+
autoScroll = true,
|
|
207
|
+
onClear,
|
|
208
|
+
className,
|
|
209
|
+
children,
|
|
210
|
+
...props
|
|
211
|
+
}: TerminalProps) => {
|
|
212
|
+
const contextValue = useMemo(
|
|
213
|
+
() => ({ autoScroll, isStreaming, onClear, output }),
|
|
214
|
+
[autoScroll, isStreaming, onClear, output],
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<TerminalContext.Provider value={contextValue}>
|
|
219
|
+
<div
|
|
220
|
+
className={cn(
|
|
221
|
+
"flex flex-col overflow-hidden rounded-lg border bg-zinc-950 text-zinc-100",
|
|
222
|
+
className,
|
|
223
|
+
)}
|
|
224
|
+
{...props}
|
|
225
|
+
>
|
|
226
|
+
{children ?? (
|
|
227
|
+
<>
|
|
228
|
+
<TerminalHeader>
|
|
229
|
+
<TerminalTitle />
|
|
230
|
+
<div className="flex items-center gap-1">
|
|
231
|
+
<TerminalStatus />
|
|
232
|
+
<TerminalActions>
|
|
233
|
+
<TerminalCopyButton />
|
|
234
|
+
{onClear && <TerminalClearButton />}
|
|
235
|
+
</TerminalActions>
|
|
236
|
+
</div>
|
|
237
|
+
</TerminalHeader>
|
|
238
|
+
<TerminalContent />
|
|
239
|
+
</>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
</TerminalContext.Provider>
|
|
243
|
+
);
|
|
244
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import {
|
|
3
|
+
Test,
|
|
4
|
+
TestError,
|
|
5
|
+
TestErrorMessage,
|
|
6
|
+
TestErrorStack,
|
|
7
|
+
TestResults,
|
|
8
|
+
TestResultsContent,
|
|
9
|
+
TestResultsDuration,
|
|
10
|
+
TestResultsHeader,
|
|
11
|
+
TestResultsMeta,
|
|
12
|
+
TestResultsProgress,
|
|
13
|
+
TestResultsSummary,
|
|
14
|
+
TestSuite,
|
|
15
|
+
TestSuiteContent,
|
|
16
|
+
TestSuiteName,
|
|
17
|
+
TestSuiteStats,
|
|
18
|
+
} from "./test-results";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* TestResults — the status colors are semantic tokens (#157): success / destructive
|
|
22
|
+
* / warning (badges + progress), success-text / destructive-text (pass/fail), and
|
|
23
|
+
* muted / primary for skipped / running. Verify they read correctly in every theme,
|
|
24
|
+
* especially low-chroma ones (where the old raw Tailwind palette did not adapt).
|
|
25
|
+
*
|
|
26
|
+
* Structure: `TestResultsHeader` (file name + duration) → `TestResultsMeta`
|
|
27
|
+
* (badges + progress bar) → `TestResultsContent` (divide-y suite list) →
|
|
28
|
+
* `TestSuite` (collapsible trigger + `TestSuiteContent` rows). No nested
|
|
29
|
+
* rounded boxes — suites are separated by dividers; the content area uses a
|
|
30
|
+
* flat `divide-y` list.
|
|
31
|
+
*/
|
|
32
|
+
const meta = {
|
|
33
|
+
title: "AI/TestResults",
|
|
34
|
+
component: TestResults,
|
|
35
|
+
parameters: { layout: "padded" },
|
|
36
|
+
tags: ["autodocs"],
|
|
37
|
+
} satisfies Meta<typeof TestResults>;
|
|
38
|
+
export default meta;
|
|
39
|
+
type Story = StoryObj<typeof meta>;
|
|
40
|
+
|
|
41
|
+
const summary = { passed: 8, failed: 2, skipped: 1, total: 11, duration: 1240 };
|
|
42
|
+
|
|
43
|
+
export const Default: Story = {
|
|
44
|
+
render: () => (
|
|
45
|
+
<div className="max-w-xl">
|
|
46
|
+
<TestResults summary={summary}>
|
|
47
|
+
<TestResultsHeader>
|
|
48
|
+
<span className="min-w-0 flex-1 truncate font-medium text-foreground text-body">
|
|
49
|
+
api.integration.test.ts
|
|
50
|
+
</span>
|
|
51
|
+
<TestResultsDuration />
|
|
52
|
+
</TestResultsHeader>
|
|
53
|
+
<TestResultsMeta>
|
|
54
|
+
<TestResultsSummary />
|
|
55
|
+
<TestResultsProgress />
|
|
56
|
+
</TestResultsMeta>
|
|
57
|
+
<TestResultsContent>
|
|
58
|
+
<TestSuite name="auth" status="failed">
|
|
59
|
+
<TestSuiteName>
|
|
60
|
+
auth.service
|
|
61
|
+
<TestSuiteStats passed={3} failed={1} skipped={1} />
|
|
62
|
+
</TestSuiteName>
|
|
63
|
+
<TestSuiteContent>
|
|
64
|
+
<Test name="signs a valid user in" status="passed" duration={42} />
|
|
65
|
+
<Test name="issues a refresh token" status="passed" duration={31} />
|
|
66
|
+
<Test name="rejects an expired token" status="failed" duration={88} />
|
|
67
|
+
<div className="px-4 py-2">
|
|
68
|
+
<TestError>
|
|
69
|
+
<TestErrorMessage>Expected status 401 but received 200</TestErrorMessage>
|
|
70
|
+
<TestErrorStack>{"at verify (auth.service.test.ts:14:9)"}</TestErrorStack>
|
|
71
|
+
</TestError>
|
|
72
|
+
</div>
|
|
73
|
+
<Test name="enforces the rate limit" status="running" />
|
|
74
|
+
<Test name="legacy SSO path" status="skipped" />
|
|
75
|
+
</TestSuiteContent>
|
|
76
|
+
</TestSuite>
|
|
77
|
+
</TestResultsContent>
|
|
78
|
+
</TestResults>
|
|
79
|
+
</div>
|
|
80
|
+
),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const AllPassing: Story = {
|
|
84
|
+
render: () => (
|
|
85
|
+
<div className="max-w-xl">
|
|
86
|
+
<TestResults summary={{ passed: 5, failed: 0, skipped: 0, total: 5, duration: 320 }}>
|
|
87
|
+
<TestResultsHeader>
|
|
88
|
+
<span className="min-w-0 flex-1 truncate font-medium text-foreground text-body">
|
|
89
|
+
utils.test.ts
|
|
90
|
+
</span>
|
|
91
|
+
<TestResultsDuration />
|
|
92
|
+
</TestResultsHeader>
|
|
93
|
+
<TestResultsMeta>
|
|
94
|
+
<TestResultsSummary />
|
|
95
|
+
<TestResultsProgress />
|
|
96
|
+
</TestResultsMeta>
|
|
97
|
+
<TestResultsContent>
|
|
98
|
+
<TestSuite name="formatDate" status="passed">
|
|
99
|
+
<TestSuiteName>
|
|
100
|
+
formatDate
|
|
101
|
+
<TestSuiteStats passed={5} />
|
|
102
|
+
</TestSuiteName>
|
|
103
|
+
<TestSuiteContent>
|
|
104
|
+
<Test name="formats ISO strings" status="passed" duration={12} />
|
|
105
|
+
<Test name="handles null gracefully" status="passed" duration={8} />
|
|
106
|
+
<Test name="respects locale" status="passed" duration={14} />
|
|
107
|
+
<Test name="handles leap years" status="passed" duration={9} />
|
|
108
|
+
<Test name="returns empty string on invalid input" status="passed" duration={11} />
|
|
109
|
+
</TestSuiteContent>
|
|
110
|
+
</TestSuite>
|
|
111
|
+
</TestResultsContent>
|
|
112
|
+
</TestResults>
|
|
113
|
+
</div>
|
|
114
|
+
),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const MultipleSuites: Story = {
|
|
118
|
+
render: () => (
|
|
119
|
+
<div className="max-w-xl">
|
|
120
|
+
<TestResults summary={{ passed: 6, failed: 1, skipped: 2, total: 9, duration: 2100 }}>
|
|
121
|
+
<TestResultsHeader>
|
|
122
|
+
<span className="min-w-0 flex-1 truncate font-medium text-foreground text-body">
|
|
123
|
+
payments/checkout.integration.test.ts
|
|
124
|
+
</span>
|
|
125
|
+
<TestResultsDuration />
|
|
126
|
+
</TestResultsHeader>
|
|
127
|
+
<TestResultsMeta>
|
|
128
|
+
<TestResultsSummary />
|
|
129
|
+
<TestResultsProgress />
|
|
130
|
+
</TestResultsMeta>
|
|
131
|
+
<TestResultsContent>
|
|
132
|
+
<TestSuite name="CartService" status="passed">
|
|
133
|
+
<TestSuiteName>
|
|
134
|
+
CartService
|
|
135
|
+
<TestSuiteStats passed={3} />
|
|
136
|
+
</TestSuiteName>
|
|
137
|
+
<TestSuiteContent>
|
|
138
|
+
<Test name="adds items to cart" status="passed" duration={55} />
|
|
139
|
+
<Test name="removes items from cart" status="passed" duration={43} />
|
|
140
|
+
<Test name="calculates totals" status="passed" duration={61} />
|
|
141
|
+
</TestSuiteContent>
|
|
142
|
+
</TestSuite>
|
|
143
|
+
<TestSuite name="PaymentService" status="failed">
|
|
144
|
+
<TestSuiteName>
|
|
145
|
+
PaymentService
|
|
146
|
+
<TestSuiteStats passed={3} failed={1} skipped={2} />
|
|
147
|
+
</TestSuiteName>
|
|
148
|
+
<TestSuiteContent>
|
|
149
|
+
<Test name="authorises a card" status="passed" duration={180} />
|
|
150
|
+
<Test name="declines an expired card" status="failed" duration={95} />
|
|
151
|
+
<div className="px-4 py-2">
|
|
152
|
+
<TestError>
|
|
153
|
+
<TestErrorMessage>Expected decline but received 200 OK</TestErrorMessage>
|
|
154
|
+
<TestErrorStack>
|
|
155
|
+
{"at PaymentService.charge (payment.test.ts:38:5)"}
|
|
156
|
+
</TestErrorStack>
|
|
157
|
+
</TestError>
|
|
158
|
+
</div>
|
|
159
|
+
<Test name="handles 3DS challenge" status="passed" duration={210} />
|
|
160
|
+
<Test name="processes refund" status="passed" duration={145} />
|
|
161
|
+
<Test name="applies discount codes" status="skipped" />
|
|
162
|
+
<Test name="handles partial refund" status="skipped" />
|
|
163
|
+
</TestSuiteContent>
|
|
164
|
+
</TestSuite>
|
|
165
|
+
</TestResultsContent>
|
|
166
|
+
</TestResults>
|
|
167
|
+
</div>
|
|
168
|
+
),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/** The minimal auto-render path: pass `summary` only, no children. */
|
|
172
|
+
export const SummaryOnly: Story = {
|
|
173
|
+
args: {
|
|
174
|
+
summary: { passed: 8, failed: 2, skipped: 1, total: 11, duration: 1240 },
|
|
175
|
+
className: "max-w-xl",
|
|
176
|
+
},
|
|
177
|
+
};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import {
|
|
4
|
+
Test,
|
|
5
|
+
TestError,
|
|
6
|
+
TestErrorMessage,
|
|
7
|
+
TestErrorStack,
|
|
8
|
+
TestResults,
|
|
9
|
+
TestResultsContent,
|
|
10
|
+
TestResultsDuration,
|
|
11
|
+
TestResultsHeader,
|
|
12
|
+
TestResultsMeta,
|
|
13
|
+
TestResultsProgress,
|
|
14
|
+
TestResultsSummary,
|
|
15
|
+
TestSuite,
|
|
16
|
+
TestSuiteContent,
|
|
17
|
+
TestSuiteName,
|
|
18
|
+
TestSuiteStats,
|
|
19
|
+
} from "./test-results";
|
|
20
|
+
|
|
21
|
+
const summary = { passed: 8, failed: 2, skipped: 1, total: 11, duration: 1240 };
|
|
22
|
+
|
|
23
|
+
describe("TestResults", () => {
|
|
24
|
+
it("renders the compound tree without crashing", () => {
|
|
25
|
+
render(
|
|
26
|
+
<TestResults summary={summary} data-testid="results">
|
|
27
|
+
<TestResultsHeader>
|
|
28
|
+
<span>api.integration.test.ts</span>
|
|
29
|
+
<TestResultsDuration />
|
|
30
|
+
</TestResultsHeader>
|
|
31
|
+
<TestResultsMeta>
|
|
32
|
+
<TestResultsSummary />
|
|
33
|
+
<TestResultsProgress />
|
|
34
|
+
</TestResultsMeta>
|
|
35
|
+
<TestResultsContent>
|
|
36
|
+
<TestSuite name="auth" status="failed">
|
|
37
|
+
<TestSuiteName />
|
|
38
|
+
<TestSuiteContent>
|
|
39
|
+
<Test name="signs a valid user in" status="passed" duration={42} />
|
|
40
|
+
<Test name="rejects an expired token" status="failed" duration={88} />
|
|
41
|
+
<Test name="legacy SSO path" status="skipped" />
|
|
42
|
+
<Test name="enforces the rate limit" status="running" />
|
|
43
|
+
</TestSuiteContent>
|
|
44
|
+
</TestSuite>
|
|
45
|
+
</TestResultsContent>
|
|
46
|
+
</TestResults>,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(screen.getByTestId("results")).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("renders duration from context when summary has a duration", () => {
|
|
53
|
+
render(
|
|
54
|
+
<TestResults summary={summary}>
|
|
55
|
+
<TestResultsHeader>
|
|
56
|
+
<TestResultsDuration />
|
|
57
|
+
</TestResultsHeader>
|
|
58
|
+
</TestResults>,
|
|
59
|
+
);
|
|
60
|
+
// 1240ms → "1.24s"
|
|
61
|
+
expect(screen.getByText("1.24s")).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("renders nothing for TestResultsDuration when duration is absent", () => {
|
|
65
|
+
const { container } = render(
|
|
66
|
+
<TestResults summary={{ passed: 3, failed: 0, skipped: 0, total: 3 }}>
|
|
67
|
+
<TestResultsHeader>
|
|
68
|
+
<TestResultsDuration />
|
|
69
|
+
</TestResultsHeader>
|
|
70
|
+
</TestResults>,
|
|
71
|
+
);
|
|
72
|
+
// No duration span rendered
|
|
73
|
+
expect(container.querySelector("span")).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("renders passed/failed/skipped badges in TestResultsSummary", () => {
|
|
77
|
+
render(
|
|
78
|
+
<TestResults summary={summary}>
|
|
79
|
+
<TestResultsMeta>
|
|
80
|
+
<TestResultsSummary />
|
|
81
|
+
</TestResultsMeta>
|
|
82
|
+
</TestResults>,
|
|
83
|
+
);
|
|
84
|
+
expect(screen.getByText("8 passed")).toBeInTheDocument();
|
|
85
|
+
expect(screen.getByText("2 failed")).toBeInTheDocument();
|
|
86
|
+
expect(screen.getByText("1 skipped")).toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("omits failed/skipped badges when counts are zero", () => {
|
|
90
|
+
render(
|
|
91
|
+
<TestResults summary={{ passed: 5, failed: 0, skipped: 0, total: 5 }}>
|
|
92
|
+
<TestResultsMeta>
|
|
93
|
+
<TestResultsSummary />
|
|
94
|
+
</TestResultsMeta>
|
|
95
|
+
</TestResults>,
|
|
96
|
+
);
|
|
97
|
+
expect(screen.getByText("5 passed")).toBeInTheDocument();
|
|
98
|
+
expect(screen.queryByText(/failed/)).toBeNull();
|
|
99
|
+
expect(screen.queryByText(/skipped/)).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("renders progress bar with tabular-nums label", () => {
|
|
103
|
+
render(
|
|
104
|
+
<TestResults summary={summary}>
|
|
105
|
+
<TestResultsMeta>
|
|
106
|
+
<TestResultsProgress />
|
|
107
|
+
</TestResultsMeta>
|
|
108
|
+
</TestResults>,
|
|
109
|
+
);
|
|
110
|
+
// 8/11 = 72.727…% → "73%"
|
|
111
|
+
expect(screen.getByText("8/11 tests passed")).toBeInTheDocument();
|
|
112
|
+
expect(screen.getByText("73%")).toBeInTheDocument();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("renders suite stats with correct counts", () => {
|
|
116
|
+
render(
|
|
117
|
+
<TestResults summary={summary}>
|
|
118
|
+
<TestResultsContent>
|
|
119
|
+
<TestSuite name="auth" status="failed">
|
|
120
|
+
<TestSuiteName>
|
|
121
|
+
auth.service
|
|
122
|
+
<TestSuiteStats passed={3} failed={1} skipped={1} />
|
|
123
|
+
</TestSuiteName>
|
|
124
|
+
</TestSuite>
|
|
125
|
+
</TestResultsContent>
|
|
126
|
+
</TestResults>,
|
|
127
|
+
);
|
|
128
|
+
expect(screen.getByText("3 passed")).toBeInTheDocument();
|
|
129
|
+
expect(screen.getByText("1 failed")).toBeInTheDocument();
|
|
130
|
+
expect(screen.getByText("1 skipped")).toBeInTheDocument();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("renders Test rows with name and duration (suite open)", () => {
|
|
134
|
+
render(
|
|
135
|
+
<TestResults summary={summary}>
|
|
136
|
+
<TestResultsContent>
|
|
137
|
+
<TestSuite name="auth" status="failed" defaultOpen>
|
|
138
|
+
<TestSuiteContent>
|
|
139
|
+
<Test name="signs a valid user in" status="passed" duration={42} />
|
|
140
|
+
</TestSuiteContent>
|
|
141
|
+
</TestSuite>
|
|
142
|
+
</TestResultsContent>
|
|
143
|
+
</TestResults>,
|
|
144
|
+
);
|
|
145
|
+
expect(screen.getByText("signs a valid user in")).toBeInTheDocument();
|
|
146
|
+
expect(screen.getByText("42ms")).toBeInTheDocument();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("omits Test duration when not provided (suite open)", () => {
|
|
150
|
+
render(
|
|
151
|
+
<TestResults summary={summary}>
|
|
152
|
+
<TestResultsContent>
|
|
153
|
+
<TestSuite name="auth" status="failed" defaultOpen>
|
|
154
|
+
<TestSuiteContent>
|
|
155
|
+
<Test name="legacy SSO path" status="skipped" />
|
|
156
|
+
</TestSuiteContent>
|
|
157
|
+
</TestSuite>
|
|
158
|
+
</TestResultsContent>
|
|
159
|
+
</TestResults>,
|
|
160
|
+
);
|
|
161
|
+
expect(screen.queryByText(/ms/)).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("renders TestError with message and stack (suite open)", () => {
|
|
165
|
+
render(
|
|
166
|
+
<TestResults summary={summary}>
|
|
167
|
+
<TestResultsContent>
|
|
168
|
+
<TestSuite name="auth" status="failed" defaultOpen>
|
|
169
|
+
<TestSuiteContent>
|
|
170
|
+
<Test name="rejects an expired token" status="failed" duration={88} />
|
|
171
|
+
<div className="px-4 py-2">
|
|
172
|
+
<TestError>
|
|
173
|
+
<TestErrorMessage>Expected status 401 but received 200</TestErrorMessage>
|
|
174
|
+
<TestErrorStack>{"at verify (auth.service.test.ts:14:9)"}</TestErrorStack>
|
|
175
|
+
</TestError>
|
|
176
|
+
</div>
|
|
177
|
+
</TestSuiteContent>
|
|
178
|
+
</TestSuite>
|
|
179
|
+
</TestResultsContent>
|
|
180
|
+
</TestResults>,
|
|
181
|
+
);
|
|
182
|
+
expect(screen.getByText("Expected status 401 but received 200")).toBeInTheDocument();
|
|
183
|
+
expect(screen.getByText("at verify (auth.service.test.ts:14:9)")).toBeInTheDocument();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("renders the minimal auto path when only summary is passed", () => {
|
|
187
|
+
render(<TestResults summary={summary} data-testid="min" />);
|
|
188
|
+
// Auto-render produces the header with badges + duration
|
|
189
|
+
expect(screen.getByTestId("min")).toBeInTheDocument();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("TestResultsMeta applies consistent padding class", () => {
|
|
193
|
+
const { container } = render(
|
|
194
|
+
<TestResults summary={summary}>
|
|
195
|
+
<TestResultsMeta data-testid="meta">
|
|
196
|
+
<TestResultsSummary />
|
|
197
|
+
</TestResultsMeta>
|
|
198
|
+
</TestResults>,
|
|
199
|
+
);
|
|
200
|
+
const meta = container.querySelector("[data-testid='meta']");
|
|
201
|
+
expect(meta?.className).toContain("px-4");
|
|
202
|
+
expect(meta?.className).toContain("pb-4");
|
|
203
|
+
expect(meta?.className).toContain("pt-3");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("TestResultsContent uses divide-y (no nested border boxes)", () => {
|
|
207
|
+
const { container } = render(
|
|
208
|
+
<TestResults summary={summary}>
|
|
209
|
+
<TestResultsContent data-testid="content">
|
|
210
|
+
<TestSuite name="s1" status="passed">
|
|
211
|
+
<TestSuiteName />
|
|
212
|
+
</TestSuite>
|
|
213
|
+
</TestResultsContent>
|
|
214
|
+
</TestResults>,
|
|
215
|
+
);
|
|
216
|
+
const content = container.querySelector("[data-testid='content']");
|
|
217
|
+
expect(content?.className).toContain("divide-y");
|
|
218
|
+
// TestSuite must NOT have a border class of its own (no nested box)
|
|
219
|
+
const suite = content?.querySelector("[data-radix-collapsible-root]") as HTMLElement | null;
|
|
220
|
+
if (suite) {
|
|
221
|
+
expect(suite.className).not.toMatch(/\bborder\b/);
|
|
222
|
+
expect(suite.className).not.toContain("rounded-lg");
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("Test rows use ps-10 for hierarchical indent alignment (suite open)", () => {
|
|
227
|
+
render(
|
|
228
|
+
<TestResults summary={summary}>
|
|
229
|
+
<TestResultsContent>
|
|
230
|
+
<TestSuite name="auth" status="failed" defaultOpen>
|
|
231
|
+
<TestSuiteContent>
|
|
232
|
+
<Test name="signs a valid user in" status="passed" data-testid="testrow" />
|
|
233
|
+
</TestSuiteContent>
|
|
234
|
+
</TestSuite>
|
|
235
|
+
</TestResultsContent>
|
|
236
|
+
</TestResults>,
|
|
237
|
+
);
|
|
238
|
+
const row = screen.getByTestId("testrow");
|
|
239
|
+
expect(row.className).toContain("ps-10");
|
|
240
|
+
});
|
|
241
|
+
});
|