@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
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, userEvent, within } from "storybook/test";
|
|
3
|
+
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
4
|
+
import { Button, Textarea } from "@elabs-ai/components-ui";
|
|
5
|
+
import { JSXPreview, JSXPreviewContent, JSXPreviewError, JSXPreviewSkeleton } from "./jsx-preview";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `JSXPreview` is the "ad-hoc JSX" path of the AI Output Contract: an agent emits
|
|
9
|
+
* a JSX markup STRING and the component renders ONLY the tags present in the
|
|
10
|
+
* `components` allow-list you pass. These stories exercise the REAL behaviors —
|
|
11
|
+
* live streaming with partial-tag completion, live editing, data bindings, and the
|
|
12
|
+
* error slot — not static snapshots.
|
|
13
|
+
*
|
|
14
|
+
* The allow-listed components are token-driven so they read in every theme.
|
|
15
|
+
*/
|
|
16
|
+
function Stat({ label, value, delta }: { label: string; value: string; delta?: string }) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="rounded-lg border bg-card p-4 text-card-foreground">
|
|
19
|
+
<div className="text-meta text-muted-foreground">{label}</div>
|
|
20
|
+
<div className="text-title tabular-nums">{value}</div>
|
|
21
|
+
{delta ? <div className="text-caption text-primary">{delta}</div> : null}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function Callout({
|
|
27
|
+
tone = "info",
|
|
28
|
+
children,
|
|
29
|
+
}: {
|
|
30
|
+
tone?: "info" | "success" | "neutral";
|
|
31
|
+
children?: ReactNode;
|
|
32
|
+
}) {
|
|
33
|
+
const rail =
|
|
34
|
+
tone === "success"
|
|
35
|
+
? "border-s-primary"
|
|
36
|
+
: tone === "neutral"
|
|
37
|
+
? "border-s-muted"
|
|
38
|
+
: "border-s-info";
|
|
39
|
+
return <div className={`border-s-2 ${rail} bg-muted/40 px-3 py-2 text-body`}>{children}</div>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The allow-list — the agent may reference `<Stat>` / `<Callout>`; nothing else custom renders. */
|
|
43
|
+
const allow = { Stat, Callout };
|
|
44
|
+
|
|
45
|
+
const DASHBOARD =
|
|
46
|
+
'<div className="grid grid-cols-3 gap-3">' +
|
|
47
|
+
'<Stat label="Revenue" value="$1.2M" delta="+12%" />' +
|
|
48
|
+
'<Stat label="Users" value="48.2k" delta="+3%" />' +
|
|
49
|
+
'<Stat label="Churn" value="1.8%" />' +
|
|
50
|
+
"</div>";
|
|
51
|
+
|
|
52
|
+
const meta = {
|
|
53
|
+
title: "AI/JSX Preview",
|
|
54
|
+
component: JSXPreview,
|
|
55
|
+
parameters: { layout: "padded" },
|
|
56
|
+
} satisfies Meta<typeof JSXPreview>;
|
|
57
|
+
export default meta;
|
|
58
|
+
|
|
59
|
+
type Story = StoryObj<typeof meta>;
|
|
60
|
+
|
|
61
|
+
/** A complete agent-emitted surface, rendered against the allow-list. */
|
|
62
|
+
export const Default: Story = {
|
|
63
|
+
render: () => (
|
|
64
|
+
<JSXPreview jsx={DASHBOARD} components={allow}>
|
|
65
|
+
<JSXPreviewContent />
|
|
66
|
+
<JSXPreviewError />
|
|
67
|
+
</JSXPreview>
|
|
68
|
+
),
|
|
69
|
+
play: async ({ canvasElement }) => {
|
|
70
|
+
const canvas = within(canvasElement);
|
|
71
|
+
await expect(canvas.getByText("Revenue")).toBeInTheDocument();
|
|
72
|
+
await expect(canvas.getByText("$1.2M")).toBeInTheDocument();
|
|
73
|
+
await expect(canvas.getByText("Churn")).toBeInTheDocument();
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* LIVE streaming — the headline behavior. The JSX string grows over time and, while
|
|
79
|
+
* `isStreaming`, partial/unclosed tags are auto-completed (`completeJsxTag`) so the
|
|
80
|
+
* surface renders progressively without flashing parse errors. `JSXPreviewSkeleton`
|
|
81
|
+
* covers the brief moment before the first tag paints. Streams on mount; "Replay"
|
|
82
|
+
* restarts it.
|
|
83
|
+
*/
|
|
84
|
+
function StreamingDemo() {
|
|
85
|
+
const [len, setLen] = useState(0);
|
|
86
|
+
const [run, setRun] = useState(0); // bump to replay
|
|
87
|
+
const timer = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
setLen(0);
|
|
91
|
+
timer.current = setInterval(() => {
|
|
92
|
+
setLen((n) => {
|
|
93
|
+
const next = Math.min(n + 12, DASHBOARD.length);
|
|
94
|
+
if (next >= DASHBOARD.length && timer.current) {
|
|
95
|
+
clearInterval(timer.current);
|
|
96
|
+
timer.current = null;
|
|
97
|
+
}
|
|
98
|
+
return next;
|
|
99
|
+
});
|
|
100
|
+
}, 18);
|
|
101
|
+
return () => {
|
|
102
|
+
if (timer.current) clearInterval(timer.current);
|
|
103
|
+
};
|
|
104
|
+
}, [run]);
|
|
105
|
+
|
|
106
|
+
const streaming = len < DASHBOARD.length;
|
|
107
|
+
return (
|
|
108
|
+
<div className="space-y-3">
|
|
109
|
+
<div className="flex items-center gap-3">
|
|
110
|
+
<Button onClick={() => setRun((r) => r + 1)} size="sm" variant="outline" type="button">
|
|
111
|
+
Replay stream
|
|
112
|
+
</Button>
|
|
113
|
+
<span className="text-meta text-muted-foreground">
|
|
114
|
+
{streaming ? "streaming…" : "complete"}
|
|
115
|
+
</span>
|
|
116
|
+
</div>
|
|
117
|
+
<JSXPreview components={allow} isStreaming={streaming} jsx={DASHBOARD.slice(0, len)}>
|
|
118
|
+
<JSXPreviewSkeleton />
|
|
119
|
+
<JSXPreviewContent />
|
|
120
|
+
</JSXPreview>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const Streaming: Story = {
|
|
126
|
+
render: () => <StreamingDemo />,
|
|
127
|
+
play: async ({ canvasElement }) => {
|
|
128
|
+
const canvas = within(canvasElement);
|
|
129
|
+
await userEvent.click(canvas.getByRole("button", { name: /replay stream/i }));
|
|
130
|
+
// After the stream completes, the full dashboard rendered (tags auto-closed mid-stream).
|
|
131
|
+
await canvas.findByText("Churn", {}, { timeout: 4000 });
|
|
132
|
+
await expect(canvas.getByText("Revenue")).toBeInTheDocument();
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* LIVE editing — type an agent's JSX string and watch it render against the
|
|
138
|
+
* allow-list in real time. Half-typed/unclosed tags are treated as INCOMPLETE
|
|
139
|
+
* (not invalid): the surface builds up via `completeJsxTag` and the error box
|
|
140
|
+
* stays hidden — no more flashing red while you type (loading-states.md). Only a
|
|
141
|
+
* *settled, genuinely malformed* input surfaces `JSXPreviewError`. Tags outside
|
|
142
|
+
* the allow-list don't render.
|
|
143
|
+
*/
|
|
144
|
+
function EditableDemo() {
|
|
145
|
+
const [jsx, setJsx] = useState(
|
|
146
|
+
'<div className="grid grid-cols-2 gap-3">\n' +
|
|
147
|
+
' <Stat label="Revenue" value="$1.2M" delta="+12%" />\n' +
|
|
148
|
+
' <Callout tone="success">On track for Q3</Callout>\n' +
|
|
149
|
+
"</div>",
|
|
150
|
+
);
|
|
151
|
+
return (
|
|
152
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
153
|
+
<div className="space-y-2">
|
|
154
|
+
<label className="text-meta text-muted-foreground" htmlFor="agent-jsx">
|
|
155
|
+
Agent JSX (edit me)
|
|
156
|
+
</label>
|
|
157
|
+
<Textarea
|
|
158
|
+
className="font-mono text-code"
|
|
159
|
+
id="agent-jsx"
|
|
160
|
+
onChange={(e) => setJsx(e.target.value)}
|
|
161
|
+
rows={8}
|
|
162
|
+
spellCheck={false}
|
|
163
|
+
value={jsx}
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
<JSXPreview components={allow} jsx={jsx}>
|
|
167
|
+
<JSXPreviewSkeleton />
|
|
168
|
+
<JSXPreviewContent />
|
|
169
|
+
<JSXPreviewError />
|
|
170
|
+
</JSXPreview>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export const Editable: Story = {
|
|
176
|
+
render: () => <EditableDemo />,
|
|
177
|
+
play: async ({ canvasElement }) => {
|
|
178
|
+
const canvas = within(canvasElement);
|
|
179
|
+
const textarea = canvas.getByLabelText(/agent jsx/i);
|
|
180
|
+
await userEvent.clear(textarea);
|
|
181
|
+
await userEvent.type(textarea, '<Callout tone="success">Shipped it</Callout>');
|
|
182
|
+
await canvas.findByText("Shipped it");
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Data bindings — the JSX references `{values}` resolved from the `bindings` prop,
|
|
188
|
+
* so the agent can emit a fixed template and you inject the data.
|
|
189
|
+
*/
|
|
190
|
+
export const DataBindings: Story = {
|
|
191
|
+
render: () => (
|
|
192
|
+
<JSXPreview
|
|
193
|
+
bindings={{ revenue: "$1.2M", delta: "+12%", users: "48.2k", churn: "1.8%" }}
|
|
194
|
+
components={allow}
|
|
195
|
+
jsx={
|
|
196
|
+
'<div className="grid grid-cols-3 gap-3">' +
|
|
197
|
+
'<Stat label="Revenue" value={revenue} delta={delta} />' +
|
|
198
|
+
'<Stat label="Users" value={users} />' +
|
|
199
|
+
'<Stat label="Churn" value={churn} />' +
|
|
200
|
+
"</div>"
|
|
201
|
+
}
|
|
202
|
+
>
|
|
203
|
+
<JSXPreviewContent />
|
|
204
|
+
</JSXPreview>
|
|
205
|
+
),
|
|
206
|
+
play: async ({ canvasElement }) => {
|
|
207
|
+
const canvas = within(canvasElement);
|
|
208
|
+
// The values come from `bindings`, not the markup.
|
|
209
|
+
await expect(canvas.getByText("$1.2M")).toBeInTheDocument();
|
|
210
|
+
await expect(canvas.getByText("48.2k")).toBeInTheDocument();
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* LOADING — fetch-then-show. Before any JSX has arrived, `loading` puts the
|
|
216
|
+
* preview in its `pending` state and `JSXPreviewSkeleton` renders a layout-shaped
|
|
217
|
+
* placeholder (a status live region for AT) instead of a blank or an error. Once
|
|
218
|
+
* the content arrives, it swaps in.
|
|
219
|
+
*/
|
|
220
|
+
function LoadingDemo() {
|
|
221
|
+
const [loading, setLoading] = useState(true);
|
|
222
|
+
return (
|
|
223
|
+
<div className="space-y-3">
|
|
224
|
+
<Button onClick={() => setLoading((l) => !l)} size="sm" variant="outline" type="button">
|
|
225
|
+
{loading ? "Finish loading" : "Reset to loading"}
|
|
226
|
+
</Button>
|
|
227
|
+
<JSXPreview components={allow} jsx={loading ? "" : DASHBOARD} loading={loading}>
|
|
228
|
+
<JSXPreviewSkeleton />
|
|
229
|
+
<JSXPreviewContent />
|
|
230
|
+
</JSXPreview>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export const Loading: Story = {
|
|
236
|
+
render: () => <LoadingDemo />,
|
|
237
|
+
play: async ({ canvasElement }) => {
|
|
238
|
+
const canvas = within(canvasElement);
|
|
239
|
+
// The skeleton's live region is present while loading; no content yet.
|
|
240
|
+
await expect(canvas.getByRole("status")).toBeInTheDocument();
|
|
241
|
+
await expect(canvas.queryByText("Revenue")).not.toBeInTheDocument();
|
|
242
|
+
await userEvent.click(canvas.getByRole("button", { name: /finish loading/i }));
|
|
243
|
+
await canvas.findByText("Revenue");
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* TERMINAL error — a *settled, syntactically complete* input that is still
|
|
249
|
+
* invalid (here a broken `{)}` expression). This is the ONLY case that surfaces
|
|
250
|
+
* `JSXPreviewError`; incomplete/streaming input never does.
|
|
251
|
+
*/
|
|
252
|
+
export const TerminalError: Story = {
|
|
253
|
+
name: "Terminal error",
|
|
254
|
+
render: () => (
|
|
255
|
+
<JSXPreview components={allow} jsx={'<Stat label="Revenue" value={)} />'}>
|
|
256
|
+
<JSXPreviewSkeleton />
|
|
257
|
+
<JSXPreviewContent />
|
|
258
|
+
<JSXPreviewError />
|
|
259
|
+
</JSXPreview>
|
|
260
|
+
),
|
|
261
|
+
play: async ({ canvasElement }) => {
|
|
262
|
+
const canvas = within(canvasElement);
|
|
263
|
+
await canvas.findByRole("alert");
|
|
264
|
+
},
|
|
265
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
3
|
+
import {
|
|
4
|
+
JSXPreview,
|
|
5
|
+
JSXPreviewContent,
|
|
6
|
+
JSXPreviewError,
|
|
7
|
+
JSXPreviewSkeleton,
|
|
8
|
+
type JSXPreviewProps,
|
|
9
|
+
} from "./jsx-preview";
|
|
10
|
+
|
|
11
|
+
afterEach(cleanup);
|
|
12
|
+
|
|
13
|
+
// Props are optional so the map is assignable to react-jsx-parser's
|
|
14
|
+
// `components` index signature (it renders against `FunctionComponent<{}>`).
|
|
15
|
+
function Stat({ label, value }: { label?: string; value?: string }) {
|
|
16
|
+
return (
|
|
17
|
+
<div>
|
|
18
|
+
<span>{label}</span>
|
|
19
|
+
{value ? <span>{value}</span> : null}
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const allow = { Stat };
|
|
25
|
+
|
|
26
|
+
function renderPreview(props: Omit<JSXPreviewProps, "components">) {
|
|
27
|
+
return render(
|
|
28
|
+
<JSXPreview components={allow} {...props}>
|
|
29
|
+
<JSXPreviewSkeleton />
|
|
30
|
+
<JSXPreviewContent />
|
|
31
|
+
<JSXPreviewError />
|
|
32
|
+
</JSXPreview>,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("JSXPreview — settled, valid input", () => {
|
|
37
|
+
it("renders complete JSX with no skeleton and no error", () => {
|
|
38
|
+
renderPreview({ jsx: '<Stat label="Revenue" value="$1.2M" />' });
|
|
39
|
+
expect(screen.getByText("Revenue")).toBeInTheDocument();
|
|
40
|
+
expect(screen.getByText("$1.2M")).toBeInTheDocument();
|
|
41
|
+
// The skeleton's live region is always mounted but silent (no label) when ready.
|
|
42
|
+
expect(screen.queryByText(/loading preview/i)).not.toBeInTheDocument();
|
|
43
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("JSXPreview — not-ready states never flash the error box (loading-states.md)", () => {
|
|
48
|
+
it("shows the skeleton (not an error) for syntactically incomplete input", () => {
|
|
49
|
+
// Ends mid-tag → incomplete, treated as pending, NOT invalid.
|
|
50
|
+
renderPreview({ jsx: '<Stat label="Rev' });
|
|
51
|
+
expect(screen.getByText(/loading preview/i)).toBeInTheDocument();
|
|
52
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("builds up streamed/incomplete content without an error", () => {
|
|
56
|
+
// Unclosed <div> with a complete child — completeJsxTag closes it.
|
|
57
|
+
renderPreview({ jsx: '<div><Stat label="Revenue" />', isStreaming: true });
|
|
58
|
+
expect(screen.getByText("Revenue")).toBeInTheDocument();
|
|
59
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("shows the skeleton while `loading` and nothing has arrived", () => {
|
|
63
|
+
renderPreview({ jsx: "", loading: true });
|
|
64
|
+
expect(screen.getByText(/loading preview/i)).toBeInTheDocument();
|
|
65
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("JSXPreview — terminal errors", () => {
|
|
70
|
+
it("surfaces the error slot only for settled, complete-but-invalid input", async () => {
|
|
71
|
+
// Tag-complete (self-closing) but the `{)}` expression is a real parse error.
|
|
72
|
+
renderPreview({ jsx: '<Stat label="Revenue" value={)} />' });
|
|
73
|
+
expect(await screen.findByRole("alert")).toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
});
|