@elabs-ai/components-ai 4.0.0 → 4.2.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/README.md +62 -12
- package/dist/{_audio-player-media-chrome-KA5DY54G.js → _audio-player-media-chrome-T3XVXWRZ.js} +8 -4
- package/dist/_audio-player-media-chrome-T3XVXWRZ.js.map +1 -0
- package/dist/{_flow-boundary-D63PJ65S.js → _flow-boundary-SHNWLQG5.js} +32 -43
- package/dist/_flow-boundary-SHNWLQG5.js.map +1 -0
- package/dist/{_persona-rive-RFR2EUWP.js → _persona-rive-JEG44YHX.js} +9 -5
- package/dist/_persona-rive-JEG44YHX.js.map +1 -0
- package/dist/index.d.ts +1152 -218
- package/dist/index.js +5601 -3241
- package/dist/index.js.map +1 -1
- package/package.json +30 -15
- package/src/__contract__/audio-visualizer.contract.test.tsx +49 -0
- package/src/__contract__/chat-shell.contract.test.tsx +49 -0
- package/src/__contract__/grouped-parts.contract.test.tsx +49 -0
- package/src/__contract__/image.contract.test.tsx +49 -0
- package/src/__contract__/markdown-view.contract.test.tsx +49 -0
- package/src/__contract__/message-feedback.contract.test.tsx +49 -0
- package/src/__contract__/message-form.contract.test.tsx +49 -0
- package/src/__contract__/message-table.contract.test.tsx +49 -0
- package/src/__contract__/model-provider-logo.contract.test.tsx +49 -0
- package/src/__contract__/persona.contract.test.tsx +49 -0
- package/src/__contract__/prompt-input-effort.contract.test.tsx +49 -0
- package/src/__contract__/prompt-input-mode.contract.test.tsx +49 -0
- package/src/_audio-player-media-chrome.tsx +102 -15
- package/src/_chat-shell-rail.tsx +2 -2
- package/src/_flow-boundary.tsx +68 -49
- package/src/_lazy-boundary-conformance.ts +38 -0
- package/src/_lazy-cjk.test.ts +43 -0
- package/src/_lazy-cjk.ts +73 -0
- package/src/_lazy-engine-boundary.tsx +61 -0
- package/src/_lazy-math.test.ts +63 -0
- package/src/_lazy-math.ts +90 -0
- package/src/_lazy-mermaid-absent.test.ts +53 -0
- package/src/_lazy-mermaid.test.ts +15 -0
- package/src/_lazy-mermaid.ts +24 -1
- package/src/_mermaid-error-panel.test.tsx +50 -0
- package/src/_mermaid-error-panel.tsx +66 -0
- package/src/_persona-rive.tsx +62 -10
- package/src/_streamdown-i18n.ts +94 -22
- package/src/_streamdown-safety.ts +170 -0
- package/src/_theme-scope-store.test.ts +83 -0
- package/src/_theme-scope-store.ts +103 -0
- package/src/agent-event.stories.tsx +97 -0
- package/src/agent-event.test.tsx +145 -0
- package/src/agent-event.tsx +187 -0
- package/src/agent-timeline.stories.tsx +9 -1
- package/src/agent.test.tsx +47 -0
- package/src/agent.tsx +43 -24
- package/src/agentic-workspace.stories.tsx +27 -20
- package/src/artifact.tsx +17 -14
- package/src/asset-preview.test.tsx +40 -0
- package/src/asset-preview.tsx +83 -12
- package/src/attachments.tsx +7 -4
- package/src/audio-player.test.tsx +78 -0
- package/src/audio-player.tsx +243 -56
- package/src/audio-visualizer.stories.tsx +126 -0
- package/src/audio-visualizer.test.tsx +438 -0
- package/src/audio-visualizer.tsx +367 -0
- package/src/canvas.stories.tsx +150 -1
- package/src/chat-shell.stories.tsx +18 -3
- package/src/chat.stories.tsx +16 -2
- package/src/code-block.stories.tsx +9 -1
- package/src/code-block.test.tsx +100 -1
- package/src/code-block.tsx +166 -103
- package/src/commit.tsx +30 -41
- package/src/composer.stories.tsx +372 -9
- package/src/composer.test.tsx +357 -6
- package/src/composer.tsx +217 -35
- package/src/confirmation.stories.tsx +72 -1
- package/src/confirmation.test.tsx +216 -2
- package/src/confirmation.tsx +264 -4
- package/src/context-panel.stories.tsx +9 -1
- package/src/context-panel.tsx +3 -2
- package/src/conversation.stories.tsx +86 -2
- package/src/conversation.test.tsx +66 -0
- package/src/conversation.tsx +52 -9
- package/src/diff-view.stories.tsx +196 -0
- package/src/diff-view.test.tsx +238 -0
- package/src/diff-view.tsx +697 -0
- package/src/environment-variables.tsx +20 -37
- package/src/file-tree.test.tsx +21 -0
- package/src/file-tree.tsx +12 -2
- package/src/gallery.tsx +4 -2
- package/src/index.ts +14 -4
- package/src/inline-citation.tsx +5 -5
- package/src/jsx-preview.stories.tsx +2 -2
- package/src/jsx-preview.tsx +151 -43
- package/src/markdown-view.stories.tsx +92 -1
- package/src/markdown-view.test.tsx +237 -2
- package/src/markdown-view.tsx +157 -6
- package/src/message-compare.stories.tsx +175 -0
- package/src/message-compare.test.tsx +207 -0
- package/src/message-compare.tsx +453 -0
- package/src/message-form.stories.tsx +65 -4
- package/src/message-form.test.tsx +8 -2
- package/src/message-form.tsx +15 -7
- package/src/message-table.stories.tsx +2 -2
- package/src/message-table.test.tsx +7 -0
- package/src/message-table.tsx +8 -4
- package/src/message.stories.tsx +9 -1
- package/src/message.test.tsx +247 -1
- package/src/message.tsx +125 -15
- package/src/microcopy.test.tsx +40 -0
- package/src/{model-selector.stories.tsx → model-provider-logo.stories.tsx} +17 -8
- package/src/model-provider-logo.test.tsx +109 -0
- package/src/model-provider-logo.tsx +183 -0
- package/src/open-in-chat.tsx +50 -29
- package/src/package-info.tsx +12 -12
- package/src/permission-mode-select.stories.tsx +82 -0
- package/src/permission-mode-select.test.tsx +100 -0
- package/src/permission-mode-select.tsx +137 -0
- package/src/persona-missing-peer.test.tsx +54 -0
- package/src/persona.tsx +68 -22
- package/src/plan.stories.tsx +166 -0
- package/src/plan.test.tsx +267 -0
- package/src/plan.tsx +182 -20
- package/src/prompt-input-effort.stories.tsx +123 -0
- package/src/prompt-input-effort.test.tsx +83 -0
- package/src/prompt-input-effort.tsx +136 -0
- package/src/prompt-input-mode.stories.tsx +108 -0
- package/src/prompt-input-mode.test.tsx +99 -0
- package/src/prompt-input-mode.tsx +169 -0
- package/src/prompt-input-slash.stories.tsx +211 -0
- package/src/prompt-input-slash.test.tsx +262 -0
- package/src/prompt-input-slash.tsx +541 -0
- package/src/prompt-input.stories.tsx +2 -2
- package/src/prompt-input.test.tsx +67 -1
- package/src/prompt-input.tsx +42 -4
- package/src/queue.tsx +4 -4
- package/src/reasoning.tsx +42 -17
- package/src/sandbox.stories.tsx +9 -1
- package/src/sandbox.tsx +3 -3
- package/src/schema-display.test.tsx +56 -0
- package/src/schema-display.tsx +71 -37
- package/src/selection-toolbar.stories.tsx +9 -1
- package/src/session-header.stories.tsx +128 -0
- package/src/session-header.test.tsx +138 -0
- package/src/session-header.tsx +243 -0
- package/src/session-status-bar.stories.tsx +73 -0
- package/src/session-status-bar.test.tsx +94 -0
- package/src/session-status-bar.tsx +165 -0
- package/src/snippet.stories.tsx +9 -1
- package/src/snippet.test.tsx +6 -2
- package/src/snippet.tsx +14 -34
- package/src/speech-input.test.tsx +109 -0
- package/src/speech-input.tsx +31 -3
- package/src/stack-trace.tsx +24 -36
- package/src/streamdown-i18n.test.tsx +24 -1
- package/src/task.stories.tsx +13 -3
- package/src/templates-ai-assistant.stories.tsx +21 -1
- package/src/test-results.tsx +47 -27
- package/src/token-usage.stories.tsx +24 -0
- package/src/token-usage.test.tsx +92 -0
- package/src/{context.tsx → token-usage.tsx} +73 -59
- package/src/tool-result-card.stories.tsx +9 -1
- package/src/tool.stories.tsx +13 -3
- package/src/tool.test.tsx +65 -0
- package/src/tool.tsx +73 -30
- package/src/transcription.tsx +1 -1
- package/src/turn-status.stories.tsx +124 -0
- package/src/turn-status.test.tsx +74 -0
- package/src/turn-status.tsx +174 -0
- package/src/use-audio-level.ts +104 -0
- package/src/voice-selector.tsx +5 -5
- package/src/web-preview.test.tsx +51 -1
- package/src/web-preview.tsx +46 -10
- package/dist/_audio-player-media-chrome-KA5DY54G.js.map +0 -1
- package/dist/_flow-boundary-D63PJ65S.js.map +0 -1
- package/dist/_interactive-terminal-xterm-ATJ5EW3G.js +0 -11
- package/dist/_interactive-terminal-xterm-ATJ5EW3G.js.map +0 -1
- package/dist/_persona-rive-RFR2EUWP.js.map +0 -1
- package/src/_interactive-terminal-xterm.ts +0 -32
- package/src/blocks-ai-composer.stories.tsx +0 -83
- package/src/context.stories.tsx +0 -16
- package/src/interactive-terminal.stories.tsx +0 -165
- package/src/interactive-terminal.test.tsx +0 -448
- package/src/interactive-terminal.tsx +0 -444
- package/src/model-selector.test.tsx +0 -55
- package/src/model-selector.tsx +0 -225
- package/src/terminal.tsx +0 -244
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, userEvent, within } from "storybook/test";
|
|
3
|
+
import { ChangeReview, type ChangeHunk, type DiffLine } from "@elabs-ai/components-ui";
|
|
4
|
+
import { DiffView } from "./diff-view";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "AI/DiffView",
|
|
8
|
+
component: DiffView,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: "padded",
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component:
|
|
14
|
+
"The READ-ONLY patch renderer. Pick a diff surface by what the human is allowed " +
|
|
15
|
+
"to do with the change: reading lines of a patch → `AI/DiffView` " +
|
|
16
|
+
"(`@elabs-ai/components-ai`); accepting or rejecting per hunk → " +
|
|
17
|
+
"`AI/ChangeReview` (`@elabs-ai/components-ui`); editing both sides, side by " +
|
|
18
|
+
"side → `Editor/DiffEditor` (`@elabs-ai/components-editor`); reading a " +
|
|
19
|
+
"patch in a console transcript → `Terminal/TerminalDiffHunk` " +
|
|
20
|
+
"(`@elabs-ai/components-terminal`). See " +
|
|
21
|
+
"[Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). " +
|
|
22
|
+
"This view renders a `DiffLine[]` someone else computed — it never diffs, " +
|
|
23
|
+
"fetches or parses a patch itself. To put those lines behind an approve/reject " +
|
|
24
|
+
"gate, the app that depends on both packages injects one into the other: pass a " +
|
|
25
|
+
"`<DiffView>` through `ChangeReview`’s `renderHunk` render-prop or a " +
|
|
26
|
+
"hunk’s `after` slot, as the *Composed into ChangeReview (ui)* story below " +
|
|
27
|
+
"does. That seam is ratified, not unfinished — `ChangeHunk` gains no " +
|
|
28
|
+
"`lines` field and `DiffLine` never moves, because `@elabs-ai/components-ui` " +
|
|
29
|
+
"may not import `@elabs-ai/components-ai` and this view needs Shiki.",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
tags: ["autodocs"],
|
|
34
|
+
} satisfies Meta<typeof DiffView>;
|
|
35
|
+
export default meta;
|
|
36
|
+
type Story = StoryObj<typeof meta>;
|
|
37
|
+
|
|
38
|
+
// A representative mix of every DiffLineType — meta headers, a hunk range, a
|
|
39
|
+
// deletion, two additions and trailing context (#102 acceptance criteria).
|
|
40
|
+
const SAMPLE_LINES: DiffLine[] = [
|
|
41
|
+
{ type: "meta", text: "diff --git a/src/math.ts b/src/math.ts" },
|
|
42
|
+
{ type: "meta", text: "index a1b2c3d..e4f5g6h 100644" },
|
|
43
|
+
{ type: "meta", text: "--- a/src/math.ts" },
|
|
44
|
+
{ type: "meta", text: "+++ b/src/math.ts" },
|
|
45
|
+
{ type: "hunk", text: "@@ -1,5 +1,6 @@" },
|
|
46
|
+
{
|
|
47
|
+
type: "context",
|
|
48
|
+
oldNumber: 1,
|
|
49
|
+
newNumber: 1,
|
|
50
|
+
text: "export function add(a: number, b: number) {",
|
|
51
|
+
},
|
|
52
|
+
{ type: "del", oldNumber: 2, text: " return a - b; // bug: subtracts instead of adds" },
|
|
53
|
+
{ type: "add", newNumber: 2, text: " return a + b;" },
|
|
54
|
+
{ type: "add", newNumber: 3, text: " // fixed the sign error" },
|
|
55
|
+
{ type: "context", oldNumber: 3, newNumber: 4, text: "}" },
|
|
56
|
+
{ type: "context", oldNumber: 4, newNumber: 5, text: "" },
|
|
57
|
+
{ type: "context", oldNumber: 5, newNumber: 6, text: "export const VERSION = 2;" },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export const Default: Story = {
|
|
61
|
+
render: () => (
|
|
62
|
+
<div className="max-w-2xl">
|
|
63
|
+
<DiffView
|
|
64
|
+
lines={SAMPLE_LINES}
|
|
65
|
+
file="src/math.ts"
|
|
66
|
+
stats={{ additions: 2, deletions: 1 }}
|
|
67
|
+
language="typescript"
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Dark theme — the row tints (`bg-success/10` / `bg-destructive/10`), the
|
|
74
|
+
// marker/gutter tokens and the Shiki-derived syntax colour must all resolve
|
|
75
|
+
// from the SCOPED dark region, not the document root (same pattern as
|
|
76
|
+
// `code-block.stories.tsx`'s `DarkTheme`).
|
|
77
|
+
export const DarkTheme: Story = {
|
|
78
|
+
render: () => (
|
|
79
|
+
<div className="max-w-2xl" data-theme="dark">
|
|
80
|
+
<div className="rounded-lg bg-background p-6 text-foreground">
|
|
81
|
+
<DiffView lines={SAMPLE_LINES} file="src/math.ts" stats={{ additions: 2, deletions: 1 }} />
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// variant="split" — two aligned columns (old-file view | new-file view).
|
|
88
|
+
export const SplitVariant: Story = {
|
|
89
|
+
name: 'variant="split"',
|
|
90
|
+
render: () => (
|
|
91
|
+
<div className="max-w-3xl">
|
|
92
|
+
<DiffView lines={SAMPLE_LINES} file="src/math.ts" variant="split" />
|
|
93
|
+
</div>
|
|
94
|
+
),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// pager — absorbs the upstream CodexDiff shape: scroll-position indicator, a
|
|
98
|
+
// key legend, and a named `role="region"`.
|
|
99
|
+
const LONG_LINES: DiffLine[] = Array.from({ length: 60 }, (_, i) => ({
|
|
100
|
+
type: i % 11 === 0 ? "del" : i % 13 === 0 ? "add" : "context",
|
|
101
|
+
oldNumber: i % 13 === 0 ? undefined : i + 1,
|
|
102
|
+
newNumber: i % 11 === 0 ? undefined : i + 1,
|
|
103
|
+
text: `line ${i + 1} of a long file`,
|
|
104
|
+
})) as DiffLine[];
|
|
105
|
+
|
|
106
|
+
export const Pager: Story = {
|
|
107
|
+
name: "pager",
|
|
108
|
+
render: () => (
|
|
109
|
+
<div className="max-w-2xl">
|
|
110
|
+
<DiffView lines={LONG_LINES} file="src/very-long-file.ts" pager />
|
|
111
|
+
</div>
|
|
112
|
+
),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// contextLines — a long run of unchanged lines collapses behind a "show more"
|
|
116
|
+
// control that restores it on click.
|
|
117
|
+
const CONTEXT_RUN_LINES: DiffLine[] = [
|
|
118
|
+
{ type: "hunk", text: "@@ -1,12 +1,13 @@" },
|
|
119
|
+
...Array.from({ length: 10 }, (_, i) => ({
|
|
120
|
+
type: "context" as const,
|
|
121
|
+
oldNumber: i + 1,
|
|
122
|
+
newNumber: i + 1,
|
|
123
|
+
text: `unchanged line ${i + 1}`,
|
|
124
|
+
})),
|
|
125
|
+
{ type: "add", newNumber: 11, text: "// a single new line at the end" },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
export const CollapsedContext: Story = {
|
|
129
|
+
name: "contextLines",
|
|
130
|
+
render: () => (
|
|
131
|
+
<div className="max-w-2xl">
|
|
132
|
+
<DiffView lines={CONTEXT_RUN_LINES} contextLines={4} />
|
|
133
|
+
</div>
|
|
134
|
+
),
|
|
135
|
+
play: async ({ canvasElement }) => {
|
|
136
|
+
const canvas = within(canvasElement);
|
|
137
|
+
const showMore = await canvas.findByRole("button", { name: /more line/ });
|
|
138
|
+
await expect(canvas.queryByText("unchanged line 5")).not.toBeInTheDocument();
|
|
139
|
+
await userEvent.click(showMore);
|
|
140
|
+
await expect(canvas.getByText("unchanged line 5")).toBeInTheDocument();
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// LOADING — no renderable diff yet (loading-states.md). Layout-shaped
|
|
145
|
+
// skeleton rows at the real row height; no layout shift once `lines` settles.
|
|
146
|
+
export const Loading: Story = {
|
|
147
|
+
render: () => (
|
|
148
|
+
<div className="max-w-2xl">
|
|
149
|
+
<DiffView lines={[]} loading file="src/math.ts" />
|
|
150
|
+
</div>
|
|
151
|
+
),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// STREAMING — lines are still arriving (loading-states.md `isStreaming`). What
|
|
155
|
+
// exists renders as-is, including a syntactically incomplete final line, and
|
|
156
|
+
// no error surface appears.
|
|
157
|
+
export const Streaming: Story = {
|
|
158
|
+
name: "isStreaming",
|
|
159
|
+
render: () => (
|
|
160
|
+
<div className="max-w-2xl">
|
|
161
|
+
<DiffView
|
|
162
|
+
lines={[
|
|
163
|
+
{ type: "context", oldNumber: 1, newNumber: 1, text: "export function add(a, b) {" },
|
|
164
|
+
{ type: "add", newNumber: 2, text: " return a + b" },
|
|
165
|
+
]}
|
|
166
|
+
isStreaming
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// ─── ChangeReview composition (binding decision — architecture § 3) ────────
|
|
173
|
+
//
|
|
174
|
+
// DiffView (ai) and ChangeReview (ui) never import each other. The app, which
|
|
175
|
+
// depends on both, wires them together through ChangeReview's existing
|
|
176
|
+
// injection seams — here, the `renderHunk` render-prop seam. `ChangeHunk`
|
|
177
|
+
// gains no `lines` field; `DiffLine` never moves into `ui`.
|
|
178
|
+
const REVIEW_HUNK_LINES: Record<string, DiffLine[]> = {
|
|
179
|
+
"hunk-1": SAMPLE_LINES.filter((l) => l.type !== "meta"),
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const REVIEW_HUNKS: ChangeHunk[] = [{ id: "hunk-1", title: "src/math.ts", status: "modified" }];
|
|
183
|
+
|
|
184
|
+
export const ChangeReviewComposition: Story = {
|
|
185
|
+
name: "Composed into ChangeReview (ui)",
|
|
186
|
+
render: () => (
|
|
187
|
+
<div className="max-w-2xl">
|
|
188
|
+
<ChangeReview
|
|
189
|
+
hunks={REVIEW_HUNKS}
|
|
190
|
+
renderHunk={(hunk) => (
|
|
191
|
+
<DiffView lines={REVIEW_HUNK_LINES[hunk.id] ?? []} variant="inline" />
|
|
192
|
+
)}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
),
|
|
196
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen, waitFor, within } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { type DiffLine } from "@elabs-ai/components-ui";
|
|
5
|
+
import { DiffView } from "./diff-view";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* No `LocaleProvider` wrapper: `useLocale()` falls back to the shipped English
|
|
9
|
+
* defaults, so these assertions prove the real `ai.diffView.*` catalogue in
|
|
10
|
+
* `packages/ui/src/components/locale-provider/messages.ts` — not a local stub
|
|
11
|
+
* that would keep passing if the central entry were missing or misspelled.
|
|
12
|
+
*/
|
|
13
|
+
function renderDiffView(ui: React.ReactElement) {
|
|
14
|
+
return render(ui);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const MIXED_LINES: DiffLine[] = [
|
|
18
|
+
{ type: "hunk", text: "@@ -1,3 +1,4 @@" },
|
|
19
|
+
{ type: "context", oldNumber: 1, newNumber: 1, text: "function add(a, b) {" },
|
|
20
|
+
{ type: "del", oldNumber: 2, text: " return a - b; // bug" },
|
|
21
|
+
{ type: "add", newNumber: 2, text: " return a + b;" },
|
|
22
|
+
{ type: "add", newNumber: 3, text: " // fixed" },
|
|
23
|
+
{ type: "context", oldNumber: 3, newNumber: 4, text: "}" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
describe("DiffView — gutter correctness (the off-by-one this model exists to prevent)", () => {
|
|
27
|
+
it("shows an add row's new number only, and a del row's old number only", () => {
|
|
28
|
+
const { container } = renderDiffView(<DiffView lines={MIXED_LINES} />);
|
|
29
|
+
const rows = container.querySelectorAll('[data-slot="diff-view-row"][data-diff-type]');
|
|
30
|
+
|
|
31
|
+
const del = Array.from(rows).find((r) => r.getAttribute("data-diff-type") === "del")!;
|
|
32
|
+
expect(within(del as HTMLElement).getByText("2")).toBeInTheDocument();
|
|
33
|
+
expect(del.querySelector('[data-slot="diff-view-gutter-old"]')?.textContent).toBe("2");
|
|
34
|
+
expect(del.querySelector('[data-slot="diff-view-gutter-new"]')?.textContent).toBe("");
|
|
35
|
+
|
|
36
|
+
const add = Array.from(rows).find(
|
|
37
|
+
(r) => r.getAttribute("data-diff-type") === "add" && r.textContent?.includes("fixed"),
|
|
38
|
+
)!;
|
|
39
|
+
expect(add.querySelector('[data-slot="diff-view-gutter-new"]')?.textContent).toBe("3");
|
|
40
|
+
expect(add.querySelector('[data-slot="diff-view-gutter-old"]')?.textContent).toBe("");
|
|
41
|
+
|
|
42
|
+
const context = Array.from(rows).find((r) => r.getAttribute("data-diff-type") === "context")!;
|
|
43
|
+
expect(context.querySelector('[data-slot="diff-view-gutter-old"]')?.textContent).toBe("1");
|
|
44
|
+
expect(context.querySelector('[data-slot="diff-view-gutter-new"]')?.textContent).toBe("1");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("DiffView — colour is never the only channel (accessibility.md)", () => {
|
|
49
|
+
it("gives add and del rows DIFFERENT accessible text and a different marker glyph — not merely different classes", () => {
|
|
50
|
+
const { container } = renderDiffView(
|
|
51
|
+
<DiffView
|
|
52
|
+
lines={[
|
|
53
|
+
{ type: "del", oldNumber: 1, text: "old line" },
|
|
54
|
+
{ type: "add", newNumber: 1, text: "new line" },
|
|
55
|
+
]}
|
|
56
|
+
/>,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Accessible text: the sr-only polarity word actually differs.
|
|
60
|
+
expect(screen.getByText("Added:", { exact: false })).toBeInTheDocument();
|
|
61
|
+
expect(screen.getByText("Removed:", { exact: false })).toBeInTheDocument();
|
|
62
|
+
|
|
63
|
+
// Marker glyph: a real, different rendered character per row — the shape
|
|
64
|
+
// channel a colour-blind or greyscale reader relies on.
|
|
65
|
+
const delMarker = container.querySelector(
|
|
66
|
+
'[data-diff-type="del"] [data-slot="diff-view-marker"]',
|
|
67
|
+
);
|
|
68
|
+
const addMarker = container.querySelector(
|
|
69
|
+
'[data-diff-type="add"] [data-slot="diff-view-marker"]',
|
|
70
|
+
);
|
|
71
|
+
expect(delMarker?.textContent).toBe("−");
|
|
72
|
+
expect(addMarker?.textContent).toBe("+");
|
|
73
|
+
expect(delMarker?.textContent).not.toBe(addMarker?.textContent);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("gives the del row's marker a distinct tone class from the add row's — but that alone is insufficient (locked above by text+glyph)", () => {
|
|
77
|
+
const { container } = renderDiffView(
|
|
78
|
+
<DiffView
|
|
79
|
+
lines={[
|
|
80
|
+
{ type: "del", oldNumber: 1, text: "old line" },
|
|
81
|
+
{ type: "add", newNumber: 1, text: "new line" },
|
|
82
|
+
]}
|
|
83
|
+
/>,
|
|
84
|
+
);
|
|
85
|
+
const delMarker = container.querySelector(
|
|
86
|
+
'[data-diff-type="del"] [data-slot="diff-view-marker"]',
|
|
87
|
+
);
|
|
88
|
+
const addMarker = container.querySelector(
|
|
89
|
+
'[data-diff-type="add"] [data-slot="diff-view-marker"]',
|
|
90
|
+
);
|
|
91
|
+
expect(delMarker?.className).not.toBe(addMarker?.className);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("DiffView — loading vocabulary (loading-states.md)", () => {
|
|
96
|
+
it("loading renders layout-shaped skeleton rows sharing the real row's height class (no shift on settle)", () => {
|
|
97
|
+
const { container: loadingContainer } = renderDiffView(<DiffView lines={[]} loading />);
|
|
98
|
+
const { container: settledContainer } = renderDiffView(<DiffView lines={MIXED_LINES} />);
|
|
99
|
+
|
|
100
|
+
const skeletonRow = loadingContainer.querySelector('[data-slot="diff-view-row"]');
|
|
101
|
+
const realRow = settledContainer.querySelector(
|
|
102
|
+
'[data-slot="diff-view-row"][data-diff-type="add"]',
|
|
103
|
+
);
|
|
104
|
+
expect(skeletonRow).toBeTruthy();
|
|
105
|
+
expect(realRow).toBeTruthy();
|
|
106
|
+
// Same grid template + padding class => same row height; only the tint axis differs.
|
|
107
|
+
expect(skeletonRow?.className).toContain("grid-cols-[2.75rem_2.75rem_1.25rem_1fr]");
|
|
108
|
+
expect(realRow?.className).toContain("grid-cols-[2.75rem_2.75rem_1.25rem_1fr]");
|
|
109
|
+
|
|
110
|
+
expect(screen.getByText("Loading diff…")).toBeInTheDocument();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("isStreaming with a truncated final line renders no error surface", () => {
|
|
114
|
+
const truncated: DiffLine[] = [
|
|
115
|
+
{ type: "context", oldNumber: 1, newNumber: 1, text: "const partial = {" },
|
|
116
|
+
{ type: "add", newNumber: 2, text: ' key: "unterminated' },
|
|
117
|
+
];
|
|
118
|
+
renderDiffView(<DiffView lines={truncated} isStreaming />);
|
|
119
|
+
|
|
120
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
121
|
+
expect(screen.getByText("Generating…")).toBeInTheDocument();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("DiffView — variants", () => {
|
|
126
|
+
it('renders one row per line for variant="inline" (default)', () => {
|
|
127
|
+
const { container } = renderDiffView(<DiffView lines={MIXED_LINES} />);
|
|
128
|
+
expect(container.querySelectorAll('[data-slot="diff-view-row"]')).toHaveLength(
|
|
129
|
+
MIXED_LINES.length,
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('renders two aligned columns for variant="split"', () => {
|
|
134
|
+
const { container } = renderDiffView(
|
|
135
|
+
<DiffView
|
|
136
|
+
lines={[
|
|
137
|
+
{ type: "del", oldNumber: 1, text: "old" },
|
|
138
|
+
{ type: "add", newNumber: 1, text: "new" },
|
|
139
|
+
]}
|
|
140
|
+
variant="split"
|
|
141
|
+
/>,
|
|
142
|
+
);
|
|
143
|
+
const rows = container.querySelectorAll('[data-slot="diff-view-row"][data-diff-type]');
|
|
144
|
+
expect(rows).toHaveLength(2);
|
|
145
|
+
for (const row of rows) {
|
|
146
|
+
// Each row is split into exactly two side-by-side panes.
|
|
147
|
+
expect(row.children).toHaveLength(2);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("DiffView — pager (absorbs CodexDiff)", () => {
|
|
153
|
+
it("renders a scroll-position indicator, a key legend, and a named region", () => {
|
|
154
|
+
renderDiffView(<DiffView lines={MIXED_LINES} pager file="src/add.ts" />);
|
|
155
|
+
|
|
156
|
+
const region = screen.getByRole("region", { name: "Code diff: src/add.ts" });
|
|
157
|
+
expect(region).toBeInTheDocument();
|
|
158
|
+
expect(
|
|
159
|
+
within(region).getByText("Arrow keys scroll, Page Up/Down page, Home/End jump"),
|
|
160
|
+
).toBeInTheDocument();
|
|
161
|
+
expect(within(region).getByText("0%")).toBeInTheDocument();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("DiffView — old/new sides tokenize SEPARATELY (perf review §2 — a deleted comment opener must not swallow the added side)", () => {
|
|
166
|
+
afterEach(() => {
|
|
167
|
+
document.documentElement.removeAttribute("data-theme");
|
|
168
|
+
for (const token of [
|
|
169
|
+
"--code-foreground",
|
|
170
|
+
"--code-comment",
|
|
171
|
+
"--code-keyword",
|
|
172
|
+
"--code-number",
|
|
173
|
+
]) {
|
|
174
|
+
document.documentElement.style.removeProperty(token);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("does not let an unterminated block comment on the del side swallow the add side's tokens", async () => {
|
|
179
|
+
// Distinct resolved colors per scope (comment vs. keyword vs. number) so
|
|
180
|
+
// a merged/degenerate single-token line is distinguishable from a real,
|
|
181
|
+
// multi-token breakdown — see the module doc comment on `useDiffTokens`.
|
|
182
|
+
document.documentElement.setAttribute("data-theme", "light");
|
|
183
|
+
document.documentElement.style.setProperty("--code-foreground", "oklch(0.1 0 0)");
|
|
184
|
+
document.documentElement.style.setProperty("--code-comment", "oklch(0.5 0 0)");
|
|
185
|
+
document.documentElement.style.setProperty("--code-keyword", "oklch(0.3 0.2 260)");
|
|
186
|
+
document.documentElement.style.setProperty("--code-number", "oklch(0.4 0.2 140)");
|
|
187
|
+
|
|
188
|
+
const lines: DiffLine[] = [
|
|
189
|
+
{ type: "del", oldNumber: 1, text: "/* unterminated" },
|
|
190
|
+
{ type: "add", newNumber: 1, text: "const value = 1;" },
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
const { container } = renderDiffView(<DiffView language="tsx" lines={lines} />);
|
|
194
|
+
|
|
195
|
+
// Combining old+new into ONE document (the bug) makes Shiki carry the
|
|
196
|
+
// del line's still-open `/*` across the line boundary, so the ENTIRE add
|
|
197
|
+
// line becomes one comment-colored token. Tokenized as its own document,
|
|
198
|
+
// `const value = 1;` is real code and splits into several distinctly
|
|
199
|
+
// colored spans.
|
|
200
|
+
await waitFor(() => {
|
|
201
|
+
const addLineText = container.querySelector(
|
|
202
|
+
'[data-diff-type="add"] [data-slot="diff-view-line-text"]',
|
|
203
|
+
);
|
|
204
|
+
const spans = addLineText?.querySelectorAll("span[style]") ?? [];
|
|
205
|
+
expect(spans.length).toBeGreaterThan(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const addLineText = container.querySelector(
|
|
209
|
+
'[data-diff-type="add"] [data-slot="diff-view-line-text"]',
|
|
210
|
+
);
|
|
211
|
+
expect(addLineText?.textContent).toContain("const value = 1;");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("DiffView — contextLines collapsing", () => {
|
|
216
|
+
const longContext: DiffLine[] = Array.from({ length: 10 }, (_, i) => ({
|
|
217
|
+
type: "context" as const,
|
|
218
|
+
oldNumber: i + 1,
|
|
219
|
+
newNumber: i + 1,
|
|
220
|
+
text: `line ${i + 1}`,
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
it("collapses a long context run behind a show-more control that restores it", async () => {
|
|
224
|
+
const user = userEvent.setup();
|
|
225
|
+
const { container } = renderDiffView(<DiffView lines={longContext} contextLines={4} />);
|
|
226
|
+
|
|
227
|
+
expect(screen.queryByText("line 5")).not.toBeInTheDocument();
|
|
228
|
+
const control = screen.getByRole("button", { name: /more line/ });
|
|
229
|
+
expect(control).toBeInTheDocument();
|
|
230
|
+
|
|
231
|
+
await user.click(control);
|
|
232
|
+
|
|
233
|
+
expect(screen.getByText("line 5")).toBeInTheDocument();
|
|
234
|
+
expect(container.querySelectorAll('[data-slot="diff-view-row"]')).toHaveLength(
|
|
235
|
+
longContext.length,
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
});
|